专栏/URP | 后处理-SSAO效果

URP | 后处理-SSAO效果

2023年07月16日 16:15--浏览 · --点赞 · --评论
粉丝:1075文章:112

内容偏多

使用版本 Unity 2021.3.15
Unity 2022.1.0

目的

  • 学习AO相关的基础知识,AO计算都有那些?

  • 实现的方法和原理,都需要注意什么?

  • URP管线下获取Depth Normals方法?

原理

AO

Ambient Occlusion(以下简称"AO")是一种基于全局照明中的环境光(Ambient Light)参数和环境几何信息来计算场景中任何一点的光照强度系数的算法. AO描述了表面上的任何一点所接受到的环境光被周围几何体所遮蔽的百分比, 因此使得渲染的结果更加富有层次感, 对比度更高.

遮蔽(Ambient Occlusion,下面简称AO) 是计算机图形学中的一种着色和渲染技术,用来计算场景中每一点是如何接受环境光的。例如,一个管道的内部显然比外表面更隐蔽(因此也更暗),越深入管道光线就越暗。环境光遮蔽可以被看作是光线能到达表面上每一点的能力的数值。[1]在拥有开放天空的场景中,这是通过估算每个点的可看见天空的大小来完成的;而在室内环境中,只考虑一定范围内的物体,并假设墙壁是环境光源。处理结果是一个漫反射、非定向的着色效果,并不会形成明确的阴影,只是能让靠近物体及被遮蔽的区域更暗,并影响渲染图像的整体色调。环境光遮蔽常被用作后期处理。 与局部方法如Phong着色法不同,环境光遮蔽是一种全局方法,意味着每个点的照明是场景中其他几何体的共同作用。然而,这只是一个非常粗略的近似全局光照。仅通过环境光遮蔽得到的物体外观与阴天下的物体相似。“

AO可以理解为一张贴图,是来描绘物体和物体相交或靠近的时候遮挡周围漫反射光线的效果,可以解决或改善漏光、飘和阴影不实等问题,解决或改善场景中缝隙、褶皱与墙角、角线以及细小物体等的表现不清晰问题,综合改善细节尤其是暗部阴影,增强空间的层次感、真实感,同时加强和改善画面明暗对比,增强画面的艺术性。

单独的物体也是有AO贴图,处理一个物体之间的阴影物体。

计算得出的AO Texture,越黑的地方代表环境光越不可能照到该点,该点是环境光的影响就越小。

  • 没有SSAO

但是这样的解决方法,可以解决单物体自己,不能解决两个物体之间。

为什么了解决这个问题,开发出屏幕空间环境光屏蔽(Screen Space Ambient Occlusion,SSAO)

AO计算公式

  • 处理

SSAO计算方法

SSAO背后的原理很简单:屏幕上的每一个像素,我们都会根据周边深度值计算一个遮蔽因子(Occlusion Factor)。这个遮蔽因子之后会被用来减少或者抵消片段的环境光照分量。遮蔽因子是通过采集片段周围球型核心(Kernel)的多个深度样本,并和当前片段深度值对比而得到的。高于片段深度值样本的个数就是我们想要的遮蔽因子。

  • 计算方法

如图,中心黑点为采样位置,蓝色箭头为平面的法向量,空心点为未受遮蔽的采样点(Sample Point),实心白点为受遮蔽的采样点,左边采样点的AO值计算方法为:未受遮蔽的采样点 / 采样点总数 = 0 / 11 = 0,故该点未受遮蔽,AO值为0;
  • 基于Ray-Tracing的AO计算模型. 红色的射线表示V = 1, 绿色的射线表示V = 0.

这样我们需要对屏幕上的每一个像素进行采样,所以SSAO很费。并且出现一些其他问题。

  • 出现摩尔纹

所以需要进行模糊,这里使用双边滤波(Bilateral filter)来解决

双边滤波是一种非线性滤波器,它可以达到保持边缘、降噪平滑的效果。和其他滤波原理一样,双边滤波也是采用加权平均的方法,用周边像素亮度值的加权平均代表某个像素的强度,所用的加权平均基于高斯分布[1]。最重要的是,双边滤波的权重不仅考虑了像素的欧氏距离(如普通的高斯低通滤波,只考虑了位置对中心像素的影响),还考虑了像素范围域中的辐射差异(例如卷积核中像素与中心像素之间相似程度、颜色强度,深度距离等),在计算中心像素的时候同时考虑这两个权重。

  • 获取到AO贴图

  • 得到AO贴图后,事情就轻松很多了,只需要一一对应游戏画面与AO贴图,改变游戏画面上每一个像素的GI值,就可以实现SSAO效果

不同的AO效果

  • SSAO

  • HBAO

  • HDAO

本次只是实现SSAO效果


SSAO理论知识

实现原理

  1. 先利用深度图和屏幕UV坐标反算出当前顶点世界坐标的位置,

  2. 再使用沿着法线正方向半球内的随机点进行采样, 获得新的坐标点.

  3. 两个点进行比较, 判断是否被遮挡, 然后加权处理获得AO.

  4. AO这时候充满噪点, 需要高斯模糊一下.

  5. 最后和场景颜色RT进行混合叠加.

SSAO原理流程图

获取深度我们是可以获取到的,法线深度URP是没有的我们需要自己获取。

Depth + Normals 实现

unity URP 管线官方不支持深度法线,我们需要自己实现深度法线,

URP | 后处理-描边 - 哔哩哔哩 (bilibili.com)


上面我们制作描边的时候,也是使用SSAO的深度法线,这次我们单独实现,深度法线。

  • Unity URP 不支持深度法线


使用自定义渲染器功能来实现

  • Scriptable Renderer Feature   自定义渲染器功能

    创建一个脚本 DepthNormalsFeature  继承 ScriptableRendererFeature

这里有两种方法,

第一种方法

using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

public class DepthNormalsFeature : ScriptableRendererFeature
{
    class DepthNormalsPass : ScriptableRenderPass
    {
        // 深度缓冲块大小
        int kDepthBufferBits = 32;
        /// <summary>深度法线纹理</summary>
        private RenderTargetHandle depthAttachmentHandle { get; set; }
        /// <summary>目标相机渲染信息</summary>
        internal RenderTextureDescriptor descriptor { get; private set; }

        /// <summary>材质</summary>
        private Material depthNormalsMaterial = null;
        /// <summary>筛选设置</summary>
        private FilteringSettings m_FilteringSettings;

        // 该Pass在帧分析器显示的标签
        string m_ProfilerTag = "Depth Normals Pre Pass";
        // Pass绘制标签,在Shader中只有声明了相同绘制标签的Pass才能被调用绘制
        ShaderTagId m_ShaderTagId = new ShaderTagId("DepthOnly");

        /// <summary>
        /// 构造函数
        /// </summary>
        /// <param name="renderQueueRange">渲染队列</param>
        /// <param name="layerMask">渲染对象层级</param>
        /// <param name="material">材质</param>
        public DepthNormalsPass(RenderQueueRange renderQueueRange, LayerMask layerMask, Material material)
        {
            m_FilteringSettings = new FilteringSettings(renderQueueRange, layerMask);
            depthNormalsMaterial = material;
        }

        /// <summary>
        /// 参数设置
        /// </summary>
        /// <param name="baseDescriptor">目标相机渲染信息</param>
        /// <param name="depthAttachmentHandle">深度法线纹理</param>
        public void Setup(RenderTextureDescriptor baseDescriptor, RenderTargetHandle depthAttachmentHandle)
        {
            // 设置纹理
            this.depthAttachmentHandle = depthAttachmentHandle;
            // 设置渲染目标信息
            baseDescriptor.colorFormat = RenderTextureFormat.ARGB32;
            baseDescriptor.depthBufferBits = kDepthBufferBits;
            descriptor = baseDescriptor;
        }

        // 该方法在执行渲染通道之前被调用。
        // 它可以用来配置渲染目标和它们的清除状态。也创建临时渲染目标纹理。
        // 当为空时,这个渲染通道将渲染到激活的摄像机渲染目标。
        // 你不应该调用CommandBuffer.SetRenderTarget。调用<c>ConfigureTarget</c> and <c> configurecclear </c>。
        // 渲染管道将确保目标设置和清除以性能方式进行。
        public override void Configure(CommandBuffer cmd, RenderTextureDescriptor cameraTextureDescriptor)
        {
            // 获取一个临时RT(深度法线纹理、目标信息、滤波模式)
            cmd.GetTemporaryRT(depthAttachmentHandle.id, descriptor, FilterMode.Point);
            // 配置目标
            ConfigureTarget(depthAttachmentHandle.Identifier());
            // 清楚未渲染配置为黑色
            ConfigureClear(ClearFlag.All, Color.black);
        }

        //这里你可以实现渲染逻辑。
        //使用<c>ScriptableRenderContext</c>来发出绘图命令或执行命令缓冲区
        // https://docs.unity3d.com/ScriptReference/Rendering.ScriptableRenderContext.html
        //你不必调用ScriptableRenderContext。提交时,渲染管道会在管道中的特定点调用它。
        public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
        {
            // 获取命令缓冲区
            CommandBuffer cmd = CommandBufferPool.Get(m_ProfilerTag);

            using (new ProfilingScope(cmd, new ProfilingSampler(m_ProfilerTag)))
            {
                // 执行命令缓存
                context.ExecuteCommandBuffer(cmd);
                // 清楚数据缓存
                cmd.Clear();

                // 相机的排序标志
                var sortFlags = renderingData.cameraData.defaultOpaqueSortFlags;
                // 创建绘制设置
                var drawSettings = CreateDrawingSettings(m_ShaderTagId, ref renderingData, sortFlags);
                // 设置对象数据
                drawSettings.perObjectData = PerObjectData.None;

                // 检测是否是VR设备
                ref CameraData cameraData = ref renderingData.cameraData;
                Camera camera = cameraData.camera;
                if (cameraData.isStereoEnabled)
                    context.StartMultiEye(camera);

                // 设置覆盖材质
                drawSettings.overrideMaterial = depthNormalsMaterial;

                // 绘制渲染器
                context.DrawRenderers(renderingData.cullResults, ref drawSettings, ref m_FilteringSettings);

                // 设置全局纹理
                cmd.SetGlobalTexture("_CameraDepthNormalsTexture", depthAttachmentHandle.id);
            }
            // 执行命令缓冲区
            context.ExecuteCommandBuffer(cmd);
            // 释放命令缓冲区
            CommandBufferPool.Release(cmd);
        }

        // 清除此呈现传递执行期间创建的任何已分配资源。
        public override void FrameCleanup(CommandBuffer cmd)
        {
            if (depthAttachmentHandle != RenderTargetHandle.CameraTarget)
            {
                cmd.ReleaseTemporaryRT(depthAttachmentHandle.id);
                depthAttachmentHandle = RenderTargetHandle.CameraTarget;
            }
        }
    }
    // 深度法线Pass
    DepthNormalsPass depthNormalsPass;
    // 深度法线纹理
    RenderTargetHandle depthNormalsTexture;
    // 处理材质
    Material depthNormalsMaterial;

    public override void Create()
    {
        // 通过Built-it管线中的Shader创建材质
        depthNormalsMaterial = CoreUtils.CreateEngineMaterial("Hidden/Internal-DepthNormalsTexture");
        // 获取Pass(渲染队列,渲染对象,材质)
        depthNormalsPass = new DepthNormalsPass(RenderQueueRange.opaque, -1, depthNormalsMaterial);
        // 设置渲染时机 = 预渲染通道后
        depthNormalsPass.renderPassEvent = RenderPassEvent.AfterRenderingPrePasses;
        // 设置纹理名
        depthNormalsTexture.Init("_CameraDepthNormalsTexture");
    }

    //这里你可以在渲染器中注入一个或多个渲染通道。
    //这个方法在设置渲染器时被调用。
    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        // 对Pass进行参数设置(当前渲染相机信息,深度法线纹理)
        depthNormalsPass.Setup(renderingData.cameraData.cameraTargetDescriptor, depthNormalsTexture);
        // 写入渲染管线队列
        renderer.EnqueuePass(depthNormalsPass);
    }
}

第二种方法

using System;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;


[DisallowMultipleRendererFeature]
[Tooltip("The Scene Normals pass enables rendering to the CameraNormalsTexture if no other pass does it already.")]
internal class DepthNormals : ScriptableRendererFeature
{
    private SceneNormalsPass m_SceneNormalsPass = null;

    public override void Create()
    {
        if (m_SceneNormalsPass == null)
        {
            m_SceneNormalsPass = new SceneNormalsPass();
        }
    }

    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        m_SceneNormalsPass.Setup();
        renderer.EnqueuePass(m_SceneNormalsPass);
    }


    // The Scene Normals Pass
    private class SceneNormalsPass : ScriptableRenderPass
    {
        public void Setup()
        {
            ConfigureInput(ScriptableRenderPassInput.Normal); // all of this to just call this one line
            return;
        }

        public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData) { }
    }
}
  • URP默认SSAO的法线效果  和我们第二种方法一样。

扩展 他们两种方法有什么不同吗?有什么区别?

  • DepthNormalsPass  是深度和法线一起获取的,在一起需要进行解码使用。

  • DepthNormals 只是获取像素深度的法线信息。

SSAO | AO实现

设置渲染管线

我们当前准备好渲染管线 ,

using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;



public class SSAO : ScriptableRendererFeature
{
    [System.Serializable]
    public class Settings
    {
        public RenderPassEvent renderPassEvent = RenderPassEvent.BeforeRenderingPostProcessing;
        public Shader shader;
    }
    public Settings settings = new Settings();

    SSAOPass ssaoPass;


    public override void Create()
    {
        this.name = "SSAO";
        ssaoPass = new SSAOPass(RenderPassEvent.BeforeRenderingPostProcessing, settings.shader);

    }
    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        renderer.EnqueuePass(ssaoPass);
    }
}


public class SSAOPass : ScriptableRenderPass
{
    static readonly string RenderTag = "SSAO Effects";

    SSAOVolume ssaoVolume;
    Material myssaoMaterial;


    public SSAOPass(RenderPassEvent evt, Shader ssaoshader)
    {
        renderPassEvent = evt;
        var shader = ssaoshader;

        if (shader == null)
        {
            Debug.LogError("û��ָ��Shader");
            return;
        }
        myssaoMaterial = CoreUtils.CreateEngineMaterial(ssaoshader);
    }

    public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
    {
        if (myssaoMaterial == null)
        {
            return;
        }

        if (!renderingData.cameraData.postProcessEnabled)
        {
            return;
        }

        var stack = VolumeManager.instance.stack;
        ssaoVolume = stack.GetComponent<SSAOVolume>();

        var cmd = CommandBufferPool.Get(RenderTag);

        Render(cmd, ref renderingData);

        context.ExecuteCommandBuffer(cmd);
        CommandBufferPool.Release(cmd);

    }

    void Render(CommandBuffer cmd, ref RenderingData renderingData)
    {
        RenderTargetIdentifier source = renderingData.cameraData.renderer.cameraColorTarget;
        RenderTextureDescriptor inRTDesc = renderingData.cameraData.cameraTargetDescriptor;
        inRTDesc.depthBufferBits = 0;


        myssaoMaterial.SetColor("_aoColor", ssaoVolume.aoColor.value);

        int destination = Shader.PropertyToID("Temp1");
        cmd.GetTemporaryRT(destination, inRTDesc.width, inRTDesc.height, 0, FilterMode.Bilinear, RenderTextureFormat.DefaultHDR); //����һ����ʱͼ�񣬲��������rt�IJ�����ȥ

        cmd.Blit(source, destination);

        cmd.Blit(destination, source, myssaoMaterial, 0);
    }
}

这里是使用基础的颜色来测试管线是否正确

Shader部分

Shader "Hidden/Bssao"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _aoColor("aoColor", Color) = (1,1,1,1)
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" "RenderPipeline"="UniversalPipeline"}
        LOD 100

        HLSLINCLUDE

        #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"


        CBUFFER_START(UnityPerMaterial)
        float4 _MainTex_ST;
        float4 _aoColor;
        CBUFFER_END

            struct appdata
            {
                float4 positionOS : POSITION;
                float2 texcoord : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 positionCS : SV_POSITION;
            };

            TEXTURE2D(_MainTex);                          SAMPLER(sampler_MainTex);
        ENDHLSL


        Pass
        {
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            v2f vert (appdata v)
            {
                v2f o;
                VertexPositionInputs  PositionInputs = GetVertexPositionInputs(v.positionOS.xyz);
                o.positionCS = PositionInputs.positionCS;
                o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);

                return o;
            }


            half4 frag (v2f i) : SV_Target
            {
                // 获取深度




                half4 col = SAMPLE_TEXTURE2D(_MainTex,sampler_MainTex,i.uv) * _aoColor;

                return col;
            }
            ENDHLSL
        }
    }
}

Volume

using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;


public class SSAOVolume : VolumeComponent, IPostProcessComponent
{
    public ClampedFloatParameter Brightness = new ClampedFloatParameter(1f, 0f, 2f);           #  测试数据
    public ColorParameter aoColor = new ColorParameter(Color.black, false);
    public BoolParameter _aoOnly = new BoolParameter(false);

    public bool IsActive() => Brightness.value > 0;

    public bool IsTileCompatible() => false;


}
  • 效果

获取世界空间位置,法线,切线数据

  1. 获取世界空间中的顶点的位置

    我们定义一个库用来放放一些我们自定义的函数

#ifndef SSAO_INCLUDED
#define SSAO_INCLUDED

// 输入UV,使用矩阵 _VPMatrix_invers  转换成世界坐标
float4 GetWorldPos(float2 uv)
{
    float rawDepth = SAMPLE_TEXTURE2D_X(_CameraDepthTexture, sampler_CameraDepthTexture, UnityStereoTransformScreenSpaceTex(uv)).r;
#if defined(UNITY_REVERSED_Z)
    rawDepth = 1 - rawDepth;
#endif
    float4 ndc = float4(uv.xy * 2 - 1, rawDepth * 2 - 1, 1);
    float4 wPos = mul(_VPMatrix_invers, ndc);
    wPos /= wPos.w;
    return wPos;
}



#endif

这个函数是输入屏幕UV坐标,从深度中转换为齐次剪裁空间坐标(ndc),使用矩阵转换成世界空间。

那这里我们需要获取两个数据,

  • 深度  _CameraDepthTexture

  • 转换世界空间的矩阵    sampler_CameraDepthTexture

深度我们URP管线自带,勾选就可以调用, 转换世界空间的矩阵我们要在管线中获取,传到Shader中。

Shader中 定义一个矩阵变量,等等我们在后处理管线中传入。

float4x4 _VPMatrix_invers;        // 视图投影矩阵的逆矩阵
  • SSAO.cs中

    我们需要获取摄像机,在由摄像机来生成矩阵,传到我们的Shader

    在SSAOPass 中我们前定义一个摄像机的变量

SSAOVolume ssaoVolume;
Material myssaoMaterial;


Camera cam;
  • Execute 中进行初始化

cam = renderingData.cameraData.camera;
  • 定义一个函数 ,这个函数用来后面传递控制变量。

private void SetMatData()
    {


        Matrix4x4 vp_Matrix = cam.projectionMatrix * cam.worldToCameraMatrix;
        myssaoMaterial.SetMatrix("_VPMatrix_invers", vp_Matrix.inverse);

    }
  • 在渲染里调用这个函数,

SetMatData();
myssaoMaterial.SetColor("_aoColor", ssaoVolume.aoColor.value);

当前完成

回去到Shader中我们调用 函数求处顶点位置。

float3 worldPos = GetWorldPos(i.uv);

return float4(worldPos,1);

效果

2. 获取世界空间中法线

我们需要需要深度法线来计算出像素在世界空间下的位置信息。

深度Unity管线为我们提供了。

这里我们使用上面的第二种方法。

在管线中增加

  1. 在库中输入我们的 贴图

float4x4 _VPMatrix_invers;

TEXTURE2D(_CameraNormalsTexture);  SAMPLER(sampler_CameraNormalsTexture);

在定义一个函数

// 输入UV
float3 GetWorldNormal(float2 uv)
{
    float3 wNor = SAMPLE_TEXTURE2D(_CameraNormalsTexture, sampler_CameraNormalsTexture, uv).xyz; //world normal
    return wNor;

}

Shader中我们调用函数

float3 worldPos = GetWorldPos(i.uv);
float3 wNor = GetWorldNormal(i.uv);


return float4(wNor,1);
  • 效果

3. 获取世界空间中切线

切线就是我们需要生成的随机向量部分,

在库里定义两个函数

float Hash(float2 p)
{
    return frac(sin(dot(p, float2(12.9898, 78.233))) * 43758.5453);
}

// 随机向量
float3 GetRandomVec(float2 p)
{
    float3 vec = float3(0, 0, 0);
    vec.x = Hash(p) * 2 - 1;
    vec.y = Hash(p * p) * 2 - 1;
    vec.z = Hash(p * p * p) * 2 - 1;
    return normalize(vec);
}


shader中调用

float3 worldPos = GetWorldPos(i.uv);
float3 wNor = GetWorldNormal(i.uv);
float3 wTan = GetRandomVec(i.uv);

注意:这些都是在世界空间下计算的,后面需要转换成View空间

效果

这就是我们生成的一些随机噪点

都获取到了,那计算我们需要的副切线,构造一个矩阵。

float3 wBin = cross(wNor, wTan);
wTan = cross(wBin, wNor);                    // 求出在顶点法线空间下 切线的显示

float3x3 TBN_line = float3x3(wTan, wBin, wNor);

矩阵准备好了.

注意:wTan = cross(wBin, wNor);  还需要cross在计算一次。       


 

计算AO效果

定义两个变量,一个变量是我们的ao 效果,一个是我们控制噪点多少。

float ao = 0;
int sampleCount = (int)_SampleCount;

定义一个循环遍历所有采样点。

使用随机向量生成一个半球向量 offDir 通过 scale控制范围

// 随机向量
                    float3 offDir = GetRandomVecHalf(j * i.uv);
                    float scale = j / _SampleCount;
                    scale = lerp(0.01, 1, scale * scale);

在HLSL库中

增加GetRandomVecHalf 函数

float3 GetRandomVecHalf(float2 p)
{
    float3 vec = float3(0, 0, 0);
    vec.x = Hash(p) * 2 - 1;
    vec.y = Hash(p * p) * 2 - 1;
    vec.z = saturate(Hash(p * p * p) + 0.2);
    return normalize(vec);
}

这里增加控制半球控制大小, 世界空间转换裁剪空间,这里需要 wPos 世界空间顶点位置和矩阵。

// 世界坐标转换成裁剪空间
                    float4 offPosW = float4(offDir, 0) + float4(worldPos, 1);
                    float4 offPosV = mul(_VMatrix, offPosW);
                    float4 offPosC = mul(_PMatrix, offPosV);
                    float2 offPosScr = offPosC.xy / offPosC.w;
                    offPosScr = offPosScr * 0.5 + 0.5;

这里我们需要两个 矩阵,

  • v_Matrix          视图矩阵

    是世界空间到观察者空间的变换矩阵。它描述了相机的位置、朝向和上方向等信息,可以用于将场景从世界坐标系转换到相机坐标系。

  • p_Matrix           投影矩阵

    是将相机坐标系中的场景投影到屏幕坐标系的变换矩阵。它描述了相机的视锥体、投影方式和屏幕尺寸等信息,可以用于将场景从相机坐标系转换到屏幕坐标系。

SetMatData函数 使用同样的方法

private void SetMatData()
    {
        Matrix4x4 vp_Matrix = cam.projectionMatrix * cam.worldToCameraMatrix;
        myssaoMaterial.SetMatrix("_VPMatrix_invers", vp_Matrix.inverse);

        Matrix4x4 v_Matrix = cam.worldToCameraMatrix;
        myssaoMaterial.SetMatrix("_VMatrix", v_Matrix);

        Matrix4x4 p_Matrix = cam.projectionMatrix;
        myssaoMaterial.SetMatrix("_PMatrix", p_Matrix);

    }

采样深度

// 采样深度
                    float sampleDepth = SampleSceneDepth(offPosScr);
                    sampleDepth = LinearEyeDepth(sampleDepth,_ZBufferParams);

SampleSceneDepth函数获取屏幕坐标对应的深度值,然后通过LinearEyeDepth函数将其转换为线性深度。该函数使用了_ZBufferParams参数,它包含了一些与深度缓冲相关的信息,如深度缓冲的近、远裁剪面等。


采样AO

这里我们需要获取默认的深度值

// 采样AO
                    float sampleZ = offPosC.w;
                    float rangeCheck = smoothstep(0, 1.0, _Radius / abs(sampleZ - sampleDepth) * _RangeCheck * 0.1);
                    float selfCheck = (sampleDepth < depth - 0.08) ?  1 : 0;

                    ao += (sampleDepth < sampleZ) ?  1 * rangeCheck * selfCheck * _AOInt * weight : 0;

首先定义一个sampleZ变量,表示采样点的深度值。然后通过计算sampleDepth和sampleZ之间的距离,并将其与_Radius参数相除,得到一个范围检查的值rangeCheck。这个值用于控制采样点的范围,使得只有距离当前像素一定范围内的采样点才会对AO值产生影响。

接下来的代码中,通过判断sampleDepth是否小于depth - 0.08来进行自遮蔽检查。如果采样点比当前像素更靠近观察者,则返回1,否则返回0。这个值用于控制自遮蔽的影响。

最后,将rangeCheck、selfCheck、_AOInt和weight相乘,得到一个权重值,并将其乘以1(即对AO值产生1的影响),最终得到AO值。



采样深度

这里在库定义一个获取深度的函数

// 深度

float GetEyeDepth(float2 uv)
{
    float rawDepth = SAMPLE_TEXTURE2D_X(_CameraDepthTexture, sampler_CameraDepthTexture, UnityStereoTransformScreenSpaceTex(uv)).r;
    return LinearEyeDepth(rawDepth, _ZBufferParams);
}

// 深度法线
float3 GetWorldNormal(float2 uv)
{
    float3 wNor = SAMPLE_TEXTURE2D(_CameraNormalsTexture, sampler_CameraNormalsTexture, uv).xyz; //world normal
    return wNor;

}

在Sahder中定义

// 获取深度
float depth = GetEyeDepth(i.uv);

输出AO

我们在Shader部分输出ao效果

ao = 1 - saturate((ao / sampleCount));

return ao;

现在的效果是白色的,啥都没有。

我们的SSAOVolume还没有关联属性

SSAOVolume

在这里我们关联属性

using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;


public class SSAOVolume : VolumeComponent, IPostProcessComponent
{
    public ClampedIntParameter sampleCount = new ClampedIntParameter(22, 1, 128);
    public ClampedFloatParameter radius = new ClampedFloatParameter(0.5f, 0f, 0.8f);
    public ClampedFloatParameter rangeCheck = new ClampedFloatParameter(0f, 0f, 10f);
    public ClampedFloatParameter aoInt  = new ClampedFloatParameter(1f, 0f, 10f);
    public ClampedFloatParameter blurRadius = new ClampedFloatParameter(1f, 0f, 3f);
    public ClampedFloatParameter bilaterFilterFactor = new ClampedFloatParameter(0.1f, 0f, 1f);

    public ColorParameter aoColor = new ColorParameter(Color.black, false);
    public BoolParameter _aoOnly = new BoolParameter(false);

    public bool IsActive() => sampleCount.value > 0;

    public bool IsTileCompatible() => false;


}

RendererFeature

关联属性,我们把属性都放到 SetMatData()函数中

private void SetMatData()
    {
        Matrix4x4 vp_Matrix = cam.projectionMatrix * cam.worldToCameraMatrix;
        myssaoMaterial.SetMatrix("_VPMatrix_invers", vp_Matrix.inverse);

        Matrix4x4 v_Matrix = cam.worldToCameraMatrix;
        myssaoMaterial.SetMatrix("_VMatrix", v_Matrix);

        Matrix4x4 p_Matrix = cam.projectionMatrix;
        myssaoMaterial.SetMatrix("_PMatrix", p_Matrix);

        myssaoMaterial.SetColor("_aoColor", ssaoVolume.aoColor.value);               // 获取value 组件的颜色
        myssaoMaterial.SetFloat("_SampleCount", ssaoVolume.sampleCount.value);
        myssaoMaterial.SetFloat("_Radius", ssaoVolume.radius.value);
        myssaoMaterial.SetFloat("_RangeCheck", ssaoVolume.rangeCheck.value);
        myssaoMaterial.SetFloat("_AOInt", ssaoVolume.aoInt.value);

        myssaoMaterial.SetFloat("_BlurRadius", ssaoVolume.blurRadius.value);
        myssaoMaterial.SetFloat("_BilaterFilterFactor", ssaoVolume.bilaterFilterFactor.value);


    }

增加完代码

效果

AO效果就计算完成,

我们增加一个判断,控制开关AO效果,方便对比。

void Render(CommandBuffer cmd, ref RenderingData renderingData)
    {



        RenderTargetIdentifier source = renderingData.cameraData.renderer.cameraColorTarget;
        RenderTextureDescriptor inRTDesc = renderingData.cameraData.cameraTargetDescriptor;
        inRTDesc.depthBufferBits = 0;

        SetMatData();
        myssaoMaterial.SetColor("_aoColor", ssaoVolume.aoColor.value);

        int destination = Shader.PropertyToID("Temp1");
        cmd.GetTemporaryRT(destination, inRTDesc.width, inRTDesc.height, 0, FilterMode.Bilinear, RenderTextureFormat.DefaultHDR);

        cmd.Blit(source, destination);


        if (ssaoVolume._aoOnly.value)
        {
            cmd.Blit(destination, source, myssaoMaterial, 0);
        }
        else
        {

        }
        cmd.ReleaseTemporaryRT(destination);                                                                 //清除临时RT
    }
  • 效果

GIF

AO部分就算完成。

SSAO | 模糊算法

SSAO我们需要多个Pass

  • AO

  • 水平模糊

  • 垂直模糊

  • AO + 原图

整理Shader

我们需要4个pass这里不适合放到一起,为了方便,我们 重新创建 hlsl文件,放我们的不同的执行过程。

  • Shader 只执行我们渲染设置,

  • Hlsl 文件执行计算,

这里重新创建两个hlsl文件

  • AOpass

  • BlurPass

Shader

Shader "Post/SSAO"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _aoColor("aoColor", Color) = (1,1,1,1)
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" "RenderPipeline"="UniversalPipeline"}
        LOD 100

        Cull Off ZWrite Off ZTest Always


        // 第一个 pass 用于计算 AO
        Pass
        {
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag_ao

            #include "AOPass.hlsl"

            ENDHLSL
        }

        // 第二个 pass  水平方向
        Pass
        {
            HLSLPROGRAM
            #pragma vertex vert_h
            #pragma fragment frag_Blur

            #include "BlurPass.hlsl"

            ENDHLSL
        }
        // 第三个 pass 垂直方向
        Pass
        {
            HLSLPROGRAM
            #pragma vertex vert_v
            #pragma fragment frag_Blur

            #include "BlurPass.hlsl"

            ENDHLSL
        }


        // 第四个 pass用于将AO和原图混合
        Pass
        {
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag_final


            #include "AOPass.hlsl"


            ENDHLSL
        }

    }
}

SSAOFunLib.hlsl

#ifndef SSAO_FunLib_INCLUDED
#define SSAO_FunLib_INCLUDED


#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/DeclareDepthTexture.hlsl"

float4x4 _VPMatrix_invers;

TEXTURE2D(_CameraNormalsTexture);  SAMPLER(sampler_CameraNormalsTexture);

// 输入UV,使用矩阵 _VPMatrix_invers  转换成世界坐标
float4 GetWorldPos(float2 uv)
{
    float rawDepth = SAMPLE_TEXTURE2D_X(_CameraDepthTexture, sampler_CameraDepthTexture, UnityStereoTransformScreenSpaceTex(uv)).r;
#if defined(UNITY_REVERSED_Z)
    rawDepth = 1 - rawDepth;
#endif
    float4 ndc = float4(uv.xy * 2 - 1, rawDepth * 2 - 1, 1);
    float4 wPos = mul(_VPMatrix_invers, ndc);
    wPos /= wPos.w;
    return wPos;
}


// 深度
float GetEyeDepth(float2 uv)
{
    float rawDepth = SAMPLE_TEXTURE2D_X(_CameraDepthTexture, sampler_CameraDepthTexture, UnityStereoTransformScreenSpaceTex(uv)).r;
    return LinearEyeDepth(rawDepth, _ZBufferParams);
}

// 深度法线
float3 GetWorldNormal(float2 uv)
{
    float3 wNor = SAMPLE_TEXTURE2D(_CameraNormalsTexture, sampler_CameraNormalsTexture, uv).xyz; //world normal
    return wNor;

}
//
float3 GetRandomVecHalf(float2 p)
{
    float3 vec = float3(0, 0, 0);
    vec.x = Hash(p) * 2 - 1;
    vec.y = Hash(p * p) * 2 - 1;
    vec.z = saturate(Hash(p * p * p) + 0.2);
    return normalize(vec);
}


float Hash(float2 p)
{
    return frac(sin(dot(p, float2(12.9898, 78.233))) * 43758.5453);
}

// 随机向量
float3 GetRandomVec(float2 p)
{
    float3 vec = float3(0, 0, 0);
    vec.x = Hash(p) * 2 - 1;
    vec.y = Hash(p * p) * 2 - 1;
    vec.z = Hash(p * p * p) * 2 - 1;
    return normalize(vec);
}


#endif

AOPass

#ifndef AO_PASS_INCLUDED
#define AO_PASS_INCLUDED


#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/DeclareDepthTexture.hlsl"
#include "SSAOFunLib.hlsl"

CBUFFER_START(UnityPerMaterial)
float4 _MainTex_ST;
float4 _aoColor;
float _SampleCount;
float _Radius;
float _RangeCheck;
float _AOInt;
float4x4 _VMatrix, _PMatrix;
CBUFFER_END


struct appdata
{
    float4 positionOS : POSITION;
    float2 texcoord : TEXCOORD0;
};
struct v2f
{
    float2 uv : TEXCOORD0;
    float4 positionCS : SV_POSITION;
};


TEXTURE2D(_MainTex);                          SAMPLER(sampler_MainTex);
TEXTURE2D(_AOTex);                          SAMPLER(sampler_AOTex);

// ao pass

v2f vert(appdata v)
{
    v2f o;
    VertexPositionInputs PositionInputs = GetVertexPositionInputs(v.positionOS.xyz);
    o.positionCS = PositionInputs.positionCS;
    o.uv = v.texcoord;
    return o;
}

half4 frag_ao (v2f i) : SV_Target
{
    // 获取深度
    float depth = GetEyeDepth(i.uv);
    float3 worldPos = GetWorldPos(i.uv);
    float3 wNor = GetWorldNormal(i.uv);
    float3 wTan = GetRandomVec(i.uv);
    float3 wBin = cross(wNor, wTan);
    wTan = cross(wBin, wNor);                    // 求出在顶点法线空间下 切线的显示
    float3x3 TBN_line = float3x3(wTan, wBin, wNor);
    float ao = 0;
    int sampleCount = (int)_SampleCount;
    [unroll(128)]
    for (int j = 0; j < sampleCount; j++)
    {
        // 随机向量
        float3 offDir = GetRandomVecHalf(j * i.uv);
        float scale = j / _SampleCount;
        scale = lerp(0.01, 1, scale * scale);
        // 扩展半径
        offDir *= scale * _Radius;
        float weight = smoothstep(0,0.2, length(offDir));
        offDir = mul(offDir, TBN_line);
        // 世界坐标转换成裁剪空间
        float4 offPosW = float4(offDir, 0) + float4(worldPos, 1);
        float4 offPosV = mul(_VMatrix, offPosW);
        float4 offPosC = mul(_PMatrix, offPosV);
        float2 offPosScr = offPosC.xy / offPosC.w;
        offPosScr = offPosScr * 0.5 + 0.5;          // offPosScr = offPosScr * 0.5 + 0.5将其映射到[0,1]范围内的屏幕坐标
        // 采样深度
        float sampleDepth = SampleSceneDepth(offPosScr);
        sampleDepth = LinearEyeDepth(sampleDepth,_ZBufferParams);
        // 采样AO
        float sampleZ = offPosC.w;
        float rangeCheck = smoothstep(0, 1.0, _Radius / abs(sampleZ - sampleDepth) * _RangeCheck * 0.1);
        float selfCheck = (sampleDepth < depth - 0.08) ?  1 : 0;
        ao += (sampleDepth < sampleZ) ?  1 * rangeCheck * selfCheck * _AOInt * weight : 0;
    }
    ao = 1 - saturate((ao / sampleCount));
    return ao;
}


// final pass
half4 frag_final (v2f i) : SV_Target
{
    half4 scrTex = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv);
    half4 aoTex = SAMPLE_TEXTURE2D(_AOTex, sampler_AOTex, i.uv);

    half4 finalCol = lerp(scrTex * _aoColor, scrTex, aoTex.r);
    return finalCol;
}


#endif

BlurPass

#ifndef BLUR_PASS_INCLUDED
#define BLUR_PASS_INCLUDED



#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "SSAOFunLib.hlsl"

CBUFFER_START(UnityPerMaterial)
float4 _MainTex_TexelSize, _AOTex_TexelSize;
float _BlurRadius;
float _BilaterFilterFactor;
CBUFFER_END


TEXTURE2D(_MainTex);     SAMPLER(sampler_MainTex);
TEXTURE2D(_AOTex);     SAMPLER(sampler_AOTex);

struct appdata
{
    float4 positionOS : POSITION;
    float2 texcoord : TEXCOORD0;
};
struct v2f
{
    float2 uv : TEXCOORD0;
    float4 positionCS : SV_POSITION;
    float2 delta : TEXCOORD1;
};


v2f vert_h (appdata v)
{
    v2f o;
    VertexPositionInputs  PositionInputs = GetVertexPositionInputs(v.positionOS);
    o.positionCS = PositionInputs.positionCS;
    o.uv = v.texcoord;
    o.delta = _AOTex_TexelSize.xy * float2(_BlurRadius,0);   //
    return o;
}
v2f vert_v (appdata v)
{
    v2f o;
    VertexPositionInputs  PositionInputs = GetVertexPositionInputs(v.positionOS);
    o.positionCS = PositionInputs.positionCS;
    o.uv = v.texcoord;
    o.delta = _AOTex_TexelSize.xy * float2(0,_BlurRadius);
    return o;
}

half CompareNormal(float3 nor1,float3 nor2)
{
	return smoothstep(_BilaterFilterFactor,1.0,dot(nor1,nor2));
}

half4 frag_Blur(v2f i) : SV_Target
{
    float2 uv = i.uv;
    float2 delta = i.delta;
    float2 uv0a = i.uv - delta;
    float2 uv0b = i.uv + delta;
    float2 uv1a = i.uv - 2.0 * delta;
    float2 uv1b = i.uv + 2.0 * delta;
    float2 uv2a = i.uv - 3.0 * delta;
    float2 uv2b = i.uv + 3.0 * delta;

    float3 normal = GetWorldNormal(uv);
    float3 normal0a = GetWorldNormal(uv0a);
    float3 normal0b = GetWorldNormal(uv0b);
    float3 normal1a = GetWorldNormal(uv1a);
    float3 normal1b = GetWorldNormal(uv1b);
    float3 normal2a = GetWorldNormal(uv2a);
    float3 normal2b = GetWorldNormal(uv2b);

    float4 col = SAMPLE_TEXTURE2D(_AOTex, sampler_AOTex,  uv);
    float4 col0a = SAMPLE_TEXTURE2D(_AOTex, sampler_AOTex, uv0a);
    float4 col0b = SAMPLE_TEXTURE2D(_AOTex, sampler_AOTex,  uv0b);
    float4 col1a = SAMPLE_TEXTURE2D(_AOTex, sampler_AOTex,  uv1a);
    float4 col1b = SAMPLE_TEXTURE2D(_AOTex, sampler_AOTex,  uv1b);
    float4 col2a = SAMPLE_TEXTURE2D(_AOTex, sampler_AOTex,  uv2a);
    float4 col2b = SAMPLE_TEXTURE2D(_AOTex, sampler_AOTex,  uv2b);

    float w = 0.37004405286;
    float w0a = CompareNormal(normal, normal0a) * 0.31718061674;
    float w0b = CompareNormal(normal, normal0b) * 0.31718061674;
    float w1a = CompareNormal(normal, normal1a) * 0.19823788546;
    float w1b = CompareNormal(normal, normal1b) * 0.19823788546;
    float w2a = CompareNormal(normal, normal2a) * 0.11453744493;
    float w2b = CompareNormal(normal, normal2b) * 0.11453744493;

    float3 result = w * col.rgb;
    result += w0a * col0a.rgb;
    result += w0b * col0b.rgb;
    result += w1a * col1a.rgb;
    result += w1b * col1b.rgb;
    result += w2a * col2a.rgb;
    result += w2b * col2b.rgb;

    result /= w + w0a + w0b + w1a + w1b + w2a + w2b;
    return float4(result, 1.0);
}


#endif

这个部分查看

URP | 后处理-模糊算法总结 - 哔哩哔哩 (bilibili.com)

在 Volume这里增加上控制模糊的变量

Volume

using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;


public class SSAOVolume : VolumeComponent, IPostProcessComponent
{
    public ClampedIntParameter sampleCount = new ClampedIntParameter(22, 1, 128);
    public ClampedFloatParameter radius = new ClampedFloatParameter(0.5f, 0f, 0.8f);
    public ClampedFloatParameter rangeCheck = new ClampedFloatParameter(0f, 0f, 10f);
    public ClampedFloatParameter aoInt  = new ClampedFloatParameter(1f, 0f, 10f);

    public ClampedFloatParameter blurRadius = new ClampedFloatParameter(1f, 0f, 3f);
    public ClampedFloatParameter bilaterFilterFactor = new ClampedFloatParameter(0.1f, 0f, 1f);

    public ColorParameter aoColor = new ColorParameter(Color.black, false);
    public BoolParameter _aoOnly = new BoolParameter(false);

    public bool IsActive() => sampleCount.value > 0;

    public bool IsTileCompatible() => false;


}

主要是在管线里处理。

RendererFeature

这里我们来处理模糊

需要定义两个临时RT用来处理模糊

int bufferid1 = Shader.PropertyToID("bufferblur1");         // 临时图像,也可以用 Handle,ID不乱就可以
int bufferid2 = Shader.PropertyToID("bufferblur2");         // 临时图像,也可以用 Handle,ID不乱就可以

获取临时RT

// 获取一张临时RT
        cmd.GetTemporaryRT(destination, inRTDesc.width, inRTDesc.height, 0, FilterMode.Bilinear, RenderTextureFormat.DefaultHDR); //申请一个临时图像,并设置相机rt的参数进去
        cmd.GetTemporaryRT(bufferid1, inRTDesc.width, inRTDesc.height, 0, FilterMode.Bilinear, RenderTextureFormat.DefaultHDR); //申请一个临时图像,并设置相机rt的参数进去
        cmd.GetTemporaryRT(bufferid2, inRTDesc.width/2, inRTDesc.height/2, 0, FilterMode.Bilinear, RenderTextureFormat.DefaultHDR); //申请一个临时图像,并设置相机rt的参数进去

注意:这里RT1 和R2是储存是不一样的,RT2是一半是我们中间过程,RT是输出结果所以使用高精度。

释放临时RT

cmd.ReleaseTemporaryRT(destination);                                         // 释放临时RT
cmd.ReleaseTemporaryRT(bufferid1);                                         // 释放临时RT
cmd.ReleaseTemporaryRT(bufferid2);                                         // 释放临时RT



在else里面增加逻辑

else
        {
            cmd.Blit(destination, bufferid1, myssaoMaterial, 0);                                 // 计算AO 储存到 Buffer1
            cmd.SetGlobalTexture("_AOTex", bufferid1);                                           // 计算完的图储存到 bufferid1
            cmd.Blit(bufferid1, bufferid2, myssaoMaterial, 1);                                   // 进行水平模糊 储存到 Buffer2
            cmd.SetGlobalTexture("_AOTex", bufferid2);                                           // 把计算完的图传入到Shader中进行下一次计算
            cmd.Blit(bufferid2, bufferid1, myssaoMaterial, 2);                                   // 进行垂直模糊 储存到 Buffer1


            cmd.Blit(bufferid1, source);                                                    //  在传到 下一个渲染层级
        }


这里在执行一遍AO效果,装修完储存到 bufferid1中,

cmd.SetGlobalTexture("_AOTex", bufferid1)  把AO效果传递到Shader中进行计算。

执行两次  水平一次,垂直一次,输出查看效果

AO原图混合

我们把处理的结果传入到Shader

cmd.SetGlobalTexture("_AOTex", bufferid1);                                           // 计算完结果传入到Shader中 和原图进行处理
cmd.Blit(destination, source, myssaoMaterial, 3);                            //  第四个Pass

这样就可以实现完成了。

  • 可以改变阴影的颜色

这个效果其实还可以在模糊一次,

SSAO.cs

using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;



public class SSAO : ScriptableRendererFeature
{
    [System.Serializable]
    public class Settings
    {
        public RenderPassEvent renderPassEvent = RenderPassEvent.BeforeRenderingPostProcessing;
        public Shader shader;
    }
    public Settings settings = new Settings();

    SSAOPass ssaoPass;


    public override void Create()
    {
        this.name = "SSAO";
        ssaoPass = new SSAOPass(RenderPassEvent.BeforeRenderingPostProcessing, settings.shader);

    }
    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        renderer.EnqueuePass(ssaoPass);
    }
}


public class SSAOPass : ScriptableRenderPass
{
    static readonly string RenderTag = "SSAO Effects";

    SSAOVolume ssaoVolume;
    Material myssaoMaterial;


    Camera cam;


    public SSAOPass(RenderPassEvent evt, Shader ssaoshader)
    {
        renderPassEvent = evt;
        var shader = ssaoshader;

        if (shader == null)
        {
            Debug.LogError("没有Shader");
            return;
        }
        myssaoMaterial = CoreUtils.CreateEngineMaterial(ssaoshader);
    }

    public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
    {
        if (myssaoMaterial == null)
        {
            return;
        }

        if (!renderingData.cameraData.postProcessEnabled)
        {
            return;
        }

        var stack = VolumeManager.instance.stack;
        ssaoVolume = stack.GetComponent<SSAOVolume>();

        cam = renderingData.cameraData.camera;

        var cmd = CommandBufferPool.Get(RenderTag);

        Render(cmd, ref renderingData);

        context.ExecuteCommandBuffer(cmd);
        CommandBufferPool.Release(cmd);

    }


    void Render(CommandBuffer cmd, ref RenderingData renderingData)
    {



        RenderTargetIdentifier source = renderingData.cameraData.renderer.cameraColorTarget;
        RenderTextureDescriptor inRTDesc = renderingData.cameraData.cameraTargetDescriptor;
        inRTDesc.depthBufferBits = 0;

        SetMatData();

        int destination = Shader.PropertyToID("Temp1");
        int bufferid1 = Shader.PropertyToID("bufferblur1");         // 临时图像,也可以用 Handle,ID不乱就可以
        int bufferid2 = Shader.PropertyToID("bufferblur2");         // 临时图像,也可以用 Handle,ID不乱就可以


        cmd.GetTemporaryRT(destination, inRTDesc.width, inRTDesc.height, 0, FilterMode.Bilinear, RenderTextureFormat.DefaultHDR);
        cmd.GetTemporaryRT(bufferid1, inRTDesc.width, inRTDesc.height, 0, FilterMode.Bilinear, RenderTextureFormat.DefaultHDR); //申请一个临时图像,并设置相机rt的参数进去
        cmd.GetTemporaryRT(bufferid2, inRTDesc.width/2, inRTDesc.height/2, 0, FilterMode.Bilinear, RenderTextureFormat.DefaultHDR); //申请一个临时图像,并设置相机rt的参数进去


        cmd.Blit(source, destination);


        if (ssaoVolume._aoOnly.value)
        {
            cmd.Blit(destination, source, myssaoMaterial, 0);
        }
        else
        {
            cmd.Blit(destination, bufferid1, myssaoMaterial, 0);                                 // 计算AO 储存到 Buffer1
            cmd.SetGlobalTexture("_AOTex", bufferid1);                                           // 计算完的图储存到 bufferid1
            cmd.Blit(bufferid1, bufferid2, myssaoMaterial, 1);                                   // 进行水平模糊 储存到 Buffer2
            cmd.SetGlobalTexture("_AOTex", bufferid2);                                           // 把计算完的图传入到Shader中进行下一次计算
            cmd.Blit(bufferid2, bufferid1, myssaoMaterial, 2);                                   // 进行垂直模糊 储存到 Buffer1


            cmd.SetGlobalTexture("_AOTex", bufferid1);                                           // 计算完结果传入到Shader中 和原图进行处理
            cmd.Blit(destination, source, myssaoMaterial, 3);                            //  第四个Pass
        }
        cmd.ReleaseTemporaryRT(destination);                                         // 释放临时RT
        cmd.ReleaseTemporaryRT(bufferid1);                                         // 释放临时RT
        cmd.ReleaseTemporaryRT(bufferid2);                                         // 释放临时RT
    }


    private void SetMatData()
    {
        Matrix4x4 vp_Matrix = cam.projectionMatrix * cam.worldToCameraMatrix;
        myssaoMaterial.SetMatrix("_VPMatrix_invers", vp_Matrix.inverse);

        Matrix4x4 v_Matrix = cam.worldToCameraMatrix;
        myssaoMaterial.SetMatrix("_VMatrix", v_Matrix);

        Matrix4x4 p_Matrix = cam.projectionMatrix;
        myssaoMaterial.SetMatrix("_PMatrix", p_Matrix);

        myssaoMaterial.SetColor("_aoColor", ssaoVolume.aoColor.value);               // 获取value 组件的颜色
        myssaoMaterial.SetFloat("_SampleCount", ssaoVolume.sampleCount.value);
        myssaoMaterial.SetFloat("_Radius", ssaoVolume.radius.value);
        myssaoMaterial.SetFloat("_RangeCheck", ssaoVolume.rangeCheck.value);
        myssaoMaterial.SetFloat("_AOInt", ssaoVolume.aoInt.value);

        myssaoMaterial.SetFloat("_BlurRadius", ssaoVolume.blurRadius.value);
        myssaoMaterial.SetFloat("_BilaterFilterFactor", ssaoVolume.bilaterFilterFactor.value);

    }


}

Volune

using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;


public class SSAOVolume : VolumeComponent, IPostProcessComponent
{
    public ClampedIntParameter sampleCount = new ClampedIntParameter(22, 1, 128);
    public ClampedFloatParameter radius = new ClampedFloatParameter(0.5f, 0f, 0.8f);
    public ClampedFloatParameter rangeCheck = new ClampedFloatParameter(0f, 0f, 10f);
    public ClampedFloatParameter aoInt  = new ClampedFloatParameter(1f, 0f, 10f);

    public ClampedFloatParameter blurRadius = new ClampedFloatParameter(1f, 0f, 3f);
    public ClampedFloatParameter bilaterFilterFactor = new ClampedFloatParameter(0.1f, 0f, 1f);

    public ColorParameter aoColor = new ColorParameter(Color.black, false);
    public BoolParameter _aoOnly = new BoolParameter(false);

    public bool IsActive() => sampleCount.value > 0;

    public bool IsTileCompatible() => false;


}

总结

  1. 在实现AO效果的时候比较麻烦,平常是有两种方法,第一种方法都是在 View视角下计算,这里使用第二种方法,在世界空间下计算完成,在转换成View视角,

    • Vertex position vectors in view space

view space中的顶点位置向量


    • Vertex normal vectors in view space

GIF
view space中的顶点法线向量
    • Sample vectors in tangent space

GIF
tangent space中的样本向量
    • Noise vectors in tangent space

GIF
tangent space中的噪声向量

2. 计算AO具体流程,使用UV计算出需要的 位置,法线,切线构建矩阵, 遍历所有的采样点,使用随机向量计算出半求,扩展半球范围大小。

    计算完成转换到裁剪空间,根据深度判断是否在阴影范围。

3. 在管线里使用摄像机获取矩阵传递给Shader

    • _VPMatrix_invers      视图投影矩阵的逆矩阵

    • _VMatrix         视图矩阵是将场景从世界坐标系转换到相机坐标系的变换矩阵

    • _PMatrix        投影矩阵则是将相机坐标系中的场景投影到屏幕坐标系的变换矩阵


投诉或建议