feat: ACES tonemap + Bloom 后处理链(Extract/BlurH/BlurV 半分辨率)

- 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 复用
This commit is contained in:
2026-09-22 19:18:51 +08:00
parent 601fd8997c
commit 908e688178
9 changed files with 352 additions and 67 deletions
+24
View File
@@ -15,6 +15,7 @@
#include <Engine/ECS/EntityHandle.h>
#include <Engine/ECS/Components.h>
#include <Engine/Assets/AssetManager.h>
#include <Engine/Renderer/Renderer.h>
#include <imgui.h>
@@ -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(),
@@ -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 状态出发)
+31 -2
View File
@@ -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<vk::DescriptorSet> 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<vk::DescriptorSet> m_BloomExtractSets; // 每帧槽位
std::vector<vk::DescriptorSet> m_BloomBlurHSets;
std::vector<vk::DescriptorSet> 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;
};
+64 -30
View File
@@ -8,6 +8,7 @@
#include <algorithm>
#include <set>
#include <stdexcept>
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<std::vector<u32>> writers(m_Textures.size());
for (u32 i = 0; i < m_Passes.size(); ++i) {
const u32 N = static_cast<u32>(m_Passes.size());
std::vector<std::set<u32>> adj(N);
std::vector<u32> 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<u32> lastWriter(m_Textures.size(), kNoWriter);
std::vector<std::set<u32>> 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<std::set<u32>> adj(m_Passes.size());
std::vector<u32> indegree(m_Passes.size(), 0);
for (u32 q = 0; q < m_Passes.size(); ++q) {
if (m_Passes[q].culled) continue;
std::set<u32> 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<u32> 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);
}
+144 -25
View File
@@ -56,6 +56,16 @@ namespace Engine
constexpr u32 kShadowMapSize = 2048;
constexpr u32 kShadowPushSize = static_cast<u32>(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<u32>(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<DescriptorBinding> tonemapBindings = {
// 7.5 全屏后处理装配(Tonemap + Bloom 共用一套 layout/sampler):
// Set0 = {HDR 颜色图, Bloom 图},push = 16B 通用结构
std::vector<DescriptorBinding> 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<vk::DescriptorSetLayout>{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<vk::DescriptorSetLayout>{m_TonemapSetLayout}, sizeof(f32));
std::vector<vk::DescriptorSetLayout>{m_TonemapSetLayout},
kFullscreenPushSize);
auto tonemapFrag = m_ShaderManager->CreateFromFile(
shaderDir + "tonemap.frag.spv",
vk::ShaderStageFlagBits::eFragment,
std::vector<vk::DescriptorSetLayout>{m_TonemapSetLayout}, sizeof(f32));
std::vector<vk::DescriptorSetLayout>{m_TonemapSetLayout},
kFullscreenPushSize);
auto bloomExtractFrag = m_ShaderManager->CreateFromFile(
shaderDir + "bloom_extract.frag.spv",
vk::ShaderStageFlagBits::eFragment,
std::vector<vk::DescriptorSetLayout>{m_TonemapSetLayout},
kFullscreenPushSize);
auto bloomBlurFrag = m_ShaderManager->CreateFromFile(
shaderDir + "bloom_blur.frag.spv",
vk::ShaderStageFlagBits::eFragment,
std::vector<vk::DescriptorSetLayout>{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<float, 4>{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<vk::ShaderEXT, 2> 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);
}
}
+29
View File
@@ -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);
}
+25
View File
@@ -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);
}
+24 -9
View File
@@ -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);
}
+8
View File
@@ -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),