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
+12 -1
View File
@@ -6,16 +6,27 @@
layout(location = 0) in vec2 fragUV;
layout(location = 0) out vec4 outColor;
layout(location = 1) out vec2 outVelocity; // TAA velocity 附件
layout(set = 0, binding = 0) uniform samplerCube envMap;
// push 与 skybox.vert 一致声明(VUID-08878)
layout(push_constant) uniform PushConstants {
mat4 invVP;
mat4 invVP; // 当前帧反投影(方向重建)
mat4 prevVP; // 上一帧 proj×view(去平移):天空 velocity
} pc;
void main() {
vec4 target = pc.invVP * vec4(fragUV * 2.0 - 1.0, 1.0, 1.0);
vec3 dir = normalize(target.xyz / target.w);
outColor = vec4(texture(envMap, dir).rgb, 1.0);
// 天空 velocity:天空是无限远表面,方向不随相机平移变化(w=0 忽略
// 平移),过上一帧 proj×view 得上一帧屏幕位置,与当前 fragUV 之差即
// 运动向量。相机移动时天空/背景因此正确重投影,不再闪烁
vec4 prevClip = pc.prevVP * vec4(dir, 0.0);
vec2 prevUV = abs(prevClip.w) > 1e-6
? (prevClip.xy / prevClip.w) * 0.5 + 0.5
: fragUV;
outVelocity = fragUV - prevUV;
}
+2 -1
View File
@@ -8,7 +8,8 @@ layout(location = 0) out vec2 fragUV;
// push 与 skybox.frag 一致声明(VUID-08878:shader object 接口一致)
layout(push_constant) uniform PushConstants {
mat4 invVP;
mat4 invVP; // 当前帧反投影(方向重建)
mat4 prevVP; // 上一帧 proj×view(去平移):天空 velocity
} pc;
void main() {
+4 -1
View File
@@ -75,7 +75,10 @@ void main() {
gl_Position = ubo.proj * ubo.view * worldPos;
// ---- TAA velocity:当前帧与上一帧的 clip 空间位置差(UV 空间)----
// 抖动只进光栅化 gl_Position,不进 velocity(否则抖动本身被当成运动)
// 抖动不进 velocity:velocity 描述表面点的真实屏幕位移,必须用未抖动投影。
// 静态内容 → velocity=0 → 历史精确采回同一纹素,只做逐帧抖动的超采样平均
//(清清楚楚、无重采样模糊)。若把抖动计入 velocity,静止画面也会得到非零
// 速度 → 每帧亚纹素重采样历史 → 持续发糊(本次"画面很糊"的根因)。
// v1:蒙皮形变不进 velocity(prev 用同一蒙皮姿态),边缘 ghosting 接受
vec4 currClip = ubo.projNoJitter * ubo.view * worldPos;
vec4 prevWorldPos = prevObjects.prevModels[gl_InstanceIndex] * vec4(skinnedPos, 1.0);
+53 -15
View File
@@ -1,26 +1,32 @@
#version 450
// TAA resolve:时域抗锯齿。
// 1. velocity 反推历史像素位置,取历史累积色
// 2. 方差裁剪(YCoCg):历史色钳向邻域均值 ± γσ(∩ AABB 极值盒)——
// 比 RGB min/max 盒稳得多:AABB 盒子被逐帧抖动的高亮像素拽来拽去,
// 收敛历史持续被重钳 → 周期性残差经 bloom 阈值放大 = 静态模型泛光闪烁
// 3. mix(cur, hist, alpha) 时域累积
// 1. 深度感知 velocity dilation:轮廓处取最近表面的速度,反推历史像素位置
// 2. 去遮挡检测(disocclusion):重投影点上的"上一帧深度"若与当前像素
// 深度显著不同 → 该像素上一帧属于别的表面(刚露出/刚被遮挡),
// 历史颜色不属于当前表面 → 拒绝历史(alpha→0),用当前帧。
// 这是"移动时一闪一闪"的结构性根因:只查 histUV 是否出屏、不查历史
// 是否同一表面时,新暴露区域会持续采到前/背景串味的历史色
// 3. 方差裁剪(YCoCg):历史色钳向邻域均值 ± γσ(∩ AABB 极值盒)
// 4. 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; // 上一帧累积结果
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; // 上一帧累积结果
layout(set = 0, binding = 3) uniform sampler2D depthImage; // 当前帧深度(近=0,远=1)
layout(set = 0, binding = 4) uniform sampler2D historyDepthImage; // 上一帧深度(同资源历史槽位)
// 全屏 pass 通用 push(16B,程序级统一声明,VUID-08878)
// 全屏 pass 通用 push(16B,程序级统一声明,VUID-08878):
// 本 pass 语义 = {ortho, alpha, zNear, zFar}
layout(push_constant) uniform PushConstants {
float exposure; // 与其它全屏 pass 布局一致(本 pass 不用)
float alpha; // 历史权重(0.9 = 90% 历史)
float param1;
float param2;
float ortho; // 1 = 正交投影(深度线性化走线性分支)
float alpha; // 历史权重(0.9 = 90% 历史)
float zNear; // 相机近裁剪面
float zFar; // 相机远裁剪面
} pc;
// YCoCg 感知空间:亮度/色度分离,方差裁剪不在 HDR RGB 直接做
@@ -36,20 +42,52 @@ vec3 YCoCgToRgb(vec3 c) {
c.x - c.y - c.z);
}
// [0,1] 深度 → 视图空间距离(正值,近小远大)。
// 透视:d = 0.5·ndc + 0.5 重映射后,z = n·f / (f − d·(f−n))
//(n → d=0,f → d=1)。正交:深度本身线性,z = n + d·(f−n)
float linearizeDepth(float d, float n, float f, float ortho) {
float persp = (n * f) / (f - d * (f - n));
float orthoL = n + d * (f - n);
return mix(persp, orthoL, ortho);
}
void main() {
vec4 cur = texture(curImage, fragUV);
vec2 ts = 1.0 / vec2(textureSize(curImage, 0));
// 每像素 velocity 直接取自 velocity 缓冲(几何体与天空各自写入),
// 不做 dilation:像素中心属谁就用谁的速度。min-depth dilation 会把
// 前景(快物体)速度串给轮廓旁的背景像素 → 背景重投影到错误位置 →
// 轮廓周围持续闪烁,故移除。
vec2 vel = texture(velocityImage, fragUV).xy;
float dCenter = texture(depthImage, fragUV).r; // 中心像素当前深度
vec2 histUV = fragUV - vel;
// 运动自适应历史权重:静止 → 高权重(充分累积抗锯齿、稳定锐利);
// 运动/遮挡 → 降权重(0.4),快速收敛,消除拖影与移动闪烁。
// 固定高权重是"移动闪烁"的根源:新暴露区域要很多帧才收敛(UE/Unity 同款)
float speed = clamp(length(vel) * 100.0, 0.0, 1.0);
float alpha = mix(pc.alpha, 0.4, speed);
vec3 result = cur.rgb;
// 历史采样:越出画面(相机快速平移)直接用当前帧,不累积
if (all(greaterThanEqual(histUV, vec2(0.0))) &&
all(lessThanEqual(histUV, vec2(1.0)))) {
// ---- 去遮挡检测:深度一致性 ----
// 比较当前像素的视图距离与重投影点上的上一帧视图距离。差异显著
// 说明该像素上一帧被另一表面占据(刚露出/刚被遮挡)→ 历史无效,
// alpha 归零(新暴露像素直接吃当前帧,下一帧起重新累积)。
// 阈值取相对量:与绝对距离无关,近/远场景同一套参数
float zCur = linearizeDepth(dCenter, pc.zNear, pc.zFar, pc.ortho);
float zHist = linearizeDepth(texture(historyDepthImage, histUV).r,
pc.zNear, pc.zFar, pc.ortho);
float relDiff = abs(zCur - zHist) / max(zCur, 1e-4);
alpha *= 1.0 - smoothstep(0.05, 0.15, relDiff);
vec3 hist = texture(historyImage, histUV).rgb;
// 邻域统计(3×3,YCoCg):均值 m1、均方 m2、极值盒 [mn, mx]
vec2 ts = 1.0 / vec2(textureSize(curImage, 0));
vec3 m1 = vec3(0.0);
vec3 m2 = vec3(0.0);
vec3 mn = vec3(1e9);
@@ -75,7 +113,7 @@ void main() {
vec3 hi = min(m1 + kGamma * sigma, mx);
vec3 clamped = YCoCgToRgb(clamp(RgbToYCoCg(hist), lo, hi));
result = mix(cur.rgb, clamped, pc.alpha);
result = mix(cur.rgb, clamped, alpha);
}
outColor = vec4(result, cur.a);