之前寫過ShadowVolume,但是那是老版本(DX8)的DXSDK中的sample。而老版本并不是一個很好的解決方案。而DX9中的ShadowVolume還算是比較可取的方案。在這裡主要對DX9中的ShadowVolume中的重點難點進行梳理。詳細的細節,必須參考sample中的代碼。
DX9中的shadowVolume最大的特點是建立了陰影體網格,而不是之前DX8中的動态生成輪廓邊集合。建立陰影體網格最大的好處就是通過GPU來分擔原來由CPU負責的輪廓邊識别。而且由于新的陰影體積總體算法的更新,使得輪廓邊識别更容易。缺點也很顯然多了很多記憶體需求。
我們還是先大體了解一下基本思路。
我們的原始模型建立陰影體網格。主要的方法是為每一個面都建立單獨頂點,這樣面與面之間不會形成牽扯,以便進行随後的背面移動。
其次在隻是用環境光的條件下渲染場景到color buffer和z buffer裡面。
再關閉z buffer和 color buffer,并開通stencil buffer,使用depth-fail方法将陰影體渲染至stencil buffer中。
最後根據stencil buffer中标記識别出不在陰影的區域,并對這些區域添加正常光照。
其中在sample的代碼裡面最難了解的區域是GenerateShadowMesh函數。而GenerateShadowMesh中最難了解的有好幾處。
首先是ConvertAdjacencyToPtRep(),這個函數是獲得用點表示的相鄰資訊。
比如下面的四邊形

其中四邊形由6個頂點組成,而不是我們常見的4個頂點,在這種情況下通過ConvertAdjacencyToPtRep()我們能得到一個和頂點個數相等的一個DWORD數組,數組的下标表示頂點索引,而數組内的值就是相應位置頂點的點索引。 比如加入上圖有6個頂點。
則通過此函數會得到這麼一個數組
DWORD PtRep[ 6 ] = { 0, 1, 2, 2, 1, 5 };
我們可以看到如果有頂點和以前出現的頂點相鄰很近,則後出現的頂點用前者的索引。通過這個數組我們可以知道下标是3的元素和下标是2的元素的值一樣,即 PtRep[ 2 ] == PtRep[ 3 ];
通過得到鄰近資訊,我們就可以借助它,識别出使用不同索引表示的相鄰點,進而識别相鄰邊。比如在上圖中我們怎麼通過計算知道 邊(V1-V2) 和邊(V2-V3)相鄰呢?
我們可以利用鄰近數組PtRep,看看邊(V1-V2)和邊(v2-V3)是不是同一條邊(暫且不管邊的方向)。那麼我們可以看到邊 (v1-v2)是( PtRep[ 1 ], PtRep[ 2 ] ),邊(V2-V3)是( PtRep[ 2 ], PtRep[ 3 ] ),也就是邊( 1, 2 ) 和邊( 2, 1 ). 進而我們可以看到在用鄰近數組表示邊的,相鄰的邊即使原本的頂點索引不同,但在鄰近數組中都是一樣。是以MS的高手們早就知道這麼秒用了。
另外就是邊映射結構體。仔細看看應該能看明白裡面的邏輯。
主要是記錄每條的三個資訊。
在鄰近數組中的邊表示
相鄰兩條邊中第一條邊在普通頂點索引清單中的邊表示
相鄰兩條邊中第二條邊在普通頂點索引清單中的邊表示
後面的兩個資訊主要是用于資訊儲存,以便後面使用的。
然後稍微有點難度的就是補洞操作。這裡我們十分注意在Depth-Fail技術中,陰影網格必須是全封閉。 是以補洞操作時必須的。
全封閉也就是3d中沒有任何漏洞。不像在Depth-pass技術中那樣隻需要延伸陰影邊,在這裡我們需要cap。也即是陰影的前冒和後帽。實際上就是陰影網格中和物體同形狀的部分。如下圖所示的Cap,也就是原本物體網格的相對于燈光的前面和後面。
如果沒有冒的話,在某些情況是會出現槽糕後果。例如
在No caps中利用Depth-Fail是無法在牆上繪出陰影的,因為眼睛方向對應的地方沒有陰影網格。而加了Cap之後就可以在牆上繪出陰影了。
另外在陰影網格處理的VertexShader中,對于當物體和燈光轉換到相機坐标中後,如果物體比燈在z方向上更遠離視點,我們采用将物體移至遠裁截面稍微靠前的位置。這樣一定能讓整個陰影網格不被裁減掉。以保證不會發現像No Caps圖中的情況。采用的公式在sample中都寫着,稍微畫個圖,或者想想就可以了解。然而當物體比燈在z方向上更靠近視點的,我們将物體的坐标改成物體和燈光構成的向量。這是為什麼?這裡我們要提到齊次坐标,在齊次空間中我們認為(1, 3, 2, 1 )和 ( 2, 6, 4, 2 )是同一點。他們的代表是(
1, 3, 2, 1 ),也就是我們将w不等于1的都通過是以分量除以w來使得w等于1.但當w=0時,我們認為( 1, 3, 2, 0 )是一個點,它離原點無限遠,并且它和原點的連線與向量( 1, 3, 2 )方向一緻。一開始可能會認為為什麼不在原來的點的基礎上沿燈光到像素方向進行平移呢?其實當我們采用無窮遠的方案時,就不必再原來點的基礎上了,我們可以忽略,而直接在原點的基礎上平移,仔細想想,無窮遠的距離與有限距離比起來,有限距離總是可以忽略的,因為它占的比率可以無限放小,以至于忽略。
最後一點小領會就是在想實作顔色疊加時,如一個白燈照亮一個場景,如果再添加一個綠燈,場景需要再添加上綠燈引起的相應的顔色。在白燈已經對場景處理一次之後,綠燈的處理仍然要用到白燈建立起來的zbuffer,為了讓綠燈處理的場景能夠通過zbuffer,我們把ZFunc改為小于并且等于。關鍵是等于,可以是的綠燈繼續使用zbuffer,而不用清除。
基本上這是索引ShadowVolume中比較難了解的地方了。我通過完全手動重新這個sample比較全面的了解裡面的細節。如果有問題可以聯系我。