最近在学习SSAO,网上参考了一些资料,不过感觉还是Unity例子里面的写的更好一些,简单分享一下源码的学习心得。
先贴一下网上的两篇不错的资料:https://blog.csdn.net/puppet_master/article/details/82929708,https://www.bilibili.com/video/BV16q4y1U7S3?p=2。
SSAO,我以前在Games202的GI相关学习中记录过,之前的想法就是首先把间接光看做一个常量,但是对于每个点来说接受到的间接光程度是不一样的,对这个点到附近进行采样,看附近的点将其遮挡的比例,遮挡比例越大,这个间接光常量的贡献就越少,这个思路应该是没什么问题的,不过这样的话,整个画面都会变亮,实际实践中往往反过来,所有点都会减去一个常量,来让画面变暗,被遮挡比例越大,减去的百分比就越大,最后的画面就会变得更暗一些,使得画面变得更加真实。
顺带一提,关于这个采样点是否被遮挡,可以简单的用该点的位置深度和相机下的实际深度作比较来近似,如果深度比实际深度大,说明被挡住了,有点类似于ShadowMapping里面的做法;同时,很多资料都会提到,我们只需要采样法线半球的点即可。
下面贴一下Unity例子的效果:
原图:
SSAO后:
可以很明显的看到,类似于墙角天花板的灯后面都带了阴影,使得画面变得更加细致更加饱满。
下面简单说一下SSAO shader的原理,重点在frag中,对于一个像素,我们可以得到其view空间下的法线,然后使用一个随机的采样方向,得到这个采样点的位置和其深度,做一下比较,根据前文提到的,计算是否被遮挡,是的话就贡献AO值;同时需要注意一点,深度差距过大的是不能贡献AO值的,例如上图中左边的一个Cube,是不能给后面的墙附近贡献AO值的,一般资料都是会做一个range check,Unity中比较简单的用saturate来做了这点,即深度差超过1m的被忽略,感觉这个实际上可以用一个参数来控制,不知道为啥unity没做,可能觉得没必要吧。
贴一下Unity的代码,有些细节做了一些注释:
1 half frag_ao (v2f_ao i, int sampleCount, float3 samples[INPUT_SAMPLE_COUNT]) 2 { 3 // read random normal from noise texture 4 half3 randN = tex2D (_RandomTexture, i.uvr).xyz * 2.0 - 1.0; 5 6 // read scene depth/normal 7 float4 depthnormal = tex2D (_CameraDepthNormalsTexture, i.uv); 8 float3 viewNorm; 9 float depth; 10 DecodeDepthNormal (depthnormal, depth, viewNorm); 11 // zzy: https://blog.csdn.net/ak47007tiger/article/details/102658128 12 // depth是[0, 1],下面的z是far plane 13 depth *= _ProjectionParams.z; 14 // zzy: _Params.x ==> SSAO radius 15 float scale = _Params.x / depth; 16 17 // accumulated occlusion factor 18 float occ = 0.0; 19 for (int s = 0; s < sampleCount; ++s) 20 { 21 // Reflect sample direction around a random vector 22 half3 randomDir = reflect(samples[s], randN); 23 24 // Make it point to the upper hemisphere 25 half flip = (dot(viewNorm,randomDir)<0) ? 1.0 : -1.0; 26 randomDir *= -flip; 27 // Add a bit of normal to reduce self shadowing 28 randomDir += viewNorm * 0.3; 29 30 float2 offset = randomDir.xy * scale; 31 float sD = depth - (randomDir.z * _Params.x); // zzy: 这个随机点的位置 32 33 // Sample depth at offset location 34 float4 sampleND = tex2D (_CameraDepthNormalsTexture, i.uv + offset); 35 float sampleD; 36 float3 sampleN; 37 DecodeDepthNormal (sampleND, sampleD, sampleN); 38 sampleD *= _ProjectionParams.z; 39 float zd = saturate(sD-sampleD); // zzy: 原来如此,使用saturate以后,在这边深度相差过大的就会被舍去 40 // zzy: 比较这个点的实际位置和其深度,如果比深度大,说明被挡住了,需要贡献AO值 41 if (zd > _Params.y) { // zzy: _Params.y ==> minZ 42 // This sample occludes, contribute to occlusion 43 occ += pow(1-zd,_Params.z); // sc2 // zzy: _Params.z ==> 1.0f / m_OcclusionAttenuation 44 //occ += 1.0-saturate(pow(1.0 - zd, 11.0) + zd); // nullsq 45 //occ += 1.0/(1.0+zd*zd*10); // iq 46 } 47 } 48 occ /= sampleCount; 49 return 1-occ; 50 }
顺带一提,直接SSAO噪点会比较大,Unity对其做了Blur来处理,通过frame debugger可以查看不Blur的效果:
经过横纵向Blur以后:
最后说一下性能,感觉Unity的性能应该比网上很多SSAO的实现要高的,不过也有文章说Asset Store中性能更高的SSAO Pro:https://indienova.com/u/dio19900/blogread/5099。之后有空继续学习一下。