feat: HDR 管线 + Tonemap pass,显示变换显式建模

管线: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 原生色彩无泛白
This commit is contained in:
2026-09-22 19:00:56 +08:00
parent 4fd88248fc
commit 952ba8135d
10 changed files with 231 additions and 11 deletions
+156 -6
View File
@@ -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<DescriptorBinding> tonemapBindings = {
{0, vk::DescriptorType::eCombinedImageSampler,
vk::ShaderStageFlagBits::eFragment},
};
m_TonemapSetLayout = m_DescriptorManager->CreateLayout(tonemapBindings);
m_TonemapPipelineLayout = m_DescriptorManager->CreatePipelineLayout(
std::vector<vk::DescriptorSetLayout>{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<vk::DescriptorSetLayout>{m_TonemapSetLayout}, sizeof(f32));
auto tonemapFrag = m_ShaderManager->CreateFromFile(
shaderDir + "tonemap.frag.spv",
vk::ShaderStageFlagBits::eFragment,
std::vector<vk::DescriptorSetLayout>{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<Swapchain>(*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<float, 4>{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<float, 4>{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<vk::ShaderStageFlagBits, 2> stages = {
vk::ShaderStageFlagBits::eVertex,
vk::ShaderStageFlagBits::eFragment
};
std::array<vk::ShaderEXT, 2> 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<vk::VertexInputBindingDescription2EXT, 0>{},
std::array<vk::VertexInputAttributeDescription2EXT, 0>{});
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<float>(ctx.extent.width),
static_cast<float>(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;