diff --git a/Extra2D/include/extra2d/graphics/backends/opengl/gl_renderer.h b/Extra2D/include/extra2d/graphics/backends/opengl/gl_renderer.h index 37f696c..b7ba020 100644 --- a/Extra2D/include/extra2d/graphics/backends/opengl/gl_renderer.h +++ b/Extra2D/include/extra2d/graphics/backends/opengl/gl_renderer.h @@ -48,6 +48,7 @@ public: void drawSprite(const Texture &texture, const Vec2 &position, const Color &tint) override; void endSpriteBatch() override; + void flush() override; void drawLine(const Vec2 &start, const Vec2 &end, const Color &color, float width) override; @@ -120,7 +121,16 @@ private: int cachedViewportWidth_ = 0; int cachedViewportHeight_ = 0; + // 自动批处理状态 + bool batchActive_ = false; // 批处理是否激活 + bool autoBatchEnabled_ = true; // 是否启用自动批处理 + const Texture* currentBatchTexture_ = nullptr; // 当前批处理的纹理 + std::vector pendingSprites_; // 待提交的精灵 + static constexpr size_t MAX_BATCH_SPRITES = 1000; // 最大批处理精灵数 + void initShapeRendering(); + void ensureBatchActive(); // 确保批处理已激活 + void submitPendingSprites(); // 提交待处理的精灵 void flushShapeBatch(); void flushLineBatch(); void addShapeVertex(float x, float y, const Color &color); diff --git a/Extra2D/include/extra2d/graphics/core/render_backend.h b/Extra2D/include/extra2d/graphics/core/render_backend.h index 895f439..3cd0fb9 100644 --- a/Extra2D/include/extra2d/graphics/core/render_backend.h +++ b/Extra2D/include/extra2d/graphics/core/render_backend.h @@ -78,6 +78,10 @@ public: // ------------------------------------------------------------------------ // 精灵批渲染 // ------------------------------------------------------------------------ + /** + * @brief 开始手动精灵批处理(高级用法) + * @note 一般情况下不需要调用,drawSprite/drawText 会自动管理批处理 + */ virtual void beginSpriteBatch() = 0; virtual void drawSprite(const Texture &texture, const Rect &destRect, const Rect &srcRect, const Color &tint, @@ -86,6 +90,12 @@ public: const Color &tint) = 0; virtual void endSpriteBatch() = 0; + /** + * @brief 立即提交当前批处理 + * @note 手动控制批处理提交时机,一般情况下不需要调用 + */ + virtual void flush() = 0; + // ------------------------------------------------------------------------ // 形状渲染 // ------------------------------------------------------------------------ diff --git a/Extra2D/src/graphics/backends/opengl/gl_renderer.cpp b/Extra2D/src/graphics/backends/opengl/gl_renderer.cpp index 11474b6..93720ef 100644 --- a/Extra2D/src/graphics/backends/opengl/gl_renderer.cpp +++ b/Extra2D/src/graphics/backends/opengl/gl_renderer.cpp @@ -134,6 +134,10 @@ void GLRenderer::beginFrame(const Color &clearColor) { * @brief 结束当前帧,刷新所有待处理的渲染批次 */ void GLRenderer::endFrame() { + // 刷新所有待处理的精灵批次(自动批处理) + if (autoBatchEnabled_ && batchActive_) { + flush(); + } // 刷新所有待处理的形状批次 flushShapeBatch(); // 刷新所有待处理的线条批次 @@ -258,9 +262,45 @@ Ptr GLRenderer::loadTexture(const std::string &filepath) { } /** - * @brief 开始精灵批处理 + * @brief 确保批处理已激活(自动批处理内部使用) */ -void GLRenderer::beginSpriteBatch() { spriteBatch_.begin(viewProjection_); } +void GLRenderer::ensureBatchActive() { + if (!batchActive_) { + spriteBatch_.begin(viewProjection_); + batchActive_ = true; + currentBatchTexture_ = nullptr; + pendingSprites_.clear(); + } +} + +/** + * @brief 提交待处理的精灵(自动批处理内部使用) + */ +void GLRenderer::submitPendingSprites() { + if (pendingSprites_.empty()) { + return; + } + + // 提交所有待处理的精灵 + spriteBatch_.drawBatch(*currentBatchTexture_, pendingSprites_); + pendingSprites_.clear(); + currentBatchTexture_ = nullptr; +} + +/** + * @brief 开始手动精灵批处理(高级用法) + * @note 一般情况下不需要调用,drawSprite/drawText 会自动管理批处理 + */ +void GLRenderer::beginSpriteBatch() { + // 如果自动批处理已激活,先提交 + if (autoBatchEnabled_ && batchActive_) { + flush(); + } + // 禁用自动批处理,进入手动模式 + autoBatchEnabled_ = false; + spriteBatch_.begin(viewProjection_); + batchActive_ = true; +} /** * @brief 绘制精灵(带完整参数) @@ -274,30 +314,69 @@ void GLRenderer::beginSpriteBatch() { spriteBatch_.begin(viewProjection_); } void GLRenderer::drawSprite(const Texture &texture, const Rect &destRect, const Rect &srcRect, const Color &tint, float rotation, const Vec2 &anchor) { - SpriteData data; - data.position = Vec2(destRect.origin.x, destRect.origin.y); - data.size = Vec2(destRect.size.width, destRect.size.height); + // 自动批处理模式 + if (autoBatchEnabled_) { + ensureBatchActive(); - Texture *tex = const_cast(&texture); - float texW = static_cast(tex->getWidth()); - float texH = static_cast(tex->getHeight()); + // 如果纹理变化或缓冲区满,先提交当前批次 + if (currentBatchTexture_ != &texture || + pendingSprites_.size() >= MAX_BATCH_SPRITES) { + submitPendingSprites(); + currentBatchTexture_ = &texture; + } - // 纹理坐标计算 - float u1 = srcRect.origin.x / texW; - float u2 = (srcRect.origin.x + srcRect.size.width) / texW; - float v1 = srcRect.origin.y / texH; - float v2 = (srcRect.origin.y + srcRect.size.height) / texH; + // 创建精灵数据 + SpriteData data; + data.position = Vec2(destRect.origin.x, destRect.origin.y); + data.size = Vec2(destRect.size.width, destRect.size.height); - data.uvRect = Rect( - Vec2(glm::min(u1, u2), glm::min(v1, v2)), - Size(glm::abs(u2 - u1), glm::abs(v2 - v1)) - ); + Texture *tex = const_cast(&texture); + float texW = static_cast(tex->getWidth()); + float texH = static_cast(tex->getHeight()); - data.color = tint; - data.rotation = rotation * 3.14159f / 180.0f; - data.pivot = Vec2(anchor.x, anchor.y); + // 纹理坐标计算 + float u1 = srcRect.origin.x / texW; + float u2 = (srcRect.origin.x + srcRect.size.width) / texW; + float v1 = srcRect.origin.y / texH; + float v2 = (srcRect.origin.y + srcRect.size.height) / texH; - spriteBatch_.draw(texture, data); + data.uvRect = Rect( + Vec2(glm::min(u1, u2), glm::min(v1, v2)), + Size(glm::abs(u2 - u1), glm::abs(v2 - v1)) + ); + + data.color = tint; + data.rotation = rotation * 3.14159f / 180.0f; + data.pivot = Vec2(anchor.x, anchor.y); + + // 添加到待处理列表 + pendingSprites_.push_back(data); + } else { + // 手动批处理模式 + SpriteData data; + data.position = Vec2(destRect.origin.x, destRect.origin.y); + data.size = Vec2(destRect.size.width, destRect.size.height); + + Texture *tex = const_cast(&texture); + float texW = static_cast(tex->getWidth()); + float texH = static_cast(tex->getHeight()); + + float u1 = srcRect.origin.x / texW; + float u2 = (srcRect.origin.x + srcRect.size.width) / texW; + float v1 = srcRect.origin.y / texH; + float v2 = (srcRect.origin.y + srcRect.size.height) / texH; + + data.uvRect = Rect( + Vec2(glm::min(u1, u2), glm::min(v1, v2)), + Size(glm::abs(u2 - u1), glm::abs(v2 - v1)) + ); + + data.color = tint; + data.rotation = rotation * 3.14159f / 180.0f; + data.pivot = Vec2(anchor.x, anchor.y); + + spriteBatch_.draw(texture, data); + } } /** @@ -316,11 +395,34 @@ void GLRenderer::drawSprite(const Texture &texture, const Vec2 &position, } /** - * @brief 结束精灵批处理并提交绘制 + * @brief 结束手动精灵批处理并提交绘制 + * @note 一般情况下不需要调用 */ void GLRenderer::endSpriteBatch() { - spriteBatch_.end(); - stats_.drawCalls += spriteBatch_.getDrawCallCount(); + if (autoBatchEnabled_) { + // 自动模式下,只是标记批处理结束 + flush(); + } else { + // 手动模式下,提交批处理并恢复自动模式 + spriteBatch_.end(); + stats_.drawCalls += spriteBatch_.getDrawCallCount(); + batchActive_ = false; + autoBatchEnabled_ = true; + } +} + +/** + * @brief 立即提交当前批处理 + * @note 手动控制批处理提交时机,一般情况下不需要调用 + */ +void GLRenderer::flush() { + if (autoBatchEnabled_ && batchActive_) { + submitPendingSprites(); + spriteBatch_.end(); + stats_.drawCalls += spriteBatch_.getDrawCallCount(); + batchActive_ = false; + currentBatchTexture_ = nullptr; + } } /** @@ -581,6 +683,11 @@ void GLRenderer::drawText(const FontAtlas &font, const std::string &text, float cursorY = y; float baselineY = cursorY + font.getAscent(); + // 确保批处理已激活(自动批处理) + if (autoBatchEnabled_) { + ensureBatchActive(); + } + // 收集所有字符数据用于批处理 std::vector sprites; sprites.reserve(text.size()); // 预分配空间 @@ -588,6 +695,11 @@ void GLRenderer::drawText(const FontAtlas &font, const std::string &text, for (char c : text) { char32_t codepoint = static_cast(static_cast(c)); if (codepoint == '\n') { + // 提交当前批次(换行时) + if (autoBatchEnabled_ && !sprites.empty()) { + spriteBatch_.drawBatch(*font.getTexture(), sprites); + sprites.clear(); + } cursorX = x; cursorY += font.getLineHeight(); baselineY = cursorY + font.getAscent(); @@ -625,12 +737,27 @@ void GLRenderer::drawText(const FontAtlas &font, const std::string &text, data.pivot = Vec2(0.5f, 0.5f); sprites.push_back(data); + + // 自动批处理:如果缓冲区满,先提交当前批次 + if (autoBatchEnabled_ && sprites.size() >= MAX_BATCH_SPRITES) { + spriteBatch_.drawBatch(*font.getTexture(), sprites); + sprites.clear(); + } } } - // 使用批处理绘制所有字符 + // 提交剩余的字符 if (!sprites.empty()) { - spriteBatch_.drawBatch(*font.getTexture(), sprites); + if (autoBatchEnabled_) { + spriteBatch_.drawBatch(*font.getTexture(), sprites); + } else { + // 手动模式下,添加到待处理列表 + if (currentBatchTexture_ != font.getTexture()) { + submitPendingSprites(); + currentBatchTexture_ = font.getTexture(); + } + pendingSprites_.insert(pendingSprites_.end(), sprites.begin(), sprites.end()); + } } } diff --git a/examples/image_display/assets/images/demo.jpg b/examples/image_display/assets/images/demo.jpg new file mode 100644 index 0000000..b75ea16 Binary files /dev/null and b/examples/image_display/assets/images/demo.jpg differ diff --git a/examples/image_display/assets/images/note.txt b/examples/image_display/assets/images/note.txt new file mode 100644 index 0000000..f1776f9 --- /dev/null +++ b/examples/image_display/assets/images/note.txt @@ -0,0 +1,4 @@ +请将 1.jpg 图片复制到此目录,并重命名为 demo.jpg + +源文件位置: C:\Users\soulcoco\Desktop\Extra2D\1.jpg +目标文件位置: C:\Users\soulcoco\Desktop\Extra2D\examples\image_display\assets\images\demo.jpg diff --git a/examples/image_display/main.cpp b/examples/image_display/main.cpp new file mode 100644 index 0000000..293415c --- /dev/null +++ b/examples/image_display/main.cpp @@ -0,0 +1,180 @@ +/** + * @file main.cpp + * @brief Extra2D 图片显示示例 + * + * 演示如何使用 RenderBackend 抽象接口加载和显示图片 + * 此示例不依赖任何特定渲染后端(如 OpenGL) + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace extra2d; + +class ImageDisplayScene : public Scene { +public: + void onEnter() override { + // 加载图片 + // 注意:请确保有图片文件在 assets/images/ 目录下 + try { + texture_ = renderer_->loadTexture("assets/images/demo.jpg"); + std::cout << "Image loaded successfully!" << std::endl; + std::cout << " Size: " << texture_->getWidth() << "x" << texture_->getHeight() << std::endl; + } catch (...) { + std::cerr << "Failed to load image from assets/images/demo.jpg" << std::endl; + // 尝试使用备用路径 + try { + texture_ = renderer_->loadTexture("examples/image_display/assets/images/demo.jpg"); + std::cout << "Image loaded from alternate path!" << std::endl; + } catch (...) { + std::cerr << "Failed to load image from alternate path!" << std::endl; + texture_ = nullptr; + } + } + } + + void onExit() override { + texture_.reset(); + } + + void onRender(RenderBackend& renderer) override { + Scene::onRender(renderer); + + if (!texture_) { + return; + } + + // 使用 RenderBackend 的抽象接口绘制图片 + // 不依赖任何特定后端(如 OpenGL) + // 自动批处理:无需手动调用 begin/endSpriteBatch + + // 计算图片显示位置(居中显示) + float windowWidth = 1280.0f; + float windowHeight = 720.0f; + float imgWidth = static_cast(texture_->getWidth()); + float imgHeight = static_cast(texture_->getHeight()); + + // 缩放图片以适应窗口(保持宽高比) + float scale = 1.0f; + if (imgWidth > windowWidth * 0.8f || imgHeight > windowHeight * 0.8f) { + float scaleX = (windowWidth * 0.8f) / imgWidth; + float scaleY = (windowHeight * 0.8f) / imgHeight; + scale = std::min(scaleX, scaleY); + } + + float displayWidth = imgWidth * scale; + float displayHeight = imgHeight * scale; + float x = (windowWidth - displayWidth) * 0.5f; + float y = (windowHeight - displayHeight) * 0.5f; + + // 使用 RenderBackend 的 drawSprite 方法绘制图片 + // 参数:纹理、目标矩形、源矩形、颜色、旋转角度、锚点 + Rect destRect(x, y, displayWidth, displayHeight); + Rect srcRect(0, 0, imgWidth, imgHeight); + renderer.drawSprite(*texture_, destRect, srcRect, Colors::White, 0.0f, Vec2(0, 0)); + + // 注意:无需手动调用 renderer.endSpriteBatch(),帧结束时会自动刷新 + } + + void setRenderer(RenderBackend* renderer) { + renderer_ = renderer; + } + +private: + Ptr texture_; + RenderBackend* renderer_ = nullptr; +}; + +int main(int argc, char* argv[]) { + (void)argc; + (void)argv; + + std::cout << "Extra2D Image Display Demo - Starting..." << std::endl; + + Application& app = Application::get(); + + // 注册模块 + app.use([](auto& cfg) { + cfg.w = 1280; + cfg.h = 720; + cfg.title = "Extra2D Image Display Demo"; + cfg.priority = 0; + cfg.backend = "glfw"; + }); + + app.use([](auto& cfg) { cfg.priority = 10; }); + + app.use([](auto& cfg) { cfg.priority = 20; }); + + std::cout << "Initializing application..." << std::endl; + if (!app.init()) { + std::cerr << "Failed to initialize application!" << std::endl; + return -1; + } + + std::cout << "Application initialized successfully!" << std::endl; + + auto* win = app.window(); + if (win) { + std::cout << "Window: " << win->width() << "x" << win->height() << std::endl; + } + + // 设置事件监听 + auto eventService = ServiceLocator::instance().getService(); + if (eventService) { + eventService->addListener(EventType::KeyPressed, [](Event& e) { + auto& keyEvent = std::get(e.data); + if (keyEvent.keyCode == static_cast(Key::Escape)) { + e.handled = true; + Application::get().quit(); + } + }); + } + + // 获取渲染器 + RenderBackend* renderer = app.renderer(); + + // 创建并配置场景 + auto scene = makeShared(); + scene->setRenderer(renderer); + scene->setBackgroundColor(Color(0.1f, 0.1f, 0.15f, 1.0f)); + + if (win) { + scene->setViewportSize(static_cast(win->width()), + static_cast(win->height())); + } + + // 配置相机 + auto cameraService = ServiceLocator::instance().getService(); + if (cameraService && win) { + ViewportConfig vpConfig; + vpConfig.logicWidth = static_cast(win->width()); + vpConfig.logicHeight = static_cast(win->height()); + vpConfig.mode = ViewportMode::AspectRatio; + cameraService->setViewportConfig(vpConfig); + cameraService->updateViewport(win->width(), win->height()); + cameraService->applyViewportAdapter(); + } + + app.enterScene(scene); + + std::cout << "\nControls:" << std::endl; + std::cout << " ESC - Exit" << std::endl; + std::cout << "\nRunning main loop...\n" << std::endl; + + app.run(); + + std::cout << "Shutting down..." << std::endl; + app.shutdown(); + + std::cout << "Goodbye!" << std::endl; + return 0; +} diff --git a/examples/text_rendering/main.cpp b/examples/text_rendering/main.cpp index d09152f..e41671b 100644 --- a/examples/text_rendering/main.cpp +++ b/examples/text_rendering/main.cpp @@ -2,13 +2,13 @@ * @file main.cpp * @brief Extra2D 文字渲染示例 * - * 演示如何使用 GLFontAtlas 渲染文字 + * 演示如何使用 RenderBackend 抽象接口渲染文字 + * 此示例不依赖任何特定渲染后端(如 OpenGL) */ #include #include -#include -#include +#include #include #include #include @@ -25,14 +25,19 @@ public: // 加载字体 // 注意:请确保有字体文件在 assets/fonts/ 目录下 // 如果没有,可以使用系统字体路径 + if (!renderer_) { + std::cerr << "Renderer not available!" << std::endl; + return; + } + try { - font_ = std::make_shared("assets/fonts/arial.ttf", 24); + font_ = renderer_->createFontAtlas("assets/fonts/arial.ttf", 24); } catch (...) { std::cerr << "Failed to load font from assets/fonts/arial.ttf" << std::endl; // 尝试使用备用路径 try { - font_ = std::make_shared("C:/Windows/Fonts/arial.ttf", 24); + font_ = renderer_->createFontAtlas("C:/Windows/Fonts/arial.ttf", 24); } catch (...) { std::cerr << "Failed to load font from system path!" << std::endl; font_ = nullptr; @@ -57,69 +62,64 @@ public: return; } - // 获取渲染器 - auto *glRenderer = dynamic_cast(&renderer); - if (!glRenderer) { - return; - } - - // 开始精灵批处理 - glRenderer->beginSpriteBatch(); - float y = 100.0f; float x = 100.0f; - // 渲染标题 - renderText(glRenderer, "Extra2D Text Rendering Demo", x, y, + // 渲染标题(自动批处理,无需手动调用 begin/end) + renderText(renderer, "Extra2D Text Rendering Demo", x, y, Color(1.0f, 0.8f, 0.2f, 1.0f)); y += font_->getLineHeight() * 2; // 渲染不同颜色的文字 - renderText(glRenderer, "Red Text", x, y, Color(1.0f, 0.2f, 0.2f, 1.0f)); + renderText(renderer, "Red Text", x, y, Color(1.0f, 0.2f, 0.2f, 1.0f)); y += font_->getLineHeight(); - renderText(glRenderer, "Green Text", x, y, Color(0.2f, 1.0f, 0.2f, 1.0f)); + renderText(renderer, "Green Text", x, y, Color(0.2f, 1.0f, 0.2f, 1.0f)); y += font_->getLineHeight(); - renderText(glRenderer, "Blue Text", x, y, Color(0.2f, 0.2f, 1.0f, 1.0f)); + renderText(renderer, "Blue Text", x, y, Color(0.2f, 0.2f, 1.0f, 1.0f)); y += font_->getLineHeight() * 2; // 渲染多行文字 - renderText(glRenderer, "This is a multi-line text example.", x, y, + renderText(renderer, "This is a multi-line text example.", x, y, Color(1.0f, 1.0f, 1.0f, 1.0f)); y += font_->getLineHeight(); - renderText(glRenderer, "You can render text with different colors", x, y, + renderText(renderer, "You can render text with different colors", x, y, Color(0.8f, 0.8f, 0.8f, 1.0f)); y += font_->getLineHeight(); - renderText(glRenderer, "and styles using GLFontAtlas.", x, y, + renderText(renderer, "and styles using FontAtlas.", x, y, Color(0.6f, 0.6f, 0.6f, 1.0f)); y += font_->getLineHeight() * 2; // 渲染半透明文字 - renderText(glRenderer, "Semi-transparent text (50% opacity)", x, y, + renderText(renderer, "Semi-transparent text (50% opacity)", x, y, Color(1.0f, 1.0f, 1.0f, 0.5f)); y += font_->getLineHeight() * 2; // 渲染操作提示 - renderText(glRenderer, "Press ESC to exit", x, y, + renderText(renderer, "Press ESC to exit", x, y, Color(0.5f, 0.5f, 0.5f, 1.0f)); - // 结束精灵批处理 - glRenderer->endSpriteBatch(); + // 注意:无需手动调用 renderer.endSpriteBatch(),帧结束时会自动刷新 + } + + void setRenderer(RenderBackend* renderer) { + renderer_ = renderer; } private: - void renderText(GLRenderer *renderer, const std::string &text, float x, + void renderText(RenderBackend &renderer, const std::string &text, float x, float y, const Color &color) { - if (!font_ || !renderer) { + if (!font_) { return; } - // 使用渲染器的 drawText 方法 - renderer->drawText(*font_, text, Vec2(x, y), color); + // 使用 RenderBackend 的 drawText 方法 + renderer.drawText(*font_, text, Vec2(x, y), color); } - std::shared_ptr font_; + Ptr font_; + RenderBackend* renderer_ = nullptr; }; int main(int argc, char *argv[]) { @@ -169,8 +169,12 @@ int main(int argc, char *argv[]) { }); } + // 获取渲染器 + RenderBackend* renderer = app.renderer(); + // 创建并配置场景 auto scene = makeShared(); + scene->setRenderer(renderer); scene->setBackgroundColor(Color(0.1f, 0.1f, 0.15f, 1.0f)); if (win) { diff --git a/xmake.lua b/xmake.lua index 0a3bd00..0f41b56 100644 --- a/xmake.lua +++ b/xmake.lua @@ -302,3 +302,62 @@ target("demo_hello_module") -- 构建后安装Shader文件 after_build(install_shaders) target_end() + +-- 图片显示示例 +target("demo_image_display") + set_kind("binary") + set_default(false) + + add_deps("extra2d") + add_files("examples/image_display/main.cpp") + + -- 平台配置 + local plat = get_config("plat") or os.host() + local backend = get_config("window_backend") or "sdl2" + if plat == "mingw" or plat == "windows" then + add_packages("glm", "nlohmann_json") + if backend == "glfw" then + add_packages("glfw") + else + add_packages("libsdl2") + end + add_syslinks("opengl32", "glu32", "winmm", "imm32", "version", "setupapi") + elseif plat == "linux" then + add_packages("glm", "nlohmann_json") + if backend == "glfw" then + add_packages("glfw") + else + add_packages("libsdl2") + end + add_syslinks("GL", "dl", "pthread") + elseif plat == "macosx" then + add_packages("glm", "nlohmann_json") + if backend == "glfw" then + add_packages("glfw") + else + add_packages("libsdl2") + end + add_frameworks("OpenGL", "Cocoa", "IOKit", "CoreVideo") + end + + -- 构建后安装Shader文件和assets + after_build(function (target) + -- 安装shaders + local plat = get_config("plat") or os.host() + local targetdir = target:targetdir() + local shader_src = "Extra2D/shaders" + local shader_dest = path.join(targetdir, "shaders") + os.rm(shader_dest) + os.cp(shader_src, shader_dest) + print("Shaders installed to: " .. shader_dest) + + -- 复制assets目录 + local assets_src = "examples/image_display/assets" + local assets_dest = path.join(targetdir, "assets") + if os.exists(assets_src) then + os.rm(assets_dest) + os.cp(assets_src, assets_dest) + print("Assets installed to: " .. assets_dest) + end + end) +target_end()