From 07792c34d735bc3e730753e18c024610c0d2dbf2 Mon Sep 17 00:00:00 2001 From: atdunbg Date: Tue, 22 Sep 2026 17:09:55 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20ImGui=20=E5=8F=A0=E5=8A=A0=E5=B1=82?= =?UTF-8?q?=E6=8E=A5=E5=85=A5=20RenderGraph=EF=BC=88docking=20=E5=88=86?= =?UTF-8?q?=E6=94=AF=EF=BC=8Cdynamic=20rendering=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 框架与 UI 的职责边界: - ImGuiLayer(Engine/ImGui):只做管线桥接——上下文初始化、 NewFrame/BuildDrawData、draw data 录入 cmd;框架自身永不绘制 任何 ImGui 控件,UI 内容全部由派生类在 OnRenderOverlay() 定义 - 每帧顺序:NewFrame → OnRenderOverlay(声明控件)→ BuildDrawData → Renderer.Render(RG: Forward → ImGui(loadOp=Load 叠加) → Present) - ImGui pass 常驻 RG,未启用叠加层时空转,无图结构开销 - 后端共存:VK_EXT_shader_object 引擎无 vk::Pipeline,ImGui 用 UseDynamicRendering + PipelineInfoMain.PipelineRenderingCreateInfo (swapchain 格式),不需要 RenderPass - 析构序:ImGuiLayer 在 Application 成员序中后声明(先于 Renderer 析构),析构内 waitIdle 保证后端资源安全销毁 App 示例(证明职责边界):Scene Debug 面板实时拖动阴影光方向, ` 键反折叠加层 --- .gitmodules | 4 + App/main.cpp | 44 +++++++- Engine/CMakeLists.txt | 15 ++- Engine/include/Engine/Core/Application.h | 19 +++- Engine/include/Engine/ImGui/ImGuiLayer.h | 64 +++++++++++ Engine/include/Engine/Renderer/Renderer.h | 10 ++ Engine/src/Core/Application.cpp | 24 +++- Engine/src/ImGui/ImGuiLayer.cpp | 128 ++++++++++++++++++++++ Engine/src/Renderer/Renderer.cpp | 40 +++++++ Lib/imgui | 1 + 10 files changed, 338 insertions(+), 11 deletions(-) create mode 100644 Engine/include/Engine/ImGui/ImGuiLayer.h create mode 100644 Engine/src/ImGui/ImGuiLayer.cpp create mode 160000 Lib/imgui diff --git a/.gitmodules b/.gitmodules index bf08744..673f390 100644 --- a/.gitmodules +++ b/.gitmodules @@ -13,3 +13,7 @@ [submodule "Lib/entt"] path = Lib/entt url = https://github.com/skypjack/entt.git +[submodule "Lib/imgui"] + path = Lib/imgui + url = https://github.com/ocornut/imgui.git + branch = docking diff --git a/App/main.cpp b/App/main.cpp index 4ccc2dc..a23f10f 100644 --- a/App/main.cpp +++ b/App/main.cpp @@ -16,6 +16,8 @@ #include #include +#include + #include #include @@ -39,6 +41,9 @@ protected: { Scene& scene = GetScene(); + // ImGui 叠加层(框架只做管线桥接,UI 内容全部在这里定义) + EnableImGui(); + // 模型资产(AssetManager 负责加载/缓存/生命周期,返回句柄) auto backpack = GetAssets().LoadModel("assets/models/backpack/backpack.obj"); @@ -72,10 +77,10 @@ protected: } // 灯光实体(第一个灯 = 阴影光源) - scene.CreateEntity("KeyLight") - .AddComponent( - LightComponent{glm::vec3(-0.5f, -0.6f, -0.35f), - glm::vec4(1.0f, 0.95f, 0.9f, 1.0f)}); + m_KeyLight = scene.CreateEntity("KeyLight"); + m_KeyLight.AddComponent( + LightComponent{glm::vec3(-0.5f, -0.6f, -0.35f), + glm::vec4(1.0f, 0.95f, 0.9f, 1.0f)}); scene.CreateEntity("FillLight") .AddComponent( @@ -103,8 +108,37 @@ protected: // ---- 事件 ---- + // ---- 叠加层 UI(唯一允许出现 ImGui 控件的地方)---- + + void OnRenderOverlay() override + { + if (!m_OverlayVisible) return; + + ImGui::Begin("Scene Debug"); + + // 阴影光源方向:拖动滑条实时看影子变化 + if (auto* light = m_KeyLight.TryGetComponent()) { + ImGui::Text("Key Light (shadow caster)"); + ImGui::SliderFloat3("direction", + &light->direction.x, -1.0f, 1.0f); + if (ImGui::Button("reset")) { + light->direction = glm::vec3(-0.5f, -0.6f, -0.35f); + } + } + + ImGui::Separator(); + ImGui::Text("fps: %.1f frame: %llu", + 1.0f / GetDeltaTime(), + static_cast(GetFrameCount())); + + ImGui::End(); + } + void OnKeyPress(const Event& event) override { + if (event.key == GLFW_KEY_GRAVE_ACCENT) { + m_OverlayVisible = !m_OverlayVisible; // ` 反折叠加层 + } if (event.key == GLFW_KEY_F1) { STATS.PrintComparison(); } @@ -121,7 +155,9 @@ protected: private: EntityHandle m_Camera{}; // Scene::CreateEntity 之前为空 EntityHandle m_Ground{}; + EntityHandle m_KeyLight{}; std::vector m_Spinners; + bool m_OverlayVisible = true; f32 m_Time = 0.0f; }; diff --git a/Engine/CMakeLists.txt b/Engine/CMakeLists.txt index 9ee53d0..592c1ed 100644 --- a/Engine/CMakeLists.txt +++ b/Engine/CMakeLists.txt @@ -2,13 +2,26 @@ file(GLOB_RECURSE ENGINE_SOURCES src/*.cpp ) +# ImGui(docking 分支,vendored submodule) +set(IMGUI_DIR ${CMAKE_SOURCE_DIR}/Lib/imgui) +set(IMGUI_SOURCES + ${IMGUI_DIR}/imgui.cpp + ${IMGUI_DIR}/imgui_draw.cpp + ${IMGUI_DIR}/imgui_tables.cpp + ${IMGUI_DIR}/imgui_widgets.cpp + ${IMGUI_DIR}/backends/imgui_impl_glfw.cpp + ${IMGUI_DIR}/backends/imgui_impl_vulkan.cpp +) + # 编译成静态库 -add_library(Engine STATIC ${ENGINE_SOURCES}) +add_library(Engine STATIC ${ENGINE_SOURCES} ${IMGUI_SOURCES}) target_include_directories(Engine PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include ${CMAKE_SOURCE_DIR}/Lib/entt/src + ${IMGUI_DIR} + ${IMGUI_DIR}/backends ) # 链接第三方库 diff --git a/Engine/include/Engine/Core/Application.h b/Engine/include/Engine/Core/Application.h index 5a27dbf..a2929f8 100644 --- a/Engine/include/Engine/Core/Application.h +++ b/Engine/include/Engine/Core/Application.h @@ -9,10 +9,12 @@ // 和事件钩子(OnEvent + 类型化事件)定义程序行为; // runtime、editor 等不同程序形态只是不同的派生类。 // -// 主循环固定为:PollEvents → OnUpdate(dt) → [主渲染 → OnRenderOverlay] → 判退出。 -// 主渲染(Renderer.Render(scene))是框架行为,派生类不可覆盖也不会丢失; -// 派生类的渲染扩展(ImGui/调试视图/编辑器叠加层)走 OnRenderOverlay, -// 在主渲染之后、Present 之前执行,不覆写则什么都不发生。 +// 主循环固定为:PollEvents → OnUpdate(dt) → [NewFrame → OnRenderOverlay → +// BuildDrawData] → 主渲染(Renderer.Render)→ 判退出。 +// UI 构建与 GPU 提交分离:OnRenderOverlay 只"声明"控件,真正的 draw data +// 提交发生在主渲染管线中的 ImGui pass(Forward 之后、Present 之前, +// loadOp=Load 叠加)。主渲染是框架行为,派生类不可覆盖也不会丢失; +// 框架自身永远不绘制任何 ImGui 控件——UI 内容全部由派生类定义。 // dt 由框架统一计时(含首帧与卡顿钳制),派生层无需自己管理。 // @@ -28,6 +30,7 @@ namespace Engine { class Window; class Renderer; + class ImGuiLayer; namespace ECS { class Scene; } namespace Assets { class AssetManager; } @@ -46,6 +49,11 @@ namespace Engine { // 请求退出(当帧结束后生效) void Close() { m_Running = false; } + // 启用 ImGui 叠加层(OnInit 内调用)。启用后每帧: + // NewFrame → OnRenderOverlay(派生类画控件)→ BuildDrawData → + // 主渲染(ImGui pass 把 draw data 叠加到场景上)。 + void EnableImGui(); + // ---- 供派生类访问的模块 ---- Window& GetWindow() const { return *m_Window; } Renderer& GetRenderer() const { return *m_Renderer; } @@ -92,6 +100,9 @@ namespace Engine { Scope m_Scene; Scope m_Renderer; Scope m_Assets; + // 注意成员序:ImGuiLayer 必须后声明(先于 Renderer/Window 析构, + // 其后端销毁需要 Device 与 GLFWwindow 存活) + Scope m_ImGuiLayer; bool m_Running = false; f32 m_DeltaTime = 0.0f; diff --git a/Engine/include/Engine/ImGui/ImGuiLayer.h b/Engine/include/Engine/ImGui/ImGuiLayer.h new file mode 100644 index 0000000..7f87b11 --- /dev/null +++ b/Engine/include/Engine/ImGui/ImGuiLayer.h @@ -0,0 +1,64 @@ +// +// Created by atdunbg on 2026/9/22. +// +// ImGuiLayer:ImGui 与 Vulkan/RenderGraph 的桥接层。 +// +// 职责边界(重要): +// - 本层只做"管线级"工作:上下文初始化、每帧 NewFrame、 +// draw data 构建、把 draw data 录进给定的 command buffer。 +// - 本层永远不创建任何 ImGui 窗口/控件(框架不渲染 UI 内容); +// 所有 UI 内容由派生类在 OnRenderOverlay() 里自行绘制。 +// - GPU 提交由 Renderer 的 "ImGui" RG pass 调 RenderDrawData 完成 +// (Forward 之后、Present 之前,loadOp=Load 叠加在场景上)。 +// + +#ifndef HELLOVULKAN_IMGUI_LAYER_H +#define HELLOVULKAN_IMGUI_LAYER_H + +#pragma once + +#include +#include + +namespace Engine { + + class Window; + class Renderer; + class Device; + + class ImGuiLayer { + public: + // 需要 Renderer 就绪后创建(依赖 Device + swapchain 格式) + ImGuiLayer(Window& window, Renderer& renderer); + ~ImGuiLayer(); + + ImGuiLayer(const ImGuiLayer&) = delete; + ImGuiLayer& operator=(const ImGuiLayer&) = delete; + + // ---- 每帧三段式(由 Application::Run 驱动)---- + + // 开始 UI 构建(ImGui::NewFrame)。此后到 BuildDrawData 之间, + // 派生类可任意调用 ImGui::* 绘制控件。 + void NewFrame(); + + // 结束 UI 构建:把本帧的控件调用编译成 draw data(ImGui::Render)。 + void BuildDrawData(); + + // 把 draw data 录入 cmd(RG ImGui pass 内调用)。 + // 无 UI 内容时 draw data 为空,是合法 no-op。 + void RenderDrawData(vk::CommandBuffer cmd); + + bool IsEnabled() const { return m_Enabled; } + void SetEnabled(bool enabled) { m_Enabled = enabled; } + + private: + // 生命周期保证:Application 成员序中 ImGuiLayer 在 Renderer 之后声明 + //(先析构),因此 Device 引用在整个层生命周期内有效 + Device& m_Device; + vk::DescriptorPool m_Pool; + bool m_Enabled = true; + }; + +} // namespace Engine + +#endif //HELLOVULKAN_IMGUI_LAYER_H diff --git a/Engine/include/Engine/Renderer/Renderer.h b/Engine/include/Engine/Renderer/Renderer.h index e4f99f5..affe97c 100644 --- a/Engine/include/Engine/Renderer/Renderer.h +++ b/Engine/include/Engine/Renderer/Renderer.h @@ -30,6 +30,7 @@ namespace Engine { namespace ECS { class Scene; } namespace Assets { class AssetManager; } + class ImGuiLayer; // Renderer 职责(收敛后): // 1. 装配底层模块(Device/Swapchain/Sync/Command/Descriptor/Shader) @@ -52,6 +53,12 @@ namespace Engine { MemoryAllocator& GetAllocator() const { return *m_Allocator; } DescriptorManager& GetDescriptorManager() const { return *m_DescriptorManager; } vk::DescriptorSetLayout GetTextureSetLayout() const { return m_TextureSetLayout; } + vk::Format GetSwapchainFormat() const; // Swapchain 不完整,实现在 cpp + u32 GetSwapchainImageCount() const; + + // 可选叠加层(ImGui):不设置则 RG 里的 ImGui pass 为空转。 + // 生命周期:层必须先于 Renderer 析构(Application 成员序保证)。 + void SetImGuiLayer(ImGuiLayer* layer) { m_ImGuiLayer = layer; } private: void BuildGraph(); // 声明 pass 结构 + 首次 Compile @@ -99,6 +106,9 @@ namespace Engine { Assets::AssetManager* m_BoundAssets = nullptr; glm::mat4 m_LightSpace{1.0f}; // 阴影光源 VP(每帧由 Render 算好) + // 可选叠加层(不拥有;Application 持有) + ImGuiLayer* m_ImGuiLayer = nullptr; + vk::Extent2D m_Extent{}; u32 m_CurrentFrame = 0; }; diff --git a/Engine/src/Core/Application.cpp b/Engine/src/Core/Application.cpp index 994edb3..5b36690 100644 --- a/Engine/src/Core/Application.cpp +++ b/Engine/src/Core/Application.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include @@ -50,6 +51,14 @@ namespace Engine { } } + void Application::EnableImGui() + { + if (m_ImGuiLayer) return; + + m_ImGuiLayer = CreateScope(GetWindow(), *m_Renderer); + m_Renderer->SetImGuiLayer(m_ImGuiLayer.get()); + } + void Application::Run() { ENGINE_INFO("Application starting"); @@ -73,10 +82,21 @@ namespace Engine { // 延迟销毁在渲染前统一 flush,保证渲染遍历时池结构不变 m_Scene->FlushDestroyQueue(); - // 主渲染是框架行为(不可覆盖);叠加层钩子在提交前执行 - m_Renderer->Render(*m_Scene, *m_Assets); + // ---- UI 构建段:OnRenderOverlay 只"声明"控件,不直接碰 GPU ---- + if (m_ImGuiLayer) { + m_ImGuiLayer->NewFrame(); + } + OnRenderOverlay(); + if (m_ImGuiLayer) { + m_ImGuiLayer->BuildDrawData(); + } + + // ---- 主渲染(框架行为):RG 顺序 Forward → ImGui(loadOp=Load) → + // Present,draw data 在 ImGui pass 里才真正提交 GPU ---- + m_Renderer->Render(*m_Scene, *m_Assets); + ++m_FrameCount; } diff --git a/Engine/src/ImGui/ImGuiLayer.cpp b/Engine/src/ImGui/ImGuiLayer.cpp new file mode 100644 index 0000000..816ce74 --- /dev/null +++ b/Engine/src/ImGui/ImGuiLayer.cpp @@ -0,0 +1,128 @@ +// +// Created by atdunbg on 2026/9/22. +// + +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace Engine { + + namespace { + void CheckVkResult(VkResult err) + { + if (err != VK_SUCCESS) { + ENGINE_ERROR("ImGui Vulkan backend error: VkResult={}", static_cast(err)); + } + } + } + + ImGuiLayer::ImGuiLayer(Window& window, Renderer& renderer) + : m_Device(renderer.GetDevice()) + { + Device& device = renderer.GetDevice(); + + // 1. 描述符池(ImGui 自用:字体贴图 + 用户贴图) + std::array poolSizes = {{ + { vk::DescriptorType::eSampler, 1000 }, + { vk::DescriptorType::eCombinedImageSampler, 1000 }, + { vk::DescriptorType::eSampledImage, 1000 }, + { vk::DescriptorType::eStorageImage, 1000 }, + { vk::DescriptorType::eUniformTexelBuffer, 1000 }, + { vk::DescriptorType::eStorageTexelBuffer, 1000 }, + { vk::DescriptorType::eUniformBuffer, 1000 }, + { vk::DescriptorType::eStorageBuffer, 1000 }, + { vk::DescriptorType::eUniformBufferDynamic, 1000 }, + { vk::DescriptorType::eStorageBufferDynamic, 1000 }, + { vk::DescriptorType::eInputAttachment, 1000 }, + }}; + vk::DescriptorPoolCreateInfo poolInfo; + poolInfo.setFlags(vk::DescriptorPoolCreateFlagBits::eFreeDescriptorSet) + .setMaxSets(1000) + .setPoolSizes(poolSizes); + m_Pool = device.GetDevice().createDescriptorPool(poolInfo); + + // 2. ImGui 上下文 + 平台/渲染后端 + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + ImGuiIO& io = ImGui::GetIO(); + io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; + io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; // docking 分支 + io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable; // 多视口 + + ImGui_ImplGlfw_InitForVulkan(window.GetHandle(), true); + + ImGui_ImplVulkan_InitInfo info = {}; + info.ApiVersion = VK_API_VERSION_1_4; + info.Instance = device.GetInstance(); + info.PhysicalDevice = device.GetPhysicalDevice(); + info.Device = device.GetDevice(); + info.QueueFamily = device.GetGraphicsFamily(); + info.Queue = device.GetGraphicsQueue(); + info.DescriptorPool = m_Pool; + info.MinImageCount = 2; // kMaxFramesInFlight + info.ImageCount = renderer.GetSwapchainImageCount(); + info.CheckVkResultFn = CheckVkResult; + + // 与引擎共存的关键:不用 RenderPass,走 dynamic rendering, + // 颜色格式 = swapchain 格式(引擎无 vk::Pipeline,全动态状态) + info.UseDynamicRendering = true; + const vk::Format colorFormat = renderer.GetSwapchainFormat(); + + VkPipelineRenderingCreateInfoKHR pri{}; + pri.sType = VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO_KHR; + pri.colorAttachmentCount = 1; + pri.pColorAttachmentFormats = + reinterpret_cast(&colorFormat); + info.PipelineInfoMain.PipelineRenderingCreateInfo = pri; + // 多视口子窗口同格式 + info.PipelineInfoForViewports.PipelineRenderingCreateInfo = pri; + + ImGui_ImplVulkan_Init(&info); + + ENGINE_INFO("ImGuiLayer initialized (docking + viewports, dynamic rendering)"); + } + + ImGuiLayer::~ImGuiLayer() + { + // 后端销毁自己的管线/字体资源前先等 GPU 空闲(层先于 Renderer 析构) + m_Device.GetDevice().waitIdle(); + + ImGui_ImplVulkan_Shutdown(); + ImGui_ImplGlfw_Shutdown(); + ImGui::DestroyContext(); + + m_Device.GetDevice().destroyDescriptorPool(m_Pool); + + ENGINE_INFO("ImGuiLayer shut down"); + } + + void ImGuiLayer::NewFrame() + { + ImGui_ImplVulkan_NewFrame(); + ImGui_ImplGlfw_NewFrame(); + ImGui::NewFrame(); + } + + void ImGuiLayer::BuildDrawData() + { + ImGui::Render(); + } + + void ImGuiLayer::RenderDrawData(vk::CommandBuffer cmd) + { + if (!m_Enabled) return; + + ImDrawData* drawData = ImGui::GetDrawData(); + if (drawData && drawData->CmdListsCount > 0) { + ImGui_ImplVulkan_RenderDrawData(drawData, cmd); + } + } + +} // namespace Engine diff --git a/Engine/src/Renderer/Renderer.cpp b/Engine/src/Renderer/Renderer.cpp index 33c4600..7894833 100644 --- a/Engine/src/Renderer/Renderer.cpp +++ b/Engine/src/Renderer/Renderer.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include @@ -208,6 +209,16 @@ namespace Engine vkDevice.destroyDescriptorSetLayout(m_ShadowSetLayout); } + vk::Format Renderer::GetSwapchainFormat() const + { + return m_Swapchain->GetFormat(); + } + + u32 Renderer::GetSwapchainImageCount() const + { + return m_Swapchain->GetImageCount(); + } + void Renderer::BuildGraph() { m_RenderGraph = CreateScope( @@ -455,6 +466,35 @@ namespace Engine .Read(m_ShadowMap, RG::ResourceUse::ShaderRead, vk::ShaderStageFlagBits::eFragment); + // ---- ImGui pass:叠加层(loadOp=Load,不清屏)---- + // 框架只负责把 draw data 录进 cmd;UI 内容全部由派生类 + // 在 OnRenderOverlay() 里构建,框架自身不画任何 ImGui 控件。 + // 未启用叠加层时空转(begin/end 一次空 rendering region)。 + m_RenderGraph->AddPass("ImGui", + [this](RG::RenderGraphContext& ctx) { + const vk::CommandBuffer cmd = ctx.cmd; + + vk::RenderingAttachmentInfo colorAttachment; + colorAttachment.setImageView(ctx.View(m_Backbuffer)) + .setImageLayout(vk::ImageLayout::eColorAttachmentOptimal) + .setLoadOp(vk::AttachmentLoadOp::eLoad) // 关键:叠加 + .setStoreOp(vk::AttachmentStoreOp::eStore); + + vk::RenderingInfo renderingInfo; + renderingInfo.setRenderArea({{0, 0}, ctx.extent}) + .setLayerCount(1) + .setColorAttachments(colorAttachment); + + cmd.beginRendering(renderingInfo); + + if (m_ImGuiLayer) { + m_ImGuiLayer->RenderDrawData(cmd); + } + + cmd.endRendering(); + }) + .Write(m_Backbuffer, RG::ResourceUse::ColorAttachment); + // ---- Present pass:transition 到 ePresentSrcKHR ---- m_RenderGraph->AddPass("Present", [](RG::RenderGraphContext&) { diff --git a/Lib/imgui b/Lib/imgui new file mode 160000 index 0000000..3bae66c --- /dev/null +++ b/Lib/imgui @@ -0,0 +1 @@ +Subproject commit 3bae66c735670619baf51391eba7f3d90a25d125