目录
1.是什么
2.原理
3.各部分解释
2.1.从屏幕空间到视图空间
2.2.以法线半球为基,获取随机向量
2.3.应用偏移,并将其转换为uv坐标
2.4.获取深度
2.5.比较并计算贡献
2.6.最后计算
4.改进
4.1.平滑过渡
4.2.模糊
5.变量和语句解释
5.1._DepthBias
5.2._RangeCheck
5.3.ssDepth >= 0.9999
6.其他事项
6.1.颜色叠加
6.2.向量的随机
6.3.墙面噪声
7.效果演示
1.是什么
SSAO,全称Screen Space Ambient Occlusion,即屏幕空间环境光遮蔽。其用处是用来模拟全局光照下,一些细节处的阴影。如墙角,缝隙等。也可以让画面整体的立体感增强。
SSAO是由AO发展而来,AO由于性能问题难以被用于实时渲染,因此出现了SSAO,以相对差的效果换取性能。
2.原理
对于一个屏幕上像素点而言,将其通过逆变换和深度纹理转换为视图空间的点。然后以其法线半球为基,生成随机向量,用该向量对该点进行偏移,再将偏移后的点转到屏幕空间,用此时的点对深度纹理进行采样,得到一个采样深度,用该深度与原深度(未偏移的点采样的深度)比较,如果采样深度小于原深度,说明被遮挡,计算其贡献;反之未遮挡,贡献为0。将一个点的每一次偏移的贡献相加除以偏移的次数,即为该点最终的颜色。
我说的可能没那么清楚,下面通过画图演示一下过程(虽然图画的也丑就是了):
3.各部分解释
2.1.从屏幕空间到视图空间
这里采用的是通过先将点转换到远裁剪面,然后通过真实深度获取真实的视图空间的坐标。参考了这篇文章(https://zhuanlan.zhihu.com/p/92315967)中的方法一。
这里简单讲述其原理:
我们知道,一个点从世界坐标转为屏幕坐标需要进行视图变换(世界空间->视图空间),投影变换(视图空间->裁剪空间),透视除法(裁剪空间->ndc空间),视口映射(ndc空间->屏幕空间)。那么我们只需要反向进行就能够得到一个屏幕坐标在视图空间的坐标。
首先是将屏幕坐标转到ndc空间,这里可以直接调用shader中的函数来计算,需要注意的是,这里的screenPos需要除以其w分量才能正常使用(如果有uv坐标,可以直接使用uv坐标):
//screenPos
float4 screenPos = ComputeScreenPos(UnityObjectToClipPos(v.vertex));
float2 ndcUV = (screenPos.xy / screenPos.w) * 2 - 1;//uv
float2 ndcUV = uv * 2 - 1;
然后是将ndc转为裁剪,我们先将该点视为远裁剪面上的点,则:
float3 clipPos = float3(ndcUV.x, ndcUV.y, 1) * _ProjectionParams.z;
然后将裁剪转为视图,由于转为视图后仍是原裁剪面上的点,所以乘以该点的线性深度值来获取真实坐标。之所以用clipPos.xyzz进行计算,则是因为裁剪空间下,远裁剪面的zw相同。
//获取深度和法线(因为后面要用,所以这里将法线一起获取了)
float4 depthNormal = tex2D(_CameraDepthNormalsTexture, f.uv);
float3 ssNormal; //ss -> Screen Space
float ssDepth;
DecodeDepthNormal(depthNormal, ssDepth, ssNormal);
float ssDepth01 = Linear01Depth(ssDepth);//变换
float3 viewPos = mul(unity_CameraInvProjection, clipPos.xyzz).xyz * ssDepth01;
至此,视图空间的坐标已得到。
2.2.以法线半球为基,获取随机向量
我们首先需要知道TBN矩阵,它是由切线,副切线,法线三者构成的一个3*3的矩阵,用途是做切线空间与其他空间转换的媒介。并且这种转换不会改变向量的长度(前提是三个都是单位向量)。
所以我们先在切线空间中随机生成一个x在[-1,1],y在[-1,1],z在[0,1]的向量,然后将其转换到视图空间下。
float3 viewNormal = normalize(ssNormal);
//随机生成,是正交基呈现随机性
float3 viewTangent = normalize(GetRandomVec(f.uv.xy));
float3 viewBitangent = cross(viewTangent, viewNormal);
viewTangent = cross(viewBitangent, viewNormal);
float3x3 TBN = float3x3(viewTangent.x, viewBitangent.x, viewNormal.x,viewTangent.y, viewBitangent.y, viewNormal.y,viewTangent.z, viewBitangent.z, viewNormal.z);//for循环中的语句
float3 randomVec = GetRandomVecHalf(f.uv.yx * i);
//_AORadius是一个由c#脚本传进的参数,目的是控制阴影的大小
float3 randomVecView = mul(TBN, randomVec) * _AORadius;
2.3.应用偏移,并将其转换为uv坐标
将随机向量应用到原点,然后通过一系列转换将其变为uv坐标,以采样深度法线纹理。
float3 viewOffPos = viewPos + randomVecView;
float4 clipOffPos = mul(unity_CameraProjection, float4(viewOffPos, 1));
float2 sampleUV = clipOffPos.xy / clipOffPos.w;
sampleUV = sampleUV * 0.5 + 0.5;
2.4.获取深度
waaaagh!
//获取深度
float4 sampleDepthNormal = tex2D(_CameraDepthNormalsTexture, sampleUV);
float sampleDepth;
//获取这个法线的原因是我懒得单独获取深度了
float3 sampleNormal;
DecodeDepthNormal(sampleDepthNormal, sampleDepth, sampleNormal);
2.5.比较并计算贡献
需要注意的是,这里比较的是实际的深度值,需要经过LinearEyeDepth转换。
我这里使用大于判断,是因为最后会将进行1 - ao的运算,相当于黑白取反。
//for外
ssDepth = LinearEyeDepth(ssDepth);//for内
sampleDepth = LinearEyeDepth(sampleDepth);
//_DepthBias:防止自遮挡
float depthDiff = sampleDepth - ssDepth - _DepthBias;
//_RangeCheck:去除不正常的阴影
if (depthDiff > 0 && depthDiff < _RangeCheck)
{ao += 1;
}
2.6.最后计算
取平均。
//1-是因为上面进行了相反的运算,所以这里1-再反一次;
//pow和AOStrength都是为了控制强度
ao = 1 - pow(ao / _SampleTime, 1) * _AOStrength;
4.改进
4.1.平滑过渡
添加距离衰减,远距离贡献小,近距离贡献大;
同样因为取反操作,所以这里的代码逻辑为:远距离贡献大,近距离贡献小。
float weight = smoothstep(0, _AORadius, length(randomVec));float depthDiff = sampleDepth - ssDepth - _DepthBias;
if (depthDiff > 0 && depthDiff < _RangeCheck)
{ao += 1 * weight;
}
4.2.模糊
因为直接生成的ssao会呈现一种噪声密布的状态

所以需要对其进行模糊,模糊的方法很多,我使用的是高斯模糊,原理就不说明了。效果如下:

5.变量和语句解释
5.1._DepthBias
用于防止自遮挡,因为有时会出现由于精度误差导致的自己遮挡自己的问题。这个变量会将最后的深度差再减小一些,或者理解为将采样的深度适当减小一些。这样就不会出现自遮挡的问题了。
5.2._RangeCheck
有时我们会发现,明明不该出现阴影的地方出现了阴影,比如两个仅仅只有前后关系的物体交界处。这个变量就是为了防止这种情况,如果深度差小于指定值,才会计算贡献,反之舍弃。

5.3.ssDepth >= 0.9999
上面没有写出来,其位置是在片元着色器中用原点获取深度之后。用处是为了防止天空盒与物体交界处出现阴影(理论上_RangeCheck就能解决这问题,但不知道什么原因不行):
if (ssDepth >= 0.9999)
{return 1;
}

6.其他事项
6.1.颜色叠加
最后ssao求出来后还有的颜色叠加过程,我是图方便直接叠上去了。
6.2.向量的随机
向量的随机性会极大程度地影响最后ssao的效果,所以请尽量保证向量的随机性。
6.3.墙面噪声
我的实现会导致_AORadius增大时,墙面上会出现噪点,可以通过增大_SampleTime来降低其存在感。当然这不是一个好办法,我解决后会更新该文章。
7.效果演示
代码在https://github.com/RedShaoWuHuaQu/UnityShader/tree/main/SSAO中。

