From 27b7f8785cd2ba290d6e30a43792a49221515eb2 Mon Sep 17 00:00:00 2001 From: atdunbg Date: Wed, 23 Sep 2026 13:29:31 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20TAA=20=E6=97=B6=E5=9F=9F=E6=8A=97?= =?UTF-8?q?=E9=94=AF=E9=BD=BF=EF=BC=88velocity=20+=20=E9=82=BB=E5=9F=9F=20?= =?UTF-8?q?clamp=20+=20=E5=AD=90=E5=83=8F=E7=B4=A0=E6=8A=96=E5=8A=A8?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Velocity buffer:Forward 第二颜色附件,vertex 计算 clip 空间运动向量 (UBO 尾部追加 projNoJitter/prevView/prevProj;上一帧实例矩阵 SSBO Set0 binding2,与当前实例数组平行对齐——转动的物体速度正确不鬼影) - 子像素抖动:SceneRenderer Halton(2,3)×8 序列,抖动只进光栅化投影 - TAA resolve pass:velocity 反推历史位置 + 3×3 邻域 clamp 去鬼影 + mix(cur,hist,0.9) 时域累积;TAAColor 自引用历史 - RG 历史读补 post-barrier:采样后恢复镜像 resting layout(TAA 的历史 是 ColorAttachment layout,不恢复则下帧 entry oldLayout 脱节) - 管线:Forward(HDR+Velocity) → TAA(TAAColor) → Bloom/Tonemap 改源 TAAColor - 编译零错误、validation 零警告、8 秒冒烟通过(10 pass 全活跃) --- .../include/Engine/RenderGraph/RenderGraph.h | 5 ++ .../Engine/Renderer/Passes/ForwardPass.h | 8 ++- .../Engine/Renderer/Passes/FullscreenGpu.h | 1 + .../include/Engine/Renderer/Passes/SceneGpu.h | 5 +- .../include/Engine/Renderer/Passes/TAAPass.h | 42 ++++++++++++++ Engine/include/Engine/Renderer/Renderer.h | 5 +- .../include/Engine/Renderer/SceneRenderer.h | 15 ++++- Engine/include/Engine/Renderer/SceneTypes.h | 13 ++++- Engine/src/RenderGraph/RenderGraph.cpp | 30 ++++++++++ Engine/src/Renderer/Passes/ForwardPass.cpp | 40 +++++++++---- Engine/src/Renderer/Passes/FullscreenGpu.cpp | 10 +++- Engine/src/Renderer/Passes/SceneGpu.cpp | 9 ++- Engine/src/Renderer/Passes/TAAPass.cpp | 51 +++++++++++++++++ Engine/src/Renderer/Renderer.cpp | 42 ++++++++++---- Engine/src/Renderer/SceneRenderer.cpp | 57 ++++++++++++++++++- shaders/simple_shader.frag | 3 + shaders/simple_shader.vert | 20 ++++++- shaders/taa.frag | 52 +++++++++++++++++ 18 files changed, 369 insertions(+), 39 deletions(-) create mode 100644 Engine/include/Engine/Renderer/Passes/TAAPass.h create mode 100644 Engine/src/Renderer/Passes/TAAPass.cpp create mode 100644 shaders/taa.frag diff --git a/Engine/include/Engine/RenderGraph/RenderGraph.h b/Engine/include/Engine/RenderGraph/RenderGraph.h index 0432e06..17f89b1 100644 --- a/Engine/include/Engine/RenderGraph/RenderGraph.h +++ b/Engine/include/Engine/RenderGraph/RenderGraph.h @@ -177,6 +177,11 @@ namespace Engine::RG { bool history = false; }; std::vector barriers; + + // 历史读的出口 barrier(exec 之后执行):历史镜像被临时转进读 + // layout 采样,读完必须恢复 resting layout,否则下一帧该槽位 + // entry barrier 的 oldLayout 与实际 layout 脱节 + std::vector postBarriers; }; // ---------- RenderGraph ---------- diff --git a/Engine/include/Engine/Renderer/Passes/ForwardPass.h b/Engine/include/Engine/Renderer/Passes/ForwardPass.h index 0af73ce..36ea424 100644 --- a/Engine/include/Engine/Renderer/Passes/ForwardPass.h +++ b/Engine/include/Engine/Renderer/Passes/ForwardPass.h @@ -18,10 +18,11 @@ namespace Engine::Passes { class ForwardPass final : public RenderPass { public: ForwardPass(SceneGpu& gpu, FrameSceneData& frame, - RG::TextureHandle hdrColor, RG::TextureHandle depth, - RG::TextureHandle shadowMap) + RG::TextureHandle hdrColor, RG::TextureHandle velocity, + RG::TextureHandle depth, RG::TextureHandle shadowMap) : m_Gpu(gpu), m_Frame(frame), - m_HDRColor(hdrColor), m_Depth(depth), m_ShadowMap(shadowMap) {} + m_HDRColor(hdrColor), m_Velocity(velocity), + m_Depth(depth), m_ShadowMap(shadowMap) {} void Register(RG::RenderGraph& rg) override; @@ -29,6 +30,7 @@ namespace Engine::Passes { SceneGpu& m_Gpu; FrameSceneData& m_Frame; RG::TextureHandle m_HDRColor; + RG::TextureHandle m_Velocity; // TAA 运动向量(第二颜色附件) RG::TextureHandle m_Depth; RG::TextureHandle m_ShadowMap; }; diff --git a/Engine/include/Engine/Renderer/Passes/FullscreenGpu.h b/Engine/include/Engine/Renderer/Passes/FullscreenGpu.h index 55073bb..339f516 100644 --- a/Engine/include/Engine/Renderer/Passes/FullscreenGpu.h +++ b/Engine/include/Engine/Renderer/Passes/FullscreenGpu.h @@ -54,6 +54,7 @@ namespace Engine { vk::Sampler sampler; vk::ShaderEXT vert{}, extractFrag{}, blurFrag{}, tonemapFrag{}; vk::ShaderEXT diffFrag{}; // temporal_diff(帧历史调试) + vk::ShaderEXT taaFrag{}; // taa(时域抗锯齿 resolve) // 各 pass 自己持有 per-slot set;分配统一走这里 vk::DescriptorSet AllocateSet(); diff --git a/Engine/include/Engine/Renderer/Passes/SceneGpu.h b/Engine/include/Engine/Renderer/Passes/SceneGpu.h index f2a816e..48f24d4 100644 --- a/Engine/include/Engine/Renderer/Passes/SceneGpu.h +++ b/Engine/include/Engine/Renderer/Passes/SceneGpu.h @@ -29,14 +29,15 @@ namespace Engine { public: SceneGpu(Device& device, DescriptorManager& descriptors, ShaderManager& shaders, - UniformBuffer& ubo, UniformBuffer& objectBuffer); + UniformBuffer& ubo, UniformBuffer& objectBuffer, + UniformBuffer& prevObjectBuffer); ~SceneGpu(); SceneGpu(const SceneGpu&) = delete; SceneGpu& operator=(const SceneGpu&) = delete; // ---- set 布局(shader 契约,改绑定必须同步 shaders/)---- - vk::DescriptorSetLayout uboSetLayout; // Set0 = {UBO, ObjectBuffer} + vk::DescriptorSetLayout uboSetLayout; // Set0 = {UBO, ObjectBuffer, PrevObjectBuffer} vk::DescriptorSetLayout textureSetLayout; // Set1 = {albedo} vk::DescriptorSetLayout shadowSetLayout; // Set2 = {shadow map} vk::PipelineLayout forwardLayout; // 3 sets,无 push(GPUDriven 经 SSBO) diff --git a/Engine/include/Engine/Renderer/Passes/TAAPass.h b/Engine/include/Engine/Renderer/Passes/TAAPass.h new file mode 100644 index 0000000..4156ef2 --- /dev/null +++ b/Engine/include/Engine/Renderer/Passes/TAAPass.h @@ -0,0 +1,42 @@ +// +// Created by atdunbg on 2026/9/23. +// +// TAAPass:时域抗锯齿 resolve。 +// 读当前帧 HDR + velocity + 历史累积(ReadHistory 自引用 TAAColor), +// 邻域 clamp 去鬼影后时域混合。TAAColor 既是输出也是下一帧的历史—— +// 这是帧历史资源最典型的"自引用"形态(UE 的 history pool 同构)。 +// + +#ifndef HELLOVULKAN_TAAPASS_H +#define HELLOVULKAN_TAAPASS_H + +#include +#include +#include +#include + +namespace Engine::Passes { + + class TAAPass final : public RenderPass { + public: + TAAPass(FullscreenGpu& gpu, f32 historyAlpha, + RG::TextureHandle hdrColor, RG::TextureHandle velocity, + RG::TextureHandle taaColor); + ~TAAPass() override = default; + + void Register(RG::RenderGraph& rg) override; + void OnTargetsRebuilt(RG::RenderGraph& rg) override; + + private: + FullscreenGpu& m_Gpu; + f32 m_HistoryAlpha; + RG::TextureHandle m_HDR; + RG::TextureHandle m_Velocity; + RG::TextureHandle m_TAAColor; // 输出 = 下一帧的历史 + + std::vector m_Sets; // per-slot + }; + +} // namespace Engine::Passes + +#endif //HELLOVULKAN_TAAPASS_H diff --git a/Engine/include/Engine/Renderer/Renderer.h b/Engine/include/Engine/Renderer/Renderer.h index 8a2932f..ecbcc9c 100644 --- a/Engine/include/Engine/Renderer/Renderer.h +++ b/Engine/include/Engine/Renderer/Renderer.h @@ -109,6 +109,7 @@ namespace Engine { Scope m_ShaderManager; Scope m_UniformBuffer; // per-slot UBO ring Scope m_ObjectBuffer; // GPUDriven 实例矩阵 ring(SSBO) + Scope m_PrevObjectBuffer; // 上一帧实例矩阵 ring(TAA velocity) Scope m_SceneGpu; // 场景类 pass 共享装配 Scope m_FullscreenGpu; // 全屏 pass 共享装配 Scope m_Swapchain; @@ -125,7 +126,9 @@ namespace Engine { RG::TextureHandle m_Backbuffer{}; RG::TextureHandle m_Depth{}; RG::TextureHandle m_ShadowMap{}; - RG::TextureHandle m_HDRColor{}; // Forward 输出(线性 HDR),Tonemap 归一 + RG::TextureHandle m_HDRColor{}; // Forward 输出(线性 HDR),TAA 归一 + RG::TextureHandle m_Velocity{}; // Forward 输出(clip 空间运动向量) + RG::TextureHandle m_TAAColor{}; // TAA 累积输出(自引用历史),后处理源 RG::TextureHandle m_BloomA{}; // 半分辨率:提取 + 垂直模糊结果 RG::TextureHandle m_BloomB{}; // 半分辨率:水平模糊中间结果 RG::TextureHandle m_TemporalDiff{}; // 半分辨率:帧历史差异(调试) diff --git a/Engine/include/Engine/Renderer/SceneRenderer.h b/Engine/include/Engine/Renderer/SceneRenderer.h index 2957600..d67775c 100644 --- a/Engine/include/Engine/Renderer/SceneRenderer.h +++ b/Engine/include/Engine/Renderer/SceneRenderer.h @@ -22,6 +22,7 @@ #include #include +#include #include namespace Engine { @@ -32,8 +33,9 @@ namespace Engine { class SceneRenderer { public: // 从场景构建当帧渲染数据。无 primary 相机返回 false(渲染跳帧)。 - // aspect 由调用方传入(跟随 swapchain,本类不感知窗口)。 - bool BuildFrame(ECS::Scene& scene, Assets::AssetManager& assets, f32 aspect); + // targetExtent 用于 TAA 子像素抖动的 NDC 偏移折算。 + bool BuildFrame(ECS::Scene& scene, Assets::AssetManager& assets, + vk::Extent2D targetExtent); FrameSceneData& Frame() { return m_Frame; } @@ -53,6 +55,15 @@ namespace Engine { std::unordered_map m_MainLookup; std::unordered_map m_ShadowLookup; + // ---- TAA 时域状态 ---- + u64 m_FrameIndex = 0; // 抖动序列游标(Halton 2,3 ×8 采样) + glm::mat4 m_LastView{1.0f}; // 上一帧视图(velocity) + glm::mat4 m_LastProjNoJitter{1.0f}; // 上一帧未抖动投影(velocity) + // 实体的上一帧世界矩阵(key = entt 实体 integral)。 + // 每帧重建 m_CurWorlds 后与 m_PrevWorlds swap——自动清理销毁实体 + std::unordered_map m_PrevWorlds; + std::unordered_map m_CurWorlds; + bool m_TruncationWarned = false; // 实例缓冲打满只告警一次 }; diff --git a/Engine/include/Engine/Renderer/SceneTypes.h b/Engine/include/Engine/Renderer/SceneTypes.h index a7b5e01..fb98c37 100644 --- a/Engine/include/Engine/Renderer/SceneTypes.h +++ b/Engine/include/Engine/Renderer/SceneTypes.h @@ -42,13 +42,19 @@ namespace Engine { glm::vec4 color; // rgb = 颜色,a = 强度 }; - // 镜像 simple_shader 的 UBO 布局(std140 语义由 shader 侧对齐) + // 镜像 simple_shader 的 UBO 布局(std140 语义由 shader 侧对齐)。 + // 字段只能追加在尾部(shadow.vert 共用本 set 前缀,前插会错位)。 struct UniformBufferObject { glm::mat4 view; - glm::mat4 proj; + glm::mat4 proj; // 抖动后的投影(光栅化用) glm::mat4 lightSpace; // 阴影光源 VP(写入与采样共用同一矩阵) glm::vec4 ambientColor; LightData lights[kMaxLights]; + + // ---- TAA(尾部追加;velocity 在 vertex 里算)---- + glm::mat4 projNoJitter; // 未抖动投影(velocity 当前帧 clip 用) + glm::mat4 prevView; // 上一帧视图(velocity) + glm::mat4 prevProj; // 上一帧未抖动投影(velocity) }; // 每帧从场景收集的渲染数据(SceneRenderer 生产,pass 消费)。 @@ -58,6 +64,9 @@ namespace Engine { // 实例矩阵数组 = [主视图段 | 阴影视图段](共享同一 per-slot SSBO) std::vector instanceModels; + // 上一帧世界矩阵,与 instanceModels 平行对齐(同一下标 = 同一 + // 实例的上一帧姿态,TAA velocity 用);首帧/新实体 = 当前矩阵 + std::vector prevInstanceModels; CullResult mainView; // 相机视锥 → Forward pass CullResult shadowView; // 光源视锥 → Shadow pass(相机外实体仍投影子) diff --git a/Engine/src/RenderGraph/RenderGraph.cpp b/Engine/src/RenderGraph/RenderGraph.cpp index 4203c4d..6ead799 100644 --- a/Engine/src/RenderGraph/RenderGraph.cpp +++ b/Engine/src/RenderGraph/RenderGraph.cpp @@ -177,6 +177,7 @@ namespace Engine::RG { for (auto& pass : m_Passes) { pass.culled = false; pass.barriers.clear(); + pass.postBarriers.clear(); } for (auto& entry : m_Textures) ResolveUsage(entry); @@ -468,6 +469,20 @@ namespace Engine::RG { .setOldLayout(entry.restingLayout); pass.barriers.push_back({use.resIndex, steady, first, true}); + + // 出口 barrier:采样完成后把历史镜像转回 resting layout。 + // (若 desired == resting 则是无害的空转换,但保留它以 + // 统一语义——src scope 同时给出读取完成的内存序) + vk::ImageMemoryBarrier2 exit; + exit.setSrcStageMask(dstStage) + .setSrcAccessMask(dstAccess) + .setDstStageMask(vk::PipelineStageFlagBits2::eNone) + .setDstAccessMask(vk::AccessFlagBits2::eNone) + .setOldLayout(desiredLayout) + .setNewLayout(entry.restingLayout) + .setSubresourceRange({entry.aspect, 0, 1, 0, 1}); + pass.postBarriers.push_back({use.resIndex, exit, exit, true}); + continue; // 不推进 chain:本帧链只描述当前槽位镜像 } @@ -763,6 +778,21 @@ namespace Engine::RG { pass.exec(ctx); } + // 历史读出口:恢复上一帧槽位镜像的 resting layout + if (!pass.postBarriers.empty()) { + m_ScratchBarriers.clear(); + for (const auto& bp : pass.postBarriers) { + auto& entry = m_Textures[bp.resIndex]; + const u32 histSlot = HistorySlot(frame.frameSlot); + vk::ImageMemoryBarrier2 b = bp.steady; + b.setImage(entry.physical[histSlot]->image); + m_ScratchBarriers.push_back(b); + } + vk::DependencyInfo dep; + dep.setImageMemoryBarriers(m_ScratchBarriers); + frame.cmd.pipelineBarrier2(dep); + } + if (m_QueryPool) { frame.cmd.writeTimestamp2( vk::PipelineStageFlagBits2::eBottomOfPipe, diff --git a/Engine/src/Renderer/Passes/ForwardPass.cpp b/Engine/src/Renderer/Passes/ForwardPass.cpp index beddd28..7550a8b 100644 --- a/Engine/src/Renderer/Passes/ForwardPass.cpp +++ b/Engine/src/Renderer/Passes/ForwardPass.cpp @@ -8,6 +8,7 @@ #include #include +#include namespace Engine::Passes { @@ -20,12 +21,21 @@ namespace Engine::Passes clearValue.color = vk::ClearColorValue( std::array{0.1f, 0.1f, 0.1f, 0.0f}); - vk::RenderingAttachmentInfo colorAttachment; - colorAttachment.setImageView(ctx.View(m_HDRColor)) - .setImageLayout(vk::ImageLayout::eColorAttachmentOptimal) - .setLoadOp(vk::AttachmentLoadOp::eClear) - .setStoreOp(vk::AttachmentStoreOp::eStore) - .setClearValue(clearValue); + vk::RenderingAttachmentInfo colorAttachments[2]; + colorAttachments[0].setImageView(ctx.View(m_HDRColor)) + .setImageLayout(vk::ImageLayout::eColorAttachmentOptimal) + .setLoadOp(vk::AttachmentLoadOp::eClear) + .setStoreOp(vk::AttachmentStoreOp::eStore) + .setClearValue(clearValue); + + // velocity 附件:清 0 = 静止(背景无几何 → 历史原位采样) + vk::ClearValue velocityClear; + velocityClear.color = vk::ClearColorValue(std::array{0.f, 0.f, 0.f, 0.f}); + colorAttachments[1].setImageView(ctx.View(m_Velocity)) + .setImageLayout(vk::ImageLayout::eColorAttachmentOptimal) + .setLoadOp(vk::AttachmentLoadOp::eClear) + .setStoreOp(vk::AttachmentStoreOp::eStore) + .setClearValue(velocityClear); vk::RenderingAttachmentInfo depthAttachment; depthAttachment.setImageView(ctx.View(m_Depth)) @@ -35,10 +45,13 @@ namespace Engine::Passes .setClearValue(vk::ClearValue( vk::ClearDepthStencilValue(1.0f, 0))); + std::array colorArray = { + colorAttachments[0], colorAttachments[1]}; + vk::RenderingInfo renderingInfo; renderingInfo.setRenderArea({{0, 0}, ctx.extent}) .setLayerCount(1) - .setColorAttachments(colorAttachment) + .setColorAttachments(colorArray) .setPDepthAttachment(&depthAttachment); cmd.beginRendering(renderingInfo); @@ -95,8 +108,6 @@ namespace Engine::Passes cmd.setScissorWithCount(scissor); vk::Bool32 blendEnable = VK_FALSE; - cmd.setColorBlendEnableEXT(0, blendEnable); - vk::ColorBlendEquationEXT blendEquation; blendEquation.setSrcColorBlendFactor(vk::BlendFactor::eOne) .setDstColorBlendFactor(vk::BlendFactor::eZero) @@ -104,14 +115,20 @@ namespace Engine::Passes .setSrcAlphaBlendFactor(vk::BlendFactor::eOne) .setDstAlphaBlendFactor(vk::BlendFactor::eZero) .setAlphaBlendOp(vk::BlendOp::eAdd); - cmd.setColorBlendEquationEXT(0, blendEquation); vk::ColorComponentFlags colorWriteMask = vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG | vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA; - cmd.setColorWriteMaskEXT(0, colorWriteMask); + + // 两个颜色附件(HDR + velocity)都是无混合直写 + cmd.setColorBlendEnableEXT(0, + std::array{blendEnable, blendEnable}); + cmd.setColorBlendEquationEXT(0, + std::array{blendEquation, blendEquation}); + cmd.setColorWriteMaskEXT(0, + std::array{colorWriteMask, colorWriteMask}); // ---- 绘制:GPUDriven 按 mesh 分 batch(相机视锥剔除后)---- // 实例矩阵已由 Renderer 写入 ObjectBuffer(per-slot SSBO); @@ -127,6 +144,7 @@ namespace Engine::Passes cmd.endRendering(); }) .Write(m_HDRColor, RG::ResourceUse::ColorAttachment) + .Write(m_Velocity, RG::ResourceUse::ColorAttachment) .Write(m_Depth, RG::ResourceUse::DepthAttachment) .Read(m_ShadowMap, RG::ResourceUse::ShaderRead, vk::ShaderStageFlagBits::eFragment); diff --git a/Engine/src/Renderer/Passes/FullscreenGpu.cpp b/Engine/src/Renderer/Passes/FullscreenGpu.cpp index bdf8f11..32b36e2 100644 --- a/Engine/src/Renderer/Passes/FullscreenGpu.cpp +++ b/Engine/src/Renderer/Passes/FullscreenGpu.cpp @@ -68,14 +68,21 @@ namespace Engine::Passes vk::ShaderStageFlagBits::eFragment, std::vector{setLayout}, static_cast(sizeof(FullscreenPush))); + auto taaF = shaders.CreateFromFile( + shaderDir + "taa.frag.spv", + vk::ShaderStageFlagBits::eFragment, + std::vector{setLayout}, + static_cast(sizeof(FullscreenPush))); vert = vertShader.Handle; tonemapFrag = tonemapF.Handle; extractFrag = extractF.Handle; blurFrag = blurF.Handle; diffFrag = diffF.Handle; + taaFrag = taaF.Handle; - if (!vert || !tonemapFrag || !extractFrag || !blurFrag || !diffFrag) { + if (!vert || !tonemapFrag || !extractFrag || !blurFrag || + !diffFrag || !taaFrag) { ENGINE_FATAL("FullscreenGpu: failed to create shader objects"); } } @@ -89,6 +96,7 @@ namespace Engine::Passes if (extractFrag) vkDevice.destroyShaderEXT(extractFrag); if (blurFrag) vkDevice.destroyShaderEXT(blurFrag); if (diffFrag) vkDevice.destroyShaderEXT(diffFrag); + if (taaFrag) vkDevice.destroyShaderEXT(taaFrag); vkDevice.destroySampler(sampler); vkDevice.destroyPipelineLayout(layout); diff --git a/Engine/src/Renderer/Passes/SceneGpu.cpp b/Engine/src/Renderer/Passes/SceneGpu.cpp index f78c770..20015c2 100644 --- a/Engine/src/Renderer/Passes/SceneGpu.cpp +++ b/Engine/src/Renderer/Passes/SceneGpu.cpp @@ -15,15 +15,19 @@ namespace Engine::Passes { SceneGpu::SceneGpu(Device& device, DescriptorManager& descriptors, ShaderManager& shaders, - UniformBuffer& ubo, UniformBuffer& objectBuffer) + UniformBuffer& ubo, UniformBuffer& objectBuffer, + UniformBuffer& prevObjectBuffer) : m_Device(device), m_Descriptors(descriptors) { // Set 0: UBO + GPUDriven 实例数据(SSBO,vertex 读取) + // + 上一帧实例矩阵(TAA velocity,vertex 读取) std::vector uboBindings = { {0, vk::DescriptorType::eUniformBuffer, vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment}, {1, vk::DescriptorType::eStorageBuffer, vk::ShaderStageFlagBits::eVertex}, + {2, vk::DescriptorType::eStorageBuffer, + vk::ShaderStageFlagBits::eVertex}, }; uboSetLayout = descriptors.CreateLayout(uboBindings); @@ -103,6 +107,9 @@ namespace Engine::Passes uboSets[slot], 0, ubo.Get(slot), ubo.GetSize()); descriptors.UpdateStorageBuffer( uboSets[slot], 1, objectBuffer.Get(slot), objectBuffer.GetSize()); + descriptors.UpdateStorageBuffer( + uboSets[slot], 2, prevObjectBuffer.Get(slot), + prevObjectBuffer.GetSize()); shadowSets[slot] = descriptors.AllocateDescriptorSet(shadowSetLayout); } diff --git a/Engine/src/Renderer/Passes/TAAPass.cpp b/Engine/src/Renderer/Passes/TAAPass.cpp new file mode 100644 index 0000000..8cd8bf6 --- /dev/null +++ b/Engine/src/Renderer/Passes/TAAPass.cpp @@ -0,0 +1,51 @@ +// +// Created by atdunbg on 2026/9/23. +// + +#include +#include + +namespace Engine::Passes +{ + TAAPass::TAAPass(FullscreenGpu& gpu, f32 historyAlpha, + RG::TextureHandle hdrColor, RG::TextureHandle velocity, + RG::TextureHandle taaColor) + : m_Gpu(gpu), m_HistoryAlpha(historyAlpha), + m_HDR(hdrColor), m_Velocity(velocity), m_TAAColor(taaColor) + { + m_Sets.resize(kMaxFramesInFlight); + for (u32 slot = 0; slot < kMaxFramesInFlight; ++slot) { + m_Sets[slot] = m_Gpu.AllocateSet(); + } + } + + void TAAPass::Register(RG::RenderGraph& rg) + { + rg.AddPass("TAA", + [this](RG::RenderGraphContext& ctx) { + m_Gpu.RecordFullscreen(ctx, ctx.View(m_TAAColor), m_Gpu.taaFrag, + m_Sets[ctx.frame.frameSlot], + FullscreenPush{0.0f, m_HistoryAlpha, 0.0f, 0.0f}); + }) + .Read(m_HDR, RG::ResourceUse::ShaderRead, + vk::ShaderStageFlagBits::eFragment) + .Read(m_Velocity, RG::ResourceUse::ShaderRead, + vk::ShaderStageFlagBits::eFragment) + // 自引用历史:TAAColor 的上一帧槽位就是累积历史 + .ReadHistory(m_TAAColor, RG::ResourceUse::ShaderRead, + vk::ShaderStageFlagBits::eFragment) + .Write(m_TAAColor, RG::ResourceUse::ColorAttachment); + } + + void TAAPass::OnTargetsRebuilt(RG::RenderGraph& rg) + { + for (u32 slot = 0; slot < kMaxFramesInFlight; ++slot) { + m_Gpu.UpdateSetInput(m_Sets[slot], 0, rg, m_HDR, slot); + m_Gpu.UpdateSetInput(m_Sets[slot], 1, rg, m_Velocity, slot); + // binding2 = 历史(上一帧槽位镜像) + m_Gpu.UpdateSetInput(m_Sets[slot], 2, rg, m_TAAColor, + rg.HistorySlot(slot)); + } + } + +} // namespace Engine::Passes diff --git a/Engine/src/Renderer/Renderer.cpp b/Engine/src/Renderer/Renderer.cpp index 82211c3..a60bf43 100644 --- a/Engine/src/Renderer/Renderer.cpp +++ b/Engine/src/Renderer/Renderer.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -48,19 +49,23 @@ namespace Engine m_DescriptorManager = CreateScope(*m_Device); m_ShaderManager = CreateScope(*m_Device); - // 2. Per-frame ring:UBO + GPUDriven 实例 SSBO(同一 ring 类, - // SSBO 只是 usage 不同;正确性保证一致) + // 2. Per-frame ring:UBO + GPUDriven 实例 SSBO(当前/上一帧, + // 同一 ring 类,SSBO 只是 usage 不同;正确性保证一致) m_UniformBuffer = CreateScope( *m_Device, *m_Allocator, sizeof(UniformBufferObject), kMaxFramesInFlight); m_ObjectBuffer = CreateScope( *m_Device, *m_Allocator, sizeof(glm::mat4) * kMaxInstances, kMaxFramesInFlight, vk::BufferUsageFlagBits::eStorageBuffer); + m_PrevObjectBuffer = CreateScope( + *m_Device, *m_Allocator, + sizeof(glm::mat4) * kMaxInstances, kMaxFramesInFlight, + vk::BufferUsageFlagBits::eStorageBuffer); // 3. pass 装配体:布局/shader/采样器/per-slot 描述符 m_SceneGpu = CreateScope( *m_Device, *m_DescriptorManager, *m_ShaderManager, - *m_UniformBuffer, *m_ObjectBuffer); + *m_UniformBuffer, *m_ObjectBuffer, *m_PrevObjectBuffer); m_FullscreenGpu = CreateScope( *m_Device, *m_DescriptorManager, *m_ShaderManager); @@ -183,12 +188,22 @@ namespace Engine "ShadowMap", RG::TextureDesc{vk::Format::eD32Sfloat, kShadowMapSize, kShadowMapSize}); - // HDR 颜色图:Forward 的输出(线性光照值),Tonemap 显式完成 - // 显示变换后写入 Backbuffer(对齐 UE/Unity 管线) + // HDR 颜色图:Forward 的输出(线性光照值 + 抖动),TAA 显式完成 + // 时域累积后写入 TAAColor(对齐 UE/Unity 管线) m_HDRColor = m_RenderGraph->CreateTexture( "HDRColor", RG::TextureDesc{vk::Format::eR16G16B16A16Sfloat}); + // velocity:clip 空间运动向量(TAA 反推历史像素位置) + m_Velocity = m_RenderGraph->CreateTexture( + "Velocity", + RG::TextureDesc{vk::Format::eR16G16B16A16Sfloat}); + + // TAA 累积输出:既是 resolve 结果也是下一帧的历史(自引用) + m_TAAColor = m_RenderGraph->CreateTexture( + "TAAColor", + RG::TextureDesc{vk::Format::eR16G16B16A16Sfloat}); + // Bloom ping-pong:半分辨率(sizeScale 相对目标尺寸,resize 自动跟随) m_BloomA = m_RenderGraph->CreateTexture( "BloomA", @@ -209,13 +224,16 @@ namespace Engine m_Passes.push_back(CreateScope( *m_SceneGpu, frame, m_ShadowMap)); m_Passes.push_back(CreateScope( - *m_SceneGpu, frame, m_HDRColor, m_Depth, m_ShadowMap)); + *m_SceneGpu, frame, m_HDRColor, m_Velocity, m_Depth, m_ShadowMap)); + m_Passes.push_back(CreateScope( + *m_FullscreenGpu, /*historyAlpha*/ 0.9f, m_HDRColor, m_Velocity, + m_TAAColor)); m_Passes.push_back(CreateScope( *m_FullscreenGpu, /*gain*/ 8.0f, m_HDRColor, m_TemporalDiff)); m_Passes.push_back(CreateScope( - *m_FullscreenGpu, *m_PostParams, m_HDRColor, m_BloomA, m_BloomB)); + *m_FullscreenGpu, *m_PostParams, m_TAAColor, m_BloomA, m_BloomB)); m_Passes.push_back(CreateScope( - *m_FullscreenGpu, *m_PostParams, m_HDRColor, m_BloomA, + *m_FullscreenGpu, *m_PostParams, m_TAAColor, m_BloomA, m_TemporalDiff, m_Backbuffer)); m_Passes.push_back(CreateScope( &m_ImGuiLayer, m_Backbuffer)); @@ -295,9 +313,7 @@ namespace Engine const auto& vkDevice = m_Device->GetDevice(); // 1. 场景 → 当帧渲染数据(相机/灯光/双视图剔除;纯 CPU,不碰 GPU) - const f32 aspect = static_cast(m_Extent.width) / - static_cast(m_Extent.height); - if (!m_SceneRenderer->BuildFrame(scene, assets, aspect)) { + if (!m_SceneRenderer->BuildFrame(scene, assets, m_Extent)) { return; // 无 primary 相机:跳帧 } const FrameSceneData& frame = m_SceneRenderer->Frame(); @@ -311,6 +327,10 @@ namespace Engine if (!frame.instanceModels.empty()) { m_ObjectBuffer->Update(frameSlot, frame.instanceModels.data(), frame.instanceModels.size() * sizeof(glm::mat4)); + // 上一帧姿态与当前帧平行对齐(velocity 顶点着色器按下标取) + m_PrevObjectBuffer->Update(frameSlot, frame.prevInstanceModels.data(), + frame.prevInstanceModels.size() * + sizeof(glm::mat4)); } // 3. acquire swapchain image diff --git a/Engine/src/Renderer/SceneRenderer.cpp b/Engine/src/Renderer/SceneRenderer.cpp index 60b5aa0..8b3ea57 100644 --- a/Engine/src/Renderer/SceneRenderer.cpp +++ b/Engine/src/Renderer/SceneRenderer.cpp @@ -28,6 +28,18 @@ namespace Engine constexpr f32 kLightDistance = 30.0f; constexpr f32 kLightNear = 1.0f; constexpr f32 kLightFar = 80.0f; + + // Halton 低差异序列(TAA 子像素抖动) + f32 Halton(u32 index, u32 base) + { + f32 f = 1.0f, r = 0.0f; + while (index > 0) { + f /= static_cast(base); + r += f * static_cast(index % base); + index /= base; + } + return r; + } } void SceneRenderer::ComputeLightSpace(const glm::vec3& shadowDir) @@ -53,10 +65,15 @@ namespace Engine } bool SceneRenderer::BuildFrame(ECS::Scene& scene, Assets::AssetManager& assets, - f32 aspect) + vk::Extent2D targetExtent) { const auto collectStart = std::chrono::steady_clock::now(); + // ---- 0. TAA 时域状态先行(velocity = 上一帧姿态与相机)---- + UniformBufferObject& ubo = m_Frame.ubo; + ubo.prevView = m_LastView; + ubo.prevProj = m_LastProjNoJitter; + // ---- 1. 相机 ---- auto* cameraComp = scene.GetPrimaryCamera(); if (!cameraComp) { @@ -65,9 +82,24 @@ namespace Engine } const Camera& camera = cameraComp->camera; - UniformBufferObject& ubo = m_Frame.ubo; + const f32 aspect = static_cast(targetExtent.width) / + static_cast(targetExtent.height); ubo.view = camera.GetView(); ubo.proj = camera.GetProjection(aspect, 0.1f, 100.0f); + + // 未抖动投影:velocity 的当前帧 clip 位置用它(抖动不能进 velocity) + ubo.projNoJitter = ubo.proj; + + // 子像素抖动(Halton×8):光栅化采样位置逐帧偏移,TAA 累积去锯齿。 + // NDC 偏移 = 像素偏移 × 2 / 尺寸 + { + const u32 idx = static_cast(m_FrameIndex % 8); + const f32 jx = Halton(idx + 1, 2) - 0.5f; + const f32 jy = Halton(idx + 1, 3) - 0.5f; + ubo.proj[2][0] += jx * 2.0f / static_cast(targetExtent.width); + ubo.proj[2][1] += jy * 2.0f / static_cast(targetExtent.height); + } + ubo.ambientColor = scene.GetSettings().ambientColor; // ---- 2. 灯光(第一个灯作为阴影光源)---- @@ -98,6 +130,7 @@ namespace Engine // 实例矩阵数组 = [主视图段 | 阴影视图段],batch.first 为全局下标。 // 哈希聚合保证各视图内 batch 区间连续。 m_Frame.instanceModels.clear(); + m_Frame.prevInstanceModels.clear(); m_Frame.mainView.batches.clear(); m_Frame.shadowView.batches.clear(); m_Frame.mainView.instanceCount = 0; @@ -112,10 +145,19 @@ namespace Engine const Frustum shadowFrustum = Frustum::FromMatrix(ubo.lightSpace); scene.Each( - [&](Entity, TransformComponent& t, MeshRendererComponent& mr) { + [&](Entity entity, TransformComponent& t, MeshRendererComponent& mr) { Model* model = assets.GetModel(mr.model); if (!model) return; + // TAA velocity:本实例的上一帧世界矩阵(首帧/新实体 = 当前 + // 矩阵 → 速度为 0,不产生假运动) + const u32 entKey = static_cast(entt::to_integral(entity)); + glm::mat4 prevWorld = t.world; + if (auto it = m_PrevWorlds.find(entKey); it != m_PrevWorlds.end()) { + prevWorld = it->second; + } + m_CurWorlds.emplace(entKey, t.world); + // 实例世界缩放(矩阵列向量最大长度,包围球半径随缩放) const f32 worldScale = glm::max( glm::length(glm::vec3(t.world[0])), @@ -145,7 +187,9 @@ namespace Engine } view.batches[it->second].count++; view.instanceCount++; + // 两个数组平行 push:同下标 = 同一实例的当前/上一帧姿态 m_Frame.instanceModels.push_back(t.world); + m_Frame.prevInstanceModels.push_back(prevWorld); }; tryAppend(m_Frame.mainView, mainFrustum, m_MainLookup); @@ -153,6 +197,13 @@ namespace Engine } }); + // 帧末滚动时域状态:cur → prev(每帧重建,销毁的实体自动清出) + m_PrevWorlds.swap(m_CurWorlds); + m_CurWorlds.clear(); + m_LastView = ubo.view; + m_LastProjNoJitter = ubo.projNoJitter; + ++m_FrameIndex; + if (instanceTruncated && !m_TruncationWarned) { // 只告警一次(每帧告警会刷屏) m_TruncationWarned = true; diff --git a/shaders/simple_shader.frag b/shaders/simple_shader.frag index 375fe9c..762f2c8 100644 --- a/shaders/simple_shader.frag +++ b/shaders/simple_shader.frag @@ -11,8 +11,10 @@ layout(location = 0) in vec2 fragUV; layout(location = 1) in vec3 fragNormal; layout(location = 2) in vec3 fragWorldPos; layout(location = 3) in vec3 fragLightSpacePos; +layout(location = 4) in vec2 fragVelocity; layout(location = 0) out vec4 outColor; +layout(location = 1) out vec2 outVelocity; // TAA velocity 附件 layout(set = 0, binding = 0) uniform UniformBufferObject { mat4 view; @@ -53,4 +55,5 @@ void main() { } outColor = vec4(result, 1.0); + outVelocity = fragVelocity; } diff --git a/shaders/simple_shader.vert b/shaders/simple_shader.vert index 7d745f5..315697e 100644 --- a/shaders/simple_shader.vert +++ b/shaders/simple_shader.vert @@ -15,6 +15,7 @@ layout(location = 0) out vec2 fragUV; layout(location = 1) out vec3 fragNormal; layout(location = 2) out vec3 fragWorldPos; layout(location = 3) out vec3 fragLightSpacePos; +layout(location = 4) out vec2 fragVelocity; // TAA:clip 空间运动向量 // GPUDriven:模型矩阵从实例缓冲按实例索引取。 // 同一 mesh 的 N 个实体 = 1 次 drawIndexed(instanceCount=N), @@ -23,12 +24,20 @@ layout(std430, set = 0, binding = 1) readonly buffer ObjectBuffer { mat4 models[]; // 连续 mat4 数组(16B 对齐,std430/std140 布局一致) } objects; +// 上一帧实例矩阵(与 ObjectBuffer 同下标 = 同一实例的上一帧姿态) +layout(std430, set = 0, binding = 2) readonly buffer PrevObjectBuffer { + mat4 prevModels[]; +} prevObjects; + layout(set = 0, binding = 0) uniform UniformBufferObject { mat4 view; - mat4 proj; + mat4 proj; // 抖动后(光栅化) mat4 lightSpace; vec4 ambientColor; Light lights[MAX_LIGHTS]; + mat4 projNoJitter; // 未抖动(velocity 当前帧) + mat4 prevView; // velocity 上一帧 + mat4 prevProj; } ubo; void main() { @@ -37,6 +46,13 @@ void main() { vec4 worldPos = model * vec4(inPosition, 1.0); gl_Position = ubo.proj * ubo.view * worldPos; + // ---- TAA velocity:当前帧与上一帧的 clip 空间位置差(UV 空间)---- + // 抖动只进光栅化 gl_Position,不进 velocity(否则抖动本身被当成运动) + vec4 currClip = ubo.projNoJitter * ubo.view * worldPos; + vec4 prevWorldPos = prevObjects.prevModels[gl_InstanceIndex] * vec4(inPosition, 1.0); + vec4 prevClip = ubo.prevProj * ubo.prevView * prevWorldPos; + fragVelocity = (currClip.xy / currClip.w - prevClip.xy / prevClip.w) * 0.5; + fragWorldPos = worldPos.xyz; // 光空间坐标(含 Vulkan [0,1] 深度重映射,与 shadow pass 写入端共用同一矩阵) fragLightSpacePos = (ubo.lightSpace * worldPos).xyz; @@ -45,4 +61,4 @@ void main() { fragNormal = normalize(normalMatrix * inNormal); fragUV = inUV; -} \ No newline at end of file +} diff --git a/shaders/taa.frag b/shaders/taa.frag new file mode 100644 index 0000000..c3074f9 --- /dev/null +++ b/shaders/taa.frag @@ -0,0 +1,52 @@ +#version 450 + +// TAA resolve:时域抗锯齿。 +// 1. velocity 反推历史像素位置,取历史累积色 +// 2. 邻域 clamp(3×3 AABB)压制鬼影:历史色超出当前帧邻域范围的部分丢弃 +// 3. mix(cur, hist, alpha) 时域累积 +// 输出写入 TAAColor,它同时也是下一帧的历史(ReadHistory 自引用)。 + +layout(location = 0) in vec2 fragUV; +layout(location = 0) out vec4 outColor; + +layout(set = 0, binding = 0) uniform sampler2D curImage; // 当前帧 HDR +layout(set = 0, binding = 1) uniform sampler2D velocityImage; // 运动向量(UV 空间) +layout(set = 0, binding = 2) uniform sampler2D historyImage; // 上一帧累积结果 + +// 全屏 pass 通用 push(16B,程序级统一声明,VUID-08878) +layout(push_constant) uniform PushConstants { + float exposure; // 与其它全屏 pass 布局一致(本 pass 不用) + float alpha; // 历史权重(0.9 = 90% 历史) + float param1; + float param2; +} pc; + +void main() { + vec4 cur = texture(curImage, fragUV); + vec2 vel = texture(velocityImage, fragUV).xy; + vec2 histUV = fragUV - vel; + + vec3 result = cur.rgb; + + // 历史采样:越出画面(相机快速平移)直接用当前帧,不累积 + if (all(greaterThanEqual(histUV, vec2(0.0))) && + all(lessThanEqual(histUV, vec2(1.0)))) { + vec3 hist = texture(historyImage, histUV).rgb; + + // 邻域 clamp:当前帧 3×3 邻域的 min/max 包围盒 + vec2 ts = 1.0 / vec2(textureSize(curImage, 0)); + vec3 minC = cur.rgb; + vec3 maxC = cur.rgb; + for (int y = -1; y <= 1; ++y) { + for (int x = -1; x <= 1; ++x) { + vec3 c = texture(curImage, fragUV + vec2(x, y) * ts).rgb; + minC = min(minC, c); + maxC = max(maxC, c); + } + } + vec3 clamped = clamp(hist, minC, maxC); + result = mix(cur.rgb, clamped, pc.alpha); + } + + outColor = vec4(result, cur.a); +}