博客 / 詳情

返回

【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的複雜光線求交。

  • 屏幕空間實現機制

    • 深度重建‌:通過深度緩衝和相機投影矩陣反推像素的世界座標,公式為:

      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

      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

      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

    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

    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開始探索遊戲渲染】專欄-直達
(歡迎點贊留言探討,更多人加入進來能更加完善這個探索的過程,🙏)
user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.