学习教程来自:【技术美术百人计划】图形 4.2 SSAO算法 屏幕空间环境光遮蔽
笔记
0. 前言
SSAO的使用一般在IPhone10及骁龙845之后的机型中使用
1. SSAO介绍
AO:环境光遮蔽 Ambient Occlusion
SSAO:屏幕空间环境光遮蔽 Screen Space Ambient Occlusion 通过深度缓冲、法线缓冲计算AO
2. SSAO原理
2.1 样本缓冲
- 深度缓冲:每一个像素距离相机的深度值
- 法线缓冲:相机空间下的法线信息
- Position
2.2 法线半球
其中:深度(深度值)+位置(相机空间下向量)—->世界空间下相机到像素的向量
相机空间下向量:
v2f vert_Ao(appdata v){
v2f o;
UNITY_INITIALIZE_OUTPUT(v2f,o);
o.vertex = UnityObjectToClipPos(v.vertex);//顶点位置:转换到裁剪空间
o.uv = v.uv;
float4 screenPos = ComputeScreenPos(o.vertex);//顶点位置:转换到屏幕空间
float4 ndcPos = (screenPos / screenPos.w) * 2 - 1;//归一化
float3 clipVec = float3(ndcPos.x, ndcPos.y, 1.0)* _ProjectionParams.z;//像素方向:倒推回裁剪空间
o.viewVec = mul(unity_CameraInvProjection, clipVec.xyzz).xyz;//像素方向:倒推回相机空间
return o;
}
3. SSAO算法实现
3.1 获取深度和法线缓冲
private void Start()
{
cam = this.GetComponent<Camera>();
cam.depthTextureMode = cam.depthTextureMode | DepthTextureMode.DepthNormals;//与运算,增加深度和法线的渲染纹理
}
3.2 重建相机空间坐标
参考:Unity从深度缓冲重建世界空间位置
其中:深度(深度值)+位置(相机空间下向量)—->相机空间下相机到像素的向量
相机空间下向量:
//第一步:获得相机空间下的像素方向
v2f vert_Ao(appdata v){
v2f o;
UNITY_INITIALIZE_OUTPUT(v2f,o);
o.vertex = UnityObjectToClipPos(v.vertex);//顶点位置:转换到裁剪空间
o.uv = v.uv;
float4 screenPos = ComputeScreenPos(o.vertex);//顶点位置:转换到屏幕空间
float4 ndcPos = (screenPos / screenPos.w) * 2 - 1;//归一化
float3 clipVec = float3(ndcPos.x, ndcPos.y, 1.0)* _ProjectionParams.z;//像素方向:倒推回裁剪空间
o.viewVec = mul(unity_CameraInvProjection, clipVec.xyzz).xyz;//像素方向:倒推回相机空间
return o;
}
//第二步:获得深度信息,相乘得到向量
fixed4 frag_Ao(v2f i) : SV_Target{
fixed4 col tex2D(_MainTex, i.uv);//屏幕纹理
float3 viewNormal;//相机空间下法线方向
float linear01Depth;//深度值0-1
float4 depthnormal = tex2D(_CameraDepthNormalsTexture, i.uv);//获得纹理信息并解码
DecodeDepthNormal(depthnormal, linear01Depthm viewNormal);
float3 viewPos = linear01Depth * i.viewVec;//相机空间下像素向量
}
3.3 构建法向量正交基(TBN)
tangent bitangent viewNormal
fixed4 frag_Ao(v2f i) : SV_Target{
//重建向量正交基
viewNormal = normalize(viewNormal) * float3(1, 1, -1);//N
float2 noiseScale = _ScreenParams.xy / 4.0;//噪声纹理的缩放
float2 noiseUV = i.uv * noiseScale;
float3 randvec = tex2D(_NoiseTex, noiseUV).xyz;//采样得到随机向量
float3 tangent = normalize(randvec - viewNormal * dot(randvec, viewNormal));//T
float3 bitangent = cross(viewNormal, tangent);//B
float3x3 TBN = float3x3(tangent, bitangent, viewNormal);
}
3.4 AO采样核心
//第一步:在C#部分生成采样核心
private void GenerateAOSampleKernel()
{
if(sampleKernelCount == sampleKernelList.Count)
{
return;//list为满则返回
}
sampleKernelList.Clear();
for(int i = 0; i < sampleKernelCount; i++)
{
var vec = new Vector4(Random.Range(-1.0f, 1.0f), Random.Range(-1.0f, 1.0f), Random.Range(0, 1.0f), 1.0f);
vec.Normalize();//初始化随机向量
var scale = (float)i / sampleKernelCount;
scale = Mathf.Lerp(0.01f, 1.0f, scale * scale);//i从0-63对应0-1的二次方程曲线
vec *= scale;
sampleKernelList.Add(vec);
}
}
//第二步:从每一个采样位置的深度变化程度累计ao
fixed4 frag_Ao(v2f i) : SV_Target{
//采样并累加
float ao = 0;//ao值
int sampleCount = _SampleKernelCount;
for(int i = 0; i < sampleCount; i++){
float3 randomVec = mul(_SampleKernelArray[i].xyz, TBN);//采样方向
float weight = smoothstep(0, 0.2, length(randomVec.xy));//针对不同的采样方向分配权重
//采样位置
float3 randomPos = viewPos + randomVec * _SampleKernelRadius;//相机空间下的采样位置
float3 rclipPos = mul((float3x3)unity_CameraProjection, randomPos);//相机空间到裁剪空间(投影空间)下
float2 rscreenPos = (rclipPos.xy / rclipPos.z) * 0.5 + 0.5;//裁剪空间到屏幕空间
float randomDepth;//采样位置的深度
float3 randomNormal;//采样位置的法线方向
//从纹理中读取上述信息
float4 rcdn = tex2D(_CameraDepthNormalsTexture, rscreenPos);
DecodeDepthNormal(rcdn, randomDepth, randomNormal);
//对比计算ao
float range = abs(randomDepth - linear01Depth) > _RangeStrength ? 0.0 : 1.0;//深度变化
float selfCheck = randomDepth + _DepthBiasValue < linear01Depth ? 1.0 : 0.0;//深度变化过大的归零
ao += range * selfCheck * weight;
}
ao = ao / sampleCount;
ao = max(0.0, 1 - ao * _AOStrength);
return float4(ao, ao, ao, 1);
}
4. AO效果改进
从上面代码中截取出来的
4.1 采样Noise获得随机向量
float2 noiseScale = _ScreenParams.xy / 4.0;//噪声纹理的缩放
float2 noiseUV = i.uv * noiseScale;
float3 randvec = tex2D(_NoiseTex, noiseUV).xyz;//采样得到随机向量
4.2 裁剪掉异常值
- 差距巨大的深度值
float selfCheck = randomDepth + _DepthBiasValue < linear01Depth ? 1.0 : 0.0;//深度变化过大的归零
- 同一平面深度值由于精度问题造成的深度变化
float range = abs(randomDepth - linear01Depth) > _RangeStrength ? 0.0 : 1.0;//深度变化
- 根据距离Smooth权重
float weight = smoothstep(0, 0.2, length(randomVec.xy));//针对不同的采样方向分配权重
- 双边滤波模糊(C#部分)
RenderTexture blurRT = RenderTexture.GetTemporary(rtW, rtH, 0);//获取模糊渲染纹理 ssaoMaterial.SetFloat("_BilaterFilterFactor", 1.0f - bilaterFilterStrength); ssaoMaterial.SetVector("_BlurRadius", new Vector4(BlurRadius, 0, 0, 0));//x方向 Graphics.Blit(aoRT, blurRT, ssaoMaterial, (int)SSAOPassName.BilateralFilter); ssaoMaterial.SetVector("_BlurRadius", new Vector4(0, BlurRadius, 0, 0));//y方向 Graphics.Blit(blurRT, aoRT, ssaoMaterial, (int)SSAOPassName.BilateralFilter);
5. 对比模型烘焙AO
5.1 烘焙方式
- 建模软件烘焙到纹理:可控性强(操作繁琐,需要UV,资源占用大),自身细节性强(缺少场景细节),不受静动态影响。
- 游戏引擎烘焙,如Unity3D Lighting:较简单,整体细节好,动态物体无法烘培
- SSAO:复杂度基于像素多少、实时性强、灵活可控;性能消耗较前两种最大,最终效果比1差(理论上)
6. SSAO性能消耗
消耗的点:
- 随机采样:IF FOR循环打破了GPU的并行性,过高的采样次数大大提高了复杂度
- 双边滤波的模糊处理:增加了屏幕采样的次数
作业
1. 实现SSAO效果
跟着敲了一遍,内容见上述。
2. 使用其他AO算法实现进行对比
比如HBAO
参考知乎:Ambient Occlusion环境遮罩1提到了以下AO算法
SSAO-Screen space ambient occlusion
SSDO-Screen space directional occlusion
HDAO-High Definition Ambient Occlusion
HBAO+-Horizon Based Ambient Occlusion+
AAO-Alchemy Ambient Occlusion
ABAO-Angle Based Ambient Occlusion
PBAOVXAO-Voxel Accelerated Ambient Occlusion
git上找了个GTAO-Ground Truth Ambient Occlusion的算法
原理参考:UE4 Mobile GTAO 实现(HBAO续)
代码参考:Unity3D Ground Truth Ambient Occlusion
整体有点偏黑: