URP中的抗锯齿算法
0 Aliasing
在归纳各个抗锯齿算法之前,我们首先需要明白锯齿出现的原因:连续的几何信号在离散的像素网格上的采样不足,导致高频细节信息丢失,进而出现Aliasing。
具体来说,我们可以从两方面进行考虑:
- 采样率不足
- 渲染是对连续几何对象的离散化采样的过程,而三角形的边缘、纹理的细节都属于连续几何
- 当几何边缘的变化频率(斜边、曲线)超过了像素的采样率时,像素无法捕捉到这样的连续变换,就会呈现出阶梯状的锯齿
- 光栅化算法的局限性
- 在光栅化阶段,每个像素通过一个采样点来判断几何覆盖的情况,进而导致亚像素细节的丢失
于是,我们可以将抗锯齿的算法分为两类:
- 增加有效采样率:SSAA
- 通过滤波或时域累积,模糊/伪造高频信息:FXAA、TAA
1 TAA
1.1 子像素抖动
TAA的核心思想在于通过多帧累积而覆盖到更多的子像素信息,从而实现近似于SSAA的效果。所以,实现TAA算法的第一步就是获取子像素信息,具体来说,我们在每帧渲染时,为摄像机的投影矩阵施加亚像素级别的偏移,使得采样点能够在像素内不同位置抖动。
在构建投影矩阵的偏移时,我们通常选择Halton序列,原因可以归纳为以下几点:
- 低差异特性
- 其采样点在空间中的分布更加均匀
- 在相同采样数下,能覆盖更多子像素区域
- 减少因采样点分布不均导致的噪点或残留锯齿
- 时域累积效率高
- 每个采样点基于互质基数(如基数为2和3的二维序列),确保连续帧的抖动模式无周期性重复
- 避免周期性抖动导致的重复性伪影(如固定模式闪烁)
而具体在URP中实现Jitter,则需要我们构建一个在渲染不透明物体之前执行的RenderPass
,调用cmd.SetViewProjectionMatrices()
,将Jitter值写入到投影矩阵的m02
与m12
中,剩余的步骤就可以交给MVP变换了。
1.2 历史帧混合
现在, 我们可以构建起一个最基础的TAA框架了。首先,分配两个RTHandle
,分别用于表示累积的历史帧与混合后的结果:
1
2
mResetHistoryFrames = RenderingUtils.ReAllocateIfNeeded(ref mHistoryAccumTexture, descriptor, FilterMode.Bilinear, TextureWrapMode.Clamp, name:kHistoryAccumTextureName);
RenderingUtils.ReAllocateIfNeeded(ref mResultTexture, descriptor, FilterMode.Bilinear, TextureWrapMode.Clamp, name:kTAAResultTextureName);
这里,我们判定了mHistoryAccumTexture
是否需要重分配,如果是,我们在进行历史帧混合时就需要排除掉废弃历史帧的影响。
在每一帧中,我们需要做三件事:
- 将当前帧与历史帧进行混合,以实现TAA抗锯齿
- 将混合结果保存,作为下一帧的历史帧
- 输出混合结果
1
2
3
4
5
6
// do temporal anti-aliasing
Blitter.BlitCameraTexture(cmd, mSource, mResultTexture, mPassMaterial, 0);
// save history
Blitter.BlitCameraTexture(cmd, mResultTexture, mHistoryAccumTexture);
// copy result to destination
Blitter.BlitCameraTexture(cmd, mResultTexture, mDestination);
Shader中对应的代码为:
1
2
3
4
half4 history = GetHistory(input.texcoord);
half4 source = GetSource(input.texcoord);
half4 color = source * _BlendFactor + history * (1 - _BlendFactor);
这样,对于一个完全静态的场景来说,TAA框架就已经成立了。
1.3 重投影
在绝大多数情况下,场景都是动态的,此时当前帧的某个像素所对应的三维位置,在历史帧中有可能映射到完全不同的屏幕坐标,进而导致画面的失真,包括:
- 模糊:颜色没有对齐,混合后边缘变模糊
- 鬼影:历史残留颜色出现在错误的位置
所以,我们需要为TAA算法引入重投影,即通过运动信息将当前帧的像素“追溯”到历史帧的正确位置,从而确保多帧混合时颜色对齐。
在URP中,Unity将屏幕空间中两帧之间的偏移距离存储在了一个精度较高的纹理中,我们采样该纹理,进而就能获取到用于采样历史帧的正确UV。当然,我们也可以手动进行计算,也就是通过深度信息,将当前片段还原到世界空间中,再分别利用当前帧与上一帧中没有Jitter偏移的投影矩阵进行变换。相关代码在Packages/com.unity.render-pipelines.universal/Shaders/CameraMotionVectors.shader
这个文件中。
此外,我们还需要考虑到镜头移动对于物体遮挡关系的影响。比方说,当镜头拉远时,原本被遮挡的物体会突然出现,这样的话,若当前像素实际属于一个近处物体,但因其自身Motion Vector可能指向远处物体(历史帧中未被遮挡的位置),直接使用会导致重投影到错误的历史颜色。所以,我们应该优先选择当前可见物体(即深度最近的物体)的运动向量,减少因遮挡/显露导致的重投影错误
1.4 数据矫正
处于种种原因,