引言:
本文作者 alpha 从事游戏前端开发已经5年,毕业后他先是入职了腾讯无线大连研发中心,而后开启了北漂生涯,在北京的这3年一直都在使用 Cocos Creator,对前端业务,包体、内存优化有很多的实践经验。最近 alpha 在学习计算机图形学相关技术,今天他将同大家分享 Cocos Creator 3.3 实现屏幕空间的环境光遮蔽(SSAO)的技术经验。
什么是 AO ?
环境光(Ambient Lighting)是场景总体光照中的一个固定光照常量,用来模拟光的散射(Scattering)。在现实中,光线会以任意方向散射,它的强度是会改变的。
其中一种间接光照的模拟叫做环境光遮蔽(Ambient Occlusion),它的原理是通过将褶皱、孔洞和非常靠近的墙面变暗的方法近似模拟出间接光照。这些区域很大程度上是被周围的几何体遮挡的,所以这些地方看起来会更暗一些。
在2007年,Crytek 公司发布了一款叫做屏幕空间环境光遮蔽(Screen Space Ambient Occlusion,SSAO)的技术,并用在了他们的看家作孤岛危机上。这一技术使用了屏幕空间场景的深度而不是真实的几何体数据来确定遮蔽量。这一做法相对于真正的环境光遮蔽(基于光线追踪)不但速度快,而且还能获得较好的效果,使得它成为近似实时环境光遮蔽的标准。
下面这幅图展示了在使用和不使用 SSAO 时场景的不同。特别注意对比电话亭后面和墙角部分,你会发现(环境)光被遮蔽了许多:
虽然这个效果不是非常明显,但是启用 AO 确实给我们更真实的感觉,这些小的遮蔽细节能让整个场景看起来更有立体感。
SSAO 原理
SSAO 背后的原理很简单:对于屏幕上的每一个片段,会根据周边深度值计算一个遮蔽因子(Occlusion Factor)。这个遮蔽因子之后会被用来决定片段的环境光分量。遮蔽因子是通过采集片段周围球型核心(Kernel)的多个深度样本,并和当前片段深度值对比而得到的。高于片段深度值样本的个数就是我们想要的遮蔽因子。
上图中在几何体内灰色的深度样本都是高于片段深度值的,他们会增加遮蔽因子;几何体内样本个数越多,片段获得的环境光照也就越少。
很明显,渲染效果的质量和精度与采样的样本数量有直接关系。如果样本数量太低,渲染的精度会急剧减少,会得到一种叫做波纹(Banding)的效果;如果它太高了,会影响性能。通过引入随机性到采样核心(Sample Kernel)从而减少样本的数目。通过随机旋转采样核心,能在有限样本数量中得到高质量的结果。然而随机性引入了一个很明显的噪声图案,需要通过模糊降噪来修复这一问题。下面这幅图片展示了波纹效果还有随机性造成的效果:
可以看到,尽管在低样本数的情况下得到了很明显的波纹效果,引入随机性之后这些波纹效果就完全消失了。最初 Crytek 的实现是用一个深度缓冲做为输入,但是这种方式存在一些问题(如自遮闭, 光环),由于这个原因,现在通常不会使用球体的采样核心,而是使用一个沿着表面法向量的半球体采样核心。
通过在法向半球体(Normal Oriented Hemisphere)周围采样,将不会考虑到片段背面的几何体,它消除了环境光遮蔽灰蒙蒙的感觉,从而产生更真实的结果。
SSAO 特点:
独立于场景复杂性,仅和投影后最终的像素有关,和场景中的顶点数三角数无关。
跟传统的 AO 处理方法相比,不需要预处理,无需加载时间,也无需系统内存中的内存分配,所以更加适用于动态场景。
对屏幕上的每个像素以相同的一致方式工作。
没有 CPU 使用 - 它可以在 GPU 上完全执行。
可以轻松集成到任何现代图形管线中。
在了解了 AO & SSAO 之后,我们来看看要怎么基于 Cocos Creator 3.3.1 实现 SSAO。
Demo 地址:
https://gitee.com/yanjifa/cc-ssao-demo
样本缓冲
SSAO 需要几何体的信息来确定一个片段的遮蔽因子,对于每个片段(像素),需要如下数据:
逐片段位置向量
逐片段法线向量
逐片段反射颜色
采样核心
用来旋转采样核心的随机旋转向量
通过使用一个逐片段观察空间位置,可以将一个采样半球核心对准片段的观察空间表面法线。对于每一个核心样本会采样线性深度纹理来比较结果。采样核心会根据旋转矢量稍微偏转一点;所获得的遮蔽因子将会之后用来限制最终的环境光照分量。
通过以上发现 SSAO 所需的数据不正是延迟管线的 G-buffer,关于 G-buffer 是什么可通过文章「延迟着色法」[1]做一个简单的了解。阅读引擎代码 editor/assets/chunks/standard-surface-entry-entry.chunk 和 cocos/core/pipeline/define.ts :
// editor/assets/chunks/standard-surface-entry-entry.chunk 33 行附近
#elif CC_PIPELINE_TYPE == CC_PIPELINE_TYPE_DEFERRED
layout(location = 0) out vec4 fragColor0;
layout(location = 1) out vec4 fragColor1;
layout(location = 2) out vec4 fragColor2;
layout(location = 3) out vec4 fragColor3;
void main () {
StandardSurface s; surf(s);
fragColor0 = s.albedo; // 漫反射颜色 -> 反照率纹理
fragColor1 = vec4(s.position, s.roughness); // 位置 -> 世界空间位置
fragColor2 = vec4(s.normal, s.metallic); // 法线 -> 世界空间法线
fragColor3 = vec4(s.emissive, s.occlusion); // 和本文无关, 不做介绍
}
#endif
// cocos/core/pipeline/define.ts 117 行 附近
export enum PipelineGlobalBindings {
UBO_GLOBAL,
UBO_CAMERA,
UBO_SHADOW,
SAMPLER_SHADOWMAP,
SAMPLER_ENVIRONMENT,
SAMPLER_SPOT_LIGHTING_MAP,
SAMPLER_GBUFFER_ALBEDOMAP, // 6
SAMPLER_GBUFFER_POSITIONMAP, // 7
SAMPLER_GBUFFER_NORMALMAP, // 8
SAMPLER_GBUFFER_EMISSIVEMAP,
SAMPLER_LIGHTING_RESULTMAP,