【笔记】【百人计划】图形4.2 SSAO算法

图形4.2 SSAO算法

GAMES202 中的SSAO

https://xzyw7.github.io/post/CbZTf-uM4/#screen-space-ambient-occlusionssao

一、SSAO介绍

SSAO相关术语,简要理解及历史

  • AO
    • 环境光遮蔽Amibent Occlusion,用于模拟光线到达物体的能力的一种粗略的全局方法,描述光线到达物体表面的能力
  • SSAO
    • 屏幕空间环境光遮蔽Screen Space Ambient Occlusion,一种用于实施近似AO的渲染技术。通过获取像素的深度缓冲、法线缓冲来计算实现,近似地表现物体在间接光下产生的阴影。
  • 历史
    • AO最早在Siggraph 2002年会上有ILM(工业光魔)的技术主管Hayden Landis所展示,当时就被叫做Ambient Occlusion
    • 2007年,Crytek发布了SSAO的技术,并用在了孤岛危机上。

二、SSAO原理

简要理解SSAO算法原理

image-20220912220609664

  • 深度缓冲
    • depth用于当前视点下场景的每一个像素距离相机的粗略表达,用于重构像素相机空间中的坐标Z,来近似重构该视点下的三维场景。
  • 法线缓冲
    • 相机空间中的法线信息,用于重构每个像素的TBN坐标轴,用于计算发现半球中的采样随机向量,随机向量用于判断和描述该像素的AO强度。
  • 法向半球
    • 黑色表示我们需要计算的样本
    • 蓝色项链表示样本的法向量
    • 白色、灰色为采样点。灰色表示被遮挡采样点,据此判断最终AO的强度
    • image-20220912221417067

三、算法实现

根据原理结合UnityC#&Shader实现SSAO

实现过程环境

  • Unity2019.3.5f1
  • 透视模式
  • 前向渲染。如果为延迟渲染,则由对应的G-buffer生成,在shader中作为全局变量访问
  • 使用OnRenderImage()来处理后期,进而实现SSAO

获取深度&法线缓冲数据

  • C#部分
1
2
3
4
private void Start() {
cam = this.GetComponent<Camera>();
cam.depthTextureMode = cam.depthTextureMode | DepthTextureMode.DepthNormals;
}
  • Shader部分
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//获取深度法线图
sampler2D _CameraDepthNormalsTexture;
//固定名称
...
//采样获得深度值和法线值
float3 viewNormal;
float linear01Depth;
float4 depthnormal = tex2D(_CameraDepthNormalsTexture, i.uv);
DecodeDepthNormal(depthnormal, linear01Depth, viewNormal);

//UnityCG.cginc
inline void DecodeDepthNormal(float enc, out float depth, out float3 noraml) {
depth = DecodeFloatRG(enc,zw);
normal = DecodeViewNormalStereo(enc);
}

重建相机空间坐标

  • 重建方法
    • 参考链接
    • https://zhuanlan.zhihu.com/p/92315967
    • 本例实现使用其中的“从NDC空间中重建”方法得到样本在相机空间中的向量,乘以深度值得到样本的坐标
  • 从NDC空间中重建

1.计算样本屏幕坐标

1
2
3
// 利用Unity内置函数
// 屏幕纹理坐标
float4 screenPos = ComputeScreenPos(o.vertex);

2.转化至NDC空间

1
float4 ndcPos = (screenPos/screenPos.w)*2-1;

3.计算相机空间中至远平屏幕方向(内置变量_ProjectionParams.z存放相机远平面值far)

1
float3 clipVec = float3(ndcPos.x, ndcPos.y, 1.0) * _ProjectionParams.z;

4.矩阵变换至相机空间中的样本相对相机的方向

1
o.viewVec = mul(unity_CameraInvProjection, clipVec.xyzz).xyz;

5.重建相机空间中的样本左边(在像素着色器)

1
2
float3 viewPos = linear01Depth * i.viewVec;
//在相机空间中通过样本的相对相机方向及深度来拟合重构坐标

构建法向量正交基

  1. 设置法向量
1
2
//获取像素相机屏幕法线,法线z方向相对于相机为负(ao需要乘以-1置反),并处理成单位向量
viewNormal = normalize(viewNormal) * float3(1,1,-1);
  1. 生成随机向量(用于构建的正交基随机,而非所有样本计算得到的正交基一致)(先处理成统一)
1
2
//randvec法线半球的随机向量
float3 randvec = normalize(float3(1,1,1));
  1. 求出切向量,再用函数叉积求副切线向量
1
2
3
4
5
//Cramm-Schimidt处理创建正交基
//TBN空间
float3 tangent = normalize(randvec - viewNormal * dot(randvec, viewNormal));
float3 bitangent = cross(viewNoraml, tangent);
float3x3 TBN = float3x3(tangent, bitangent, viewNormal);

AO采样

  1. 传入给定的随机采样向量,并通过法向量正交基转化至法线半球中的向量(在C#中计算出采样的随机点)
1
2
//随机向量,转化至TBN空间
float3 randomVec = mul(_SampleKernelArray[i].xyz, TBN);
  1. 获取随机坐标点
1
2
//计算随机法线半球后的向量
float3 randomPos = viewPos + randomVec * _SampleKeneralRadius;
  1. 转换至屏幕空间坐标
1
2
float3 rclipPos = mul((float3x3)unity_CameraProjection, randomPos);
float2 rscreenPos = (rclipPos.xy / rclipPos.z) * 0.5 + 0.5;
  1. 计算随机向量转化至屏幕空间后对应的深度值,并判断累加AO
1
2
3
4
5
6
7
float randomDepth;
float3 randomNormal;
float4 rcdn = tex2D(_CameraDepthNormalsTexture, rscreenPos);
DecodeDepthNormal(rcdn, randomDepth, randomNormal);

//判断累加ao
ao +=(randomDepth >= linear01Depth)? 1.0 : 0.0;

四、效果改进

效果后期改进说明

随机正交基(增加随机性)

  1. 为了不使求得的法向半球的正交基一致,我们引入随机向量。
1
2
3
4
5
//Cramm-Schimidt处理创建正交基
//TBN空间
float3 tangent = normalize(randvec - viewNormal * dot(randvec, viewNormal));
float3 bitangent = cross(viewNoraml, tangent);
float3x3 TBN = float3x3(tangent, bitangent, viewNormal);
  1. 利用uv采样一张noise贴图(如4x4像素(可选择其他尺寸)的noise贴图)或者随机向量
1
2
3
4
5
//铺平纹理
float2 noiseScale = _ScreenParams.xy / 4.0;
float2 noiseUV = i.uv * noiseScale;
//randvec法线半球的随机向量
float3 randvec = tex2D(_NoiseTex, noiseUV).xyz;

在C#中传入噪声图

image-20220912234322536

1
ssaoMaterial.SetTexture("_NoiseTex", Noise)

AO累加平滑优化

范围判定(模型边界)
  • 样本采样可能会采集到深度差非常大的随机点,导致边界出现不该有的AO

image-20220912234446963

  • 加入样本深度和随机点的深度值范围判定
1
float range = abs(randomDepth - linear01Depth) > _RangeStrength ? 0.0 : 1.0;

效果如下

image-20220912234529623

自身判定(同一深度值情况下)

如果随机点深度值和自身一样或非常接近,可能导致虽在同一平面,也会出现AO

  • 判断深度值大小的时候,增加一个Bias来改善该问题
1
float selfCheck = randomDepth + _DepthBiasValue < linear01Depth ? 1.0 : 0.0;
AO权重

AO深度判断非0即1,比较生硬,为其增加一权重

本例中权重为:法线半球中随机采样后的点x,y(切平面)距离样本的距离为参考

1
float weight = smoothstep(0, 0.2, length(randomVec.xy));
结合
1
ao += range * selfCheck * weight;
模糊(只展示效果对比)

采用基于法线的双边滤波(Bilateral Filtering)

image-20220912235055005

五、对比模型烘焙AO

同模型烘焙AO方式对比,了解SSAO优缺点

三维建模软件烘焙AO

通过DCC设定好渲染参数,对模型烘焙AO到纹理

  • 优点
    • 单一物体可控性强(通过单一物体的材质球上的AO纹理贴图),可以控制单一物体的AO强弱
    • 弥补场景烘焙的细节,整体场景的烘焙(包含AO信息),并不能完全包含单一物体细节上的AO,而通过DCC烘焙到纹理的方式,增加物体的AO细节
    • 不影响其(Unity场景中)静态或者动态
  • 缺点
    • 操作较其他方式繁琐,需要对模型进行UV处理,再烘焙到纹理
    • 不利于整体场景整合(如3Dmax烘焙到纹理只能选择单一物体,针对整体场景的处理工作量巨大)
    • 增加AO纹理贴图,不利于资源优化(后期可通过其他纹理通道利用整合资源)
    • 只有物体本身具有AO信息,获取物体之间的AO信息工作量巨大(不是不可能)

游戏引擎烘焙AO(Unity3D Lighting)

通过Unity的Lighting功能(主菜单/Window/Rendering/Lighting Settings) 进行整体场景的烘焙,AO信息包含于此

  • 优点
    • 操作简易,整体场景的烘焙,包含AO的选择
    • 不受物体本身UV的影响,unity可以通过Generate Lightmap UVs生成模型第二个纹理坐标数据
    • 可生产场景中物体与物体之间的AO信息
  • 缺点
    • 缺少单一物体的细节(可调整参数提高烘焙细节,但将增加烘焙纹理数量和尺寸以及烘焙时间)
    • 受物体是否静态影响,动态物体无法烘焙,获得AO信息

SSAO

  • 优点
    • 不依赖场景的复杂度,其效果质量依赖于最终图片像素大小
    • 实时计算,可用于动态场景
    • 可控性强,灵活性强,操作简单
  • 缺点
    • 性能消耗较上述两种方式更多,计算昂贵
    • AO质量弱于离线烘焙

六、性能消耗

主要性能消耗点

image-20220913000029828
  • AO法向半球的随机采样
  • 双边滤波的多重采样

AO核心采样消耗说明

本例SSAO中,主要核心为计算AO随机法向半球的采样点

  1. 使用For结构进行半球随机法向的采样,If,For对GPU计算性能上不友好
1
2
3
4
5
6
7
8
9
10
11
//采样核心
float ao = 0;
int sampleCount = _SampleKernelCount;//每个像素点上的采样次数
//https://blog.csdn.net/qq_39300235/article/details/102460405

for (int i=0; i<sampleCount; i++) {
//...
}
ao = ao/sampleCount;
ao = max(0.0, 1 - ao*_AOStrength);
return float4(ao,ao,ao,1);

作业

实现SSAO

使用其他算法实现进行对比

参考资料

[1] https://www.bilibili.com/video/BV16q4y1U7S3

【技术美术百人计划】图形 4.2 SSAO算法 屏幕空间环境光遮蔽

[2] https://learnopengl-cn.github.io/05%20Advanced%20Lighting/09%20SSAO/