Unity自定义渲染管线中SSAO实现:从原理到性能优化实战

Unity自定义渲染管线中SSAO实现:从原理到性能优化实战 1. 项目概述为什么要在自定义管线中实现SSAO屏幕空间环境光遮蔽也就是我们常说的SSAO是现在游戏和实时渲染中提升画面真实感的一个关键技术。简单来说它模拟了现实世界中物体在角落、缝隙、凹陷处因为光线难以到达而产生的柔和阴影。没有它场景看起来会显得很“平”物体像是漂浮在空中缺乏与环境的重量感和接触感。Unity内置的URP和HDRP管线都提供了SSAO后处理效果开箱即用非常方便。那为什么我们还要费劲在自定义渲染管线里自己实现一遍呢这恰恰是很多从应用层转向引擎底层开发的伙伴们会遇到的第一个“灵魂拷问”。我的答案是知其然更要知其所以然。使用内置效果你只是一个调参师而亲手实现它你才能成为那个定义规则的人。在自定义管线中实现SSAO意味着你完全掌控了从渲染目标分配、深度/法线纹理获取、采样核生成、到最终的模糊与混合的每一个环节。这种掌控力带来的好处是巨大的首先是极致的性能优化你可以根据项目需求比如是移动端还是PC端裁剪采样数量、调整采样半径、选择最合适的模糊算法把每一毫秒的GPU时间都用在刀刃上其次是无与伦比的定制灵活性你可以轻松地修改算法来创造独特的艺术风格或者将SSAO与其他自定义的后处理效果比如自定义的Bloom、色调映射进行深度集成避免不必要的全屏Blit开销。从最近的热搜词也能看出社区对Unity底层技术的关注度很高无论是“Unity Shader”、“Unity ECS”还是各种性能优化和面试题“Unity八股文”、“Unity面经”都说明深入理解渲染管线是进阶的必经之路。自己实现SSAO就是一个绝佳的、目标明确的实践切入点。它涉及了现代实时渲染的多个核心概念屏幕空间技术、随机采样、后处理、以及Shader编写。通过这个项目你不仅能得到一个可用的SSAO效果更能建立起一套处理复杂后处理效果的通用方法论。2. 核心原理拆解SSAO是如何“算”出阴影的在深入代码之前我们必须先搞清楚SSAO的基本算法逻辑。它完全在屏幕空间即当前帧渲染出的图像中工作不需要场景的全局信息这也是其高效的原因。核心输入通常只需要深度缓冲Depth Buffer和法线缓冲Normal Buffer。2.1 算法思想基于邻域深度比较的遮蔽估算SSAO的核心思想非常直观对于屏幕上的每一个像素我们称之为当前像素在其周围的半球形空间内随机撒一堆采样点。然后将这些采样点从屏幕空间变换回三维的观察空间或世界空间。接着检查每个采样点的实际深度值通过采样深度纹理获得与采样点所在位置的“预期”深度值假设该点位于当前像素所在的几何表面之间的关系。如果采样点的实际深度值比预期深度值更靠近摄像机即深度值更小说明这个采样点位于几何表面“之前”它很可能被其他几何体挡住了那么当前像素的这个方向就被认为是“被遮蔽”的。最后统计所有采样点中被遮蔽的比例这个比例值在0到1之间就代表了当前像素的环境光遮蔽强度。值越接近1该位置越暗。这里有一个关键技巧采样是在当前像素法线方向所定义的法线半球内进行的而不是整个球体。因为背对当前表面的点即法线反方向不可能对当前点产生遮蔽这样可以减少一半无效采样提升结果的物理正确性和性能。2.2 关键组件与数据准备要实现这个算法我们需要在Shader中准备几个核心组件深度与法线纹理这是SSAO的“眼睛”。深度纹理存储了每个像素到摄像机的距离通常是非线性的需要小心处理。法线纹理存储了每个像素所在表面的法线方向。在自定义管线中我们需要在渲染不透明物体时主动将这些信息渲染到指定的渲染目标RenderTarget中通常通过配置Shader的SV_Depth和SV_Normal输出来实现。采样核与随机旋转纹理采样核一个包含数十个例如16或32个三维向量的数组。这些向量应该在一个单位球体内或单位半球内呈大致均匀的随机分布。我们可以在CPU端C#脚本预计算这个数组然后通过Shader属性VectorArray传递给GPU。为了获得更真实的衰减采样点通常不是均匀分布而是让更多的点集中在原点附近例如使用sample.xyz normalize(sample.xyz) * random(0.0, 1.0) 其中random生成0到1的随机数这样分布更符合平方反比衰减。随机旋转纹理如果对每个像素都使用完全相同的采样核那么计算出的AO图案会有明显的重复和带状瑕疵。为了解决这个问题我们引入一张小的如4x4随机旋转向量纹理。对于屏幕上的每个像素我们根据其屏幕位置采样这张纹理得到一个随机的旋转向量或者一个旋转矩阵然后用它来扰动采样核的方向。这样每个像素的采样模式都略有不同能有效消除图案重复。观察空间重建这是算法中最容易出错的一步。采样核是三维向量但我们需要在观察空间View Space中进行采样和深度比较。因此对于当前像素我们需要 a. 用它的屏幕坐标和深度值反算出它在观察空间中的三维位置viewPos。 b. 同样对于每一个偏移后的采样点也需要将其从观察空间投影到屏幕空间去采样深度纹理然后再将采样到的深度值反算回观察空间位置sampleViewPos。 这个过程涉及大量的矩阵运算投影矩阵的逆矩阵和坐标系统转换必须非常精确。3. 自定义管线框架搭建与资源准备在动手写SSAO Shader之前我们需要先搭建一个能运行它的自定义渲染管线环境。这里我们基于Unity的Scriptable Render Pipeline (SRP)核心API来构建一个最简化的自定义管线。3.1 创建自定义RenderPipelineAsset和RenderPipeline首先创建两个C#脚本CustomRenderPipelineAsset.cs和CustomRenderPipeline.cs。Asset是管线资源的定义而Pipeline是实际的渲染逻辑执行者。// CustomRenderPipelineAsset.cs using UnityEngine; using UnityEngine.Rendering; [CreateAssetMenu(menuName Rendering/Custom Render Pipeline)] public class CustomRenderPipelineAsset : RenderPipelineAsset { protected override RenderPipeline CreatePipeline() { return new CustomRenderPipeline(); } }// CustomRenderPipeline.cs using UnityEngine; using UnityEngine.Rendering; public class CustomRenderPipeline : RenderPipeline { private CameraRenderer _renderer new CameraRenderer(); protected override void Render(ScriptableRenderContext context, Camera[] cameras) { // 遍历所有摄像机每个摄像机独立渲染 foreach (var camera in cameras) { _renderer.Render(context, camera); } } }创建完CustomRenderPipelineAsset后将其拖入Project Settings - Graphics - Scriptable Render Pipeline Settings中Unity就会使用你的自定义管线进行渲染。3.2 实现基础的CameraRendererCameraRenderer是管线的核心它负责组织一个摄像机的完整渲染流程。我们需要在其中加入渲染深度/法线纹理以及执行SSAO后处理的步骤。// CameraRenderer.cs 简化版框架 using UnityEngine; using UnityEngine.Rendering; public partial class CameraRenderer { private ScriptableRenderContext _context; private Camera _camera; private const string _bufferName Render Camera; private CommandBuffer _cmd new CommandBuffer { name _bufferName }; // SSAO相关的渲染纹理和材质 private RenderTexture _aoRT; // 存储AO结果的RT private Material _ssaoMaterial; public void Render(ScriptableRenderContext context, Camera camera) { _context context; _camera camera; PrepareBuffer(); PrepareForSceneWindow(); // 处理Scene窗口UI if (!Cull()) return; // 视锥体剔除 Setup(); // 设置摄像机矩阵、清空目标等 DrawVisibleGeometry(); // 渲染不透明和透明几何体 DrawGizmos(); // 绘制Gizmo Submit(); // 提交命令队列 } private void DrawVisibleGeometry() { // 1. 先渲染深度和法线到纹理 RenderDepthAndNormals(); // 2. 执行SSAO计算 ExecuteSSAO(); // 3. 渲染不透明物体可能使用上一步的AO结果 var sortingSettings new SortingSettings(_camera) { criteria SortingCriteria.CommonOpaque }; var drawingSettings new DrawingSettings(new ShaderTagId(SRPDefaultUnlit), sortingSettings); var filteringSettings new FilteringSettings(RenderQueueRange.opaque); _context.DrawRenderers(_cullingResults, ref drawingSettings, ref filteringSettings); // 4. 渲染天空盒、透明物体等... } private void RenderDepthAndNormals() { // 创建临时的RT来存储深度和法线 _cmd.GetTemporaryRT(_depthNormalsID, _camera.pixelWidth, _camera.pixelHeight, 24, FilterMode.Point, RenderTextureFormat.ARGBFloat); // 设置渲染目标并指定Shader Pass来输出深度和法线 _cmd.SetRenderTarget(_depthNormalsID, RenderBufferLoadAction.DontCare, RenderBufferStoreAction.Store); _cmd.ClearRenderTarget(true, true, Color.clear); // 绘制所有需要输出深度法线的物体通常是不透明物体 // ... 执行绘制命令 } private void ExecuteSSAO() { if (_ssaoMaterial null) _ssaoMaterial new Material(Shader.Find(Hidden/Custom/SSAO)); // 将必要的参数如采样核、噪声纹理、投影参数设置到材质中 ConfigureSSAOMaterialProperties(); // 申请一个临时RT用于存储AO结果 _cmd.GetTemporaryRT(_aoRTID, _camera.pixelWidth / 2, _camera.pixelHeight / 2, 0, FilterMode.Bilinear); // 通常用半分辨率计算以节省性能 _cmd.Blit(null, _aoRTID, _ssaoMaterial, 0); // 使用Shader的第一个Pass计算AO // 可选对aoRT进行模糊处理Pass 1 _cmd.Blit(_aoRTID, _blurRTID, _ssaoMaterial, 1); // 将模糊后的结果存回aoRTPass 2 _cmd.Blit(_blurRTID, _aoRTID, _ssaoMaterial, 2); } }注意在实际项目中渲染深度/法线纹理和SSAO计算需要精细的Pass管理和资源生命周期控制使用CommandBuffer的GetTemporaryRT和ReleaseTemporaryRT。为了清晰起见上面的代码是高度简化的概念展示。3.3 生成采样核与随机噪声纹理这部分工作在C#中完成在管线初始化或摄像机渲染开始时执行一次。// 在某个管理类如SSAOSettings中 public Vector4[] GenerateSampleKernel(int kernelSize) { Vector4[] kernel new Vector4[kernelSize]; for (int i 0; i kernelSize; i) { // 生成在单位球体内的随机点 Vector3 sample UnityEngine.Random.insideUnitSphere; sample.Normalize(); // 让采样点分布更偏向原点加速衰减 float scale (float)i / kernelSize; scale Mathf.Lerp(0.1f, 1.0f, scale * scale); // 平方插值让更多点靠近中心 sample * scale; kernel[i] new Vector4(sample.x, sample.y, sample.z, 0); } return kernel; } public Texture2D GenerateNoiseTexture(int size) { Texture2D noiseTex new Texture2D(size, size, TextureFormat.RGBAFloat, false); for (int y 0; y size; y) { for (int x 0; x size; x) { // 生成随机旋转向量在XY平面上的随机方向 Vector3 noise new Vector3( UnityEngine.Random.Range(-1f, 1f), UnityEngine.Random.Range(-1f, 1f), 0).normalized; noiseTex.SetPixel(x, y, new Color(noise.x, noise.y, 0, 0)); } } noiseTex.Apply(); noiseTex.wrapMode TextureWrapMode.Repeat; noiseTex.filterMode FilterMode.Point; // 非常重要必须使用Point过滤否则模糊的噪声会破坏随机性 return noiseTex; }将生成的kernel数组通过Material.SetVectorArray传递给Shader将noiseTex通过Material.SetTexture传递。4. SSAO Shader核心实现详解接下来是重头戏Shader。我们将创建一个包含多个Pass的Shader分别用于计算AO和模糊。4.1 计算AO的Pass (Pass 0)这个Pass是一个全屏的后期处理Shader。它接收深度/法线纹理、采样核、噪声纹理作为输入输出一张AO图。// CustomSSAO.shader (部分代码) Shader Hidden/Custom/SSAO { Properties { _MainTex (Texture, 2D) white {} _CameraDepthNormalsTexture (Depth Normals, 2D) white {} _NoiseTex (Noise Tex, 2D) white {} _SampleKernel (Sample Kernel, Vector) (0,0,0,0) _KernelSize (Kernel Size, Int) 16 _Radius (Radius, Range(0.1, 5.0)) 0.5 _Bias (Bias, Range(0, 0.5)) 0.025 _Intensity (Intensity, Range(0, 5)) 1.0 _Contrast (Contrast, Range(0, 5)) 1.0 } SubShader { Pass // Pass 0: 计算AO { CGPROGRAM #pragma vertex vert #pragma fragment frag #include UnityCG.cginc #define SAMPLE_KERNEL_COUNT 16 // 与C#中生成的核数量一致 struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 pos : SV_POSITION; }; sampler2D _CameraDepthNormalsTexture; sampler2D _NoiseTex; float4 _NoiseTex_TexelSize; float4 _SampleKernel[SAMPLE_KERNEL_COUNT]; float _Radius; float _Bias; float _Intensity; float _Contrast; v2f vert (appdata v) { ... } // 标准全屏三角形顶点着色器 float3 DecodeViewNormal(float4 enc) { // 从编码的深度法线纹理中解码出观察空间法线 float3 n enc.xyz * 2.0 - 1.0; // 这里假设法线是观察空间的且编码为[0,1]范围 return normalize(n); } float DecodeLinearDepth(float4 enc) { // 从深度纹理中解码出线性深度 float depth enc.w; // 假设深度存储在alpha通道 // 根据你的深度纹理编码方式调整这里是一个示例 return LinearEyeDepth(depth); } float4 frag (v2f i) : SV_Target { // 1. 解码当前像素的深度和法线 float4 depthnormal tex2D(_CameraDepthNormalsTexture, i.uv); float3 viewNormal DecodeViewNormal(depthnormal); float viewDepth DecodeLinearDepth(depthnormal); // 2. 重建当前像素在观察空间的位置 float2 p11_22 float2(unity_CameraProjection._11, unity_CameraProjection._22); float3 viewPos float3((i.uv * 2.0 - 1.0) / p11_22, -1.0) * viewDepth; // 3. 生成随机旋转矩阵基于噪声纹理 float2 noiseScale _ScreenParams.xy / _NoiseTex_TexelSize.zw; // 让噪声纹理平铺覆盖屏幕 float3 randomVec tex2D(_NoiseTex, i.uv * noiseScale).xyz * 2.0 - 1.0; // 构建TBN矩阵Tangent, Bitangent, Normal float3 tangent normalize(randomVec - viewNormal * dot(randomVec, viewNormal)); float3 bitangent cross(viewNormal, tangent); float3x3 TBN float3x3(tangent, bitangent, viewNormal); // 4. 循环采样核计算遮蔽 float occlusion 0.0; for (int j 0; j SAMPLE_KERNEL_COUNT; j) { // 获取采样点偏移切线空间 - 观察空间 float3 sampleOffset mul(TBN, _SampleKernel[j].xyz); float3 sampleViewPos viewPos sampleOffset * _Radius; // 将采样点从观察空间投影到屏幕空间 float4 sampleClipPos mul(unity_CameraProjection, float4(sampleViewPos, 1.0)); sampleClipPos.xy / sampleClipPos.w; float2 sampleUV sampleClipPos.xy * 0.5 0.5; // 采样深度纹理获取采样点处的实际线性深度 float4 sampleDepthNormal tex2D(_CameraDepthNormalsTexture, sampleUV); float sampleSceneDepth DecodeLinearDepth(sampleDepthNormal); // 计算采样点处的预期深度即sampleViewPos的Z值在观察空间中Z为负 float sampleZ sampleViewPos.z; // 注意观察空间Z轴方向 // 深度比较实际深度 vs 预期深度 偏置 float rangeCheck smoothstep(0.0, 1.0, _Radius / abs(viewDepth - sampleSceneDepth)); occlusion (sampleSceneDepth sampleZ _Bias ? 1.0 : 0.0) * rangeCheck; } occlusion 1.0 - (occlusion / SAMPLE_KERNEL_COUNT); // 5. 应用强度和对比度调整 occlusion pow(saturate(occlusion), _Contrast) * _Intensity; return float4(occlusion.xxx, 1.0); } ENDCG } // ... 后续模糊Pass } }4.2 双边模糊Pass (Pass 1 2)直接计算出的AO图噪声很大因为采样是随机的。我们需要一个保边的模糊来平滑噪声同时不越过物体的边界否则AO会泄露到不该有的地方。双边模糊是SSAO后处理的标配。// 接上面的SubShader Pass // Pass 1: 水平方向双边模糊 { CGPROGRAM #pragma vertex vert #pragma fragment frag_horizontal_blur // ... 属性定义需要_AOTex和_CameraDepthNormalsTexture sampler2D _AOTex; // 上一步计算的原始AO图 float4 _AOTex_TexelSize; float4 frag_horizontal_blur (v2f i) : SV_Target { float2 texelSize _AOTex_TexelSize.xy; float2 uv i.uv; // 获取中心点的深度/法线用于边缘判断 float4 centerDepthNormal tex2D(_CameraDepthNormalsTexture, uv); float centerDepth DecodeLinearDepth(centerDepthNormal); float3 centerNormal DecodeViewNormal(centerDepthNormal); float ao 0.0; float totalWeight 0.0; // 水平方向采样例如半径2 for (int x -2; x 2; x) { float2 sampleUV uv float2(x * texelSize.x, 0); float4 sampleDepthNormal tex2D(_CameraDepthNormalsTexture, sampleUV); float sampleDepth DecodeLinearDepth(sampleDepthNormal); float3 sampleNormal DecodeViewNormal(sampleDepthNormal); // 空间距离权重高斯核 float spaceWeight exp(-(x * x) / (2.0 * 1.5 * 1.5)); // 1.5是标准差 // 深度差异权重防止跨越深度不连续处模糊 float depthDiff abs(centerDepth - sampleDepth); float depthWeight exp(-(depthDiff * depthDiff) / (2.0 * 0.01 * 0.01)); // 0.01是深度差异的敏感度 // 法线差异权重可选防止跨越法线不连续处模糊 float normalDiff dot(centerNormal, sampleNormal); float normalWeight max(0.0, normalDiff); // 法线越相似权重越高 float weight spaceWeight * depthWeight * normalWeight; ao tex2D(_AOTex, sampleUV).r * weight; totalWeight weight; } ao / totalWeight; return float4(ao.xxx, 1.0); } ENDCG } Pass // Pass 2: 垂直方向双边模糊与水平Pass几乎相同只是偏移方向改为y轴 { // ... 代码结构与水平模糊类似将偏移方向改为 float2(0, y * texelSize.y) }这样经过水平和垂直两次双边模糊后我们得到了一张平滑且边界清晰的AO图。5. 性能优化与参数调校实战心得实现功能只是第一步让它在目标平台上流畅运行才是真正的挑战。SSAO是一个典型的带宽和ALU算术逻辑单元密集型后处理效果。5.1 核心性能优化策略降低分辨率计算这是最有效的一招。AO是一种低频信息对分辨率不敏感。完全可以在半分辨率甚至四分之一分辨率下进行计算和模糊最后再上采样到全分辨率。在我的项目中将计算分辨率从1080p降到540p性能直接提升了近3倍而视觉质量损失在大多数情况下难以察觉。在Shader中只需调整渲染目标的大小和UV的缩放即可。精心控制采样数量采样核的数量SAMPLE_KERNEL_COUNT直接决定了Fragment Shader的循环次数。对于移动端16个采样可能已经是上限PC端则可以尝试32或64个以获得更高质量。一个技巧是使用可变采样数量根据摄像机距离或者根据性能档位动态调整。在远处物体细节减少可以用更少的采样。利用硬件线性过滤在采样深度/法线纹理时使用tex2D的硬件双线性过滤可以在一定程度上平滑采样结果有时可以减少噪声从而允许你使用更少的采样次数或更弱的模糊。但要注意对法线纹理进行线性过滤在数学上是不正确的法线是方向向量可能会引入误差需要权衡。优化模糊Pass双边模糊很耗性能因为每个像素需要采样多次深度/法线纹理。可以尝试降低模糊半径从5降到3。使用可分离的高斯模糊就像我们上面做的那样先水平后垂直将O(n²)的采样复杂度降为O(2n)。在更低分辨率下模糊在半分辨率下计算AO并模糊然后上采样比全分辨率模糊快得多。5.2 参数调校的艺术从“塑料感”到“电影感”SSAO的参数调整是门玄学调好了画面质感飙升调不好就满屏脏污或塑料感。_Radius(采样半径)这是最重要的参数之一。它决定了AO影响的空间范围。值太小只有非常紧贴的缝隙才有阴影显得假值太大阴影会弥漫到不该有的地方让物体看起来像是沉在雾里。一个实用的技巧是让半径与场景的尺度相关联。你可以用世界空间单位来定义半径然后在Shader中根据当前像素的深度即距离来缩放它实现随距离变化的采样半径这样近处细节丰富远处效率更高且自然。_Bias(深度偏置)这个参数是为了解决自遮蔽问题。由于深度缓冲的精度限制和法线插值的影响一个采样点可能会错误地判断它自己被它所在的表面遮蔽了即自己遮蔽自己导致表面出现难看的黑点。增加Bias可以缓解这个问题但加得太多又会削弱真实的遮蔽效果使角落变亮。通常一个很小的值如0.01-0.05就够了。_Intensity(强度) 与_Contrast(对比度)强度控制AO阴影的明暗程度对比度控制阴影的软硬。我个人的工作流是先将对比度调到1.5-2.0让明暗区分更明显然后再用强度来控制整体暗度。避免将强度调得过高2.0否则画面会显得很脏。实操心得调试时我习惯单独将AO输出到屏幕上覆盖整个画面关闭颜色和其他后期效果这样能最纯粹地观察AO图案的质量检查是否有带状瑕疵、噪声是否均匀、边界是否清晰。确认AO本身没问题后再以“Screen”混合模式即AO * SceneColor与场景颜色相乘观察最终合成效果。6. 常见问题、排查与进阶思路即使按照步骤实现你也一定会遇到各种问题。下面是我踩过的一些坑和解决方案。6.1 问题排查清单问题现象可能原因排查与解决思路全屏纯白或纯黑AO计算Pass没有正确执行或渲染目标未清空。1. 检查CommandBuffer中Blit的源目标null表示当前激活的RT和目标RT ID是否正确。2. 在Frame Debugger中查看渲染事件确认SSAO Pass被调用且输入纹理绑定正确。3. 在Shader中先返回一个固定颜色如return float4(0.5,0,0,1);测试Pass是否运行。AO图案有规律的带状或网格状瑕疵随机旋转纹理的平铺Tiling过于明显或者噪声纹理过滤模式不是Point。1. 确保_NoiseTex的wrapMode为RepeatfilterMode为Point。2. 增大噪声纹理的平铺倍数noiseScale计算中的除数让噪声变化更频繁。3. 尝试使用一张更大的如8x8噪声纹理。物体边缘有黑边或光晕双边模糊的深度/法线权重阈值设置不当导致AO跨越边界进行了模糊。1. 在双边模糊的Shader中增加depthWeight计算中的敏感度参数分母让深度差异的惩罚更严厉。2. 引入并调高法线差异权重normalWeight。3. 检查深度/法线纹理的获取是否正确特别是在透明物体或天空盒区域。性能极差帧率暴跌采样数量过多、半径过大、或在全分辨率下进行复杂模糊。1. 使用RenderDoc或Unity Profiler的GPU模块定位是Fragment Shader开销大还是纹理采样带宽高。2.首要优化将AO计算降到半分辨率。3. 次选减少采样核数量如从32减到16。4. 检查模糊Pass的采样半径。近处效果正常远处AO闪烁或消失采样半径_Radius是固定值在观察空间中远处的物体覆盖的屏幕区域小固定的世界空间半径在NDC中占比过大或过小。实现基于深度的半径缩放。在Shader中根据当前像素的观察空间深度或线性深度来动态缩放采样偏移量。float scaledRadius _Radius / (viewDepth 1.0);这是一个简化公式需根据场景调整。与透明物体结合时出错深度纹理通常只包含不透明物体的深度透明物体不会写入深度。SSAO在透明物体区域会采样到后面不透明物体的深度导致错误遮蔽。这是一个已知限制。通常的解决方案是1. 忽略透明物体区域的SSAO通过Stencil Buffer标记透明物体区域。2. 或者为透明物体单独渲染一张深度图并参与AO计算但这非常复杂。大多数高质量游戏选择在透明物体上禁用或减弱SSAO。6.2 从SSAO到HBAO与GTAO基础的SSAO实现后你可以探索更先进的AO算法它们能提供更高质量、更物理准确的结果当然复杂度也更高。HBAO (Horizon-Based Ambient Occlusion)不再进行球体内的随机采样而是基于当前像素的法线在多个方向如8个上搜索“地平线”角度。遮蔽程度由被遮挡的地平线角度范围决定。HBAO的优点是对参数不那么敏感结果更加稳定和一致噪点更少但计算量更大。GTAO (Ground Truth Ambient Occlusion)或Ray Traced AO这是目前的高端解决方案。GTAO通过更复杂的积分计算来逼近光线追踪的参考结果。而真正的光线追踪环境光遮蔽 (Ray Traced AO)则是通过硬件光追如DXR、Vulkan Ray Tracing实时计算质量最高是未来发展方向。在自定义管线中集成这些需要更深入的图形学知识和硬件支持。实现基础SSAO是理解这些高级技术的第一步。它让你熟悉了屏幕空间技术的整个流水线数据准备深度/法线、屏幕空间计算、后处理模糊、与场景合成。掌握了这套流程再去学习HBAO或尝试集成一个简单的光线追踪Pass思路就会清晰很多。最后别忘了将你的AO结果与其他的后期效果如Color Grading, Bloom妥善结合。通常的合成顺序是先计算AO然后将AO图作为输入之一参与到最终的光照计算或屏幕空间反射中或者在所有光照计算完成后简单地将AO与场景颜色相乘。不同的合成顺序会产生不同的视觉风格这取决于你想要达到的艺术效果。