【URP】Unity[RendererFeatures]屏幕空间环境光遮蔽SSAO
【从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和深度图进行世界坐标重建与遮蔽计算。
- Renderer Feature:需创建独立Feature并配置
- 示例代码
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的更多相关文章
- DirectX11 With Windows SDK--32 SSAO(屏幕空间环境光遮蔽)
前言 由于性能的限制,实时光照模型往往会忽略间接光因素(即场景中其他物体所反弹的光线).但在现实生活中,大部分光照其实是间接光.在第7章里面的光照方程里面引入了环境光项: \[C_a = \mathb ...
- Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第二十一章:环境光遮蔽(AMBIENT OCCLUSION)
原文:Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第二十一章:环境光遮蔽(AMBIENT OCCLUSION) 学习目标 ...
- 基于屏幕空间的实时全局光照(Real-time Global Illumination Based On Screen Space)
目录 Reflective Shadow Maps(RSM) RSM 的重要性采样 RSM 的应用与缺陷 Screen Space Ambient Occulsion(SSAO) SSAO Blur ...
- [帖子收集]环境光遮蔽(Ambient Occlusion)
环境光遮蔽,效果示例图 图片左边是一条龙的简单模型,呈现在一个均匀照明的环境中.尽管模型中有一些明暗不同的区域,但大部分光照都是均匀的.虽然模型有着相当复杂的几何形状,但看上去比较光滑平坦,没有明显的 ...
- 在Unity中实现屏幕空间阴影(2)
参考文章: https://www.imgtec.com/blog/implementing-fast-ray-traced-soft-shadows-in-a-game-engine/ 完成的工程: ...
- 在Unity中实现屏幕空间阴影(1)
接着上篇文章,我们实现了SSR效果. 其中的在屏幕空间进行光线追踪的方法是通用的.借此我们再实现一种屏幕空间的效果,即屏幕空间阴影. 文中的图片来自Catlike coding http://catl ...
- Cesium源码剖析---Ambient Occlusion(环境光遮蔽)
Ambient Occlusion简称AO,中文没有太确定的叫法,一般译作环境光遮蔽.百度百科上对AO的解释是这样的:AO是来描绘物体和物体相交或靠近的时候遮挡周围漫反射光线的效果,可以解决或改善漏光 ...
- Unity3d 屏幕空间人体皮肤知觉渲染&次表面散射Screen-Space Perceptual Rendering & Subsurface Scattering of Human Skin
之前的人皮渲染相关 前篇1:unity3d Human skin real time rendering 真实模拟人皮实时渲染 前篇2:unity3d Human skin real time ren ...
- Unity3D 屏幕空间雪场景Shader渲染
笔者介绍:姜雪伟,IT公司技术合伙人,IT高级讲师,CSDN社区专家,特邀编辑,畅销书作者,已出版书籍:<手把手教你架构3D游戏引擎>电子工业出版社和<Unity3D实战核心技术详解 ...
- Unity ugui屏幕适配与世界坐标到ugui屏幕坐标的转换
我们知道,如今的移动端设备分辨率五花八门,而开发过程中往往只取一种分辨率作为设计参考,例如采用1920*1080分辨率作为参考分辨率. 选定了一种参考分辨率后,美术设计人员就会固定以这样的分辨率来设计 ...
随机推荐
- 视频平台、NVR、摄像头通过GB28181接入实现WEB分屏播放以及大屏展示
青柿流媒体解决方案https://www.liveqing.com包含:LiveQing云平台直播点播流媒体服务,LiveGBS国标GB28181无插件流媒体服务,LiveNVR安防Onvif/RTS ...
- Linux之生成和使用静态库与动态库
本文介绍了Linux环境下如何生成和使用静态库和动态库. 动态库 vs 静态库 特性 动态库(.so) 静态库(.a) 链接方式 运行时动态加载 编译时直接嵌入可执行文件 文件大小 可执行文件较小 可 ...
- 科技+时尚,华为P50 Pocket很“拼”
手机作为人们亲密的"伙伴",几乎是与我们一天24小时"窝"在一起.所以,除了功能强大之外,另外一个吸引用户的关键要素--就是"美". 这就如 ...
- Litho(deepwiki-rs):让代码自己说话——AI驱动的自动化架构文档生成革命
作为对标Davin商业化版本DeepWiki的开源项目,Litho(deepwiki-rs)通过多智能体协同架构与大语言模型推理,实现了从"代码即文档"到"文档即知识&q ...
- [ABC176F] Brave CHAIN
[ABC176F] Brave CHAIN 题意 给你 \(3n\) 个数字.每次你可以选取前 \(5\) 个数字,拿走里面任意三个数字,剩下两个,如果拿走的 \(3\) 个数字相等,得分 \(+1\ ...
- 打造AI IDE标杆产品,腾讯CodeBuddy深度全方位解析
腾讯云 CodeBuddy 作为国内 AI IDE 领域的标杆产品,凭借其全栈开发能力.多形态工具矩阵和深度云原生集成,已成为开发者打造 AI IDE 的最优选择之一.以下基于腾讯云官网及最新公开数据 ...
- KAL1 LINUX 官方文档之通用---元包
什么是元包 元软件包用于一次安装许多软件包,创建作为其他软件包的依赖关系列表.Kali Linux以几种方式使用它们.一种方法是允许用户确定他们要安装的Kali列表中有多少个软件包.是否需要足够使用? ...
- Ada
Ada 是一种专为高可靠性.高安全性系统设计的编程语言,诞生于1980年代(由美国国防部主导开发).其核心使命是减少软件错误,尤其适用于航空.航天.交通控制等"失误即灾难" 的领域 ...
- 软件研发 --- hello world 项目 之 数据之王 python
Python Hello World 程序安装运行完整指南 本指南将从零开始,详细说明如何安装Python环境.运行Hello World程序的全过程. 1. Python简介 Python是一种高级 ...
- m基于码率兼容打孔LDPC码ms最小和译码算法的LDPC编译码matlab误码率仿真
1.算法仿真效果 matlab2022a仿真结果如下: 2.算法涉及理论知识概要 码率兼容打孔LDPC码BP译码算法是一种改进的LDPC译码算法,能够在不同码率下实现更好的译码性能.该算法通过在LDP ...