天天看點

體積陰影生成算法

首先聲明:這不是我寫的,從百度上弄到的,不知哪位仁兄寫的,在此萬分感謝!

目前普遍采用的一般有三種:Planar Shadow、Shadow Mapping和Shadow Volume,前者類似投影,計算最簡單,缺點隻能繪制抛射在平面上的陰影;Shadow mapping利用站在光源處所沿光源法線看去所生成的深度圖來檢測場景中的體象素是否處于陰影中,缺點是光源與物體位置相對固定、且在極端情況下計算精度差,不太适合精确到象素的動态光陰場合;Shadow Volume是目前最适合精确表現動态光陰場景的技術,适用性最廣,其典型的适用範例便是Doom 3,不足在于陰影體積引入了額外的頂點和面,加大了存儲和處理強度,同時渲染出的陰影比較硬,如果要實作軟陰影,仍需其他技術配合。

這裡我們快速往前跳,Perspective projection、Depth test、Stencil buffer等概念就不多談了。Shadow Volume的一般步驟為:生成陰影體積(Mesh)和陰影渲染,陰影體積生成算法又分兩種,一種是D3D SDK sample中所采用的方法,先分離/插補物體面,然後用Vertex Shader加速,對複制所得的未拉伸的陰影體積進行沿光線方向的Extrusion;另一種則是多數tutorial中常用的Software Renderer方式,首先揀選出所有向光面,将每一個向光面的所有邊均加入一個list,每加入一個邊時,如果list中已經存在相同的邊了,則不加入,同時将list中相同的邊删除,這樣處理完所有的向光面後,list中便隻會剩下物體向光面輪廓的邊資訊,将該輪廓沿光矢量方向進行延伸,然後完成側面Quad插補和前後封口(如果使用的是Z-fail)即可。而對于陰影渲染,則一般用4個pass,具體有Z-Pass/Z-Fail兩種做法:

Z-pass算法

1. 先關閉光源,将整個scence渲染一遍,此時一片漆黑,但獲得了深度值

2. 關閉深度寫,渲染陰影體的正面,深度測試通過則模闆值加1

3. 然後渲染陰影體的背面,深度測試通過則模闆值減1

4. 最後模闆值不為0的面就在陰影體中,開啟深度寫

5. 用模闆手法重新渲染一次加光的scence即可,讓陰影部分為黑色

6. 其緻命缺點是當視點在陰影中時,會導緻模闆計數錯誤

7. 同時,有可能因為Z-near clip plane過近而導緻模闆計數錯誤

Z-fail算法(John Carmack's Reverse)

1. 先關閉光源,将整個scence渲染一遍,獲得深度值

2. 關閉深度寫,渲染陰影體的背面,深度測試失敗則模闆值加1

3. 渲染陰影體的正面,深度測試失敗則模闆值減1

4. 最後模闆值不為0的面便處于陰影體中,開啟深度寫

5. 用模闆手法重新渲染一次加光的scence即可,陰影部分不渲染色度

6. 注意,該算法要求陰影體積是閉合的,即需要前後封口

7. 該方法不是沒有缺陷的,有可能因為Z-far clip plane過近而導緻模闆計數錯誤

值得注意的方面

1. Z-pass由于不用封口,是以速度比Z-fail快,但存在處理不了的情況

2. Quake 3貌似使用的是z-pass shadow volume和planar shadow

3. 為保證足夠robust,必須確定z-near/z-far中至少一個不出問題,Nvidia的論文推薦采用z-fail,用w=0來實作無窮遠的z-far平面

4. 記住使用Z-fail一定要封口,而且陰影體積的每個面的法線必須正确地指向物體之外,包括front cap和back cap

5. 上面給出的是簡單過程,陰影很硬,可以稍微變通一下:先以環境光渲染一遍,然後計算模闆,再用模闆渲染打光的scene,最後以alpha blend加深陰影

下面開始大面積地貼圖,首先來看個Z-fail的具體例子(z-pass簡單,就不舉例了):從外面看一個面向光源的單面的情況。圖中,虛線框起來的為陰影體積,圓形為點光源,其中,面ADFB和面DEF是back faces,而面ABC、ACED和CBFE則是三個front faces(注意這裡頂點順序用的是左手系),為說明問題(主要是為了說明在計算陰影體積時如何避免z-fighting),我故意把面ABC畫在了黃色三角形下面一點點的位置,而且其中的灰色陰影是與平面共面的,而面DEF則處于平面下面一點點的地方。

體積陰影生成算法

首先進行2個back faces的depth test:粉紅色的是stencil buffer中因Z- fail而加1的區域;然後進行3個front faces的depth test:亮綠色的是stencil buffer中因Z- fail而減1的區域

體積陰影生成算法

最後紅綠區域正負相抵,stencil buffer中隻剩下平面上的灰色三角區域中的值不為0,即原圖陰影所在的位置。上面是個從單面外看的例子,值得注意的是,單面與兩個背靠背雙面的情況是不一樣的,那麼後者的計算結果是不是也是正常的呢?繼續看圖:

體積陰影生成算法

上圖的紅、綠細線分别表示兩個背靠背單面,而粗線則構成了體積陰影,紅粗線、綠粗線分别表示紅面、綠面沿光線方向的的投影,一般而言,back cap處于無窮遠處,而front cap則值得注意,為避免z-fighting,它的位置應該是在紅面之後、綠面之前,也許你會說:“hey, 這裡的front cap完全可以直接等于紅面嘛”,是的,當在eye看來紅面是背面時的确可以這麼做,可當eye位置變化、紅面不再是背面時,簡單的以紅面為front cap便會導緻z-fighting。從圖上可看出,當eye處于陰影内部時,模闆計數也是正确的。下面再來看看僅有一個單面、且處于陰影内部的情況:

體積陰影生成算法

隻要front cap注意了z-fighting的情況,其計算結果也是正确的,有意思的是,實際上當eye處于陰影内部時,在back culling的時候,三角形的背面便已經被去除了,是以在實際渲染時,在三角面的背面處并不會存在其深度資訊,但即便如此,z-fail的模闆計數仍是正确的。

體積陰影生成算法

最後,再來看一個計算方塊的陰影的例子,可以看出,當front cap為背面時,陰影體積的計算很簡單,無需考慮其z-fighting的情況,直接用方塊的向光面作為front cap即可;但當front cap為正面時,還是面臨着位置需要微調的問題。

總結一下,用z-fail方式來計算沒有厚度的面的陰影和具有厚度的物體的陰影還是有點微妙差別的;在計算陰影體積時,要注意避免front cap處z-fighting的情況,稍微調整一下front cap的位置,即将front cap沿光線方向稍微向後挪那麼一點點;或者是.... 等等,還有一種更簡潔優雅的辦法,即将Z-fail算法中的"Depth Test失敗"了解成象素深度">="所在位置的深度而不僅僅隻是">",将ZBufferFunction設為Campare.Less而不是Campare.LessEqual,這樣的話便完全避開了z-fighting的問題,微調什麼的都可以免了,無論是面還是物體,在任何情況下都可以直接用物體向光面作為front cap....faint...快寫完了才考慮清楚...白廢話了那麼多....無比郁悶-_-b....收工.

volume 的 frontface( 既面對視點的這一面 ) ,如果 depth test 的結果是 pass 那麼和這個象素對應的 stencil 值加一。如果depth test 的結果是 fail stencil 值不變。而對于 shadow volume 的 back face(遠離視點的一側 ) ,如果 depth test 的結果是 fail stencil 值減一,否則保持不變。

[email protected]

繼續閱讀