TAA 时域速度链路贯通:天空盒补写 velocity(此前恒 0,相机移动时天空/背景重投影错误 → 闪烁);skyPrevVP(上一帧未抖动 proj × 去平移视图)落帧;TAA 加去遮挡深度检测(重投影点历史深度与当前像素比对,新暴露区拒历史,需 near/far + 投影模式落帧);移除 min-depth dilation(把前景速度泄漏给轮廓旁背景像素 → 边界闪);Forward 不透明段 velocity 附件 Clear→Load 承接天空结果;velocity 不入抖动注释订正(抖动进 velocity = 静止画面持续亚纹素重采样发糊)

This commit is contained in:
2026-10-18 15:22:48 +08:00
parent beba491bcb
commit 2024772bac
13 changed files with 178 additions and 45 deletions
@@ -35,7 +35,8 @@ namespace Engine {
// env 可为空(HDR 资产缺失):仅承担清屏,天空保持清屏色
SkyboxPass(Device& device, ShaderManager& shaders,
DescriptorManager& descriptors, FrameSceneData& frame,
RG::TextureHandle hdrColor, const Cubemap* env);
RG::TextureHandle hdrColor, RG::TextureHandle velocity,
const Cubemap* env);
~SkyboxPass();
SkyboxPass(const SkyboxPass&) = delete;
@@ -49,10 +50,11 @@ namespace Engine {
DescriptorManager& m_Descriptors;
FrameSceneData& m_Frame;
RG::TextureHandle m_HDRColor;
RG::TextureHandle m_Velocity; // 天空 velocity 附件(TAA)
const Cubemap* m_Env; // 可空
vk::DescriptorSetLayout m_SetLayout; // Set0 = {samplerCube}
vk::PipelineLayout m_Layout; // push = invVP 64B
vk::PipelineLayout m_Layout; // push = {invVP, prevVP} 128B
ShaderManager::ShaderRef m_Vert, m_Frag; // 析构自动销毁
std::array<vk::DescriptorSet, kMaxFramesInFlight> m_Sets{};
};
@@ -22,7 +22,7 @@ namespace Engine::Passes {
public:
TAAPass(FullscreenGpu& gpu, FrameSceneData& frame, f32 historyAlpha,
RG::TextureHandle hdrColor, RG::TextureHandle velocity,
RG::TextureHandle taaColor);
RG::TextureHandle depth, RG::TextureHandle taaColor);
~TAAPass() override = default;
void Register(RG::RenderGraph& rg) override;
@@ -34,6 +34,7 @@ namespace Engine::Passes {
f32 m_HistoryAlpha; // 开启时的历史权重
RG::TextureHandle m_HDR;
RG::TextureHandle m_Velocity;
RG::TextureHandle m_Depth; // 场景深度(深度感知 dilation 用)
RG::TextureHandle m_TAAColor; // 输出 = 下一帧的历史
std::vector<vk::DescriptorSet> m_Sets; // per-slot
@@ -183,7 +183,7 @@ namespace Engine {
// ---- TAA 时域状态 ----
u64 m_FrameIndex = 0; // 抖动序列游标(Halton 2,3 ×8 采样)
glm::mat4 m_LastView{1.0f}; // 上一帧视图(velocity)
glm::mat4 m_LastProjNoJitter{1.0f}; // 上一帧未抖动投影(velocity)
glm::mat4 m_LastProjNoJitter{1.0f}; // 上一帧未抖动投影(velocity 用)
// 实体的上一帧世界矩阵(key = entt 实体 integral)。
// 用 sorted vector + binary_search 替代 unordered_map:
// unordered_map 在每帧大量 emplace 时 rehash 抖动明显,
+15 -2
View File
@@ -164,9 +164,9 @@ namespace Engine {
LightData lights[kMaxLights];
// ---- TAA(velocity 在 vertex 里算)----
glm::mat4 projNoJitter; // 未抖动投影(velocity 当前帧 clip 用)
glm::mat4 projNoJitter; // 未抖动投影(天空盒反投影/折射同位采样/静态预览)
glm::mat4 prevView; // 上一帧视图(velocity)
glm::mat4 prevProj; // 上一帧未抖动投影(velocity)
glm::mat4 prevProj; // 上一帧"已抖动"投影(velocity 历史 UV 对齐)
glm::vec4 cameraPos; // 相机世界位置(高光 V 向量)
};
@@ -183,6 +183,13 @@ namespace Engine {
struct FrameSceneData {
UniformBufferObject ubo{};
// 相机裁剪面 + 投影模式(TAA 去遮挡检测用:把 [0,1] 深度线性化为
// 视图空间距离再比较。透视深度非线性,需 n·f/(f−d(f−n));正交
// 深度本身线性,用 cameraOrtho 分支)
f32 cameraNearZ = 0.1f;
f32 cameraFarZ = 10000.0f;
bool cameraOrtho = false;
glm::vec4 ClearColor{0.1, 0.1, 0.1, 1.0};
bool DrawSkybox = true;
// 本帧生效的环境贴图资产路径(来自第一个 active 的
@@ -194,6 +201,12 @@ namespace Engine {
//(视图去平移只留旋转 → 反投影结果即世界方向;Skybox pass push)
glm::mat4 skyInvVP{1.0f};
// 天空盒 velocity 用:上一帧未抖动 proj × 上一帧视图(去平移)。
// 天空是"无限远"表面,只受相机旋转影响:frag 把当前世界方向过
// 此矩阵得上一帧屏幕位置,差即天空的运动向量(相机移动时天空/
// 背景不再闪烁的根治点——此前天空从不写 velocity = 恒 0)
glm::mat4 skyPrevVP{1.0f};
// 实例矩阵数组 = [主视图段 | 阴影视图段](共享同一 per-slot SSBO)
std::vector<glm::mat4> instanceModels;
// 上一帧世界矩阵,与 instanceModels 平行对齐(同一下标 = 同一
+4 -2
View File
@@ -131,12 +131,14 @@ namespace Engine::Passes
.setStoreOp(vk::AttachmentStoreOp::eStore)
.setClearValue(clearValue);
// velocity 附件:清 0 = 静止(背景无几何 → 历史原位采样)
// velocity 附件由 Skybox 段首写(清 0 + 天空运动向量),这里 Load
// 承接其结果,再叠加几何体运动向量。若此处 Clear 会擦掉天空
// velocity → 相机移动时天空重投影错误 → 背景闪烁。
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)
.setLoadOp(vk::AttachmentLoadOp::eLoad)
.setStoreOp(vk::AttachmentStoreOp::eStore)
.setClearValue(velocityClear);
+45 -13
View File
@@ -18,11 +18,20 @@
namespace Engine::Passes
{
// push 布局与 skybox.vert/.frag 的 PushConstants 一致(128B):
// invVP = 当前帧反投影;prevVP = 上一帧 proj×view(天空 velocity)
struct SkyboxPush
{
glm::mat4 invVP;
glm::mat4 prevVP;
};
SkyboxPass::SkyboxPass(Device& device, ShaderManager& shaders,
DescriptorManager& descriptors, FrameSceneData& frame,
RG::TextureHandle hdrColor, const Cubemap* env)
RG::TextureHandle hdrColor, RG::TextureHandle velocity,
const Cubemap* env)
: m_Device(device), m_Descriptors(descriptors), m_Frame(frame),
m_HDRColor(hdrColor), m_Env(env)
m_HDRColor(hdrColor), m_Velocity(velocity), m_Env(env)
{
// Set0 = {samplerCube}(fragment 采样)
std::vector<DescriptorBinding> bindings = {
@@ -31,22 +40,22 @@ namespace Engine::Passes
};
m_SetLayout = descriptors.CreateLayout(bindings);
// push = invVP 64B(skybox.vert/.frag 声明一致,VUID-08878)
// push = {invVP, prevVP} 128B(skybox.vert/.frag 声明一致,VUID-08878)
m_Layout = descriptors.CreatePipelineLayout(
std::vector<vk::DescriptorSetLayout>{m_SetLayout},
sizeof(glm::mat4));
sizeof(SkyboxPush));
const std::string shaderDir = "shaders/env/";
auto vert = shaders.CreateFromFile(
shaderDir + "skybox.vert.spv",
vk::ShaderStageFlagBits::eVertex,
std::vector<vk::DescriptorSetLayout>{m_SetLayout},
sizeof(glm::mat4));
sizeof(SkyboxPush));
auto frag = shaders.CreateFromFile(
shaderDir + "skybox.frag.spv",
vk::ShaderStageFlagBits::eFragment,
std::vector<vk::DescriptorSetLayout>{m_SetLayout},
sizeof(glm::mat4));
sizeof(SkyboxPush));
m_Vert = std::move(vert);
m_Frag = std::move(frag);
@@ -108,10 +117,26 @@ namespace Engine::Passes
.setStoreOp(vk::AttachmentStoreOp::eStore)
.setClearValue(clear);
// velocity 附件:本 pass 是 velocity 的 first writer。清 0 后,
// 画天空时写天空运动向量;无天空/不画时保持 0(背景静止)。
// Forward 段改为 Load 承接本 pass 的结果
vk::ClearValue velocityClear;
velocityClear.color = vk::ClearColorValue(
std::array<float, 4>{0.f, 0.f, 0.f, 0.f});
vk::RenderingAttachmentInfo velocity;
velocity.setImageView(ctx.View(m_Velocity))
.setImageLayout(vk::ImageLayout::eColorAttachmentOptimal)
.setLoadOp(vk::AttachmentLoadOp::eClear)
.setStoreOp(vk::AttachmentStoreOp::eStore)
.setClearValue(velocityClear);
const std::array<vk::RenderingAttachmentInfo, 2> colorArray = {
color, velocity};
vk::RenderingInfo renderingInfo;
renderingInfo.setRenderArea({{0, 0}, ctx.extent})
.setLayerCount(1)
.setColorAttachments(color); // 无深度附件
.setColorAttachments(colorArray); // 无深度附件
cmd.beginRendering(renderingInfo);
@@ -128,11 +153,12 @@ namespace Engine::Passes
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
m_Layout, 0,
m_Sets[ctx.frame.frameSlot], {});
cmd.pushConstants<glm::mat4>(
const SkyboxPush push{m_Frame.skyInvVP, m_Frame.skyPrevVP};
cmd.pushConstants<SkyboxPush>(
m_Layout,
vk::ShaderStageFlagBits::eVertex |
vk::ShaderStageFlagBits::eFragment,
0, m_Frame.skyInvVP);
0, push);
// ---- 动态状态 ----
cmd.setRasterizerDiscardEnable(VK_FALSE);
@@ -180,16 +206,22 @@ namespace Engine::Passes
vk::ColorComponentFlagBits::eG |
vk::ColorComponentFlagBits::eB |
vk::ColorComponentFlagBits::eA;
cmd.setColorBlendEnableEXT(0, blendEnable);
cmd.setColorBlendEquationEXT(0, blendEquation);
cmd.setColorWriteMaskEXT(0, colorWriteMask);
cmd.setColorBlendEnableEXT(0,
std::array<vk::Bool32, 2>{VK_FALSE, VK_FALSE});
const std::array<vk::ColorBlendEquationEXT, 2> blendEquations = {
blendEquation, blendEquation};
cmd.setColorBlendEquationEXT(0, blendEquations);
const std::array<vk::ColorComponentFlags, 2> writeMasks = {
colorWriteMask, colorWriteMask};
cmd.setColorWriteMaskEXT(0, writeMasks);
cmd.draw(3, 1, 0, 0);
}
cmd.endRendering();
})
.Write(m_HDRColor, RG::ResourceUse::ColorAttachment);
.Write(m_HDRColor, RG::ResourceUse::ColorAttachment)
.Write(m_Velocity, RG::ResourceUse::ColorAttachment);
}
} // namespace Engine::Passes
+20 -3
View File
@@ -9,9 +9,10 @@ namespace Engine::Passes
{
TAAPass::TAAPass(FullscreenGpu& gpu, FrameSceneData& frame, f32 historyAlpha,
RG::TextureHandle hdrColor, RG::TextureHandle velocity,
RG::TextureHandle taaColor)
RG::TextureHandle depth, RG::TextureHandle taaColor)
: m_Gpu(gpu), m_Frame(frame), m_HistoryAlpha(historyAlpha),
m_HDR(hdrColor), m_Velocity(velocity), m_TAAColor(taaColor)
m_HDR(hdrColor), m_Velocity(velocity), m_Depth(depth),
m_TAAColor(taaColor)
{
m_Sets.resize(kMaxFramesInFlight);
for (u32 slot = 0; slot < kMaxFramesInFlight; ++slot) {
@@ -26,14 +27,25 @@ namespace Engine::Passes
// TAA 关闭:alpha=0 → mix(cur, hist, 0) 直通当前帧
//(抖动已由 SceneRenderer 同步停掉,画面静止无锯齿晃动)
const f32 alpha = m_Frame.taaEnabled ? m_HistoryAlpha : 0.0f;
// push 槽位:{ortho, alpha, zNear, zFar}——去遮挡检测在 shader
// 内用 near/far 把深度线性化,正交模式走另一分支
m_Gpu.RecordFullscreen(ctx, ctx.View(m_TAAColor), m_Gpu.taaFrag,
m_Sets[ctx.frame.frameSlot],
FullscreenPush{0.0f, alpha, 0.0f, 0.0f});
FullscreenPush{m_Frame.cameraOrtho ? 1.0f : 0.0f,
alpha,
m_Frame.cameraNearZ,
m_Frame.cameraFarZ});
})
.Read(m_HDR, RG::ResourceUse::ShaderRead,
vk::ShaderStageFlagBits::eFragment)
.Read(m_Velocity, RG::ResourceUse::ShaderRead,
vk::ShaderStageFlagBits::eFragment)
.Read(m_Depth, RG::ResourceUse::ShaderRead,
vk::ShaderStageFlagBits::eFragment)
// 上一帧深度(同 m_Depth 的上一槽位镜像):去遮挡检测比较
// 当前像素深度与重投影点上的历史深度,判断历史是否属于同一表面
.ReadHistory(m_Depth, RG::ResourceUse::ShaderRead,
vk::ShaderStageFlagBits::eFragment)
// 自引用历史:TAAColor 的上一帧槽位就是累积历史
.ReadHistory(m_TAAColor, RG::ResourceUse::ShaderRead,
vk::ShaderStageFlagBits::eFragment)
@@ -45,6 +57,11 @@ namespace Engine::Passes
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);
// binding3 = 场景深度(深度感知 dilation)
m_Gpu.UpdateSetInput(m_Sets[slot], 3, rg, m_Depth, slot);
// binding4 = 上一帧深度(去遮挡检测,历史槽位镜像)
m_Gpu.UpdateSetInput(m_Sets[slot], 4, rg, m_Depth,
rg.HistorySlot(slot));
// binding2 = 历史(上一帧槽位镜像)
m_Gpu.UpdateSetInput(m_Sets[slot], 2, rg, m_TAAColor,
rg.HistorySlot(slot));
+2 -2
View File
@@ -500,7 +500,7 @@ namespace Engine
// Skybox = HDRColor 的 first writer(清屏 + 画背景),先于 Forward 注册
m_Passes.push_back(CreateScope<Passes::SkyboxPass>(
*m_Device, *m_ShaderManager, *m_DescriptorManager, frame,
m_HDRColor, m_Environment.get()));
m_HDRColor, m_Velocity, m_Environment.get()));
m_Passes.push_back(CreateScope<Passes::ForwardPass>(
*m_SceneGpu, frame, m_HDRColor, m_Velocity, m_Depth,
m_SceneColor, m_ShadowMaps));
@@ -539,7 +539,7 @@ namespace Engine
*m_ShaderManager, *m_SceneGpu, frame, m_SelectionMask));
m_Passes.push_back(CreateScope<Passes::TAAPass>(
*m_FullscreenGpu, frame, /*historyAlpha*/ 0.9f, m_HDRColor,
m_Velocity, m_TAAColor));
m_Velocity, m_Depth, 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>(
+14 -1
View File
@@ -466,7 +466,7 @@ namespace Engine
// ---- 0. TAA 时域状态先行(velocity = 上一帧姿态与相机)----
UniformBufferObject& ubo = m_Frame.ubo;
ubo.prevView = m_LastView;
ubo.prevProj = m_LastProjNoJitter;
ubo.prevProj = m_LastProjNoJitter; // 上一帧未抖动投影(velocity 用)
// ---- 1. 相机:渲染源按形态分派(UE 语义)。编辑器形态
// Scene view 恒用编辑器相机(视口 UI 状态,空场景可用);
@@ -506,6 +506,13 @@ namespace Engine
// 编辑器形态 = 编辑器相机,游戏形态 = primary 相机实体
ubo.proj = camera->GetProjection(aspect);
// 相机裁剪面 + 投影模式落帧:TAA 去遮挡检测把 [0,1] 深度线性化
// 为视图空间距离做比较,需要 near/far 与投影模式
m_Frame.cameraNearZ = camera->GetNearZ();
m_Frame.cameraFarZ = camera->GetFarZ();
m_Frame.cameraOrtho =
camera->GetProjectionMode() == Camera::ProjectionMode::Orthographic;
// 未抖动投影:velocity 的当前帧 clip 位置用它(抖动不能进 velocity)
ubo.projNoJitter = ubo.proj;
@@ -581,6 +588,12 @@ namespace Engine
m_Frame.skyInvVP =
glm::inverse(ubo.projNoJitter * glm::mat4(glm::mat3(ubo.view)));
// 天空 velocity:上一帧未抖动 proj × 上一帧视图(去平移)。天空是
// 无限远表面,方向不随时间平移变化,故用 w=0 传入方向即忽略平移;
// 相机旋转/移动时天空的运动向量由此精确得出
m_Frame.skyPrevVP =
m_LastProjNoJitter * glm::mat4(glm::mat3(m_LastView));
// ---- 2. 灯光(CSM 阴影光源约定 = lights[0] 必是方向光)----
// entt Each 遍历序与实体创建序不保证一致——直接按迭代序填充会让
// lights[0] 落到点光上(params.x=1)→ shader 阴影分支整个跳过。