返回作品索引WORK / CASE STUDY
工具 / Shader

URP - RendererFeature :SSAO

SSAO屏幕空间阴影

独立作品

让周围轻一点,文字近一点。
URP - RendererFeature :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
        }
    
    }
}