参考资源

https://de45xmedrsdbp.cloudfront.net/Resources/files/TemporalAA_small-59732822.pdf

https://developer.download.nvidia.com/gameworks/events/GDC2016/msalvi_temporal_supersampling.pdf

https://www.gdcvault.com/play/1022970/Temporal-Reprojection-Anti-Aliasing-in

采样算法

对于一般的采样偏移处理有两种,一种是采样平均使用不同的位移多次采样一张RT(一帧),这样的话处理多帧的话就需要每帧做多次处理。另一种指数平均则只需要一张历史帧,就是可以得到所有帧的累计,其实就是lerp,当前帧和历史帧进行lerp处理,α则是权重,因为不断累积,越旧的帧随指数衰减,所以叫指数平均

抖动采样

因为成像都在相机,所以偏移可以直接对投影矩阵处理就行,针对这里的采样偏移计算则使用Halton序列 另外说明的是这里矩阵是在ndc空间下计算,如果对应重映射到-1~1的范围,这里的偏移量大概是一个像素

对应unity代码如下,使用sampleCount来控制采样混合的历史帧数量,经我测试这里的jitter还是偏大,可能会导致严重较为抖动问题

所以还是将其缩小一半,还是在亚像素范围抖动,这样可以解决抖动过强,同时将jitter值传入Shader

Halton(哈尔顿)序列

这个序列是一种低差异序列,理解成一种撒点尽量均匀的散布算法就行

具体算法是使用镜像翻转后的不同进制转换 比如输入是十进制整数 —> 转成对应进制 —> 镜像翻转放到小数点后面(对应进制的小数)—> 转回十进制小数输出 代码如下

总体实现流程

基础混合和重投影

处理完上面的jitter后,Shader最基础的操作就是混合前后两帧图像,因为jitter进行了位移,默认UV对应的当前帧,但是历史帧也做了jitter,则需要对齐上一帧的UV,对齐这个操作就是重投影,因为静态物体不会运动,只需要Unjitter,而动态物体则需要使用MotionVector去处理了,代码非常简单,将jitter传给Shader然后重新计算一下UV

混合输出

因为最终混合是lerp,如果随着历史帧的混合权重增加,就会出现鬼影

具体影响的效果是这样

处理静态物体

Neighborhood Clamp

具体处理就是从当前帧的3x3邻域找到max和min来进行钳制历史帧混合,防止越界

YCoCg Box

虽然可以直接拿RGB计算Neighborhood Clamp,但是RGB是不区分亮度和色度的,包围盒太粗糙,针对亮度需要转换到YCoCg色彩空间,把亮度单独拿出来,让包围盒的形状更符合人眼感知,Clamp 更精确,既不会放过鬼影,也不会过度裁剪导致闪烁

RGB 转换为 YCoCg 的标准公式如下:

Y=14(R+2G+B)Co=12(R−B)Cg=14(−R+2G−B)\begin{aligned} Y &= \frac{1}{4}(R + 2G + B) \\ Co &= \frac{1}{2}(R - B) \\ Cg &= \frac{1}{4}(-R + 2G - B) \end{aligned}
  • Y (Luminance): 亮度分量,代表图像的明暗。

  • Co (Chrominance orange): 偏橙色度分量。

  • Cg (Chrominance green): 偏绿色度分量。 参考代码:

提前色彩处理(Tone map)

对于高光特别丰富,亮度极高的情况,可以先对颜色进行Tone map然后在进行TAA混合

文档给出的参考对比

我测试场景对比

参考代码:

输出

额外的钳制优化A

使用YCoCg Box 进行 Neighborhood Clamp 的问题是这个范围可能很宽,一个异常亮的邻域像素会把 maxC 拉得很大,很多本该被裁掉的历史帧颜色都能通过,不是很准确。英伟达的文档中提到使用Variance Clipping(方差裁剪)进行包围盒的钳制

平均标准差是变化程度,因为是平均变化程度,虽然不能完全准确控制范围,但是可以自适应变化的快慢防止有的像素差异很大,这里乘上一个gamma控制具体变化程度,和平均值相加得到一个大最小范围 参考代码:

此外还需要对颜色压制,保留了被原本包围盒外的颜色,不过进行了压制到包围盒边缘

设计逻辑就是,将颜色压制到边缘,所以计算history + r * scale = maxC 或者 history + r * scale = minC,求scale是多少就行 参考代码:

额外的钳制优化B

在Temporal Reprojection Anti-Aliasing in INSIDE文档中使用了另外的采样邻域方法,RoundBox3x3

实现参考代码

同时也使用了另外的clip方法,相对中心进行clip

参考代码:

两种邻域钳制效果对比

同等参数下效果如下。效果差异还是比较明显的,中心压缩可以更好的抗模糊,方差的话则是抗锯齿更加平滑,个人感觉还是方差更好,更加平滑,反正TAA后面还需要加个锐化来抗模糊

重影效果因为邻域钳制也会减少

处理动态物体(Motion Vector)

为了解决动态物体鬼影的问题,需要计算对应运动方向,来控制这个混合程度,运动越快,当前帧权重更大,历史帧权重更小

关于Motion Vector我直接偷懒使用的unity内置的,自己写太麻烦了,而且内置的肯定比我写的准确,它能处理蒙皮动画、粒子等复杂情况,手动矩阵重投影只能处理相机运动,覆盖不了运动物体。

开启Motion Vector

AddRenderPasses中设置

OnCameraSetup设置

shader添加参数

替换手动jitter

因为Motion Vector包含了jitter的运动,所以直接替换掉

最终效果

方差模式效果如下,我把分辨率调的很低720x480,因为算法问题,画面轻微模糊是必然的,不过在高分辨率下模糊就不是很明显了,后续需要可以加锐化进行调整

如果是中心压缩模式,那么模糊感没那么强

完整代码

C#代码

using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

public class CustomTAA : ScriptableRendererFeature
{
    [System.Serializable]
    public class TAASettings
    {
        public Shader taaShader;   

        [Tooltip("历史帧混合程度")]
        [Range(0f, 1f)]public float  BlendFactor = 0.5f;
        [Tooltip("历史帧混合数量")]
        [Range(8, 24)] public int sampleCount = 8; // Halton 序列长度
        
        public enum AABBClipMode
        {
            Variance,
            Center
        };
        
        [Tooltip("历史帧颜色AABB包围盒钳制模式: 方差、中心")]
        public AABBClipMode aabbClipMode =  AABBClipMode.Variance;
        [Tooltip("历史帧颜色AABB包围盒大小缩放: 仅方差模式生效")]
        [Range(0, 10)] public float VarianceScale = 1;

    }
    public TAASettings settings = new TAASettings();
    CustomTAAPass pass;
    private Material material;

    private int frameIndex = 0;
    private Camera camera;
    private Matrix4x4 originalProjMatrix;

    private Vector2 currentJitter;



    public override void Create()
    { 
        if (settings.taaShader == null) return;
        if (material == null)
        {
            material = CoreUtils.CreateEngineMaterial(settings.taaShader);
        }
            
        pass = new CustomTAAPass(settings, material);

        pass.renderPassEvent = RenderPassEvent.BeforeRenderingPostProcessing;
    }

    // Halton 序列生成
    private float HaltonSequence(int value, int baseNumberSystem)
    {
        float result = 0f;
        float n = 1f / baseNumberSystem;
        float i = value;
        while(i > 0)
        {
            result += n * (i % baseNumberSystem);
            i = Mathf.FloorToInt(i / baseNumberSystem);
            n /= baseNumberSystem;
        }
        return result;
    }
    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        if (material == null || settings.taaShader == null) return;
        if (renderingData.cameraData.cameraType != CameraType.Game) return; // ← 加这行
        camera = renderingData.cameraData.camera;

        camera.ResetProjectionMatrix();
        // 保存原始投影矩阵
        originalProjMatrix = camera.projectionMatrix;

        int index = frameIndex % settings.sampleCount;
        float jitterX = (HaltonSequence(index + 1, 2) * -0.5f) / camera.pixelWidth;
        float jitterY = (HaltonSequence(index + 1, 3) * -0.5f) / camera.pixelHeight;
        currentJitter = new Vector2(jitterX, jitterY);
        // 偏移投影矩阵
        Matrix4x4 jitteredProj = originalProjMatrix;
        jitteredProj[0, 2] += jitterX;
        jitteredProj[1, 2] += jitterY;
        camera.projectionMatrix = jitteredProj;
        //Debug.Log($"jitterX: {jitterX}, jitterY: {jitterY}");

        frameIndex++;

        renderingData.cameraData.camera.depthTextureMode |= DepthTextureMode.MotionVectors | DepthTextureMode.Depth;

        pass.Setup(renderer.cameraColorTarget,currentJitter);
        renderer.EnqueuePass(pass);
    }

    protected override void Dispose(bool disposing)
    {
        CoreUtils.Destroy(material);
        pass?.Dispose();
    }
}

public class CustomTAAPass : ScriptableRenderPass
{
    private CustomTAA.TAASettings settings;
    private Material material;

    private RenderTargetIdentifier cameraColorTarget;


    private int _tempRTID = Shader.PropertyToID("_TAATemp");
    private RenderTargetIdentifier _tempRT;

    private RenderTexture  _historyRT;
    private static readonly int _HistoryTexID = Shader.PropertyToID("_HistoryTex");
    private bool _historyRTInitialized = false;

    private Vector2 currentJitter;
    private static readonly int JitterID  = Shader.PropertyToID("_Jitter");

    private static readonly int VarianceScaleID = Shader.PropertyToID("_VarianceScale");



    public CustomTAAPass(CustomTAA.TAASettings settings, Material material)
    {
        this.settings = settings;
        this.material = material;
    } 
    public void Setup(RenderTargetIdentifier cameraColorTarget, Vector2 currentJitter)
    {
        this.cameraColorTarget = cameraColorTarget;
        this.currentJitter = currentJitter;
    }
    public void UpdateMaterialValue(RenderingData renderingData)
    {
        material.DisableKeyword("_AABB_CLIP_MODE_CENTER");
        if(settings.aabbClipMode == CustomTAA.TAASettings.AABBClipMode.Center)
        {
            material.EnableKeyword("_AABB_CLIP_MODE_CENTER");
        }
        material.SetFloat("_BlendFactor", settings.BlendFactor);
        material.SetFloat(VarianceScaleID, settings.VarianceScale);
        material.SetVector(JitterID, currentJitter);

       
    }   
    
    public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData)
    {
        var desc = renderingData.cameraData.cameraTargetDescriptor;
        desc.depthBufferBits = 0;
        //内置MotionVector
        ConfigureInput(ScriptableRenderPassInput.Motion);

        // 分辨率变化重建
        if (_historyRT == null || _historyRT.width != desc.width || _historyRT.height != desc.height)
        {
            if (_historyRT != null) _historyRT.Release();
            _historyRT = new RenderTexture(desc);
            _historyRT.name = "_TAAHistory";
            _historyRT.filterMode = FilterMode.Bilinear;
            _historyRT.Create();
            _historyRTInitialized = false;
        }

        // 临时 RT
        cmd.GetTemporaryRT(_tempRTID, desc, FilterMode.Bilinear);
        _tempRT = new RenderTargetIdentifier(_tempRTID);
       
    }
   
    
    public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
    {
        var cmd = CommandBufferPool.Get("Custom TAA");
        // 第一帧历史帧为空,先用当前帧初始化
        if (!_historyRTInitialized)
        {
            cmd.Blit(cameraColorTarget, _historyRT);
            _historyRTInitialized = true;
        }
        //更新材质参数
        UpdateMaterialValue(renderingData);
       

        cmd.SetGlobalTexture(_HistoryTexID, _historyRT);
        cmd.Blit(cameraColorTarget, _tempRT, material, 0);

        // 把 resolve 结果写回 cameraColorTarget
        cmd.Blit(_tempRT, cameraColorTarget);

        // 把 resolve 结果存入历史帧,供下一帧使用
        cmd.Blit(_tempRT, _historyRT);

        context.ExecuteCommandBuffer(cmd);
        CommandBufferPool.Release(cmd);

        renderingData.cameraData.camera.ResetProjectionMatrix();
    }

    // Cleanup any allocated resources that were created during the execution of this render pass.
    public override void OnCameraCleanup(CommandBuffer cmd)
    {
        cmd.ReleaseTemporaryRT(_tempRTID);
    }

    public void Dispose()
    {
        if (_historyRT != null)
        {
            _historyRT.Release();
            _historyRT = null;
        }
    }
}


Shader代码

Shader "Hidden/TAA"
{
    Properties
    {
        _MainTex ("Source", 2D) = "white" {}
    }

    SubShader
    {
        Tags { "RenderType"="Opaque" "RenderPipeline"="UniversalPipeline" }
        Cull Off ZWrite Off ZTest Always

        Pass
        {
            Name "TAA"
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            #pragma shader_feature_local _AABB_CLIP_MODE_CENTER
            TEXTURE2D(_MainTex);    
            SAMPLER(sampler_MainTex);

            TEXTURE2D(_HistoryTex); 
            SAMPLER(sampler_HistoryTex);

            TEXTURE2D(_MotionVectorTexture); 
            SAMPLER(sampler_MotionVectorTexture);

            float4 _MainTex_TexelSize;
            float _BlendFactor;
            float2 _Jitter;      
            float _VarianceScale;

            struct Attributes 
            { 
                float4 positionOS : POSITION; 
                float2 uv : TEXCOORD0; 
                
            };
            struct Varyings   
            { 
                float4 positionCS : SV_POSITION; 
                float2 uv : TEXCOORD0; 
            };

            Varyings vert(Attributes v)
            {
                Varyings o;
                o.positionCS = TransformObjectToHClip(v.positionOS.xyz);
                o.uv = v.uv;
    
                return o;
            }

            // RGB → 亮度(感知亮度公式)
            float Luma(float3 rgb)
            {
                //return dot(rgb, float3(0.299, 0.587, 0.114));
                return dot(rgb, float3(0.2126, 0.7152, 0.0722));
            }

            // 压缩函数
            float4 ToneMap(float4 color)
            {
                return color / (1.0 + Luma(color.rgb));
            }

            // 还原函数
            float4 ToneMapInverse(float4 color)
            {
                return color /  max(1.0 - Luma(color.rgb), 0.0001);;
            }

            // RGB → YCoCg
            float3 RGBToYCoCg(float3 rgb)
            {
                float y  =  0.25 * rgb.r + 0.5 * rgb.g + 0.25 * rgb.b;
                float co =  0.5  * rgb.r - 0.5  * rgb.b;
                float cg = -0.25 * rgb.r + 0.5 * rgb.g - 0.25 * rgb.b;
                return float3(y, co, cg);
            }

            // YCoCg → RGB
            float3 YCoCgToRGB(float3 ycocg)
            {
                float r = ycocg.x + ycocg.y - ycocg.z;
                float g = ycocg.x + ycocg.z;
                float b = ycocg.x - ycocg.y - ycocg.z;
                return float3(r, g, b);
            }

        
            //https://developer.download.nvidia.com/gameworks/events/GDC2016/msalvi_temporal_supersampling.pdf
            // AABB边界压制
            float3 ClipToAABB(float3 history, float3 mu, float3 minC, float3 maxC)
            {
                float3 r = mu - history;        //距离中心的方向和距离
                float3 rmax = maxC - history;   //距离外边缘的距离
                float3 rmin = minC - history;   //距离最近边缘的距离

                float scale = 1.0;

                if (r.x > rmax.x) scale = min(scale, rmax.x / r.x);
                if (r.y > rmax.y) scale = min(scale, rmax.y / r.y);
                if (r.z > rmax.z) scale = min(scale, rmax.z / r.z);
                if (r.x < rmin.x) scale = min(scale, rmin.x / r.x);
                if (r.y < rmin.y) scale = min(scale, rmin.y / r.y);
                if (r.z < rmin.z) scale = min(scale, rmin.z / r.z);

                return history + r * scale;
            }

            // AABB 中心缩放 Clip(Pedersen_LasseJonFuglsang_TemporalReprojectionAntiAliasing)
            float3 ClipToAABB_Center(float3 history, float3 minC, float3 maxC)
            {
                float3 AABBCenter = 0.5 * (maxC + minC);        // AABB 中心
                float3 AABBHalf =0.5 * (maxC - minC) + 0.0001;   // AABB 半尺寸

                float3 offset = history - AABBCenter;       // history 相对中心的偏移
                float3 proportion = offset / AABBHalf;      // 偏移占比
                proportion = abs(proportion);               
                float maxP = max(proportion.x, max(proportion.y, proportion.z)); // 最大分量
                if (maxP > 1.0)
                    return AABBCenter + offset / maxP;      // 压缩到边界
                else
                    return history;                         // 已在 AABB 内
                
            }
            float4 NeighborhoodClamp_Round3x3(float2 uv, float4 historyColor)
            {
                float2 d = _MainTex_TexelSize.xy;

                // 9-tap (3x3)
                float3 min9 = 10000.0;
                float3 max9 = -10000.0; 
                // 5-tap (十字)
                float3 min5 = 10000.0;
                float3 max5 = -10000.0; 

                // 3x3 邻域
                for (int x = -1; x <= 1; x++)
                for (int y = -1; y <= 1; y++)
                {
                    float3 s = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv + float2(x, y) * d).rgb;
                    min9 = min(min9, s);
                    max9 = max(max9, s);
                  
                    if (abs(x) + abs(y) <= 1) 
                    {
                        min5 = min(min5, s);
                        max5 = max(max5, s);
                    }
                }


                // 取平均值实现 "Rounding"
                float3 minC = (min9 + min5) * 0.5;
                float3 maxC = (max9 + max5) * 0.5;
              
           
                float3 historyRGB = historyColor.rgb;
                historyRGB = ClipToAABB_Center(historyRGB, minC, maxC);

              
                return float4(historyRGB, historyColor.a);
            }

            float4 NeighborhoodClamp_Variance3x3(float2 uv, float4 historyColor)
            {
                float2 d = _MainTex_TexelSize.xy;

                float3 m1 = 0;
                float3 m2 = 0;

                // 3x3 邻域累积一阶矩和二阶矩
                for (int x = -1; x <= 1; x++)
                for (int y = -1; y <= 1; y++)
                {
                    float3 s = RGBToYCoCg(SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv + float2(x, y) * d).rgb);
                    m1 += s;
                    m2 += s * s;
                }

                // 均值和标准差
                float3 mu = m1 / 9.0;
                float3 sigma = sqrt(max(m2 / 9.0 - mu * mu, 0.0));

                // Variance Clipping 的 AABB
                float varianceScale = _VarianceScale;
                float3 minC = mu - varianceScale * sigma;
                float3 maxC = mu + varianceScale * sigma;

                float3 historyYCoCg = RGBToYCoCg(historyColor.rgb);  
                historyYCoCg = ClipToAABB(historyYCoCg, mu, minC, maxC);    
  

                return float4(YCoCgToRGB(historyYCoCg), historyColor.a);
                
            }

            

            float4 frag(Varyings input) : SV_Target
            {
                float2 uv = input.uv;
                //ndc -> uv
                float2 jitterUV =  _Jitter * 0.5 ;
                
                // 当前帧颜色
                float2 currentUV = uv;
                float4 currentColor = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, currentUV);
                           
                float2 motionVector = SAMPLE_TEXTURE2D(_MotionVectorTexture, sampler_MotionVectorTexture, uv).xy;
                //float2 historyUV = uv - jitterUV;
                float2 historyUV = uv - motionVector;
                float4 historyColor = SAMPLE_TEXTURE2D(_HistoryTex, sampler_HistoryTex, historyUV);
       

                //约束历史帧颜色
                #if defined(_AABB_CLIP_MODE_CENTER) 
                    historyColor = NeighborhoodClamp_Round3x3(currentUV, historyColor);          
                #else    
                    historyColor = NeighborhoodClamp_Variance3x3(currentUV, historyColor); 
                #endif   
               

                //混合前压缩
                float4 compressedCurrent = ToneMap(currentColor);
                float4 compressedHistory = ToneMap(historyColor);
    

                // 混合
                float4 result = lerp(compressedCurrent, compressedHistory, _BlendFactor);
                result = ToneMapInverse(result);

               
                return result;
            }
            ENDHLSL
        }
    }
}