【从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和深度图进行世界坐标重建与遮蔽计算。
  • 示例代码
    • 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开始探索游戏渲染】专栏-直达

(欢迎点赞留言探讨,更多人加入进来能更加完善这个探索的过程,)

【URP】Unity[RendererFeatures]屏幕空间环境光遮蔽SSAO的更多相关文章

  1. DirectX11 With Windows SDK--32 SSAO(屏幕空间环境光遮蔽)

    前言 由于性能的限制,实时光照模型往往会忽略间接光因素(即场景中其他物体所反弹的光线).但在现实生活中,大部分光照其实是间接光.在第7章里面的光照方程里面引入了环境光项: \[C_a = \mathb ...

  2. Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第二十一章:环境光遮蔽(AMBIENT OCCLUSION)

    原文:Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第二十一章:环境光遮蔽(AMBIENT OCCLUSION) 学习目标 ...

  3. 基于屏幕空间的实时全局光照(Real-time Global Illumination Based On Screen Space)

    目录 Reflective Shadow Maps(RSM) RSM 的重要性采样 RSM 的应用与缺陷 Screen Space Ambient Occulsion(SSAO) SSAO Blur ...

  4. [帖子收集]环境光遮蔽(Ambient Occlusion)

    环境光遮蔽,效果示例图 图片左边是一条龙的简单模型,呈现在一个均匀照明的环境中.尽管模型中有一些明暗不同的区域,但大部分光照都是均匀的.虽然模型有着相当复杂的几何形状,但看上去比较光滑平坦,没有明显的 ...

  5. 在Unity中实现屏幕空间阴影(2)

    参考文章: https://www.imgtec.com/blog/implementing-fast-ray-traced-soft-shadows-in-a-game-engine/ 完成的工程: ...

  6. 在Unity中实现屏幕空间阴影(1)

    接着上篇文章,我们实现了SSR效果. 其中的在屏幕空间进行光线追踪的方法是通用的.借此我们再实现一种屏幕空间的效果,即屏幕空间阴影. 文中的图片来自Catlike coding http://catl ...

  7. Cesium源码剖析---Ambient Occlusion(环境光遮蔽)

    Ambient Occlusion简称AO,中文没有太确定的叫法,一般译作环境光遮蔽.百度百科上对AO的解释是这样的:AO是来描绘物体和物体相交或靠近的时候遮挡周围漫反射光线的效果,可以解决或改善漏光 ...

  8. Unity3d 屏幕空间人体皮肤知觉渲染&次表面散射Screen-Space Perceptual Rendering & Subsurface Scattering of Human Skin

    之前的人皮渲染相关 前篇1:unity3d Human skin real time rendering 真实模拟人皮实时渲染 前篇2:unity3d Human skin real time ren ...

  9. Unity3D 屏幕空间雪场景Shader渲染

    笔者介绍:姜雪伟,IT公司技术合伙人,IT高级讲师,CSDN社区专家,特邀编辑,畅销书作者,已出版书籍:<手把手教你架构3D游戏引擎>电子工业出版社和<Unity3D实战核心技术详解 ...

  10. Unity ugui屏幕适配与世界坐标到ugui屏幕坐标的转换

    我们知道,如今的移动端设备分辨率五花八门,而开发过程中往往只取一种分辨率作为设计参考,例如采用1920*1080分辨率作为参考分辨率. 选定了一种参考分辨率后,美术设计人员就会固定以这样的分辨率来设计 ...

随机推荐

  1. 视频平台、NVR、摄像头通过GB28181接入实现WEB分屏播放以及大屏展示

    青柿流媒体解决方案https://www.liveqing.com包含:LiveQing云平台直播点播流媒体服务,LiveGBS国标GB28181无插件流媒体服务,LiveNVR安防Onvif/RTS ...

  2. Linux之生成和使用静态库与动态库

    本文介绍了Linux环境下如何生成和使用静态库和动态库. 动态库 vs 静态库 特性 动态库(.so) 静态库(.a) 链接方式 运行时动态加载 编译时直接嵌入可执行文件 文件大小 可执行文件较小 可 ...

  3. 科技+时尚,华为P50 Pocket很“拼”

    手机作为人们亲密的"伙伴",几乎是与我们一天24小时"窝"在一起.所以,除了功能强大之外,另外一个吸引用户的关键要素--就是"美". 这就如 ...

  4. Litho(deepwiki-rs):让代码自己说话——AI驱动的自动化架构文档生成革命

    作为对标Davin商业化版本DeepWiki的开源项目,Litho(deepwiki-rs)通过多智能体协同架构与大语言模型推理,实现了从"代码即文档"到"文档即知识&q ...

  5. [ABC176F] Brave CHAIN

    [ABC176F] Brave CHAIN 题意 给你 \(3n\) 个数字.每次你可以选取前 \(5\) 个数字,拿走里面任意三个数字,剩下两个,如果拿走的 \(3\) 个数字相等,得分 \(+1\ ...

  6. 打造AI IDE标杆产品,腾讯CodeBuddy深度全方位解析

    腾讯云 CodeBuddy 作为国内 AI IDE 领域的标杆产品,凭借其全栈开发能力.多形态工具矩阵和深度云原生集成,已成为开发者打造 AI IDE 的最优选择之一.以下基于腾讯云官网及最新公开数据 ...

  7. KAL1 LINUX 官方文档之通用---元包

    什么是元包 元软件包用于一次安装许多软件包,创建作为其他软件包的依赖关系列表.Kali Linux以几种方式使用它们.一种方法是允许用户确定他们要安装的Kali列表中有多少个软件包.是否需要足够使用? ...

  8. Ada

    Ada 是一种专为高可靠性.高安全性系统设计的编程语言,诞生于1980年代(由美国国防部主导开发).其核心使命是减少软件错误,尤其适用于航空.航天.交通控制等"失误即灾难" 的领域 ...

  9. 软件研发 --- hello world 项目 之 数据之王 python

    Python Hello World 程序安装运行完整指南 本指南将从零开始,详细说明如何安装Python环境.运行Hello World程序的全过程. 1. Python简介 Python是一种高级 ...

  10. m基于码率兼容打孔LDPC码ms最小和译码算法的LDPC编译码matlab误码率仿真

    1.算法仿真效果 matlab2022a仿真结果如下: 2.算法涉及理论知识概要 码率兼容打孔LDPC码BP译码算法是一种改进的LDPC译码算法,能够在不同码率下实现更好的译码性能.该算法通过在LDP ...