【URP】Unity[抗锯齿]原理实现与对比

【从UnityURP开始探索游戏渲染】专栏-直达

历史发展节点

  • 2001年‌:MSAA成为DirectX 8标准配置,通过硬件多采样解决几何锯齿
  • 2009年‌:NVIDIA推出FXAA,开创后处理抗锯齿时代
  • 2011年‌:SMAA 1.0发布,平衡性能与画质
  • 2014年‌:TAA开始普及,解决动态场景抗锯齿问题
  • 2017年‌:Unity URP集成全系列抗锯齿方案

抗锯齿技术实现原理

快速近似抗锯齿(FXAA)

通过全屏后处理检测边缘像素并进行颜色混合,采用亮度对比度阈值识别锯齿区域,使用低通滤波器平滑边缘。其核心是牺牲少量锐度换取性能优势,处理过程完全在像素空间进行,不依赖几何信息。

实现原理‌:

通过全屏后处理检测像素间亮度差异(如RGB通道对比度),对超过阈值的边缘区域进行低通滤波混合。例如,当检测到斜线边缘时,会模糊相邻像素以消除阶梯状锯齿‌。

核心流程‌:

  • 亮度计算:使用RGB转亮度公式luma = dot(rgb, float3(0.299, 0.587, 0.114)) 采用ITU-R BT.709标准权重.

  • 边缘检测:对比3x3区域内像素亮度差,超过阈值则标记为边缘

  • 方向判定:计算水平/垂直亮度梯度,确定边缘走向(NW-SE或NE-SW)

  • 混合执行:沿边缘方向进行5-tap滤波,加权平均相邻像素颜色

  • FXAA.shader

    • 关键参数说明
      • 亮度计算‌:采用0.2126729, 0.7151522, 0.0721750权重符合sRGB标准
      • 边缘阈值‌:edgeThresholdMin防止过度处理平滑区域,edgeThreshold动态适应高亮度区域
      • 方向判定‌:通过水平和垂直方向的二阶差分确定主边缘方向
      • 子像素混合‌:subpixelBlend控制亚像素级混合强度,改善细线表现
    • URP集成要点
      • 通过RenderFeature添加到URP渲染管线
      • 需在相机设置中禁用MSAA/TAA等冲突抗锯齿
      • 纹理采样使用URP标准的SAMPLE_TEXTURE2D
    Shader "Hidden/Universal Render Pipeline/FXAA"
    {
        HLSLINCLUDE
        #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
    
        TEXTURE2D(_MainTex);
        SAMPLER(sampler_MainTex);
        float4 _MainTex_TexelSize;
    
        // ITU-R BT.709亮度系数
        float Luminance(float3 rgb)
        {
            return dot(rgb, float3(0.2126729, 0.7151522, 0.0721750));
        }
    
        // 边缘检测结构体
        struct EdgeData {
            float m, n, e, s, w;
            float highest, lowest, contrast;
        };
    
        EdgeData SampleLumaNeighborhood(float2 uv)
        {
            EdgeData ed;
            float2 offset = _MainTex_TexelSize.xy;
    
            ed.m = Luminance(SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv).rgb);
            ed.n = Luminance(SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv + float2(0,  offset.y)).rgb);
            ed.e = Luminance(SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv + float2(offset.x, 0)).rgb);
            ed.s = Luminance(SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv - float2(0,  offset.y)).rgb);
            ed.w = Luminance(SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv - float2(offset.x, 0)).rgb);
    
            ed.highest = max(max(max(max(ed.n, ed.e), ed.s), ed.w), ed.m);
            ed.lowest = min(min(min(min(ed.n, ed.e), ed.s), ed.w), ed.m);
            ed.contrast = ed.highest - ed.lowest;
            return ed;
        }
    
        float4 FXAA_Pass(float2 uv)
        {
            // 参数配置
            float edgeThresholdMin = 0.03125;
            float edgeThreshold = 0.125;
            float subpixelBlend = 0.75;
    
            EdgeData ed = SampleLumaNeighborhood(uv);
    
            // 边缘检测条件
            if(ed.contrast < max(edgeThresholdMin, ed.highest * edgeThreshold))
                return SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv);
    
            // 计算混合方向
            float horizontal = abs(ed.n + ed.s - 2.0 * ed.m) * 2.0 + 
                              abs(ed.e + ed.w - 2.0 * ed.m);
            float vertical = abs(ed.e + ed.w - 2.0 * ed.m) * 2.0 + 
                            abs(ed.n + ed.s - 2.0 * ed.m);
    
            bool isHorizontal = horizontal >= vertical;
    
            // 边缘端点检测
            float2 edgeDir = isHorizontal ? 
                float2(0, _MainTex_TexelSize.y) : 
                float2(_MainTex_TexelSize.x, 0);
    
            // 5-tap混合
            float3 rgbA = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv - edgeDir * 0.5).rgb;
            float3 rgbB = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv + edgeDir * 0.5).rgb;
            float3 rgbC = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv - edgeDir).rgb;
            float3 rgbD = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv + edgeDir).rgb;
    
            // 加权混合
            float blendFactor = 0.5 * (Luminance(rgbA) + Luminance(rgbB)) - Luminance(ed.m);
            blendFactor = saturate(blendFactor / ed.contrast) * subpixelBlend;
    
            float3 finalColor = lerp(
                lerp(rgbC, rgbD, 0.5),
                lerp(rgbA, rgbB, 0.5),
                blendFactor
            );
    
            return float4(finalColor, 1.0);
        }
    
        ENDHLSL
    
        SubShader
        {
            Pass
            {
                Name "FXAA"
                HLSLPROGRAM
                #pragma vertex Vert
                #pragma fragment Frag
    
                struct Attributes {
                    float4 positionOS : POSITION;
                    float2 uv : TEXCOORD0;
                };
    
                struct Varyings {
                    float4 positionCS : SV_POSITION;
                    float2 uv : TEXCOORD0;
                };
    
                Varyings Vert(Attributes input)
                {
                    Varyings output;
                    output.positionCS = TransformObjectToHClip(input.positionOS.xyz);
                    output.uv = input.uv;
                    return output;
                }
    
                float4 Frag(Varyings input) : SV_Target
                {
                    return FXAA_Pass(input.uv);
                }
                ENDHLSL
            }
        }
    }
    

优势‌:

  • 性能消耗最低(仅需1次全屏采样)
  • 兼容所有GPU架构‌

劣势‌:

  • 导致画面整体模糊(尤其影响高光区域)
  • 无法处理时间性锯齿(如动态物体)‌

限制‌:

  • 不适用于HDRP的延迟渲染管线‌

子像素形态抗锯齿(SMAA)

分三阶段实现:边缘检测(基于颜色/深度差)、权重计算(分析边缘模式)、混合执行(沿边缘方向插值)。相比FXAA能保留更多高频细节,通过形态学处理识别像素级边缘走向。

核心原理流程

  • 边缘检测阶段‌:使用Sobel算子分析像素亮度梯度,生成边缘纹理(分为水平和垂直边缘)

  • 权重计算阶段‌:通过AreaTex和SearchTex分析边缘形态(L形/T形/对角线),计算混合权重

  • 混合执行阶段‌:根据权重对边缘像素进行双线性插值混合,保留高频细节

  • SMAA.hlsl

    • 关键实现解析
      • 三阶段架构‌:需创建三个独立Pass分别对应边缘检测、权重计算和混合阶段
      • 纹理资源‌:依赖预计算的AreaTex(存储混合模式)和SearchTex(存储搜索方向),需导入为Texture2D资源
      • 动态阈值‌:采用相对亮度差(0.1阈值)检测边缘,避免固定阈值导致的过检测
    • URP集成要点
      • RenderPass配置‌:在URP Renderer中按顺序添加三个RenderFeature
      • 纹理绑定‌:通过_AreaTex_SearchTex参数传递预计算纹理
      • 性能优化‌:使用linear_clamp_sampler减少纹理采样开销
    // 边缘检测阶段
    Texture2D _MainTex;
    Texture2D _BlendTex;
    SamplerState linear_clamp_sampler;
    
    // 预计算纹理
    Texture2D _AreaTex; // 存储混合模式(512x512)
    Texture2D _SearchTex; // 存储搜索方向(64x16)
    
    struct EdgeData {
        float2 uv;
        float4 offsets[3];
    };
    
    EdgeData SMAAEdgeDetectionVS(float4 position : POSITION, float2 uv : TEXCOORD0) {
        EdgeData output;
        output.uv = uv;
        float4 texelSize = _MainTex_TexelSize.xyxy * float4(1.0, 1.0, -1.0, -1.0);
        output.offsets[0] = uv.xyxy + texelSize.xyxy * float4(-1.0, 0.0, 0.0, -1.0);
        output.offsets[1] = uv.xyxy + texelSize.xyxy * float4(1.0, 0.0, 0.0, 1.0);
        output.offsets[2] = uv.xyxy + texelSize.xyxy * float4(-2.0, 0.0, 0.0, -2.0);
        return output;
    }
    
    float4 SMAAColorEdgeDetectionPS(EdgeData input) : SV_Target {
        float L = Luminance(_MainTex.Sample(linear_clamp_sampler, input.uv).rgb);
        float delta1 = Luminance(_MainTex.Sample(linear_clamp_sampler, input.offsets[0].xy).rgb) - L;
        float delta2 = L - Luminance(_MainTex.Sample(linear_clamp_sampler, input.offsets[0].zw).rgb);
        float2 edges = step(float2(0.1, 0.1), abs(float2(delta1, delta2)));
        return float4(edges, 0.0, 1.0);
    }
    
    // 权重计算阶段
    float4 SMAABlendingWeightCalculationPS(EdgeData input) : SV_Target {
        float2 area = _AreaTex.Sample(linear_clamp_sampler, input.uv).rg;
        float2 search = _SearchTex.Sample(linear_clamp_sampler, input.uv).rg;
        float4 weights = float4(area.r, area.g, search.r, search.g);
        return weights;
    }
    
    // 混合阶段
    float4 SMAANeighborhoodBlendingPS(EdgeData input) : SV_Target {
        float4 weights = _BlendTex.Sample(linear_clamp_sampler, input.uv);
        float3 color = _MainTex.Sample(linear_clamp_sampler, input.uv).rgb;
        float3 color1 = _MainTex.Sample(linear_clamp_sampler, input.uv + float2(weights.r, 0.0)).rgb;
        float3 color2 = _MainTex.Sample(linear_clamp_sampler, input.uv + float2(0.0, weights.g)).rgb;
        return float4(lerp(color, (color1 + color2) * 0.5, weights.b), 1.0);
    }
    

实现原理‌:

分三阶段处理:

  • 边缘检测‌:基于颜色/深度梯度识别锯齿边缘
  • 模式分析‌:通过形态学算法(如腐蚀/膨胀)确定边缘走向
  • 像素混合‌:沿检测到的边缘方向插值(如斜线边缘按45°方向混合)‌

优势‌:

  • 保留更多高频细节(如UI文字锐度)
  • 性能消耗仅为MSAA的1/3‌

劣势‌:

  • 对复杂光照锯齿(如SSR反射)效果有限‌

限制‌:

  • 需URP 12.0+版本支持‌

多重采样抗锯齿(MSAA)

在光栅化阶段对每个像素进行多重采样(2x/4x/8x),计算覆盖率和深度值后合并样本。仅对几何边缘有效,通过硬件加速实现物理级抗锯齿,但对着色锯齿无效且消耗显存带宽。

实现原理‌:

在光栅化阶段对每个像素进行多重采样(如4x MSAA采样4个深度/颜色值),合并时通过权重计算平滑边缘。例如,三角形边缘像素会混合部分覆盖的样本‌。

核心原理流程

  • 多重采样阶段‌:硬件在光栅化时对每个像素生成多个子样本(2x/4x/8x),分别计算深度和模板值

  • 样本合并阶段‌:通过加权平均子样本颜色值生成最终像素输出,平滑几何边缘锯齿

  • 深度一致性检测‌:自动处理子样本间的深度差异,保留锐利几何轮廓

  • MSAA_URP.shader

    • 实现解析
      • 硬件级集成‌:MSAA通过#pragma multi_compile指令激活GPU硬件支持,无需手动实现采样逻辑
      • 深度处理优化‌:自动处理子样本间的深度差异,保留几何边缘锐度
      • 光照兼容性‌:演示与URP光照系统的无缝集成,阴影计算同样受益于MSAA
    • URP配置要点
      • 质量设置‌:在URP Asset中启用MSAA(2x/4x/8x)
      • 渲染目标‌:需使用支持MSAA的RenderTexture格式(如RenderTextureFormat.DefaultHDR
      • 性能考量‌:4x MSAA在移动端TBR架构上性能损耗较低,适合高端移动设备
    Shader "Universal Render Pipeline/MSAA"
    {
        Properties
        {
            _MainTex ("Base (RGB)", 2D) = "white" {}
        }
    
        SubShader
        {
            Tags { "RenderType"="Opaque" "RenderPipeline"="UniversalPipeline" }
    
            Pass
            {
                Name "MSAA_Pass"
                HLSLPROGRAM
                #pragma vertex vert
                #pragma fragment frag
                #pragma multi_compile _ _MAIN_LIGHT_SHADOWS
                #pragma multi_compile _ _MAIN_LIGHT_SHADOWS_CASCADE
                #pragma multi_compile _ _ADDITIONAL_LIGHTS
                #pragma multi_compile _ _ADDITIONAL_LIGHT_SHADOWS
                #pragma multi_compile _ _SHADOWS_SOFT
    
                #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
                #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
    
                struct Attributes
                {
                    float4 positionOS : POSITION;
                    float2 uv : TEXCOORD0;
                };
    
                struct Varyings
                {
                    float4 positionCS : SV_POSITION;
                    float2 uv : TEXCOORD0;
                    float3 positionWS : TEXCOORD1;
                };
    
                TEXTURE2D(_MainTex);
                SAMPLER(sampler_MainTex);
    
                Varyings vert(Attributes input)
                {
                    Varyings output;
                    VertexPositionInputs vertexInput = GetVertexPositionInputs(input.positionOS.xyz);
                    output.positionCS = vertexInput.positionCS;
                    output.uv = input.uv;
                    output.positionWS = vertexInput.positionWS;
                    return output;
                }
    
                half4 frag(Varyings input) : SV_Target
                {
                    // 硬件自动处理MSAA采样
                    half4 color = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv);
    
                    // 光照计算(演示MSAA与光照的兼容性)
                    Light mainLight = GetMainLight();
                    float3 N = normalize(cross(ddy(input.positionWS), ddx(input.positionWS)));
                    float diffuse = saturate(dot(N, mainLight.direction));
                    return color * (diffuse * mainLight.color + mainLight.shadowAttenuation);
                }
                ENDHLSL
            }
        }
    }
    

优势‌:

  • 物理级抗锯齿(对几何边缘效果最佳)
  • 支持硬件加速(如DX12的MSAA优化)‌

劣势‌:

  • 显存带宽消耗高(8x MSAA增加50%带宽)
  • 对着色器锯齿无效(如纹理过滤)‌

限制‌:

  • 需关闭URP的延迟渲染功能‌

    | 特性 | MSAA | FXAA/SMAA |
    | --- | --- | --- |
    | 处理阶段 | 光栅化阶段 | 后处理阶段 |
    | 效果范围 | 仅几何边缘 | 全图像 |
    | 性能消耗 | 中-高(取决于采样数) | 低-中 |
    | 兼容性 | 需硬件支持 | 全平台通用 |

时间抗锯齿(TAA)

利用历史帧数据和运动向量,将当前帧与前一帧抗锯齿结果进行时域混合。通过重投影技术解决动态物体问题,需配合动态模糊抑制重影现象,对动态场景效果最佳。

实现原理‌:

利用历史帧数据(运动向量+深度缓冲)进行时域混合:

  • 重投影‌:将当前帧与历史帧对齐
  • 抖动补偿‌:通过随机抖动减少重影
  • 累积滤波‌:加权融合多帧结果‌

核心原理流程

  • 帧间抖动采样‌:通过Halton序列对投影矩阵施加微小偏移,使采样点在时间维度上均匀分布
  • 运动向量追踪‌:利用_CameraMotionVectorsTexture记录像素位移,结合深度纹理处理边缘运动
  • 历史帧混合‌:通过线性插值(lerp)将当前帧与历史缓冲数据融合,动态调整混合权重
  • TAA.shader
    • 关键技术解析

      • 运动向量处理‌:通过_CameraMotionVectorsTexture获取像素位移,确保历史帧采样位置准确
      • 动态混合策略‌:基于运动向量长度调整混合权重,静态区域权重低(保留更多历史数据),动态区域权重高(减少拖影)
      • 投影矩阵抖动‌:在C#脚本中修改相机投影矩阵实现Halton序列偏移,需配合UNITY_MATRIX_PREV_VP矩阵使用
    • URP集成要点

      • RenderFeature配置‌:需创建TAARenderFeature并设置执行时机为RenderPassEvent.BeforeRenderingPostProcessing
      • 双缓冲历史纹理‌:使用两个RenderTexture交替存储历史帧数据,避免读写冲突
      • 运动向量生成‌:需为动态物体添加MotionVector Pass,静态物体可直接使用相机运动矩阵
    • 性能优化建议

      • 分辨率降采样‌:对历史缓冲使用半分辨率纹理(需配合双线性滤波)
      • 边缘锐化后处理‌:在TAA后添加FXAA或自定义锐化Pass补偿过度模糊
      • 移动端适配‌:将运动向量计算移至顶点着色器,减少Fragment计算量
      • 该方案相比SMAA能有效减少次像素闪烁,特别适合处理动态植被和细小网格的锯齿问题。实际部署时需注意处理透明物体的运动向量生成问题
      Shader "Universal Render Pipeline/TAA"
      {
          Properties
          {
              _MainTex("Base (RGB)", 2D) = "white" {}
              _HistoryTex("History Buffer", 2D) = "black" {}
          }
      
          HLSLINCLUDE
          #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
      
          TEXTURE2D(_MainTex);
          TEXTURE2D(_HistoryTex);
          TEXTURE2D(_CameraMotionVectorsTexture);
          SAMPLER(sampler_linear_clamp);
      
          struct Varyings
          {
              float4 positionCS : SV_POSITION;
              float2 uv : TEXCOORD0;
          };
      
          // Halton序列生成抖动偏移
          float2 GetJitterOffset(uint frameIndex)
          {
              const float2 seq = float2(
                  0.5f * (frameIndex % 8 + 1) / 8.0f,
                  0.5f * (frameIndex % 16 + 1) / 16.0f
              );
              return (seq - 0.5f) * _ScreenParams.zw;
          }
      
          Varyings Vert(uint vertexID : SV_VertexID)
          {
              Varyings output;
              output.positionCS = GetFullScreenTriangleVertexPosition(vertexID);
              output.uv = GetFullScreenTriangleTexCoord(vertexID);
              return output;
          }
      
          float4 Frag(Varyings input) : SV_Target
          {
              // 获取运动向量
              float2 motion = SAMPLE_TEXTURE2D(_CameraMotionVectorsTexture, sampler_linear_clamp, input.uv).xy;
      
              // 采样当前帧和历史帧
              float3 current = SAMPLE_TEXTURE2D(_MainTex, sampler_linear_clamp, input.uv).rgb;
              float3 history = SAMPLE_TEXTURE2D(_HistoryTex, sampler_linear_clamp, input.uv - motion).rgb;
      
              // 动态混合权重(基于运动向量长度)
              float blendFactor = saturate(length(motion) * 10.0f);
              return float4(lerp(history, current, blendFactor), 1.0);
          }
          ENDHLSL
      
          SubShader
          {
              Pass
              {
                  Name "TAA_Pass"
                  HLSLPROGRAM
                  #pragma vertex Vert
                  #pragma fragment Frag
                  ENDHLSL
              }
          }
      }
      

优势‌:

  • 动态场景抗锯齿效果最佳(如快速移动的物体)
  • 支持复杂光照(如HDRP的全局光照)‌

劣势‌:

  • 极端情况下出现重影(如快速切换场景)‌

限制‌:

  • 需启用运动向量(Motion Vectors)
  • 不兼容动态分辨率‌

URP中的选择策略

性能优先场景

选择FXAA:移动端或VR项目需保持60FPS时,其性能消耗仅为SMAA的60%。

画质优先场景

  • 静态场景:MSAA 4x/8x(需关闭延迟渲染)
  • 动态场景:TAA(需启用运动向量)
  • 风格化渲染:SMAA(保留清晰边缘)

特殊配置建议

  • WebGL项目:避免MSAA(内存限制),推荐SMAA
  • VR项目:FXAA+TAA组合减少动态模糊
  • HDRP管线:优先TAA解决复杂光照锯齿

技术决策矩阵:

指标 FXAA SMAA MSAA TAA
几何边缘
着色锯齿 无效
动态场景
GPU消耗 1x 1.5x 3-8x 2x
显存占用

注:消耗基准以FXAA为1x计算


【从UnityURP开始探索游戏渲染】专栏-直达
(欢迎点赞留言探讨,更多人加入进来能更加完善这个探索的过程,🙏)

posted @ 2025-10-30 08:26  SmalBox  阅读(66)  评论(0)    收藏  举报