一般在光照模型中,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最終的效果如下: