一般在光照模型中,ambient light的计算方法为:A = l * m,其中l表示表面接收到的来自光源的ambient light的总量,而m表示表面接收到ambient light后,反射和吸收的量。出于性能考虑,在计算光照时,我们是不考虑那些从场景中其他物体反弹过来的光的,因为通常我们认为这些光在场景中被发散和弹射许许多多次以至于最后从各个方向照射到物体上的量是相同的。所以ambient light所做的就是提亮物体,它没有任何真实的物理光照计算,所以它最终的渲染效果就是一个常量颜色:
而这里要介绍的Ambient Occlusion技术的作用就是改善ambient light的效果,使物体看起来饱满有层次感。
前面提到,在普通的ambient light计算中,我们认为所有的光线从各个角度照射到物体上时是等量的:
而在Ambient Occlusion技术中,我们将遮挡考虑进去,也就是说,表面从各个方向接受到的光的总量不再是等量的,而是取决于从表面的上半球体照射到表面的光线被遮挡了多少(这里为什么只考虑表面的上半球体呢,是因为从表面下半部分照射到表面的光是不会照亮表面的,所以不需要考虑):
那么在程序中我们该如何模拟这种遮挡呢?具体来说,就是我们从顶点随机生成围绕表面上半球体的射线,然后检测这些射线是否和网格相交:
根据上图,我们发射了7条射线,其中有5条是和网格相交的,那么对于这个顶点p,他的遮挡值occlusion = 5 / 7。所以,我们对于遮挡的定义如下:对于顶点p,如果我们发射了N条射线,其中有h条和网格相交,那么顶点p的遮挡值就是occlusion = h / N。但在计算时我们还有个需要注意的地方是需要定义一个距离distance,只有当顶点到射线和网格的交点的距离小于distance时,我们才认为顶点是被遮挡的,原因是如果距离太远,尽管射线和网格相交了,但是我们认为这个网格其实是遮挡不住顶点的。
好了,至此,我们已经了解了Ambient Occlusion的基本原理,可以开始动手实现了,基本的程序流程是这样的:
对于每个三角面,我们计算每个顶点的遮挡值,但是这个顶点可能被多个三角面共享,因此,我们的处理方式是加权平均,假设顶点v被2个三角面共享,对于三角面1,我们计算出来他的遮挡值是0.7,而对于三角面2,我们计算出他的遮挡值是0.5,那么他最终的遮挡值就是:(0.7 + 0.5) / 2 = 0.6。下面展示我写的计算代码片段,其中,对于每个三角面,我会发射32条射线用于做相交性检测:
1 UINT uTriangleCount = vIndices.size() / 3; 2 std::vector<UINT> vVertexSharedCount( uVertexCount ); // Used to count how many triangles contain the same vertex 3 4 for ( UINT triangleIndex = 0; triangleIndex < uTriangleCount; ++triangleIndex ) 5 { 6 UINT index_0 = vIndices[triangleIndex * 3]; 7 UINT index_1 = vIndices[triangleIndex * 3 + 1]; 8 UINT index_2 = vIndices[triangleIndex * 3 + 2]; 9 10 XMVECTOR vertex_0 = XMLoadFloat3( &vVertices[index_0].v3Position ); 11 XMVECTOR vertex_1 = XMLoadFloat3( &vVertices[index_1].v3Position ); 12 XMVECTOR vertex_2 = XMLoadFloat3( &vVertices[index_2].v3Position ); 13 14 // Calculate normal and centroid of this triangle 15 XMVECTOR edge_0 = vertex_1 - vertex_0; 16 XMVECTOR edge_1 = vertex_2 - vertex_0; 17 XMVECTOR normal = XMVector3Normalize( XMVector3Cross(edge_0, edge_1) ); 18 19 XMVECTOR centroid = (vertex_0 + vertex_1 + vertex_2) / 3.0f; 20 centroid += 0.001f * normal; // Offset to avoid self intersection 21 22 // 23 UINT UnoccludedCount = 0; 24 static const UINT SAMPLE_RAY_COUNT = 32; 25 for ( UINT index = 0; index < SAMPLE_RAY_COUNT; ++index ) 26 { 27 XMVECTOR vRandomDir = CUtils::RandHemisphereUnitVector3( normal ); 28 29 if ( !g_pOctree->RayOctreeIntersect(centroid, vRandomDir) ) 30 { 31 ++UnoccludedCount; 32 } 33 } 34 35 FLOAT fAmbientAccess = static_cast<FLOAT>(UnoccludedCount) / static_cast<FLOAT>(SAMPLE_RAY_COUNT); 36 37 // Average with vertices that share this triangle 38 vVertexAmbientAccesses[index_0] += fAmbientAccess; 39 vVertexAmbientAccesses[index_1] += fAmbientAccess; 40 vVertexAmbientAccesses[index_2] += fAmbientAccess; 41 42 ++vVertexSharedCount[index_0]; 43 ++vVertexSharedCount[index_1]; 44 ++vVertexSharedCount[index_2]; 45 } 46 47 for ( UINT vertexIndex = 0; vertexIndex < uVertexCount; ++vertexIndex ) 48 { 49 vVertexAmbientAccesses[vertexIndex] /= vVertexSharedCount[vertexIndex]; 50 }
好了,至此我们已经讲解完Ambient Occlusion技术了,这里还要补充的是,Ambient Occlusion的计算开销其实是非常大的,在我写的Demo中,有32000多个顶点,60000多个三角面,对于每个三角面发射32条射线,在我使用了八叉树进行优化的情况下,仍然需要5分钟左右的时间才能计算完毕,因此,我们通常会事先计算完遮挡值,存在文件中,然后运行时直接读取而不再计算,所以这个技术通常只能用于静态网格模型,因为对于动态网格模型他不可能实时运算。
在我的Demo中,我将每个顶点的遮挡值存在一张纹理中,其中每个像素对应一个顶点的遮挡值:
Demo最终的效果如下: