URP - RendererFeature :SSAO
SSAO屏幕空间阴影
独立作品
让周围轻一点,文字近一点。
作品主视觉点击画面,查看细节
- ENGINE
- Unity · URP
- TECH
- rendering · 后处理 · Shader · unity · URP · Raymarching · Compute Shader · RenderFeature
- ROLE
- 工具 · Shader
- YEAR
- 2026
案例目录12 小节
SSAO原理
相关网站: https://learnopengl-cn.github.io/05%20Advanced%20Lighting/09%20SSAO/ 实现方法和代码参考上述网站。主要计算原理其实就是对采样点都半圆区域进行随机采样。采样的点是可以理解为虚拟点计算虚拟点的深度,然后和深度图中真正几何的表面深度进行比较,如果虚拟点深度值更大,就表示被遮蔽
球形采样

半球采样
因为球形采样的原因,球形采样有一半的点在地面以下,或者理解为会计算到模型内侧,有一半被埋进几何体里,但那是模型内部,根本不应该参与遮蔽计算。结果就是平坦物体也显示出很强的遮蔽,变暗,不符合物理直觉,所以解决这个问题就是需要使用半球采样,根据模型法线走

部分实现步骤
基本步骤都是和openGL上是一样的,下面只写关键部分
传递矩阵

重建坐标
ndc空间是-1~1,所以对UV进行重映射,然后根据深度和逆投影矩阵进行计算还原观察空间的坐标,因为有透视除法所以需要除去w分量

采样方向翻转
防止采样点落到模型内部,所以进行方向判断,然后对采样方向进行翻转,可以防止过黑 注意事项是这里的采样方向转换到法线空间,是先向量在乘矩阵,这和OpenGL上是不一样的

对比如下

遮蔽权重
DX的SSAO实现中加了针对夹角大小或者说前后距离的遮蔽值权重

我将这个权重引入到了代码中

对比来说我觉得加上权重会更好

最终效果

完整代码
C#代码
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
public class CustomSSAO : ScriptableRendererFeature
{
//==========参数============
[System.Serializable]
public class Settings
{
public Shader CustomSSAOShader;
public float Intensity = 1f;
public float Radius = 1f;
public float Bias = 0.25f;
public enum Quality
{
Low, //16
Medium, //32
High, //64
Most //128
};
public Quality quality = Quality.Medium;
}
public Settings settings = new Settings();
CustomSSAOPass pass;
Material material;
public override void Create()
{
if(settings.CustomSSAOShader == null) return;
if(material == null || material.shader != settings.CustomSSAOShader)
{
CoreUtils.Destroy(material);
material = CoreUtils.CreateEngineMaterial(settings.CustomSSAOShader);
}
pass = new CustomSSAOPass(settings, material);
pass.renderPassEvent = RenderPassEvent.BeforeRenderingPostProcessing;
}
public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
{
// 过滤相机,只对主相机生效:
// if (renderingData.cameraData.cameraType != CameraType.Game) return;
if(settings.CustomSSAOShader == null || pass == null || material == null) return;
pass.Setup(renderer.cameraColorTarget);
renderer.EnqueuePass(pass);
}
protected override void Dispose(bool disposing)
{
CoreUtils.Destroy(material);
if (pass != null && pass.noiseTexture != null)
{
Object.DestroyImmediate(pass.noiseTexture);
}
}
}
public class CustomSSAOPass : ScriptableRenderPass
{
CustomSSAO.Settings settings;
Material material;
RenderTargetIdentifier cameraColorTarget; // 相机颜色 RT
public Texture2D noiseTexture;
Vector4[] samples;
private int _SSAORTID;
private RenderTargetIdentifier _SSAORT;
private int _BlurRTID;
private RenderTargetIdentifier _BlurRT;
public CustomSSAOPass(CustomSSAO.Settings settings, Material material)
{
this.settings = settings;
this.material = material;
_SSAORTID = Shader.PropertyToID("_SSAORT");
_BlurRTID = Shader.PropertyToID("_BlurRT");
ConfigureInput(ScriptableRenderPassInput.Normal);
InitSSAOData();
}
public void Setup(RenderTargetIdentifier cameraColorTarget)
{
this.cameraColorTarget = cameraColorTarget;
}
public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData)
{
// 不需要临时 RT 时可以删掉整个函数
var desc = renderingData.cameraData.cameraTargetDescriptor;
desc.depthBufferBits = 0; // 后处理 RT 不需要深度
cmd.GetTemporaryRT(_SSAORTID, desc);
cmd.GetTemporaryRT(_BlurRTID, desc);
_SSAORT = new RenderTargetIdentifier(_SSAORTID);
_BlurRT = new RenderTargetIdentifier(_BlurRTID);
}
public void InitSSAOData()
{
int sampleCount = 0;
const int MAX_SAMPLE_COUNT = 128;
samples = new Vector4[MAX_SAMPLE_COUNT];
switch(settings.quality)
{
case CustomSSAO.Settings.Quality.Low:
sampleCount = 16;
break;
case CustomSSAO.Settings.Quality.Medium:
sampleCount = 32;
break;
case CustomSSAO.Settings.Quality.High:
sampleCount = 64;
break;
case CustomSSAO.Settings.Quality.Most:
sampleCount = 128;
break;
}
// 1. 生成64个随机采样向量
//samples = new Vector4[sampleCount];
for (int i = 0; i < sampleCount; i++)
{
Vector4 sample = new Vector4(
Random.Range(-1f, 1f),
Random.Range(-1f, 1f),
Random.Range(0f, 1f), // Z只取正,保证在半球内
0f
);
float length = Mathf.Sqrt(sample.x * sample.x + sample.y * sample.y + sample.z * sample.z);
sample.x /= length;
sample.y /= length;
sample.z /= length;
// 让采样点集中在靠近原点的地方
float scale = (float)i / sampleCount;
scale = Mathf.Lerp(0.1f, 1.0f, scale * scale);
sample *= scale;
samples[i] = sample;
}
// 2. 生成4x4随机旋转纹理(用来打破采样规律性)
if (noiseTexture == null)
{
noiseTexture = new Texture2D(4, 4, TextureFormat.RGBAFloat, false);
noiseTexture.wrapMode = TextureWrapMode.Repeat;
noiseTexture.filterMode = FilterMode.Point; // SSAO 噪声不需要线性过滤
}
Color[] noiseColors = new Color[16];
for (int i = 0; i < 16; i++)
{
noiseColors[i] = new Color(
Random.Range(-1f, 1f),
Random.Range(-1f, 1f),
0f,
0f
);
}
noiseTexture.SetPixels(noiseColors);
noiseTexture.Apply();
material.SetVectorArray("_Samples", samples);
material.SetTexture("_NoiseTex", noiseTexture);
material.DisableKeyword("_SSAO_QUALITY_LOW");
material.DisableKeyword("_SSAO_QUALITY_MEDIUM");
material.DisableKeyword("_SSAO_QUALITY_HIGH");
material.DisableKeyword("_SSAO_QUALITY_MOST");
}
public void UpdateMaterialProperties(RenderingData renderingData)
{
//float noiseScale = (float)(Mathf.Sqrt(Screen.width * Screen.height) / (4.0 * (Screen.width + Screen.height)));
float noiseScaleX = (float)(Screen.width / 4.0);
float noiseScaleY = (float)(Screen.height / 4.0);
//material.SetFloat("_NoiseScaleX", settings.NoiseScale);
material.SetFloat("_NoiseScaleX", noiseScaleX);
material.SetFloat("_NoiseScaleY", noiseScaleY);
material.SetFloat("_Intensity", settings.Intensity);
material.SetFloat("_Radius", settings.Radius);
material.SetFloat("_Bias", settings.Bias);
switch(settings.quality)
{
case CustomSSAO.Settings.Quality.Low:
material.EnableKeyword("_SSAO_QUALITY_LOW");
break;
case CustomSSAO.Settings.Quality.Medium:
material.EnableKeyword("_SSAO_QUALITY_MEDIUM");
break;
case CustomSSAO.Settings.Quality.High:
material.EnableKeyword("_SSAO_QUALITY_HIGH");
break;
case CustomSSAO.Settings.Quality.Most:
material.EnableKeyword("_SSAO_QUALITY_MOST");
break;
}
// 传矩阵
Camera camera = renderingData.cameraData.camera;
Matrix4x4 proj = GL.GetGPUProjectionMatrix(camera.projectionMatrix, false);
Matrix4x4 viewMatrix = camera.worldToCameraMatrix;
material.SetMatrix("_ProjectionMatrix", proj);
material.SetMatrix("_CameraViewMatrix", viewMatrix);
material.SetMatrix("_InverseProjection", proj.inverse);
}
public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
{
var cmd = CommandBufferPool.Get("CustomSSAOPass");
UpdateMaterialProperties(renderingData);
//执行SSAO Pass
Blit(cmd, cameraColorTarget, _SSAORT, material, 0);
//模糊
Blit(cmd, _SSAORT, _BlurRT, material, 1);
//合成
Blit(cmd, cameraColorTarget, _SSAORT, material, 3);
cmd.SetGlobalTexture("_SceneColor", _SSAORT);
Blit(cmd, _BlurRT, cameraColorTarget, material, 2);
context.ExecuteCommandBuffer(cmd);
CommandBufferPool.Release(cmd);
}
public override void OnCameraCleanup(CommandBuffer cmd)
{
cmd.ReleaseTemporaryRT(_SSAORTID);
cmd.ReleaseTemporaryRT(_BlurRTID);
}
}
Shader代码
Shader "Hidden/CustomSSAO"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
Cull Off
ZWrite Off
ZTest Always
HLSLINCLUDE
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/DeclareDepthTexture.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/DeclareNormalsTexture.hlsl"
#pragma shader_feature_local _SSAO_QUALITY_LOW
#pragma shader_feature_local _SSAO_QUALITY_MEDIUM
#pragma shader_feature_local _SSAO_QUALITY_HIGH
#pragma shader_feature_local _SSAO_QUALITY_MOST
float4 _Samples[128];
float _Radius;
float _Bias;
float _NoiseScaleX;
float _NoiseScaleY;
float _Intensity;
float4x4 _InverseProjection;
float4x4 _ProjectionMatrix;
float4x4 _CameraViewMatrix;
float4 _MainTex_ST;
TEXTURE2D(_MainTex);
SAMPLER(sampler_MainTex);
TEXTURE2D(_NoiseTex);
SAMPLER(sampler_NoiseTex);
TEXTURE2D(_SceneColor);
SAMPLER(sampler_SceneColor);
// 采样深度并线性化
float SampleDepth(float2 uv)
{
float rawDepth = SampleSceneDepth(uv);
return LinearEyeDepth(rawDepth, _ZBufferParams);
}
struct Attributes
{
float4 positionOS : POSITION;
float2 uv : TEXCOORD0;
//UNITY_VERTEX_INPUT_INSTANCE_ID //VR兼容
};
struct Varyings
{
float4 positionCS : SV_POSITION;
float2 uv : TEXCOORD0;
//UNITY_VERTEX_OUTPUT_STEREO //VR兼容
};
Varyings vert(Attributes input)
{
Varyings output;
//UNITY_SETUP_INSTANCE_ID(input); //VR兼容
//UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output);
output.positionCS = TransformObjectToHClip(input.positionOS.xyz);
output.uv = input.uv;
return output;
}
#if defined(_SSAO_QUALITY_MOST)
static const float SAMPLE_COUNT = 128;
#elif defined(_SSAO_QUALITY_HIGH)
static const float SAMPLE_COUNT = 64;
#elif defined(_SSAO_QUALITY_MEDIUM)
static const float SAMPLE_COUNT = 32;
#else
static const float SAMPLE_COUNT = 16;
#endif
ENDHLSL
// Pass 0
Pass
{
Name "SSAO"
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag_ssao
float4 frag_ssao(Varyings input) : SV_Target
{
float2 uv = input.uv;
float rawDepth = SampleSceneDepth(uv);
float depth = LinearEyeDepth(rawDepth, _ZBufferParams);
//方法1:利用从 C# 传过来的 _InverseProjection 重建观察空间坐标
float4 ndc = float4(uv * 2.0 - 1.0, rawDepth, 1.0);
float4 viewPos = mul(_InverseProjection, ndc);
viewPos /= viewPos.w;
float3 fragPos = viewPos.xyz;
//方法2:内置矩阵 重建观察空间坐标
// float rawDepth = SampleSceneDepth(uv);
// float3 posWS = ComputeWorldSpacePosition(uv, rawDepth, UNITY_MATRIX_I_VP);
// float3 fragPos = mul(_CameraViewMatrix, float4(posWS, 1.0)).xyz;
float3 normalWS = SampleSceneNormals(uv);
float3 normalVS = normalize(mul((float3x3)_CameraViewMatrix, normalWS));
float3 randomVec = normalize(
SAMPLE_TEXTURE2D(_NoiseTex, sampler_NoiseTex, uv * float2(_NoiseScaleX, _NoiseScaleY)).xyz
);
float3 tangent = normalize(randomVec - normalVS * dot(randomVec, normalVS));
float3 bitangent = normalize(cross(normalVS, tangent));
float3x3 TBN = float3x3(tangent, bitangent, normalVS);
float occlusion = 0.0;
for (int i = 0; i < SAMPLE_COUNT; i++)
{
float3 sampleVec = mul(_Samples[i].xyz, TBN);
//方向翻转
// float flip = sign(dot(normalVS, sampleVec));
// sampleVec *= flip;
float3 samplePos = fragPos + sampleVec * _Radius;
float4 offset = mul(_ProjectionMatrix, float4(samplePos, 1.0));
offset.xy /= offset.w;
offset.xy = offset.xy * 0.5 + 0.5;
float2 sampleUV = clamp(offset.xy, 0.0, 1.0);
float sampleDepth = SampleDepth(sampleUV);
float sampleDist = -samplePos.z; // 采样点自身的深度
// 前后遮蔽权重
float3 sampleDir = normalize(samplePos - fragPos);
float posOcclusionWeight = max(dot(normalVS, sampleDir), 0);
float rangeCheck = smoothstep(0.0, 1.0, _Radius / (abs(sampleDist - sampleDepth) + 1e-5));
occlusion += (sampleDist >= sampleDepth + _Bias ? 1.0 : 0.0) * rangeCheck * posOcclusionWeight;
}
float ao = 1.0 - occlusion / SAMPLE_COUNT;
ao = pow(ao, _Intensity);
return float4(ao.xxx, 1);
}
ENDHLSL
}
// Pass 1
Pass
{
Name "Blur"
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag_blur
float4 frag_blur(Varyings input) : SV_Target
{
float2 texelSize = 1.0 / _ScreenParams.xy ;
float result = 0.0;
for (int x = -2; x < 2; x++)
for (int y = -2; y < 2; y++)
{
float2 offset = float2(x, y) * texelSize;
result += SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv + offset).r;
}
result = result / 16.0;
return float4(result.xxx, 1);
}
ENDHLSL
}
// Pass 2 合成
Pass
{
Name "Combine"
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
float4 frag(Varyings input) : SV_Target
{
float4 sceneColor = SAMPLE_TEXTURE2D(_SceneColor, sampler_SceneColor, input.uv);
float4 SSAOColor = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv);
//return SSAOColor;
return sceneColor * SSAOColor;
}
ENDHLSL
}
// Pass 3
Pass
{
Name "Copy"
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
float4 frag(Varyings input) : SV_Target
{
float4 sceneColor = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv);
return sceneColor;
}
ENDHLSL
}
}
}