【URP】Unity[RendererFeatures]屏幕空间环境光遮蔽SSAO

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

SSAO概述与作用

SSAO(Screen Space Ambient Occlusion)是一种基于屏幕空间的全局环境光遮蔽技术,它通过计算场景中物体间的遮蔽关系来增强场景的深度感和真实感。在Unity URP中,SSAO通过Renderer Feature实现,作为URP渲染管线的扩展模块插入到渲染流程中。

SSAO的主要作用包括:

  • 增强场景深度感知,使物体间的接触区域产生自然阴影
  • 提升场景细节表现,特别是角落和凹陷处的视觉效果
  • 无需额外光照计算即可增强场景的空间感
  • 相比传统AO技术性能开销更低

SSAO发展历史

SSAO技术起源于2007年,由Crytek公司在《孤岛危机》中首次实现并商业化应用。随后该技术经历了多个发展阶段:

  • 早期SSAO‌(2007-2010):基于深度缓冲的简单采样,存在明显的噪点和性能问题
  • HBAO‌(2010-2013):NVIDIA提出的Horizon-Based AO,提高了精度但计算量较大
  • SSDO‌(2013-2015):Screen Space Directional Occlusion,考虑了光线方向
  • 现代SSAO‌(2015至今):结合了降噪技术和自适应采样,如GTAO(Ground Truth AO)

Unity自2018版开始将SSAO集成到URP中,通过Renderer Feature方式提供灵活的配置选项。

SSAO实现原理

SSAO在URP中的实现主要分为以下步骤:

  • 深度/法线信息采集‌:从摄像机深度纹理和法线纹理获取场景几何信息
  • 采样点生成‌:在像素周围半球空间内生成随机采样点
  • 遮蔽计算‌:比较采样点深度与场景深度,计算遮蔽值
  • 模糊处理‌:通过双边滤波消除噪点
  • 合成输出‌:将AO效果与场景颜色混合

SSAO核心原理

  • 环境光遮蔽基础

    AO通过模拟物体表面因几何遮挡导致的环境光衰减,增强场景深度感。其数学本质是法线半球面上可见性函数的积分计算。SSAO在屏幕空间利用深度/法线缓冲近似这一过程,避免传统AO的复杂光线求交。

  • 屏幕空间实现机制

    • 深度重建‌:通过深度缓冲和相机投影矩阵反推像素的世界坐标,公式为:

      c 复制代码
      float3 clipVec = float3(ndcPos.x, ndcPos.y, 1.0) * _ProjectionParams.z;
      float3 viewVec = mul(unity_CameraInvProjection, clipVec.xyzz).xyz;
    • 法向半球采样‌:在像素法线方向构建半球采样核,对比周围深度值计算遮蔽因子。深度更高的采样点计数越多,遮蔽效果越强。

URP实现流程

  • 关键组件
    • Renderer Feature ‌:需创建独立Feature并配置ScriptableRenderPassInput.Normal以获取法线缓冲。
    • Shader计算‌:结合_CameraNormalsTexture和深度图进行世界坐标重建与遮蔽计算。
  • 示例代码
    • SSAORendererFeature.cs

      csharp 复制代码
      using UnityEngine;
      using UnityEngine.Rendering;
      using UnityEngine.Rendering.Universal;
      
      public class SSAORendererFeature : ScriptableRendererFeature {
          class SSAOPass : ScriptableRenderPass {
              public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData) {
                  ConfigureInput(ScriptableRenderPassInput.Normal);
              }
              // 实现Execute方法进行SSAO计算
          }
          public override void Create() {
              m_SSAOPass = new SSAOPass();
              m_SSAOPass.renderPassEvent = RenderPassEvent.AfterRenderingOpaques;
          }
      }
    • SSAO.shader

      c 复制代码
      Shader "Hidden/SSAO" {
          Properties {
              _Radius ("采样半径", Range(0.1, 5)) = 1
              _Intensity ("强度", Range(0, 10)) = 1
          }
          SubShader {
              Pass {
                  // 深度重建与采样核计算代码
              }
          }
      }

参数解析

参数 作用 典型值
_Radius 控制采样范围 0.5-2.0
_Intensity 遮蔽强度 1.0-3.0
_SampleCount 采样点数量 16-32

性能优化建议

  • 降低采样数(如16个)并配合噪声纹理
  • 使用双边滤波消除噪点
  • 仅在高端设备启用(移动端需谨慎)

完整Unity URP实现示例

以下是完整的SSAO Renderer Feature实现流程:

  • SSAORendererFeature.cs

    csharp 复制代码
    using UnityEngine;
    using UnityEngine.Rendering;
    using UnityEngine.Rendering.Universal;
    
    public class SSAORendererFeature : ScriptableRendererFeature
    {
        [System.Serializable]
        public class SSAOSettings
        {
            public RenderPassEvent renderPassEvent = RenderPassEvent.AfterRenderingOpaques;
            public Material blitMaterial = null;
            public float radius = 0.5f;
            public float intensity = 1.0f;
            public float power = 2.0f;
            public int sampleCount = 16;
            public float bias = 0.025f;
            public float downsampling = 1;
            public bool blur = true;
            public float blurRadius = 1.0f;
        }
    
        public SSAOSettings settings = new SSAOSettings();
        private SSAORenderPass ssaoPass;
    
        public override void Create()
        {
            ssaoPass = new SSAORenderPass(settings);
        }
    
        public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
        {
            if (settings.blitMaterial == null)
            {
                Debug.LogWarning("Missing SSAO material");
                return;
            }
            renderer.EnqueuePass(ssaoPass);
        }
    }
    
    public class SSAORenderPass : ScriptableRenderPass
    {
        private Material ssaoMaterial;
        private SSAORendererFeature.SSAOSettings settings;
        private RenderTargetIdentifier source;
        private RenderTargetHandle tempTexture;
        private RenderTargetHandle tempTexture2;
    
        public SSAORenderPass(SSAORendererFeature.SSAOSettings settings)
        {
            this.settings = settings;
            this.renderPassEvent = settings.renderPassEvent;
            tempTexture.Init("_TempSSAOTexture");
            tempTexture2.Init("_TempSSAOTexture2");
        }
    
        public void Setup(RenderTargetIdentifier source)
        {
            this.source = source;
        }
    
        public override void Configure(CommandBuffer cmd, RenderTextureDescriptor cameraTextureDescriptor)
        {
            if (settings.downsampling > 1)
            {
                cameraTextureDescriptor.width = (int)(cameraTextureDescriptor.width / settings.downsampling);
                cameraTextureDescriptor.height = (int)(cameraTextureDescriptor.height / settings.downsampling);
            }
            cmd.GetTemporaryRT(tempTexture.id, cameraTextureDescriptor, FilterMode.Bilinear);
            cmd.GetTemporaryRT(tempTexture2.id, cameraTextureDescriptor, FilterMode.Bilinear);
        }
    
        public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
        {
            CommandBuffer cmd = CommandBufferPool.Get("SSAO");
    
            // Set SSAO material properties
            ssaoMaterial = settings.blitMaterial;
            ssaoMaterial.SetFloat("_Radius", settings.radius);
            ssaoMaterial.SetFloat("_Intensity", settings.intensity);
            ssaoMaterial.SetFloat("_Power", settings.power);
            ssaoMaterial.SetInt("_SampleCount", settings.sampleCount);
            ssaoMaterial.SetFloat("_Bias", settings.bias);
    
            // First pass - generate AO
            Blit(cmd, source, tempTexture.Identifier(), ssaoMaterial, 0);
    
            if (settings.blur)
            {
                // Second pass - horizontal blur
                ssaoMaterial.SetVector("_Direction", new Vector2(settings.blurRadius, 0));
                Blit(cmd, tempTexture.Identifier(), tempTexture2.Identifier(), ssaoMaterial, 1);
    
                // Third pass - vertical blur
                ssaoMaterial.SetVector("_Direction", new Vector2(0, settings.blurRadius));
                Blit(cmd, tempTexture2.Identifier(), tempTexture.Identifier(), ssaoMaterial, 1);
            }
    
            // Final pass - composite
            Blit(cmd, tempTexture.Identifier(), source, ssaoMaterial, 2);
    
            context.ExecuteCommandBuffer(cmd);
            CommandBufferPool.Release(cmd);
        }
    
        public override void FrameCleanup(CommandBuffer cmd)
        {
            cmd.ReleaseTemporaryRT(tempTexture.id);
            cmd.ReleaseTemporaryRT(tempTexture2.id);
        }
    }
  • SSAO.shader

    c 复制代码
    Shader "Hidden/SSAO"
    {
        Properties
        {
            _MainTex ("Texture", 2D) = "white" {}
        }
    
        SubShader
        {
            Cull Off ZWrite Off ZTest Always
    
            Pass // 0: Generate AO
            {
                CGPROGRAM
                #pragma vertex vert
                #pragma fragment frag
                #include "UnityCG.cginc"
    
                struct appdata
                {
                    float4 vertex : POSITION;
                    float2 uv : TEXCOORD0;
                };
    
                struct v2f
                {
                    float2 uv : TEXCOORD0;
                    float4 vertex : SV_POSITION;
                };
    
                v2f vert(appdata v)
                {
                    v2f o;
                    o.vertex = UnityObjectToClipPos(v.vertex);
                    o.uv = v.uv;
                    return o;
                }
    
                sampler2D _MainTex;
                sampler2D _CameraDepthNormalsTexture;
                float _Radius;
                float _Intensity;
                float _Power;
                int _SampleCount;
                float _Bias;
    
                float3 GetPosition(float2 uv)
                {
                    float depth;
                    float3 normal;
                    DecodeDepthNormal(tex2D(_CameraDepthNormalsTexture, uv), depth, normal);
                    float4 pos = float4(uv * 2 - 1, depth * 2 - 1, 1);
                    pos = mul(unity_CameraInvProjection, pos);
                    return pos.xyz / pos.w;
                }
    
                float3 GetNormal(float2 uv)
                {
                    float depth;
                    float3 normal;
                    DecodeDepthNormal(tex2D(_CameraDepthNormalsTexture, uv), depth, normal);
                    return normal;
                }
    
                float random(float2 uv)
                {
                    return frac(sin(dot(uv, float2(12.9898, 78.233))) * 43758.5453);
                }
    
                float3 getSampleKernel(int i, float2 uv)
                {
                    float r = random(uv * (i+1));
                    float theta = random(uv * (i+2)) * 2 * 3.1415926;
                    float phi = random(uv * (i+3)) * 3.1415926 * 0.5;
    
                    float x = r * sin(phi) * cos(theta);
                    float y = r * sin(phi) * sin(theta);
                    float z = r * cos(phi);
    
                    return normalize(float3(x, y, z));
                }
    
                float frag(v2f i) : SV_Target
                {
                    float3 pos = GetPosition(i.uv);
                    float3 normal = GetNormal(i.uv);
    
                    float occlusion = 0.0;
                    for(int j = 0; j < _SampleCount; j++)
                    {
                        float3 sampleKernel = getSampleKernel(j, i.uv);
                        sampleKernel = reflect(sampleKernel, normal);
    
                        float3 samplePos = pos + sampleKernel * _Radius;
                        float4 sampleClipPos = mul(unity_CameraProjection, float4(samplePos, 1.0));
                        sampleClipPos.xy /= sampleClipPos.w;
                        sampleClipPos.xy = sampleClipPos.xy * 0.5 + 0.5;
    
                        float sampleDepth = GetPosition(sampleClipPos.xy).z;
                        float rangeCheck = smoothstep(0.0, 1.0, _Radius / abs(pos.z - sampleDepth));
                        occlusion += (sampleDepth >= samplePos.z + _Bias ? 1.0 : 0.0) * rangeCheck;
                    }
    
                    occlusion = 1.0 - (occlusion / _SampleCount);
                    return pow(occlusion, _Power) * _Intensity;
                }
                ENDCG
            }
    
            Pass // 1: Blur
            {
                CGPROGRAM
                #pragma vertex vert
                #pragma fragment frag
                #include "UnityCG.cginc"
    
                struct appdata
                {
                    float4 vertex : POSITION;
                    float2 uv : TEXCOORD0;
                };
    
                struct v2f
                {
                    float2 uv : TEXCOORD0;
                    float4 vertex : SV_POSITION;
                };
    
                v2f vert(appdata v)
                {
                    v2f o;
                    o.vertex = UnityObjectToClipPos(v.vertex);
                    o.uv = v.uv;
                    return o;
                }
    
                sampler2D _MainTex;
                float4 _MainTex_TexelSize;
                float2 _Direction;
    
                float frag(v2f i) : SV_Target
                {
                    float2 texelSize = _MainTex_TexelSize.xy;
                    float result = 0.0;
                    float weightSum = 0.0;
    
                    for(int x = -2; x <= 2; x++)
                    {
                        float weight = exp(-(x*x) / (2.0 * 2.0));
                        float2 offset = _Direction * x * texelSize;
                        result += tex2D(_MainTex, i.uv + offset).r * weight;
                        weightSum += weight;
                    }
    
                    return result / weightSum;
                }
                ENDCG
            }
    
            Pass // 2: Composite
            {
                CGPROGRAM
                #pragma vertex vert
                #pragma fragment frag
                #include "UnityCG.cginc"
    
                struct appdata
                {
                    float4 vertex : POSITION;
                    float2 uv : TEXCOORD0;
                };
    
                struct v2f
                {
                    float2 uv : TEXCOORD0;
                    float4 vertex : SV_POSITION;
                };
    
                v2f vert(appdata v)
                {
                    v2f o;
                    o.vertex = UnityObjectToClipPos(v.vertex);
                    o.uv = v.uv;
                    return o;
                }
    
                sampler2D _MainTex;
                sampler2D _SSAOTex;
    
                float4 frag(v2f i) : SV_Target
                {
                    float4 color = tex2D(_MainTex, i.uv);
                    float ao = tex2D(_SSAOTex, i.uv).r;
                    return color * ao;
                }
                ENDCG
            }
        }
    }

SSAO参数详解与使用指南

参数含义与调整建议

  • Radius 半径
    • 含义:控制采样点的搜索半径
    • 范围:0.1-2.0
    • 用例:小半径适合细节丰富的场景,大半径适合开阔场景
  • Intensity 强度
    • 含义:控制AO效果的强度
    • 范围:0.5-4.0
    • 用例:值越大,遮蔽效果越明显
  • Power 幂次
    • 含义:控制AO效果的对比度
    • 范围:1.0-4.0
    • 用例:值越大,暗部越暗,亮部越亮
  • Sample Count 采样数
    • 含义:每个像素的采样点数
    • 范围:8-32
    • 用例:值越高效果越平滑但性能消耗越大
  • Bias 偏移
    • 含义:防止自遮蔽的偏移量
    • 范围:0.01-0.1
    • 用例:值过小会产生噪点,值过

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

(欢迎点赞留言探讨,更多人加入进来能更加完善这个探索的过程,🙏)