【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和深度图进行世界坐标重建与遮蔽计算。
- 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开始探索游戏渲染】专栏-直达
(欢迎点赞留言探讨,更多人加入进来能更加完善这个探索的过程,🙏)

SSAO概述与作用 SSAO(Screen Space Ambient Occlusion)是一种基于屏幕空间的全局环境光遮蔽技术,它通过计算场景中物体间的遮蔽关系来增强场景的深度感和真实感。在Uni
浙公网安备 33010602011771号