【從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和深度圖進行世界座標重建與遮蔽計算。
- Renderer Feature:需創建獨立Feature並配置
-
示例代碼
-
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開始探索遊戲渲染】專欄-直達
(歡迎點贊留言探討,更多人加入進來能更加完善這個探索的過程,🙏)