feat: TAA 时域抗锯齿(velocity + 邻域 clamp + 子像素抖动)

- 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 全活跃)
This commit is contained in:
2026-09-23 13:29:31 +08:00
parent a29aed2803
commit 27b7f8785c
18 changed files with 369 additions and 39 deletions
+29 -11
View File
@@ -8,6 +8,7 @@
#include <Engine/Renderer/Material.h>
#include <array>
#include <span>
namespace Engine::Passes
{
@@ -20,12 +21,21 @@ namespace Engine::Passes
clearValue.color = vk::ClearColorValue(
std::array<float, 4>{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<float, 4>{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<vk::RenderingAttachmentInfo, 2> 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<vk::Bool32, 2>{blendEnable, blendEnable});
cmd.setColorBlendEquationEXT(0,
std::array<vk::ColorBlendEquationEXT, 2>{blendEquation, blendEquation});
cmd.setColorWriteMaskEXT(0,
std::array<vk::ColorComponentFlags, 2>{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);
+9 -1
View File
@@ -68,14 +68,21 @@ namespace Engine::Passes
vk::ShaderStageFlagBits::eFragment,
std::vector<vk::DescriptorSetLayout>{setLayout},
static_cast<u32>(sizeof(FullscreenPush)));
auto taaF = shaders.CreateFromFile(
shaderDir + "taa.frag.spv",
vk::ShaderStageFlagBits::eFragment,
std::vector<vk::DescriptorSetLayout>{setLayout},
static_cast<u32>(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);
+8 -1
View File
@@ -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<DescriptorBinding> 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);
}
+51
View File
@@ -0,0 +1,51 @@
//
// Created by atdunbg on 2026/9/23.
//
#include <Engine/Renderer/Passes/TAAPass.h>
#include <Engine/Core/FrameContext.h>
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
+31 -11
View File
@@ -14,6 +14,7 @@
#include <Engine/Renderer/Passes/RenderPass.h>
#include <Engine/Renderer/Passes/ShadowPass.h>
#include <Engine/Renderer/Passes/ForwardPass.h>
#include <Engine/Renderer/Passes/TAAPass.h>
#include <Engine/Renderer/Passes/BloomPass.h>
#include <Engine/Renderer/Passes/TonemapPass.h>
#include <Engine/Renderer/Passes/TemporalDiffPass.h>
@@ -48,19 +49,23 @@ namespace Engine
m_DescriptorManager = CreateScope<DescriptorManager>(*m_Device);
m_ShaderManager = CreateScope<ShaderManager>(*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<UniformBuffer>(
*m_Device, *m_Allocator, sizeof(UniformBufferObject), kMaxFramesInFlight);
m_ObjectBuffer = CreateScope<UniformBuffer>(
*m_Device, *m_Allocator,
sizeof(glm::mat4) * kMaxInstances, kMaxFramesInFlight,
vk::BufferUsageFlagBits::eStorageBuffer);
m_PrevObjectBuffer = CreateScope<UniformBuffer>(
*m_Device, *m_Allocator,
sizeof(glm::mat4) * kMaxInstances, kMaxFramesInFlight,
vk::BufferUsageFlagBits::eStorageBuffer);
// 3. pass 装配体:布局/shader/采样器/per-slot 描述符
m_SceneGpu = CreateScope<Passes::SceneGpu>(
*m_Device, *m_DescriptorManager, *m_ShaderManager,
*m_UniformBuffer, *m_ObjectBuffer);
*m_UniformBuffer, *m_ObjectBuffer, *m_PrevObjectBuffer);
m_FullscreenGpu = CreateScope<Passes::FullscreenGpu>(
*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<Passes::ShadowPass>(
*m_SceneGpu, frame, m_ShadowMap));
m_Passes.push_back(CreateScope<Passes::ForwardPass>(
*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<Passes::TAAPass>(
*m_FullscreenGpu, /*historyAlpha*/ 0.9f, m_HDRColor, m_Velocity,
m_TAAColor));
m_Passes.push_back(CreateScope<Passes::TemporalDiffPass>(
*m_FullscreenGpu, /*gain*/ 8.0f, m_HDRColor, m_TemporalDiff));
m_Passes.push_back(CreateScope<Passes::BloomPass>(
*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<Passes::TonemapPass>(
*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<Passes::ImGuiPass>(
&m_ImGuiLayer, m_Backbuffer));
@@ -295,9 +313,7 @@ namespace Engine
const auto& vkDevice = m_Device->GetDevice();
// 1. 场景 → 当帧渲染数据(相机/灯光/双视图剔除;纯 CPU,不碰 GPU)
const f32 aspect = static_cast<f32>(m_Extent.width) /
static_cast<f32>(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
+54 -3
View File
@@ -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<f32>(base);
r += f * static_cast<f32>(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<f32>(targetExtent.width) /
static_cast<f32>(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<u32>(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<f32>(targetExtent.width);
ubo.proj[2][1] += jy * 2.0f / static_cast<f32>(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<TransformComponent, MeshRendererComponent>(
[&](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<u32>(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;