文章

基于Unity URP实现TAA

基于Unity URP实现TAA

​ ​ 今天我们来实现在 URP管线中实现TAA算法。虽然最新的 URP中已经提供了实现,但是为了理解透彻,我们将从零开始实现,当然也会借鉴已有的实现来来分析。最终的结果应该就是把 URP原本的实现抄一遍。

使用 RenderFeature搭建基本流程

​ ​ 首先创建一个 RenderFeature,命名为 TAARenderFeature

image-20260505024223359-rTQV.png

​ ​ 然后实现Feature的内容

using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

public class TAARenderFeature : ScriptableRendererFeature
{
    class TAARenderPass : ScriptableRenderPass
    {
        private static readonly string s_PassName = "TAA Render Pass";
        private static readonly string s_TempRTName = "_TempRT";
        private RTHandle m_Source;

        private float m_BlendFactor;
        private RenderTexture m_HistoryTexture;
        private Shader m_TAAShader;
        private Material m_Material;
  
        public TAARenderPass(float blendFactor)
        {
            m_BlendFactor = blendFactor;
            m_TAAShader = Shader.Find("Hidden/TAAShader");
            if (m_TAAShader != null)
            {
                m_Material = new Material(m_TAAShader);
            }
        }
  
        public void SetUp(RTHandle source)
        {
            m_Source = source;
            if (m_HistoryTexture == null || m_HistoryTexture.width != m_Source.rt.width ||
                m_HistoryTexture.height != m_Source.rt.height)
            {
                if (m_HistoryTexture != null)
                {
                    m_HistoryTexture.Release();
                }
                m_HistoryTexture = new RenderTexture(m_Source.rt.width, m_Source.rt.height, 0,RenderTextureFormat.ARGBHalf);
            }
        }
  
        public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
        {
            if (m_TAAShader == null || m_Material == null)
            {
                return;
            }

            if (m_HistoryTexture == null)
            {
                return;
            }
    
            CommandBuffer cmd = CommandBufferPool.Get();
            cmd.name = s_PassName;
            int tempRTId = Shader.PropertyToID(s_TempRTName);
            cmd.SetGlobalFloat("_BlendFactor", m_BlendFactor);
            cmd.SetGlobalTexture("_HistoryTex", m_HistoryTexture);
            cmd.GetTemporaryRT(tempRTId, m_Source.rt.descriptor);
            cmd.Blit(m_Source, tempRTId, m_Material);
    

            cmd.Blit(tempRTId, m_HistoryTexture);
    
            cmd.Blit(tempRTId, m_Source);
    
            cmd.ReleaseTemporaryRT(tempRTId);
    
            context.ExecuteCommandBuffer(cmd);
    
            CommandBufferPool.Release(cmd);
        }
    }

    [Range(0.01f,1.0f)]
    public float m_BlendFactor = 0.01f;
  
    TAARenderPass m_TAARenderPass;
  
    public override void Create()
    {
        m_TAARenderPass = new TAARenderPass(m_BlendFactor);

        m_TAARenderPass.renderPassEvent = RenderPassEvent.BeforeRenderingPostProcessing;
    }
  
    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        renderer.EnqueuePass(m_TAARenderPass);
    }

    public override void SetupRenderPasses(ScriptableRenderer renderer, in RenderingData renderingData)
    {
        if (m_TAARenderPass != null)
        {
            m_TAARenderPass.SetUp(renderer.cameraColorTargetHandle);
        }
    }
}

​ ​ 还有Shader的内容:

HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
    
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
    
            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };
    
            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };
    
            sampler2D _MainTex;
            sampler2D _HistoryTex;
            float _BlendFactor;
    
            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = TransformObjectToHClip(v.vertex);
                o.uv = v.uv;
                return o;
            }
    
            float4 frag (v2f i) : SV_Target
            {
                // 1. 采样当前帧
                float3 currentColor = tex2D(_MainTex, i.uv).rgb;
        
                // 2. 采样历史帧
                float3 historyColor = tex2D(_HistoryTex, i.uv).rgb;
        
                // 3. 混合结果
                float3 result = lerp(historyColor, currentColor, _BlendFactor);
        
                return float4(result, 1.0);
            }
            ENDHLSL
        }

​ ​ 这样就实现了TAA的基本流程,没有抖动,没有颜色空间变换,没有裁剪,只有一个简单的插值。接下来就是在这个基础上去改进。

加入投影抖动(Jitter Matrix)

​ ​ 在进行抖动数值计算之前,这里要提到一个 低差异序列的概念。诚然,我没有理解什么叫低差异序列,但根据公开的资料表示,它是一个均匀的序列。在大多数TAA的实现中,用的是 Halton Sequence。它的实现如下,我们可以直接使用,输入的 index表明序列中第几个数,radix是底数

public static class HaltonSequence
{
        /// <summary>
        /// Gets a deterministic sample in the Halton sequence.
        /// </summary>
        /// <param name="index">The index in the sequence.</param>
        /// <param name="radix">The radix of the sequence.</param>
        /// <returns>A sample from the Halton sequence.</returns>
        public static float Get(int index, int radix)
        {
            float result = 0f;
            float fraction = 1f / radix;

            while (index > 0)
            {
                result += (index % radix) * fraction;

                index /= radix;
                fraction /= radix;
            }

            return result;
        }
}

​ ​ 在Unity URP中,取的是前1024个序列点,但是根据公开资料,似乎取前8或前16即可,所以在网上看到的都是模8或模16。

static internal Matrix4x4 CalculateJitterMatrix(ref CameraData cameraData)
{
            Matrix4x4 jitterMat = Matrix4x4.identity;

            bool isJitter = cameraData.IsTemporalAAEnabled();
            if (isJitter)
            {
                int taaFrameCountOffset = cameraData.taaSettings.jitterFrameCountOffset;
                int taaFrameIndex = Time.frameCount + taaFrameCountOffset;

                float actualWidth = cameraData.cameraTargetDescriptor.width;
                float actualHeight = cameraData.cameraTargetDescriptor.height;
                float jitterScale = cameraData.taaSettings.jitterScale;

                var jitter = CalculateJitter(taaFrameIndex) * jitterScale;

                float offsetX = jitter.x * (2.0f / actualWidth);
                float offsetY = jitter.y * (2.0f / actualHeight);

                jitterMat = Matrix4x4.Translate(new Vector3(offsetX, offsetY, 0.0f));
            }

            return jitterMat;
}

static internal Vector2 CalculateJitter(int frameIndex)
{
            // The variance between 0 and the actual halton sequence values reveals noticeable
            // instability in Unity's shadow maps, so we avoid index 0.
            float jitterX = HaltonSequence.Get((frameIndex & 1023) + 1, 2) - 0.5f;
            float jitterY = HaltonSequence.Get((frameIndex & 1023) + 1, 3) - 0.5f;

            return new Vector2(jitterX, jitterY);
}

​ ​ Jitter矩阵的设置,是在渲染物体之前设置的,设置在投影矩阵上,所以可以知道,该抖动影响的是物体的渲染投影的位置,而不是在后处理时给 uv施加偏移。所以如果单纯地用 RenderFeature来实现,是做不到的,需要修改URP的一些方法,来允许我们设置Jitter矩阵,并最终应用到物体渲染中。

image-20260505201300067.png

​ ​ 我们还需要查找 m_JitterMatrix使用的位置,替换成我们新加的 JitterMatrix,这样就可以根据是否被外部修改来选择 JitterMatrix的值。当然,这是为了将我们新加的内容,嵌入到URP管线中,并且改动最小。

​ ​ 接下来增加两个 RenderPass,用来设置和还原 JitterMatrix

image-20260505201902830.png

image-20260505201911519.png

image-20260505201934779.png

颜色空间转换和裁剪

​ ​ 转换颜色空间,即将线性的 RGB转换到 YCoCg空间再进行裁剪计算。裁剪就是取当前采样位置周围的颜色值,计算出 MinMax,然后将历史帧颜色 History,限制在该范围内,比较简单的实现是直接采样中心点周围九宫格的位置,然后计算出 MinMax,进行截断计算。这样就能减少鬼影的影响。

image-20260505221455147.png

​ ​ Unity的实现就比较复杂,抛开 Montion Vector。默认等级只采5个点。配置等级稍高一点就采到周围9个,而更高等级计算方差和均值[2],实现更好的效果,计算量也更大。

image-20260505222208735.png

image-20260505222218183.png

Motion Vector

​ ​ 根据AI给出的建议,Motion Vector可以从深度去还原世界空间坐标,然后再重投影回原来的投影位置,计算出 uv偏移,这就是 Motion Vector,这样就是需要采样一次深度图和矩阵计算。Unity的做法是专门渲染一张图,用来记录偏移量,这部分渲染的内容还没有找到,不过如果是记录到图上,那么就是对物体进行多一次渲染,我估计是只针对运动物体渲染,不然感觉性能上得不偿失。

image-20260506003037990.png

​ ​ 那么我们这里就只从深度来还原 Motion Vector,代码如下:

// --- Motion Vector: 深度重投影 ---
float2 ComputeMotionVector(float2 uv)
{
                float rawDepth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, uv);
                float4 ndcPos = float4(uv.x * 2.0 - 1.0, uv.y * 2.0 - 1.0, rawDepth, 1.0);
        
                float4 worldPos = mul(_CurrentInverseVP, ndcPos);
                worldPos.xyz /= worldPos.w;
                worldPos.w = 1.0;
        
                float4 prevClipPos = mul(_PreviousVP, worldPos);
                float2 prevUV = prevClipPos.xy / prevClipPos.w * 0.5 + 0.5;
        
                return uv - prevUV;
}

​ ​ 可以看到,需要采样深度图,增加一些变换矩阵:

  • _CameraDepthTexture:场景深度图
  • _CurrentInverseVP:当前帧 View - Projection
  • _PreviousVP:前一帧的 View - Projection

​ ​ 该函数可以计算出 uv的偏移量,然后结合偏移量去采样历史帧,得到的颜色会更接近物体原本的颜色,减少由于视图信息变化导致的历史信息不正确的现象。

float3 currentColor = tex2D(_MainTex, i.uv).rgb;
        
// Motion Vector 重投影
float2 motion = ComputeMotionVector(i.uv);
float2 historyUV = i.uv - motion;
        
// 越界检测:历史UV在屏幕外则直接用当前帧
bool validHistory = all(historyUV >= 0.0) && all(historyUV <= 1.0);
float3 historyColor = validHistory ? tex2D(_HistoryTex, historyUV).rgb : currentColor;
        
// YCoCg 空间裁剪历史
historyColor = ClampHistory(historyColor, i.uv);
        
float3 result = lerp(historyColor, currentColor, _BlendFactor);
return float4(result, 1.0);

​ ​ 可以对比下面两张图,可以看到边缘的变化:

image-20260506010349201.png

image-20260506010416374.png

参考资料

[1] 深入浅出Temporal Antialising

[2] GDC 2016. "An excursion in temporal super sampling"

许可协议:  CC BY 4.0