From 952ba8135d52cc519809ff06d79271ce374e21ed Mon Sep 17 00:00:00 2001 From: atdunbg Date: Tue, 22 Sep 2026 19:00:56 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20HDR=20=E7=AE=A1=E7=BA=BF=20+=20Tonemap?= =?UTF-8?q?=20pass=EF=BC=8C=E6=98=BE=E7=A4=BA=E5=8F=98=E6=8D=A2=E6=98=BE?= =?UTF-8?q?=E5=BC=8F=E5=BB=BA=E6=A8=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 管线:Shadow → Forward(写 HDRColor R16G16B16A16F) → Tonemap → ImGui(loadOp=Load) → Present。对齐 UE/Unity 结构:场景线性值 进 HDR 离屏图,Tonemap(全屏三角,exposure push + gamma 编码) 显式完成显示变换,Backbuffer 只接收显示空间颜色。 - Swapchain 改 B8G8R8A8Unorm:Srgb 附件会硬件二次提亮,UI 泛白 (#4296FA→#8CCBFC);Unorm 下写什么显示什么,编码责任归 Tonemap - Shader object push range 规则落地:程序级声明——同一 pass 的 全部 shader EXT range 定义必须一致(VUID-08878),layout 同样 按 vertex|fragment 声明,push 调用点覆盖全部声明 stage(VUID-01796) - Tonemap 装配:Set0 = HDR 图(per-frame-slot 描述符 + GetPhysicalView), clamp 占位待 ACES 替换,bloom/FXAA 等后处理在 HDR 域插 pass - CMake 加 imgui_demo.cpp(ShowDemoWindow 链接) - 修复:m_TonemapDescriptorSets 未 resize 即下标写入(启动即崩) 验证:5 pass 全活,validation 0 error 0 warning,UI 原生色彩无泛白 --- App/main.cpp | 4 + Engine/CMakeLists.txt | 1 + Engine/include/Engine/Renderer/Renderer.h | 10 ++ .../include/Engine/Vulkan/DescriptorManager.h | 5 +- Engine/src/Renderer/Renderer.cpp | 162 +++++++++++++++++- Engine/src/Vulkan/DescriptorManager.cpp | 9 +- Engine/src/Vulkan/ShaderManager.cpp | 7 +- Engine/src/Vulkan/Swapchain.cpp | 6 +- shaders/tonemap.frag | 23 +++ shaders/tonemap.vert | 15 ++ 10 files changed, 231 insertions(+), 11 deletions(-) create mode 100644 shaders/tonemap.frag create mode 100644 shaders/tonemap.vert diff --git a/App/main.cpp b/App/main.cpp index a23f10f..e18a747 100644 --- a/App/main.cpp +++ b/App/main.cpp @@ -23,6 +23,8 @@ #include +#include "Engine/ImGui/ImGuiLayer.h" + using namespace Engine; using namespace Engine::ECS; @@ -114,6 +116,8 @@ protected: { if (!m_OverlayVisible) return; + ImGui::ShowDemoWindow(); + ImGui::Begin("Scene Debug"); // 阴影光源方向:拖动滑条实时看影子变化 diff --git a/Engine/CMakeLists.txt b/Engine/CMakeLists.txt index 592c1ed..83ee939 100644 --- a/Engine/CMakeLists.txt +++ b/Engine/CMakeLists.txt @@ -9,6 +9,7 @@ set(IMGUI_SOURCES ${IMGUI_DIR}/imgui_draw.cpp ${IMGUI_DIR}/imgui_tables.cpp ${IMGUI_DIR}/imgui_widgets.cpp + ${IMGUI_DIR}/imgui_demo.cpp # ShowDemoWindow 的实现(调试期保留) ${IMGUI_DIR}/backends/imgui_impl_glfw.cpp ${IMGUI_DIR}/backends/imgui_impl_vulkan.cpp ) diff --git a/Engine/include/Engine/Renderer/Renderer.h b/Engine/include/Engine/Renderer/Renderer.h index affe97c..3c1eaae 100644 --- a/Engine/include/Engine/Renderer/Renderer.h +++ b/Engine/include/Engine/Renderer/Renderer.h @@ -64,6 +64,7 @@ namespace Engine { void BuildGraph(); // 声明 pass 结构 + 首次 Compile void HandleResize(); void UpdateShadowDescriptors(); + void UpdateTonemapDescriptors(); Window& m_Window; @@ -96,10 +97,19 @@ namespace Engine { vk::ShaderEXT m_ShadowVertShader{}; vk::ShaderEXT m_ShadowFragShader{}; + // Tonemap pass 装配:Set0 = HDR 颜色图,push = {exposure}(fragment) + vk::PipelineLayout m_TonemapPipelineLayout; + vk::DescriptorSetLayout m_TonemapSetLayout; + std::vector m_TonemapDescriptorSets; // 每帧槽位一个 + vk::Sampler m_TonemapSampler; + vk::ShaderEXT m_TonemapVertShader{}; + vk::ShaderEXT m_TonemapFragShader{}; + // ---- RenderGraph 资源句柄 ---- RG::TextureHandle m_Backbuffer{}; RG::TextureHandle m_Depth{}; RG::TextureHandle m_ShadowMap{}; + RG::TextureHandle m_HDRColor{}; // Forward 输出(线性 HDR),Tonemap 归一 // ---- 当帧绑定(Render() 期间有效,供 pass 回调读取)---- ECS::Scene* m_BoundScene = nullptr; diff --git a/Engine/include/Engine/Vulkan/DescriptorManager.h b/Engine/include/Engine/Vulkan/DescriptorManager.h index e699c70..d016476 100644 --- a/Engine/include/Engine/Vulkan/DescriptorManager.h +++ b/Engine/include/Engine/Vulkan/DescriptorManager.h @@ -42,7 +42,10 @@ namespace Engine { vk::PipelineLayout CreatePipelineLayout(vk::DescriptorSetLayout layout, u32 pushConstantSize); - // 多 set layout 的新接口 + // 多 set layout 的新接口。 + // push constant range 按程序级 vertex|fragment 声明(与 + // ShaderManager 的 shader EXT 声明一致,VUID-08878); + // push 调用点必须带同样的双 stage(VUID-01796) vk::PipelineLayout CreatePipelineLayout( const std::vector& layouts, u32 pushConstantSize); diff --git a/Engine/src/Renderer/Renderer.cpp b/Engine/src/Renderer/Renderer.cpp index 7894833..36f6f22 100644 --- a/Engine/src/Renderer/Renderer.cpp +++ b/Engine/src/Renderer/Renderer.cpp @@ -147,6 +147,42 @@ namespace Engine .setCompareEnable(VK_FALSE); m_ShadowSampler = m_Device->GetDevice().createSampler(shadowSamplerInfo); + // 7.5 Tonemap pass 装配:Set0 = HDR 颜色图,push = {exposure} + std::vector tonemapBindings = { + {0, vk::DescriptorType::eCombinedImageSampler, + vk::ShaderStageFlagBits::eFragment}, + }; + m_TonemapSetLayout = m_DescriptorManager->CreateLayout(tonemapBindings); + + m_TonemapPipelineLayout = m_DescriptorManager->CreatePipelineLayout( + std::vector{m_TonemapSetLayout}, + sizeof(f32)); + + m_TonemapSampler = m_Device->GetDevice().createSampler( + vk::SamplerCreateInfo{} + .setMagFilter(vk::Filter::eLinear) + .setMinFilter(vk::Filter::eLinear) + .setMipmapMode(vk::SamplerMipmapMode::eNearest) + .setAddressModeU(vk::SamplerAddressMode::eClampToEdge) + .setAddressModeV(vk::SamplerAddressMode::eClampToEdge) + .setAddressModeW(vk::SamplerAddressMode::eClampToEdge)); + + auto tonemapVert = m_ShaderManager->CreateFromFile( + shaderDir + "tonemap.vert.spv", + vk::ShaderStageFlagBits::eVertex, + std::vector{m_TonemapSetLayout}, sizeof(f32)); + auto tonemapFrag = m_ShaderManager->CreateFromFile( + shaderDir + "tonemap.frag.spv", + vk::ShaderStageFlagBits::eFragment, + std::vector{m_TonemapSetLayout}, sizeof(f32)); + + m_TonemapVertShader = tonemapVert.Handle; + m_TonemapFragShader = tonemapFrag.Handle; + + if (!m_TonemapVertShader || !m_TonemapFragShader) { + ENGINE_FATAL("Failed to create tonemap shader objects"); + } + // 8. Swapchain(只管呈现;深度图由 RenderGraph 创建) m_Swapchain = CreateScope(*m_Device, *m_Allocator, m_Window); m_Extent = m_Swapchain->GetExtent(); @@ -162,6 +198,7 @@ namespace Engine m_UBODescriptorSets.resize(kMaxFramesInFlight); m_ShadowDescriptorSets.resize(kMaxFramesInFlight); + m_TonemapDescriptorSets.resize(kMaxFramesInFlight); for (u32 slot = 0; slot < kMaxFramesInFlight; ++slot) { m_UBODescriptorSets[slot] = m_DescriptorManager->AllocateDescriptorSet(m_UBOSetLayout); @@ -171,13 +208,17 @@ namespace Engine m_ShadowDescriptorSets[slot] = m_DescriptorManager->AllocateDescriptorSet(m_ShadowSetLayout); + + m_TonemapDescriptorSets[slot] = + m_DescriptorManager->AllocateDescriptorSet(m_TonemapSetLayout); } // 11. RenderGraph:声明 + 编译(场景在 Render(Scene&) 时按帧绑定) BuildGraph(); - // Compile 已分配物理镜像:把各槽位的阴影图 view 绑进描述符 + // Compile 已分配物理镜像:把各槽位的阴影图/HDR view 绑进描述符 UpdateShadowDescriptors(); + UpdateTonemapDescriptors(); ENGINE_INFO("Renderer initialized"); } @@ -200,13 +241,22 @@ namespace Engine if (m_ShadowFragShader) { vkDevice.destroyShaderEXT(m_ShadowFragShader); } + if (m_TonemapVertShader) { + vkDevice.destroyShaderEXT(m_TonemapVertShader); + } + if (m_TonemapFragShader) { + vkDevice.destroyShaderEXT(m_TonemapFragShader); + } vkDevice.destroySampler(m_ShadowSampler); + vkDevice.destroySampler(m_TonemapSampler); vkDevice.destroyPipelineLayout(m_PipelineLayout); vkDevice.destroyPipelineLayout(m_ShadowPipelineLayout); + vkDevice.destroyPipelineLayout(m_TonemapPipelineLayout); vkDevice.destroyDescriptorSetLayout(m_UBOSetLayout); vkDevice.destroyDescriptorSetLayout(m_TextureSetLayout); vkDevice.destroyDescriptorSetLayout(m_ShadowSetLayout); + vkDevice.destroyDescriptorSetLayout(m_TonemapSetLayout); } vk::Format Renderer::GetSwapchainFormat() const @@ -243,6 +293,13 @@ namespace Engine "ShadowMap", RG::TextureDesc{vk::Format::eD32Sfloat, kShadowMapSize, kShadowMapSize}); + // HDR 颜色图:Forward 的输出(线性光照值),Tonemap 显式完成 + // 显示变换后写入 Backbuffer。对齐 UE/Unity 管线: + // SceneColor(HDR) → tonemapper → UI(UI 在 tonemap 之后叠加) + m_HDRColor = m_RenderGraph->CreateTexture( + "HDRColor", + RG::TextureDesc{vk::Format::eR16G16B16A16Sfloat}); + // ---- Shadow pass:光源视角写深度(所有渲染体都写入,不做实体级开关—— // castShadow 应由材质/实体数据驱动剔除,而非 pass 逻辑特判)---- m_RenderGraph->AddPass("Shadow", @@ -321,8 +378,10 @@ namespace Engine if (!model) return; const ShadowPush push{m_LightSpace, transform.world}; + // stage 必须覆盖 layout range 声明的全部 stage(VUID-01796) cmd.pushConstants(m_ShadowPipelineLayout, - vk::ShaderStageFlagBits::eVertex, + vk::ShaderStageFlagBits::eVertex | + vk::ShaderStageFlagBits::eFragment, 0, kShadowPushSize, &push); for (auto& mesh : model->GetMeshes()) { @@ -345,7 +404,7 @@ namespace Engine std::array{0.1f, 0.1f, 0.1f, 1.0f}); vk::RenderingAttachmentInfo colorAttachment; - colorAttachment.setImageView(ctx.View(m_Backbuffer)) + colorAttachment.setImageView(ctx.View(m_HDRColor)) .setImageLayout(vk::ImageLayout::eColorAttachmentOptimal) .setLoadOp(vk::AttachmentLoadOp::eClear) .setStoreOp(vk::AttachmentStoreOp::eStore) @@ -447,8 +506,10 @@ namespace Engine Model* model = m_BoundAssets->GetModel(meshRenderer.model); if (!model) return; + // stage 必须覆盖 layout range 声明的全部 stage(VUID-01796) cmd.pushConstants(m_PipelineLayout, - vk::ShaderStageFlagBits::eVertex, + vk::ShaderStageFlagBits::eVertex | + vk::ShaderStageFlagBits::eFragment, 0, sizeof(glm::mat4), &transform.world); for (auto& mesh : model->GetMeshes()) { @@ -461,11 +522,89 @@ namespace Engine cmd.endRendering(); }) - .Write(m_Backbuffer, RG::ResourceUse::ColorAttachment) + .Write(m_HDRColor, RG::ResourceUse::ColorAttachment) .Write(m_Depth, RG::ResourceUse::DepthAttachment) .Read(m_ShadowMap, RG::ResourceUse::ShaderRead, vk::ShaderStageFlagBits::eFragment); + // ---- Tonemap pass:HDR → 显示空间,全屏三角无顶点缓冲 ---- + // 显示变换在此显式建模(此前隐式依赖 Srgb 附件,是设计欠账)。 + // Backbuffer 从此只接收"显示空间"颜色:tonemap 直接写、ImGui 叠加。 + m_RenderGraph->AddPass("Tonemap", + [this](RG::RenderGraphContext& ctx) { + const vk::CommandBuffer cmd = ctx.cmd; + + vk::ClearValue clearValue; + clearValue.color = vk::ClearColorValue( + std::array{0.0f, 0.0f, 0.0f, 1.0f}); + + vk::RenderingAttachmentInfo colorAttachment; + colorAttachment.setImageView(ctx.View(m_Backbuffer)) + .setImageLayout(vk::ImageLayout::eColorAttachmentOptimal) + .setLoadOp(vk::AttachmentLoadOp::eClear) + .setStoreOp(vk::AttachmentStoreOp::eStore) + .setClearValue(clearValue); + + vk::RenderingInfo renderingInfo; + renderingInfo.setRenderArea({{0, 0}, ctx.extent}) + .setLayerCount(1) + .setColorAttachments(colorAttachment); + + cmd.beginRendering(renderingInfo); + + std::array stages = { + vk::ShaderStageFlagBits::eVertex, + vk::ShaderStageFlagBits::eFragment + }; + std::array shaders = { + m_TonemapVertShader, m_TonemapFragShader}; + cmd.bindShadersEXT(stages, shaders); + + cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, + m_TonemapPipelineLayout, 0, + m_TonemapDescriptorSets[ctx.frame.frameSlot], {}); + + // ---- 动态状态(无深度附件、无顶点输入)---- + cmd.setRasterizerDiscardEnable(VK_FALSE); + cmd.setDepthTestEnable(VK_FALSE); + cmd.setDepthWriteEnable(VK_FALSE); + cmd.setDepthBoundsTestEnable(VK_FALSE); + cmd.setStencilTestEnable(VK_FALSE); + cmd.setDepthBiasEnable(VK_FALSE); + cmd.setPolygonModeEXT(vk::PolygonMode::eFill); + cmd.setCullMode(vk::CullModeFlagBits::eNone); + cmd.setFrontFace(vk::FrontFace::eCounterClockwise); + cmd.setPrimitiveTopology(vk::PrimitiveTopology::eTriangleList); + cmd.setPrimitiveRestartEnable(VK_FALSE); + cmd.setVertexInputEXT( + std::array{}, + std::array{}); + cmd.setRasterizationSamplesEXT(vk::SampleCountFlagBits::e1); + vk::SampleMask sampleMask = 0xFFFFFFFF; + cmd.setSampleMaskEXT(vk::SampleCountFlagBits::e1, sampleMask); + cmd.setAlphaToCoverageEnableEXT(VK_FALSE); + cmd.setAlphaToOneEnableEXT(VK_FALSE); + + vk::Viewport viewport(0.0f, 0.0f, + static_cast(ctx.extent.width), + static_cast(ctx.extent.height), + 0.0f, 1.0f); + cmd.setViewportWithCount(viewport); + cmd.setScissorWithCount(vk::Rect2D({0, 0}, ctx.extent)); + + const f32 exposure = 1.0f; // TODO: 接 ImGui 曝光滑条 + cmd.pushConstants(m_TonemapPipelineLayout, + vk::ShaderStageFlagBits::eVertex | + vk::ShaderStageFlagBits::eFragment, + 0, sizeof(f32), &exposure); + + cmd.draw(3, 1, 0, 0); + cmd.endRendering(); + }) + .Write(m_Backbuffer, RG::ResourceUse::ColorAttachment) + .Read(m_HDRColor, RG::ResourceUse::ShaderRead, + vk::ShaderStageFlagBits::eFragment); + // ---- ImGui pass:叠加层(loadOp=Load,不清屏)---- // 框架只负责把 draw data 录进 cmd;UI 内容全部由派生类 // 在 OnRenderOverlay() 里构建,框架自身不画任何 ImGui 控件。 @@ -520,8 +659,9 @@ namespace Engine // RG 重编译:物理镜像按新尺寸轮换 m_RenderGraph->SetTargetExtent(m_Extent); - // 重编译后物理镜像可能变化,阴影描述符重新绑定 + // 重编译后物理镜像可能变化,描述符重新绑定 UpdateShadowDescriptors(); + UpdateTonemapDescriptors(); } void Renderer::UpdateShadowDescriptors() @@ -535,6 +675,16 @@ namespace Engine } } + void Renderer::UpdateTonemapDescriptors() + { + for (u32 slot = 0; slot < kMaxFramesInFlight; ++slot) { + m_DescriptorManager->UpdateTexture( + m_TonemapDescriptorSets[slot], 0, + m_TonemapSampler, + m_RenderGraph->GetPhysicalView(m_HDRColor, slot)); + } + } + void Renderer::Render(ECS::Scene& scene, Assets::AssetManager& assets) { m_BoundScene = &scene; diff --git a/Engine/src/Vulkan/DescriptorManager.cpp b/Engine/src/Vulkan/DescriptorManager.cpp index 713d9a7..a7005e8 100644 --- a/Engine/src/Vulkan/DescriptorManager.cpp +++ b/Engine/src/Vulkan/DescriptorManager.cpp @@ -115,14 +115,19 @@ namespace Engine return m_Device.GetDevice().createPipelineLayout(info); } - vk::PipelineLayout DescriptorManager::CreatePipelineLayout(const std::vector& layouts,u32 pushConstantSize) + vk::PipelineLayout DescriptorManager::CreatePipelineLayout(const std::vector& layouts, + u32 pushConstantSize) { vk::PipelineLayoutCreateInfo info; info.setSetLayouts(layouts); std::array pushRanges; if (pushConstantSize > 0) { - pushRanges[0].setStageFlags(vk::ShaderStageFlagBits::eVertex) + // 与 ShaderManager 一致:程序级 vertex|fragment 声明。 + // 注意 vkCmdPushConstants 的 stageFlags 必须覆盖 range 的 + // 全部 stage(VUID-01796),因此 push 调用点也要带双 stage + pushRanges[0].setStageFlags(vk::ShaderStageFlagBits::eVertex | + vk::ShaderStageFlagBits::eFragment) .setOffset(0) .setSize(pushConstantSize); info.setPushConstantRanges(pushRanges); diff --git a/Engine/src/Vulkan/ShaderManager.cpp b/Engine/src/Vulkan/ShaderManager.cpp index 454f40b..7f8627b 100644 --- a/Engine/src/Vulkan/ShaderManager.cpp +++ b/Engine/src/Vulkan/ShaderManager.cpp @@ -57,7 +57,12 @@ ShaderObject ShaderManager::CreateFromSPIRV(const std::vector& spirv, std::array pushRanges; if (pushConstantSize > 0) { - pushRanges[0].setStageFlags(vk::ShaderStageFlagBits::eVertex) + // push range 是"程序级"声明:shader object 要求同一程序绑定的 + // 全部 shader(vert+frag)push range 定义完全一致(VUID-08878), + // 因此统一按 vertex|fragment 声明;shader 实际读取哪些字节由 + // GLSL 的 push_constant block 决定,声明范围只是能力上限 + pushRanges[0].setStageFlags(vk::ShaderStageFlagBits::eVertex | + vk::ShaderStageFlagBits::eFragment) .setOffset(0) .setSize(pushConstantSize); createInfo.setPushConstantRanges(pushRanges); diff --git a/Engine/src/Vulkan/Swapchain.cpp b/Engine/src/Vulkan/Swapchain.cpp index 933bb47..38e1291 100644 --- a/Engine/src/Vulkan/Swapchain.cpp +++ b/Engine/src/Vulkan/Swapchain.cpp @@ -134,9 +134,13 @@ namespace Engine vk::SurfaceFormatKHR Swapchain::ChooseSurfaceFormat(const std::vector& formats) { + // 用 Unorm 而非 Srgb:当前管线(场景光照 + ImGui)都是把 + // "显示空间"颜色直接写进 framebuffer,若用 Srgb 附件硬件会再 + // 做一次 linear→sRGB 提亮 → 整体泛白(ImGui 原生蓝 #4296FA + // 会被洗成 #8CCBFC)。等上 HDR/tonemap 管线后再切回 Srgb。 for (const auto& f : formats) { - if (f.format == vk::Format::eB8G8R8A8Srgb && + if (f.format == vk::Format::eB8G8R8A8Unorm && f.colorSpace == vk::ColorSpaceKHR::eSrgbNonlinear) { return f; diff --git a/shaders/tonemap.frag b/shaders/tonemap.frag new file mode 100644 index 0000000..cd581e8 --- /dev/null +++ b/shaders/tonemap.frag @@ -0,0 +1,23 @@ +#version 450 + +// Tonemap pass:HDR 线性颜色 → 显示空间。 +// 这是显示变换的显式建模(对齐 UE/Unity:HDR SceneColor → tonemapper → UI)。 +// 当前 = 曝光 + gamma 编码(与旧 Srgb 附件行为等价,观感不变); +// ACES/Reinhard 色调曲线、bloom 等后处理在此 pass 基础上扩展。 + +layout(location = 0) in vec2 fragUV; +layout(location = 0) out vec4 outColor; + +layout(set = 0, binding = 0) uniform sampler2D hdrImage; + +layout(push_constant) uniform PushConstants { + float exposure; // 曝光系数(后续接 ImGui) +} pc; + +void main() { + vec3 hdr = texture(hdrImage, fragUV).rgb * pc.exposure; + + // 显示编码:linear → sRGB(gamma 2.2 近似)。 + // TODO: ACES 拟合曲线接入点(HDR 泛光/色调映射时替换此行) + outColor = vec4(pow(clamp(hdr, 0.0, 1.0), vec3(1.0 / 2.2)), 1.0); +} diff --git a/shaders/tonemap.vert b/shaders/tonemap.vert new file mode 100644 index 0000000..85540e4 --- /dev/null +++ b/shaders/tonemap.vert @@ -0,0 +1,15 @@ +#version 450 + +// 全屏三角形:无顶点缓冲,从 gl_VertexIndex 派生 UV。 +// 0 → (-1,3)? 经典三角覆盖整个屏幕: +// idx0:(-1,-1) idx1:(3,-1) idx2:(-1,3),UV 相应 [0..2], +// 光栅化插值后屏幕内 UV 恰好落在 [0,1]。 +layout(location = 0) out vec2 fragUV; + +void main() { + vec2 p = vec2( + float((gl_VertexIndex << 1) & 2), + float(gl_VertexIndex & 2)); + fragUV = p; + gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0); +}