轉自:http://blog.csdn.net/hippig/article/details/7858574ios
shadow volume 這個術語幾乎是隨着 DOOM3 的發佈而成爲FPS 玩家和圖形學愛好者談論的對象的。雖然這個遊戲尚未上市,可是憑藉 John Carmack 的傳奇經歷以及 DOOM3發佈的一些讓人驚訝的預覽圖片,咱們仍然有理由認爲它將會是 2004 年最熱門的 FPS 遊戲之一。 id software向來都不吝惜爲了達到最好的圖像效果而使用最早進的渲染技術,這曾經使得玩家爲了玩它開發的遊戲而不得不掏光口袋裏面的錢來升級電腦,不知道此次咱們能夠倖免嗎?程序員
自DX9 發佈以來,你們的注意力彷佛都被 shader 吸引住了, BBS裏面談論的話題也老是離不開 shader based rendering ,前一段時間關於 GPU內部精度的討論大有遮天蔽日之感,但其實和閃閃發光的金屬小球以及波光鱗鱗的水面比較,幾個簡簡單單的影子經常能帶給場景更多的真實感。也許這就是爲何
DOOM3 可以在多如牛毛的 FPS 遊戲中脫穎而出的緣由之一。算法
陰影的實現方法有不少種,如今比較流行的主要是 shadow mapping 和shadow volume. 前者實現起來相對簡單,能夠發揮如今 GPU 可編程流水線的能力,可是因爲先天不足,shadow mapping在處理動態光源/物體的時候開銷過大,常常做爲一種靜態場景中的廉價替代物。而 Shadow volume 的強項偏偏是 shadow mapping 的短處,像 DOOM3 這種大量運用動態光源,而且要對時刻都在運動中的物體投射陰影,shadow volume是現階段惟一的選擇。編程
Shadow mapping 的原理: 一個物體之因此會處在陰影當中,是因爲在它和光源之間存在着遮蔽物,或者說遮蔽物離光源的距離比物體要近,這就是 shadow mapping 算法的基本原理。緩存 Pass1: 以光源爲視點,或者說在光源座標系下面對整個場景進行渲染,目的是要獲得一副全部物體相對於光源的 depth map (也就是咱們所說的shadow map ) , 也就是這副圖像中每一個象素的值表明着場景裏面離光源最近的 fragment 的深度值。因爲這個 pass中咱們感興趣的只是象素的深度值,因此能夠把全部的光照計算關掉,打開 z-test 和 z-write 的 render state 。app Pass2: 將視點恢復到原來的正常位置,渲染整個場景,對每一個象素計算它和光源的距離,而後將這個值和 depth map中相應的值比較,以肯定這個象素點是否處在陰影當中。而後根據比較的結果,對 shadowed fragment 和 lightedfragment 分別進行不一樣的光照計算,這樣就能夠獲得陰影的效果了。性能 |
從上面的分析能夠看出來,depth map的渲染只和光源的位置以及場景中物體的位置有關,不管視點怎麼運動,只要光源和物體的相互位置關係不變,shadow map就能夠被重複使用,所以對於沒有動態光源的場景, shadow mapping 是很明智的一種選擇。測試
除了上面提到的不能很好應付動態光源場景的限制以外,shadow mapping 還存在着全部使用 texture
的場景面臨的共同問題-鋸齒。根據採樣定理,只有紋理分辨率小於或者等於物體的實際分辨率時纔不會失真,而當一副很大的紋理被貼到尺寸比它小的物體上時,會出現一個 fragment 覆蓋多個 texel 的狀況,這時要準確的再現這個 fragment 的顏色信息,就要綜合考慮全部被它覆蓋的texel 產生的影響,這就是各類紋理濾波方法最基本的原理。可是因爲 depth map 是在不斷的變化當中,因此不能像通常的紋理那樣把各個mip -map 事先計算好放到顯存裏面。有一種利用 pixel shader 的方法對 depth map 作 bilinear filtering, 可是開銷很大,在現階段不具有實用意義。優化
一樣的問題在紋理分辨率小於屏幕分辨率的時候仍然存在,這時多個 fragment會被投射到同一個 texel 上面,雖然從再現紋理的角度來講並不存在失真,可是因爲多個 fragment 共用同一個紋理值,鋸齒問題仍是存在。更糟糕的是,沒有一種濾波技術能夠從根本上解決這樣的鋸齒,由於從數學上講,人們不可能經過運算來創造出比原始量更多 的信息。近年來,爲了解決 shadow mapping 的鋸齒問題,人們作了不少努力,比較有前景的是 adaptive shadowmap(ASM) 和 perspective shadow map(PSM) 。二者的基本原理都是在可能產生鋸齒的地方人爲增長採樣率,使得一個fragment 至少對應一個 texel , 區別是 ASM 增長採樣率的地方是在 shadow 邊緣,而 PSM是在靠近視點的地方。修補一個自己存在缺陷的方法從數學上來講是缺少美感的,正像 John Carmack 在 2002年8月的一封 email中所說:spa
「 Shadow buffers make
good looking demos with controlled circumstances, but when you start
using them for a 「real」 application, you find that you need absolutely
massive resolution to get acceptable results for omni - directional
lights, and a lot of the artifacts need to be tweaked on a per-light
basis. While it is possible to do shadow buffers on GF1/radeon class
hardware, without percentage closer filtering they look wretched. If we
were targeting only the newest hardware, shadow buffers would have a
better shot, but even then, they have more drawbacks than are commonly
appreciated. 」
看起來彷佛 John Carmack 找到了實現陰影更好的方法?讓咱們來看看它到底是什麼。
Shadow volume 的原理:
Shadow volume 這種算法第一次被提出是在Franklin C. Crow 在 1977 年寫的一篇論文 「SHADOW ALGORITHMS FOR COMPUTERGRAPHICS 」裏。其基本原理是根據光源和遮蔽物的位置關係計算出場景中會產生陰影的區域( shadow volume),而後對全部物體進行檢測,以肯定其會不會受陰影的影響。
圖中的綠色物體就是所謂的遮蔽物,而灰色的區域就是 shadow volume。
只有處於 shadow volume 裏面的物體纔會受陰影的影響。
shadow volume的算法
如今清楚了 shadow volume 的基本原理,那麼如何肯定一個物體或者一個物體的某一部分處於 shadow volume 中呢?這就要用到 stencil buffer 的幫助了。
z-pass 算法:
z-pass 是 shadow volume 一開始的標準算法,用來肯定某一個象素是否處於陰影當中。其原理是:
Pass1:eNABle
z-buffer write ,渲染整個場景,獲得關於全部物體的 depth map 。注意這裏的 depth map 和 shadow
mapping 裏面的區別是 shadow volume 裏面的 depth map 是以真實視點做爲視點獲得的,而 shadow
mapping 裏面的 depth map 是以光源爲視點獲得的。
Pass2:disable z-buffer write , eNABle
stencil buffer write, 而後渲染全部的 shadow volume 。對於 shadow volume 的 front
face( 既面對視點的這一面 ) ,若是 depth test 的結果是 pass, 那麼和這個象素對應的 stencil 值加一。若是
depth test 的結果是 fail, stencil 值不變。而對於 shadow volume 的 back face(
遠離視點的一側 ) ,若是 depth test 的結果是 fail, stencil 值減一,不然保持不變。
用一句簡單的話來歸納 z-pass的算法就是從視點向物體引一條視線,當這條射線進入 shadow volume 的時候, stencil 值加一,而當這條射線離開 shadow volume 的時候,stencil 值減一。若是 stencil 值爲零,則表示實現進入和離開 shadow volume的次數相等,天然就表示物體不在 shadow volume 內了。
Pass3:第二步完成之後,根據每一個象素的 stencil 值判斷其是否處於陰影當中(若是 stencil 的值大於零,則這個象素在 shadow volume 內,不然在 shadow volume 的外面),而後據此繪製陰影效果。
在這副圖裏面,視線三進三出 shadow volume, 最後的 stencil 值爲零,表示物體在 shadow volume 外,不受陰影的影響。
這副圖裏面視線三進一出, stencil 值爲 2 ,表示物體在 shadow volume 內,有陰影產生。
這副圖裏面從視點到物體的視線停止於 shadow volume 前,也就是說全部的 z-test 都是 fail, 相應的 stencil 值爲零,表示物體在陰影外面。
z-pass 算法缺點及補救辦法
以上的討論都是基於視點在 shadow volume 外面的狀況。在這個條件能夠獲得知足的狀況下,z-pass 算法工做的很好,不過一旦視點進入到了 shadow volume 裏面,z-pass 算法就會當即失效。
這副圖裏面的視線二進二出,按照 z-pass的算法,最後的 stencil 值爲0,表示物體在陰影外,可實際上物體是處於陰影內的。錯誤的緣由就在於視點進入到陰影內,使得視線失去了一次進入 shadow volume的機會,讓本來應該是 1 的 stencil 值變成了 0 。
Z-Pass 這種錯誤的行爲能夠從下圖中看出 :
注意地下的影子
Z-Fail 算法:
Z-Fail 算法是 John Carmack,Bill Bilodeau 和 Mike Songy 各自獨立發明的,其目的就是解決視點進入 shadow volume 後 z-pass 算法失效的問題。
Pass1:eNABle z-write/z-test, 渲染整個場景,獲得 depth map 。 ( 這一步和 z-pass 的徹底同樣 )
Pass2:disable z-write, eNABle
z-test/stencil-write 。渲染 shadow volume, 對於它的 back face ,若是 z-test 的結果是fail, stencil 值加一,若是 z-test 的結果是 pass, stencil 值不變。對於 front face, 若是z-test 的結果是 fail, stencil 值減 一 ,若是結果是 pass, stencil 值不變。
圖中全部的 shadow volume 都處在 z-pass 的位置,所以 stencil 值不會改變。
視點在 shadow volume 內也沒有問題,最後 stencil 的值是 2, 表示物體在陰影內。
上面那個 Z-Pass 沒法處理的場景,用 Z-Fail 計算則能夠獲得正確的結果:
使用 z-Fail 算法的條件
Capping For Z-Fail
因爲 Z-Fail 算法依靠計算 shadow volume 不能經過 Z-test 的部分來肯定 stencil buffer 的值,因此要求 shadow volume是閉合的。下面的那張圖裏面紅色的實線表示 capping, 能夠想象,假如不人爲的添加 capping, 那麼 shadow object1/2 的 stencil 值都會是 0 ,而實際上正確的 stencil 值應該是 1 ,由於它們都在陰影內。
Z-Pass 和近剪裁面的關係:
在 Z-PASS 算法中,當 shadow volume 和視圖體 (view frustum) 發生剪切關係的時候,須要附加的capping 才能保證最後的結果正確。由於通過view frustum 的剪裁做用之後,shadow volume 的一部分有可能變成敞開的,好比在圖中 additional capping 的位置,假如不人爲的附加一部分多邊形,在渲染 shadow volume 的時候 stencil buffer 就不會發生+1 的操做 ( 由於這裏沒有任何多邊形,天然也就不會和原來的 depth map 比較 ) ,最後的結果顯然是不對的。
如何創建 shadow volume?
shadow volume的創建是整個算法裏面最重要的部分,在 GPU 出現之前, shadow volume 的創建都是基於 CPU 的。隨着 GPU應用的逐漸開展,人們又將 shadow volume 運算移植到了 GPU上,不事後面一種方法須要對物體的幾何數據進行預處理,下面就對兩種方法分別進行解釋:
CPU based method(基於CPU創建方法):
想必熟悉 shadow volume 的朋友對silhouette edge 這個詞會很熟悉。它表示從光源的角度看物體所獲得的輪廓線。 Shadow volume 就是由silhouette edge 擴展到必定距離之外或者無窮遠處獲得的。silhouette edge
的肯定方法有不少種,基本思想就是找出那些被朝向相反 ( 一個面向光源,另外一個背向光源) 的兩個三角形 ( 相對於光源來講)所共享的邊,由於只有這樣的邊會最終成爲 silhouette edge ,其餘的邊在光源看來都在物體投影的內部而不是邊緣。
這副圖是一個由 4 個三角形組成的多邊形,假設光源處在讀者頭部的位置,那麼外圍的一圈實線就是所謂的 silhouette edge 。咱們所要作的就是從原始數據裏面將內部多餘的 4 條邊 ( 虛線 ) 去掉。具體實現是這樣:
# 遍歷模型的全部三角形
# 計算 dot3( light_direction , triangle_normal ) 。用這個結果判斷三角形是面向光源 (dot3>0) 仍是背向光源 (dot<0) 。
# 對於面向光源的三角形,將全部的三條邊壓入一個 棧 ,和裏面的邊進行比較,若是發現重複的 (edge1 和 edge2) ,將這些邊刪除
#檢測過全部三角形的 全部邊 之後, 棧 裏面剩下的 邊就是 當前光源 / 物體位置下面的 silhouette edge.
# 根據光源方向 , 利用 CPU 或者 vertex shader 將這些 silhouette edge 投射出去造成 shadow volume.
值得一提的是,這種方法正是 DOOM3所採用的方案,可是其中有一個問題就是 silhouette edge是由光源和物體的相互位置肯定的,也就是說這兩者之間有一個的位置發生了變化, silhouette edge就要從新計算,更新的數據也要傳回顯卡才能渲染 shadow volume ,這對 CPU 的計算能力以及 AGP的帶寬不能不說是一個不小的考驗。
GPU based method(基於GPU創建方法):
Vertex shader一出現人們就在思考能不能利用它來加速 shadow volume 的渲染速度。但即便是如今最早進的 vertex shader 3.0也不具有建立新的幾何物體的能力。簡單點說 vertex shader 只能接受一個頂點,修改這個頂點的屬性 ( 位置,顏色,紋理座標,etc), 以後輸出這個頂點到光柵化部分,繼而進行 pixel shader 運算。碰到須要建立新頂點的地方,就只有依靠 CPU 直接操做vertex buffer 了。
另一個方法就是事先把 shadow volume須要的空間留出來,而後再經過 vertex shader的運算使以外形達到咱們須要的樣子。這就比如我要存儲一串數據,但又不很肯定具體的規模是多大,只好事先分配一塊很大的區域,這樣難免會形成很大浪費,但也是不得以而爲之。
因爲物體上的每條邊都有可能成爲 silhouette edge ,因此咱們須要事先插入 degenerate quad( 上圖的紅色三角形 ), 這些 quad的面積爲零,不做任何變換的話是不可見的,不會形成視覺瑕疵。可是在須要的地方,能夠把這些 quad 拉伸成爲 shadow volume 的側壁。
顯然,插入冗餘的頂點會形成極大的浪費。由於大部分的邊最終 並不會成爲 silhouette edge ,也就是說插入的 degenerate quad是無用的。不過這樣作的好處是幾何數據只須要傳輸到顯卡一次,以後不管光源的位置在哪裏,預處理事後的幾何體均可以用來生成 shadow volume ,不像剛纔解釋過的方法那樣一旦光源和物體的相對位置發生變化,就須要從新用 CPU 計算 silhouette edge,以後再把結果 傳送給顯卡。
實際編程的時候,能夠作一下改進,因爲平坦的表面是不會產生陰影的,因此在這些表面所包含的邊上就不必插入 degenerate quad。並且全部的預處理應該在軟件開發過程當中完成,用戶啓動程序之後直接調用的就是插入過 quad 的模型,不須要 CPU 再進行計算。
創建/渲染 shadow volume 的 shader 代碼: // c0 : Light position in object space // c1 : 1, 1, 1, 0 // c2- c5 : Light * View * Proj = LightClip // c6- c9 : WorldInvLight matrix // c10 : Color for exposing the shadow volume vs.2.0 mov oD0, c10 // 輸出特定的顏色使 shadow volume 可見 sub r1, v0, c0 // 光源方向 m4×4 r4, v0, c[6] // 將頂點變換到光源座標系 nrm r1, r1 // 光源向量歸一化,這是爲了 shadow volume 的各個邊同樣長 mov r10, c1 dp3 r10.w, v1, r1 //dp3 頂點法向量和光源向量,肯定頂點的朝向 slt r10, c1.w, r10 // 根據 dp3 的結果設置 r10 寄存器的第四個單元 mul r4, r4, r10 // 設定 r4 的 w 位 m4×4 r5, r4, c[2] // 輸出頂點到 clip space mov oPos , r5 |
Shadow volume 的算法優化(一)
Shadow volume 的基本算法講到這裏就基本完成了,下面說一下如今比較經常使用的一些優化算法。
(一)Z-Pass .VS. Z-Fail
前面提到過,Z-Pass 比 Z-Fail 速度要快,所以咱們能夠在不會產生問題的場合下適當使用 Z-Pass 來提升性能,可是如何肯定什麼時候 Z-Pass 不會帶來問題呢? Z-Pass 失效主要是因爲兩種緣由 :
緣由一:視點進入 shadow volume 內,好比下圖:
只要能探測出這兩種狀況,就能在須要的時候切換到 Z-Fail 算法。條件 A 的斷定能夠參照下圖,在視點和光源之間作一條連線,若是這條線和遮蔽物相交,那麼能夠確定視點在 shadow volume 內,將切換到 Z-Fail 算法。
緣由二:shadow volume 與近 剪裁面相交
至於狀況 B 的斷定能夠利用光源和近 剪裁面 造成的light-pyramid( 紅色陰影部分 ) 與遮蔽物的交匯關係。若是遮蔽物徹底在 light-pyramid 以外,則由它生成的shadow volume 不會和近 剪裁面 相交,可使用 Z-Pass 算法,不然將只能使用 Z-Fail 算法。
Shadow volume 的算法優化(二)
(二)tricks to save fillrate :
前面提到過,shadow volume算法裏面兩個最耗時的步驟就是 silhouette edge determination 和 shadow volume rendering。其中 shadow volume rendering 是徹底考驗 GPU 填充率的步驟,雖然如今的顯卡動輒就有幾十 G fragment/s的填充率能力,可是遇到複雜的場景,流水線也難免不堪重負。此外,頻繁的 stencil buffer操做也會佔據一部分顯存帶寬,若是可以找出一些辦法儘可能減少 shadow volume 的尺寸,將會是效果很明顯的一種優化方法:
限定光照的範圍(Attenuated Light Bounds):
若是所用的光源有衰減效應,則能夠利用 scissor test 將渲染的範圍限定在光源的做用範圍以內,由於超出了這個範圍就不會有陰影存在,天然用不着去渲染那部分的shadow volume了。所謂 scissor test 就是人爲地在屏幕座標系下面定義一個矩形,只有座標處在這個矩形範圍內的 fragment纔可以經過測試,其內容才能被寫入幀緩存。
NVIDIA的陰影加速技術(ultra shadow):
ultra shadow這項技術是隨着NV35 的發佈而浮出水面的,進而在 NV36/38 中獲得了繼承,咱們基本上能夠在 NVIDIA 從此的產品中,這項技術會獲得持續的應用。
id software 的當家程序員 John Carmack 曾經說過 NV35 是爲 DOOM3 量身打造的 GPU ,咱們在這裏有理由懷疑 Carmack說這番話的緣由頗有可能就是因爲 NV35 中集成了 ultra shadow 陰影加速技術(近日GeForceFX系列已經成爲DOOM3的推薦GPU),那麼 ultra shadow 到底是什麼,它如何加速陰影的渲染速度呢?
其實 ultra shadow 技術僅僅利用了一個 NVIDIA 新近提交的 OpenGL 擴展—— EXT_depth_bounds_test,咱們先來看一下 NVIDIA 官方在 GDC2003 上對這個擴展的介紹:
首先注意一下名稱的問題,GDC2003在三月舉行,那時這個擴展還只是 NVIDIA 獨家的東西,到了 4 月這個擴展改名爲 EXT_depth_bounds_test 。 EXT開頭的擴展表示有多家廠商在開發這項技術,也許不久之後咱們就會看到 ultra shadow 在 ATI 的 GPU 上面實現。
Depth bounds test 的做用是比較由當前 fragment 的屏幕座標( xw , yw )指定的 depth buffer 中的 z 值與用戶經過 glDepthBoundsNV(GLclampd zmin , GLclampd zmax )所指定的 [ zmin,zmax ], 若是 z 值在次範圍以外,則將當前的 fragment 從流水線中剔除掉,不進行此處的 stencil buffer 操做。注意這裏比較的並非 fragment(shadow volume) 的 z 值,而是前一個 path 中已經渲染過的shadow receiver 的 z 值。具體狀況請看下圖:
能夠看到,因爲 A 點的 z 值在 [ zmin,zmax ] 範圍以外,此點沒有可能被陰影遮住,所以 A1/A2 點處的 fragment 就能夠被丟棄。而 B 點的 z 值在 [ zmin,zmax ] 以外,因此 B1 點處的 fragment 就必須進行 stencil buffer 操做。
(詳細的技術介紹請看:《NVIDIA的復仇計劃 GF FX 5900 Ultra》)
陰影渲染實現技術的展望
shadow volume是近階段實現統一光照模型比較好的一種技術,如今主要的問題是基於 CPU 的方法對處理器依賴比較重,在 AI/ 物理運算較多的場景中 CPU的運算能力可能不足,而基於 GPU 的方法效率過低,會產生大量的冗餘頂點,其緣由仍是因爲如今的 GPU( 包括即將發佈的 NV40/R420)都不具有在芯片內部產生新頂點的能力。 Microsoft 意識到了這一點,在 DirectX Next的發展規劃中將這種能力列爲了要實現的目標之一:
從更長遠的角度來講,基於真實物理模型的光照模型(好比spherical harmonic lighting、ray-tracing、radiosity)纔是發展的方向,那時咱們沒有必要設計單獨的算法來實現陰影,全部的光照/陰影效果都被包擴在了一個統一的光照模型之中,任何效果實現起來都是天然而然的,就像它們在真實世界中的狀況同樣。固然,全部這些設想都要基於半導體生產技術的支持才行,咱們在近期(5-10年)將不會看到它們在硬件上的實現。