From 908e6881786ea8714032474002f8fb677fe1dfd6 Mon Sep 17 00:00:00 2001 From: atdunbg Date: Tue, 22 Sep 2026 19:18:51 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20ACES=20tonemap=20+=20Bloom=20=E5=90=8E?= =?UTF-8?q?=E5=A4=84=E7=90=86=E9=93=BE=EF=BC=88Extract/BlurH/BlurV=20?= =?UTF-8?q?=E5=8D=8A=E5=88=86=E8=BE=A8=E7=8E=87=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ACES(Narkowicz) 经 push 开关启用,exposure 接 ImGui 滑条 - RG: TextureDesc 支持 sizeScale 相对尺寸;per-pass 渲染尺寸解析 (半分辨率 pass 的 viewport 由此决定) - RG Toposort 重写:RAW+WAR+WAW 按声明序逐资源建边,支持多写者 ping-pong 资源(旧逻辑对 reader 连接所有 writer,ping-pong 成环) - GetPhysicalView 缺物理镜像时 throw(ENGINE_FATAL 只记日志不中止) - 全屏 pass 通用 16B push + 公共录制 lambda,Extract/Blur/Tonemap 复用 --- App/main.cpp | 24 +++ .../include/Engine/RenderGraph/RenderGraph.h | 4 +- Engine/include/Engine/Renderer/Renderer.h | 33 +++- Engine/src/RenderGraph/RenderGraph.cpp | 94 ++++++---- Engine/src/Renderer/Renderer.cpp | 169 +++++++++++++++--- shaders/bloom_blur.frag | 29 +++ shaders/bloom_extract.frag | 25 +++ shaders/tonemap.frag | 33 +++- shaders/tonemap.vert | 8 + 9 files changed, 352 insertions(+), 67 deletions(-) create mode 100644 shaders/bloom_blur.frag create mode 100644 shaders/bloom_extract.frag diff --git a/App/main.cpp b/App/main.cpp index e18a747..2c57146 100644 --- a/App/main.cpp +++ b/App/main.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include @@ -130,6 +131,29 @@ protected: } } + // Tonemap + Bloom 参数(HDR 后处理输出端) + auto& renderer = GetRenderer(); + ImGui::Separator(); + ImGui::Text("Tonemap"); + f32 exposure = renderer.GetExposure(); + if (ImGui::SliderFloat("exposure", &exposure, 0.1f, 4.0f)) { + renderer.SetExposure(exposure); + } + bool aces = renderer.GetUseACES(); + if (ImGui::Checkbox("ACES", &aces)) { + renderer.SetUseACES(aces); + } + + ImGui::Text("Bloom"); + f32 threshold = renderer.GetBloomThreshold(); + if (ImGui::SliderFloat("threshold", &threshold, 0.0f, 2.0f)) { + renderer.SetBloomThreshold(threshold); + } + f32 strength = renderer.GetBloomStrength(); + if (ImGui::SliderFloat("strength", &strength, 0.0f, 2.0f)) { + renderer.SetBloomStrength(strength); + } + ImGui::Separator(); ImGui::Text("fps: %.1f frame: %llu", 1.0f / GetDeltaTime(), diff --git a/Engine/include/Engine/RenderGraph/RenderGraph.h b/Engine/include/Engine/RenderGraph/RenderGraph.h index b8834ff..8af9577 100644 --- a/Engine/include/Engine/RenderGraph/RenderGraph.h +++ b/Engine/include/Engine/RenderGraph/RenderGraph.h @@ -70,8 +70,9 @@ namespace Engine::RG { struct TextureDesc { vk::Format format = vk::Format::eUndefined; - u32 width = 0; // 0 = 跟随目标(swapchain)尺寸 + u32 width = 0; // 0 = 跟随目标(swapchain)尺寸 × sizeScale u32 height = 0; + f32 sizeScale = 1.0f; // 相对尺寸(后处理降采样用,如 bloom 0.5) }; // ---------- 物理镜像 ---------- @@ -154,6 +155,7 @@ namespace Engine::RG { ExecuteFn exec; bool sideEffect = false; // 无读写但必须执行(compute 写 buffer 等) bool culled = false; + vk::Extent2D extent{}; // 本 pass 的渲染尺寸(写入附件的最大分辨率,Compile 期解析) // Compile 期预算好的 barrier:每个资源两个变体 // steady = 该槽位已被使用过(从 resting 状态出发) diff --git a/Engine/include/Engine/Renderer/Renderer.h b/Engine/include/Engine/Renderer/Renderer.h index 3c1eaae..1f26ab0 100644 --- a/Engine/include/Engine/Renderer/Renderer.h +++ b/Engine/include/Engine/Renderer/Renderer.h @@ -64,7 +64,7 @@ namespace Engine { void BuildGraph(); // 声明 pass 结构 + 首次 Compile void HandleResize(); void UpdateShadowDescriptors(); - void UpdateTonemapDescriptors(); + void UpdateFullscreenDescriptors(); Window& m_Window; @@ -97,7 +97,8 @@ namespace Engine { vk::ShaderEXT m_ShadowVertShader{}; vk::ShaderEXT m_ShadowFragShader{}; - // Tonemap pass 装配:Set0 = HDR 颜色图,push = {exposure}(fragment) + // Tonemap pass 装配:Set0 = {HDR 颜色图, Bloom 图},push = 16B + //(全屏 pass 通用:exposure/aces/bloomStrength/pad) vk::PipelineLayout m_TonemapPipelineLayout; vk::DescriptorSetLayout m_TonemapSetLayout; std::vector m_TonemapDescriptorSets; // 每帧槽位一个 @@ -105,11 +106,21 @@ namespace Engine { vk::ShaderEXT m_TonemapVertShader{}; vk::ShaderEXT m_TonemapFragShader{}; + // Bloom:Extract(HDR→A) → BlurH(A→B) → BlurV(B→A),半分辨率 + // 与 Tonemap 共用 set layout/pipeline layout/vertex shader + std::vector m_BloomExtractSets; // 每帧槽位 + std::vector m_BloomBlurHSets; + std::vector m_BloomBlurVSets; + vk::ShaderEXT m_BloomExtractFrag{}; + vk::ShaderEXT m_BloomBlurFrag{}; + // ---- RenderGraph 资源句柄 ---- RG::TextureHandle m_Backbuffer{}; RG::TextureHandle m_Depth{}; RG::TextureHandle m_ShadowMap{}; RG::TextureHandle m_HDRColor{}; // Forward 输出(线性 HDR),Tonemap 归一 + RG::TextureHandle m_BloomA{}; // 半分辨率:提取 + 垂直模糊结果 + RG::TextureHandle m_BloomB{}; // 半分辨率:水平模糊中间结果 // ---- 当帧绑定(Render() 期间有效,供 pass 回调读取)---- ECS::Scene* m_BoundScene = nullptr; @@ -119,6 +130,24 @@ namespace Engine { // 可选叠加层(不拥有;Application 持有) ImGuiLayer* m_ImGuiLayer = nullptr; + // ---- Tonemap/Bloom 参数(ImGui 可调)---- + f32 m_Exposure = 1.0f; + bool m_UseACES = true; + f32 m_BloomThreshold = 1.0f; // HDR 域亮度阈值 + f32 m_BloomStrength = 0.8f; // 叠加强度 + + public: + f32 GetExposure() const { return m_Exposure; } + void SetExposure(f32 e) { m_Exposure = e; } + bool GetUseACES() const { return m_UseACES; } + void SetUseACES(bool b) { m_UseACES = b; } + f32 GetBloomThreshold() const { return m_BloomThreshold; } + void SetBloomThreshold(f32 v) { m_BloomThreshold = v; } + f32 GetBloomStrength() const { return m_BloomStrength; } + void SetBloomStrength(f32 v) { m_BloomStrength = v; } + + private: + vk::Extent2D m_Extent{}; u32 m_CurrentFrame = 0; }; diff --git a/Engine/src/RenderGraph/RenderGraph.cpp b/Engine/src/RenderGraph/RenderGraph.cpp index 3c132c9..304e7ae 100644 --- a/Engine/src/RenderGraph/RenderGraph.cpp +++ b/Engine/src/RenderGraph/RenderGraph.cpp @@ -8,6 +8,7 @@ #include #include +#include namespace Engine::RG { @@ -164,6 +165,22 @@ namespace Engine::RG { for (auto& entry : m_Textures) ResolveUsage(entry); + // 每 pass 渲染尺寸 = 其写入附件的最大分辨率(半分辨率后处理的 + // viewport/渲染区域由此决定);无写入(如 Present)回退目标尺寸 + for (auto& pass : m_Passes) { + u32 w = 0, h = 0; + for (const auto& use : pass.uses) { + if (!IsWriteUse(use.use)) continue; + const auto& d = m_Textures[use.resIndex].desc; + w = std::max(w, d.width ? d.width + : std::max(1u, u32(m_TargetExtent.width * d.sizeScale))); + h = std::max(h, d.height ? d.height + : std::max(1u, u32(m_TargetExtent.height * d.sizeScale))); + } + pass.extent = vk::Extent2D(w ? w : m_TargetExtent.width, + h ? h : m_TargetExtent.height); + } + CullPasses(); Toposort(); ComputeLifetimes(); @@ -272,34 +289,43 @@ namespace Engine::RG { void RenderGraph::Toposort() { - // writer → reader 建边(去重),Kahn 拓扑排序。 - // 声明顺序通常已是拓扑序,但引擎级实现必须显式校验, - // 避免上层重构 pass 顺序时引入环。 + // 按资源在声明序上的使用序列建边,Kahn 拓扑排序。 + // RAW:reader → 依赖声明序上它的上一个 writer + // WAR:writer → 依赖上一次 write 之后出现过的所有 reader + // WAW:writer → 上一个 writer + // 多写者(ping-pong 资源)合法:若对 reader 连接"所有" + // writer(含声明序在其后的),BlurH 会同时收到 Extract→BlurH 与 + // BlurV→BlurH,而 BlurH→BlurV 又存在,直接成环。 + // 声明顺序通常已是拓扑序,但引擎级实现必须显式校验。 - std::vector> writers(m_Textures.size()); - for (u32 i = 0; i < m_Passes.size(); ++i) { + const u32 N = static_cast(m_Passes.size()); + std::vector> adj(N); + std::vector indegree(N, 0); + + auto addEdge = [&](u32 from, u32 to) { + if (from == to) return; + if (adj[from].insert(to).second) ++indegree[to]; + }; + + constexpr u32 kNoWriter = UINT32_MAX; + std::vector lastWriter(m_Textures.size(), kNoWriter); + std::vector> pendingReaders(m_Textures.size()); + + for (u32 i = 0; i < N; ++i) { if (m_Passes[i].culled) continue; + for (const auto& use : m_Passes[i].uses) { + auto& lw = lastWriter[use.resIndex]; + auto& readers = pendingReaders[use.resIndex]; + if (IsWriteUse(use.use)) { - writers[use.resIndex].push_back(i); - } - } - } - - std::vector> adj(m_Passes.size()); - std::vector indegree(m_Passes.size(), 0); - - for (u32 q = 0; q < m_Passes.size(); ++q) { - if (m_Passes[q].culled) continue; - std::set seenWriters; - for (const auto& use : m_Passes[q].uses) { - if (IsWriteUse(use.use)) continue; - for (u32 p : writers[use.resIndex]) { - if (p == q || m_Passes[p].culled) continue; - if (seenWriters.insert(p).second) { - adj[p].insert(q); - ++indegree[q]; - } + for (u32 r : readers) addEdge(r, i); // WAR + readers.clear(); + if (lw != kNoWriter) addEdge(lw, i); // WAW + lw = i; + } else { + if (lw != kNoWriter) addEdge(lw, i); // RAW + readers.insert(i); } } } @@ -309,7 +335,7 @@ namespace Engine::RG { // 同层按声明序出队,保证确定性 std::set ready; - for (u32 i = 0; i < m_Passes.size(); ++i) { + for (u32 i = 0; i < N; ++i) { if (!m_Passes[i].culled && indegree[i] == 0) ready.insert(i); } @@ -458,9 +484,14 @@ namespace Engine::RG { for (auto& entry : m_Textures) { if (entry.imported || entry.firstPass < 0) continue; + // 显式尺寸优先;否则 = 目标尺寸 × sizeScale(后处理降采样) vk::Extent2D extent{ - entry.desc.width ? entry.desc.width : m_TargetExtent.width, - entry.desc.height ? entry.desc.height : m_TargetExtent.height + entry.desc.width + ? entry.desc.width + : std::max(1u, u32(m_TargetExtent.width * entry.desc.sizeScale)), + entry.desc.height + ? entry.desc.height + : std::max(1u, u32(m_TargetExtent.height * entry.desc.sizeScale)) }; for (u32 slot = 0; slot < m_FramesInFlight; ++slot) { @@ -567,8 +598,11 @@ namespace Engine::RG { const auto& entry = GetEntry(h); if (entry.imported) return entry.importedView; if (frameSlot >= entry.physical.size() || !entry.physical[frameSlot]) { - ENGINE_FATAL("RenderGraph: no physical image for '{}' slot {} " - "(compile first?)", entry.name, frameSlot); + // ENGINE_FATAL 只记日志不中止,这里必须 throw 才能拦住空指针 + //(Application::Run 的 catch 会接住并干净退出) + throw std::runtime_error( + "RenderGraph: no physical image for '" + entry.name + + "' slot " + std::to_string(frameSlot)); } return entry.physical[frameSlot]->view; } @@ -614,7 +648,7 @@ namespace Engine::RG { RenderGraphContext ctx; ctx.cmd = frame.cmd; ctx.frame = frame; - ctx.extent = m_TargetExtent; + ctx.extent = pass.extent; ctx.graph = this; pass.exec(ctx); } diff --git a/Engine/src/Renderer/Renderer.cpp b/Engine/src/Renderer/Renderer.cpp index 36f6f22..0cfd086 100644 --- a/Engine/src/Renderer/Renderer.cpp +++ b/Engine/src/Renderer/Renderer.cpp @@ -56,6 +56,16 @@ namespace Engine constexpr u32 kShadowMapSize = 2048; constexpr u32 kShadowPushSize = static_cast(2 * sizeof(glm::mat4)); + // 全屏 pass 通用 push(16B):所有全屏 pass(Tonemap/Bloom 系) + // 统一声明与推送,规避 VUID-08878 的"range 定义必须一致" + struct FullscreenPush { + f32 exposure; + f32 param0; // 各 pass 自定语义(tonemap: aces 开关) + f32 param1; + f32 param2; + }; + constexpr u32 kFullscreenPushSize = static_cast(sizeof(FullscreenPush)); + // 光源正交包围盒(覆盖当前场景;后续按场景 AABB 自适应) constexpr f32 kLightOrthoExtent = 15.0f; constexpr f32 kLightDistance = 30.0f; @@ -147,16 +157,19 @@ namespace Engine .setCompareEnable(VK_FALSE); m_ShadowSampler = m_Device->GetDevice().createSampler(shadowSamplerInfo); - // 7.5 Tonemap pass 装配:Set0 = HDR 颜色图,push = {exposure} - std::vector tonemapBindings = { + // 7.5 全屏后处理装配(Tonemap + Bloom 共用一套 layout/sampler): + // Set0 = {HDR 颜色图, Bloom 图},push = 16B 通用结构 + std::vector fullscreenBindings = { {0, vk::DescriptorType::eCombinedImageSampler, vk::ShaderStageFlagBits::eFragment}, + {1, vk::DescriptorType::eCombinedImageSampler, + vk::ShaderStageFlagBits::eFragment}, }; - m_TonemapSetLayout = m_DescriptorManager->CreateLayout(tonemapBindings); + m_TonemapSetLayout = m_DescriptorManager->CreateLayout(fullscreenBindings); m_TonemapPipelineLayout = m_DescriptorManager->CreatePipelineLayout( std::vector{m_TonemapSetLayout}, - sizeof(f32)); + kFullscreenPushSize); m_TonemapSampler = m_Device->GetDevice().createSampler( vk::SamplerCreateInfo{} @@ -170,17 +183,32 @@ namespace Engine auto tonemapVert = m_ShaderManager->CreateFromFile( shaderDir + "tonemap.vert.spv", vk::ShaderStageFlagBits::eVertex, - std::vector{m_TonemapSetLayout}, sizeof(f32)); + std::vector{m_TonemapSetLayout}, + kFullscreenPushSize); auto tonemapFrag = m_ShaderManager->CreateFromFile( shaderDir + "tonemap.frag.spv", vk::ShaderStageFlagBits::eFragment, - std::vector{m_TonemapSetLayout}, sizeof(f32)); + std::vector{m_TonemapSetLayout}, + kFullscreenPushSize); + auto bloomExtractFrag = m_ShaderManager->CreateFromFile( + shaderDir + "bloom_extract.frag.spv", + vk::ShaderStageFlagBits::eFragment, + std::vector{m_TonemapSetLayout}, + kFullscreenPushSize); + auto bloomBlurFrag = m_ShaderManager->CreateFromFile( + shaderDir + "bloom_blur.frag.spv", + vk::ShaderStageFlagBits::eFragment, + std::vector{m_TonemapSetLayout}, + kFullscreenPushSize); m_TonemapVertShader = tonemapVert.Handle; m_TonemapFragShader = tonemapFrag.Handle; + m_BloomExtractFrag = bloomExtractFrag.Handle; + m_BloomBlurFrag = bloomBlurFrag.Handle; - if (!m_TonemapVertShader || !m_TonemapFragShader) { - ENGINE_FATAL("Failed to create tonemap shader objects"); + if (!m_TonemapVertShader || !m_TonemapFragShader || + !m_BloomExtractFrag || !m_BloomBlurFrag) { + ENGINE_FATAL("Failed to create fullscreen shader objects"); } // 8. Swapchain(只管呈现;深度图由 RenderGraph 创建) @@ -199,6 +227,9 @@ namespace Engine m_UBODescriptorSets.resize(kMaxFramesInFlight); m_ShadowDescriptorSets.resize(kMaxFramesInFlight); m_TonemapDescriptorSets.resize(kMaxFramesInFlight); + m_BloomExtractSets.resize(kMaxFramesInFlight); + m_BloomBlurHSets.resize(kMaxFramesInFlight); + m_BloomBlurVSets.resize(kMaxFramesInFlight); for (u32 slot = 0; slot < kMaxFramesInFlight; ++slot) { m_UBODescriptorSets[slot] = m_DescriptorManager->AllocateDescriptorSet(m_UBOSetLayout); @@ -211,6 +242,13 @@ namespace Engine m_TonemapDescriptorSets[slot] = m_DescriptorManager->AllocateDescriptorSet(m_TonemapSetLayout); + + m_BloomExtractSets[slot] = + m_DescriptorManager->AllocateDescriptorSet(m_TonemapSetLayout); + m_BloomBlurHSets[slot] = + m_DescriptorManager->AllocateDescriptorSet(m_TonemapSetLayout); + m_BloomBlurVSets[slot] = + m_DescriptorManager->AllocateDescriptorSet(m_TonemapSetLayout); } // 11. RenderGraph:声明 + 编译(场景在 Render(Scene&) 时按帧绑定) @@ -218,7 +256,7 @@ namespace Engine // Compile 已分配物理镜像:把各槽位的阴影图/HDR view 绑进描述符 UpdateShadowDescriptors(); - UpdateTonemapDescriptors(); + UpdateFullscreenDescriptors(); ENGINE_INFO("Renderer initialized"); } @@ -247,6 +285,12 @@ namespace Engine if (m_TonemapFragShader) { vkDevice.destroyShaderEXT(m_TonemapFragShader); } + if (m_BloomExtractFrag) { + vkDevice.destroyShaderEXT(m_BloomExtractFrag); + } + if (m_BloomBlurFrag) { + vkDevice.destroyShaderEXT(m_BloomBlurFrag); + } vkDevice.destroySampler(m_ShadowSampler); vkDevice.destroySampler(m_TonemapSampler); @@ -300,6 +344,14 @@ namespace Engine "HDRColor", RG::TextureDesc{vk::Format::eR16G16B16A16Sfloat}); + // Bloom ping-pong:半分辨率(sizeScale 相对目标尺寸,resize 自动跟随) + m_BloomA = m_RenderGraph->CreateTexture( + "BloomA", + RG::TextureDesc{vk::Format::eR16G16B16A16Sfloat, 0, 0, 0.5f}); + m_BloomB = m_RenderGraph->CreateTexture( + "BloomB", + RG::TextureDesc{vk::Format::eR16G16B16A16Sfloat, 0, 0, 0.5f}); + // ---- Shadow pass:光源视角写深度(所有渲染体都写入,不做实体级开关—— // castShadow 应由材质/实体数据驱动剔除,而非 pass 逻辑特判)---- m_RenderGraph->AddPass("Shadow", @@ -527,11 +579,12 @@ namespace Engine .Read(m_ShadowMap, RG::ResourceUse::ShaderRead, vk::ShaderStageFlagBits::eFragment); - // ---- Tonemap pass:HDR → 显示空间,全屏三角无顶点缓冲 ---- - // 显示变换在此显式建模(此前隐式依赖 Srgb 附件,是设计欠账)。 - // Backbuffer 从此只接收"显示空间"颜色:tonemap 直接写、ImGui 叠加。 - m_RenderGraph->AddPass("Tonemap", - [this](RG::RenderGraphContext& ctx) { + // ---- 全屏 pass 公共录制:动态状态/draw 全一致,仅附件 view、 + // 输入 set、push 值不同(Extract/BlurH/BlurV/Tonemap 复用)---- + auto recordFullscreen = + [this](RG::RenderGraphContext& ctx, vk::ImageView view, + vk::ShaderEXT frag, const vk::DescriptorSet& set, + const FullscreenPush& push) { const vk::CommandBuffer cmd = ctx.cmd; vk::ClearValue clearValue; @@ -539,7 +592,7 @@ namespace Engine std::array{0.0f, 0.0f, 0.0f, 1.0f}); vk::RenderingAttachmentInfo colorAttachment; - colorAttachment.setImageView(ctx.View(m_Backbuffer)) + colorAttachment.setImageView(view) .setImageLayout(vk::ImageLayout::eColorAttachmentOptimal) .setLoadOp(vk::AttachmentLoadOp::eClear) .setStoreOp(vk::AttachmentStoreOp::eStore) @@ -557,12 +610,11 @@ namespace Engine vk::ShaderStageFlagBits::eFragment }; std::array shaders = { - m_TonemapVertShader, m_TonemapFragShader}; + m_TonemapVertShader, frag}; cmd.bindShadersEXT(stages, shaders); cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, - m_TonemapPipelineLayout, 0, - m_TonemapDescriptorSets[ctx.frame.frameSlot], {}); + m_TonemapPipelineLayout, 0, set, {}); // ---- 动态状态(无深度附件、无顶点输入)---- cmd.setRasterizerDiscardEnable(VK_FALSE); @@ -592,17 +644,65 @@ namespace Engine 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); + 0, kFullscreenPushSize, &push); cmd.draw(3, 1, 0, 0); cmd.endRendering(); + }; + + // ---- Bloom:Extract(HDR 软阈值)→ BlurH → BlurV,全部半分辨率 ---- + m_RenderGraph->AddPass("BloomExtract", + [this, recordFullscreen](RG::RenderGraphContext& ctx) { + recordFullscreen(ctx, ctx.View(m_BloomA), m_BloomExtractFrag, + m_BloomExtractSets[ctx.frame.frameSlot], + FullscreenPush{0.0f, m_BloomThreshold, 0.0f, 0.0f}); + }) + .Read(m_HDRColor, RG::ResourceUse::ShaderRead, + vk::ShaderStageFlagBits::eFragment) + .Write(m_BloomA, RG::ResourceUse::ColorAttachment); + + m_RenderGraph->AddPass("BloomBlurH", + [this, recordFullscreen](RG::RenderGraphContext& ctx) { + recordFullscreen(ctx, ctx.View(m_BloomB), m_BloomBlurFrag, + m_BloomBlurHSets[ctx.frame.frameSlot], + FullscreenPush{0.0f, + 1.0f / ctx.extent.width, 0.0f, + 0.0f}); + }) + .Read(m_BloomA, RG::ResourceUse::ShaderRead, + vk::ShaderStageFlagBits::eFragment) + .Write(m_BloomB, RG::ResourceUse::ColorAttachment); + + m_RenderGraph->AddPass("BloomBlurV", + [this, recordFullscreen](RG::RenderGraphContext& ctx) { + recordFullscreen(ctx, ctx.View(m_BloomA), m_BloomBlurFrag, + m_BloomBlurVSets[ctx.frame.frameSlot], + FullscreenPush{0.0f, 0.0f, + 1.0f / ctx.extent.height, + 0.0f}); + }) + .Read(m_BloomB, RG::ResourceUse::ShaderRead, + vk::ShaderStageFlagBits::eFragment) + .Write(m_BloomA, RG::ResourceUse::ColorAttachment); + + // ---- Tonemap pass:HDR(+bloom)→ 显示空间,全屏三角无顶点缓冲 ---- + // 显示变换在此显式建模(此前隐式依赖 Srgb 附件,是设计欠账)。 + // Backbuffer 从此只接收"显示空间"颜色:tonemap 直接写、ImGui 叠加。 + m_RenderGraph->AddPass("Tonemap", + [this, recordFullscreen](RG::RenderGraphContext& ctx) { + recordFullscreen(ctx, ctx.View(m_Backbuffer), m_TonemapFragShader, + m_TonemapDescriptorSets[ctx.frame.frameSlot], + FullscreenPush{m_Exposure, + m_UseACES ? 1.0f : 0.0f, + m_BloomStrength, 0.0f}); }) .Write(m_Backbuffer, RG::ResourceUse::ColorAttachment) .Read(m_HDRColor, RG::ResourceUse::ShaderRead, + vk::ShaderStageFlagBits::eFragment) + .Read(m_BloomA, RG::ResourceUse::ShaderRead, vk::ShaderStageFlagBits::eFragment); // ---- ImGui pass:叠加层(loadOp=Load,不清屏)---- @@ -661,7 +761,7 @@ namespace Engine // 重编译后物理镜像可能变化,描述符重新绑定 UpdateShadowDescriptors(); - UpdateTonemapDescriptors(); + UpdateFullscreenDescriptors(); } void Renderer::UpdateShadowDescriptors() @@ -675,13 +775,32 @@ namespace Engine } } - void Renderer::UpdateTonemapDescriptors() + void Renderer::UpdateFullscreenDescriptors() { + // 全屏 pass 的输入绑定(per-slot 物理镜像): + // Tonemap = {0: HDRColor, 1: BloomA} + // Extract = {0: HDRColor} (binding1 不用,可不写) + // BlurH = {0: BloomA} + // BlurV = {0: BloomB} for (u32 slot = 0; slot < kMaxFramesInFlight; ++slot) { + const vk::ImageView hdr = + m_RenderGraph->GetPhysicalView(m_HDRColor, slot); + const vk::ImageView bloomA = + m_RenderGraph->GetPhysicalView(m_BloomA, slot); + const vk::ImageView bloomB = + m_RenderGraph->GetPhysicalView(m_BloomB, slot); + m_DescriptorManager->UpdateTexture( - m_TonemapDescriptorSets[slot], 0, - m_TonemapSampler, - m_RenderGraph->GetPhysicalView(m_HDRColor, slot)); + m_TonemapDescriptorSets[slot], 0, m_TonemapSampler, hdr); + m_DescriptorManager->UpdateTexture( + m_TonemapDescriptorSets[slot], 1, m_TonemapSampler, bloomA); + + m_DescriptorManager->UpdateTexture( + m_BloomExtractSets[slot], 0, m_TonemapSampler, hdr); + m_DescriptorManager->UpdateTexture( + m_BloomBlurHSets[slot], 0, m_TonemapSampler, bloomA); + m_DescriptorManager->UpdateTexture( + m_BloomBlurVSets[slot], 0, m_TonemapSampler, bloomB); } } diff --git a/shaders/bloom_blur.frag b/shaders/bloom_blur.frag new file mode 100644 index 0000000..b7c9c40 --- /dev/null +++ b/shaders/bloom_blur.frag @@ -0,0 +1,29 @@ +#version 450 + +// Bloom 高斯模糊:可分离 9-tap,方向由 push 给出(H/V 各跑一次)。 +// 源图为半分辨率,线性采样 + 权重对应的偏移天然抗锯齿。 + +layout(location = 0) in vec2 fragUV; +layout(location = 0) out vec4 outColor; + +layout(set = 0, binding = 0) uniform sampler2D srcImage; + +layout(push_constant) uniform PushConstants { + float exposure; // 未用(程序级统一布局) + float stepX; // param0: UV 步长 x(水平 pass = 1/width,垂直 = 0) + float stepY; // param1: UV 步长 y + float _pad0; +} pc; + +const float weights[5] = float[](0.227027, 0.1945946, 0.1216216, 0.054054, 0.016216); + +void main() { + vec2 off = vec2(pc.stepX, pc.stepY); + + vec3 c = texture(srcImage, fragUV).rgb * weights[0]; + for (int i = 1; i < 5; ++i) { + c += texture(srcImage, fragUV + off * float(i)).rgb * weights[i]; + c += texture(srcImage, fragUV - off * float(i)).rgb * weights[i]; + } + outColor = vec4(c, 1.0); +} diff --git a/shaders/bloom_extract.frag b/shaders/bloom_extract.frag new file mode 100644 index 0000000..4602835 --- /dev/null +++ b/shaders/bloom_extract.frag @@ -0,0 +1,25 @@ +#version 450 + +// Bloom 提取:从 HDR 颜色图中滤出超过阈值的部分(软阈值,保留超出比例)。 +// 全屏 pass 通用 push(16B):param0 = 亮度阈值(HDR 域,1.0 = 仅高光溢出)。 + +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; // 未用(程序级统一布局) + float threshold; // param0 + float _pad0; + float _pad1; +} pc; + +void main() { + vec3 c = texture(hdrImage, fragUV).rgb; + float l = max(c.r, max(c.g, c.b)); + + // 软阈值:只保留超出 threshold 的能量比例,避免硬切产生的块状边缘 + float k = max(l - pc.threshold, 0.0) / max(l, 1e-4); + outColor = vec4(c * k, 1.0); +} diff --git a/shaders/tonemap.frag b/shaders/tonemap.frag index cd581e8..3eac200 100644 --- a/shaders/tonemap.frag +++ b/shaders/tonemap.frag @@ -2,22 +2,37 @@ // Tonemap pass:HDR 线性颜色 → 显示空间。 // 这是显示变换的显式建模(对齐 UE/Unity:HDR SceneColor → tonemapper → UI)。 -// 当前 = 曝光 + gamma 编码(与旧 Srgb 附件行为等价,观感不变); -// ACES/Reinhard 色调曲线、bloom 等后处理在此 pass 基础上扩展。 +// ACES = Narkowicz 拟合(线性域映射到 [0,1]),可经 push constant 关闭 +// 退回纯 gamma(旧观感);bloom 等后处理在 HDR 域(Tonemap 之前)插 pass。 layout(location = 0) in vec2 fragUV; layout(location = 0) out vec4 outColor; layout(set = 0, binding = 0) uniform sampler2D hdrImage; +layout(set = 0, binding = 1) uniform sampler2D bloomImage; +// 全屏 pass 通用 push(16B,程序级统一声明,VUID-08878) layout(push_constant) uniform PushConstants { - float exposure; // 曝光系数(后续接 ImGui) + float exposure; // 曝光系数 + float aces; // >0.5 启用 ACES 曲线,否则纯 gamma + float bloomStrength; // bloom 叠加强度(0 = 关闭) + float _pad0; } 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); +vec3 acesFilm(vec3 x) { + return clamp((x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14), 0.0, 1.0); +} + +void main() { + // bloom 在 HDR 域叠加(半分辨率模糊图线性采样上采样) + vec3 hdr = texture(hdrImage, fragUV).rgb + + texture(bloomImage, fragUV).rgb * pc.bloomStrength; + + hdr *= pc.exposure; + + vec3 mapped = (pc.aces > 0.5) + ? acesFilm(hdr) + : clamp(hdr, 0.0, 1.0); + + outColor = vec4(pow(mapped, vec3(1.0 / 2.2)), 1.0); } diff --git a/shaders/tonemap.vert b/shaders/tonemap.vert index 85540e4..09ff7b0 100644 --- a/shaders/tonemap.vert +++ b/shaders/tonemap.vert @@ -6,6 +6,14 @@ // 光栅化插值后屏幕内 UV 恰好落在 [0,1]。 layout(location = 0) out vec2 fragUV; +// 全屏 pass 通用 push(16B,与 tonemap.frag 及所有全屏 pass 一致,VUID-08878) +layout(push_constant) uniform PushConstants { + float exposure; + float aces; + float _pad0; + float _pad1; +} pc; + void main() { vec2 p = vec2( float((gl_VertexIndex << 1) & 2),