学习教程来自:【技术美术百人计划】图形 4.2 SSAO算法 屏幕空间环境光遮蔽

笔记

0. 前言

SSAO的使用一般在IPhone10及骁龙845之后的机型中使用

1. SSAO介绍

AO:环境光遮蔽 Ambient Occlusion
SSAO:屏幕空间环境光遮蔽 Screen Space Ambient Occlusion 通过深度缓冲、法线缓冲计算AO

2. SSAO原理

2.1 样本缓冲

  1. 深度缓冲:每一个像素距离相机的深度值
  2. 法线缓冲:相机空间下的法线信息
  3. 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 裁剪掉异常值

  1. 差距巨大的深度值
    float selfCheck = randomDepth + _DepthBiasValue < linear01Depth ? 1.0 : 0.0;//深度变化过大的归零
    
  2. 同一平面深度值由于精度问题造成的深度变化
    float range = abs(randomDepth - linear01Depth) > _RangeStrength ? 0.0 : 1.0;//深度变化
    
  3. 根据距离Smooth权重
    float weight = smoothstep(0, 0.2, length(randomVec.xy));//针对不同的采样方向分配权重
    
  4. 双边滤波模糊(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 烘焙方式

  1. 建模软件烘焙到纹理:可控性强(操作繁琐,需要UV,资源占用大),自身细节性强(缺少场景细节),不受静动态影响。
  2. 游戏引擎烘焙,如Unity3D Lighting:较简单,整体细节好,动态物体无法烘培
  3. SSAO:复杂度基于像素多少、实时性强、灵活可控;性能消耗较前两种最大,最终效果比1差(理论上)

6. SSAO性能消耗

消耗的点:

  1. 随机采样:IF FOR循环打破了GPU的并行性,过高的采样次数大大提高了复杂度
  2. 双边滤波的模糊处理:增加了屏幕采样的次数

作业

1. 实现SSAO效果

跟着敲了一遍,内容见上述。

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

整体有点偏黑:

GTAO