早在上世紀七十年代末,Williams在他的“Casting Curved Shadows on Curved Surface”一文中提出了名為Shadow Map的陰影生成技術。之后,他人在此基礎上針對相關問題做了許多改進。現在,Shadow Map仍被作為主流的陰影生成技術被廣泛應用。
Z緩沖在一開始就是Shadow Map技術的實現基礎。討論Shadow Map技術的意義,不僅在於了解一種陰影生成技術,還在於可借此掌握一種很有用的技術手段。物體表面上一點,只有在與光源之間沒有障礙阻隔時,它的深度值才會被保存到Z緩沖中。換個角度看,這就相當於,在物體表面上某點的深度值被保存到Z-Buffer之前,用此點與光源間連線與場景中所有對象做了一次碰撞檢測。借用Z-Buffer做碰撞檢測的這一方法,還可以用來幫助處理許多其它問題。
一、Shadow Map 原理
Shadow Map實際上比陰影體的原理要簡單一些。陰影體是借助Stencil Buffer來做碰撞(觀察者視線與陰影體中可能存在的障礙物之間),而Shadow Map則借助Z-Buffer來做碰撞檢測。
圖 一
如圖一所示,假設三維空間中,有物體W在光源L照射下形成陰影。空間中的a點位於W與L之間,c 點位於W之后,而b點是W表面上的一點。a、b、c、d經透視投影變換,在屏幕S上對應着a'、b'、c'、d'四個像素區域。
Shadow Map的思想方法是:假設先在光源L處放置一個攝像機(形成所謂的Light Space),則此像機將會把整個場景投影到相應的投影平面H上,其視錐在H平面上的投影是h1和h2兩塊區域之合。平面H所對應的Z-Buffer保存的是Light Space的所有對象(本例中僅有W)的深度值。在實際生成觀察平面S上的像素時,會先將像素對應的空間中的點(如上圖中a'、b'、c'、d'所對應的a、b、c、d)轉換到Light Space中,投影到H平面上,並將相應的深度值與事先保存在H平面所對應的Z-Buffer的深度值進行比較,以圖一為例,a點會投影到區域h1中,由於它位於W之前,其深度值會比H平面的相應Z-Buffer中的值小;b點在h1上的投影點的深度值等於H平面的相應Z-Buffer中的值;c點在h1上的投影點的深度值,則會大於H平面的相應Z-Buffer中的值;由於在生成H平面的投影時,會事先刷新其Z-Buffer的值,刷新值為1,所以在本例中,空間d點在H上的投影的深度值也將小於相應點的Z-Buffer值;因此,通過空間中某一點在平面H上的投影的深度值與H平面原Z-Buffer中的值的比較結果,就可以判斷此點是否處於陰影中,並可根據這個判斷來設置觀察平面S上的相應像素的顏色。
考慮這樣一種情況,空間中的一點如果處於觀察者V的視錐中,同時又位於Light Space的視錐之外,那么顯然就無法通過上面的方法來判斷它是否被陰影所覆蓋。這也是Shadow Map的局限之處。
Z-Buffer值一般由圖形引擎結合相應硬件,在渲染管線內部計算,用戶只需直接調用即可。因此直接使用Z-Buffer的值高效而又方便。但是,通常情況下,Z-Buffer與Stencil Buffer合用4字節空間來描述一個像素,在Shadow Map中用來保存Light Space相應場景對象的深度值一般只有一個字節,而深度值是一個處於0~1之間的浮點數,這樣勢必會影響到后面的計算精度。這也可看作是傳統Shadow Map的另一不足。
繞過Z-Buffer來實現Shadow Map,可以為解決這一問題提供一種方法。
二、Shadow Map的實踐
本文的實驗是通過Fx Composer 2.5在一台 Laptop上進行的,其內置一塊 nVidia GT420M顯卡。
圖 二
圖 三
圖二與圖三是使用陰影前后的比較。這里沒做鏡面反射,處於影陰區的像素則被簡單地直接塗黑。
首先要做的,是生成一張Shadow Map數據圖。因為不使用Z-Buffer,就要做一些額外的創建工作,為了把DIY精神貫徹到底,索性一切從頭開始。
1. 先來構建Light Space的相關轉換矩陣
設置光源的位置和及Light Space的視錐投射方向:
1 float3 Lamp0Point = {0.0f,20.0f,0.1f}; 2 float3 Lamp0LookAt = {0.0f,0.0f,0.0f};
1) 計算Light Space的View轉換矩陣
設:
則根據仿射坐標系變換公式有:
其中,(xt, yt, zt) 為空間一點p在Light Space坐標系中的坐標;(xr, yr, zr)是點p在原世界坐標系中的坐標;M是原世界坐標系到Light Space坐標系的過渡矩陣;(x0, y0, z0)是Light Space坐標系原點在原世界坐標系中的坐標值。α1、 α2、 α3是Light Space坐標系的基向量,(a11, a12, a13)、(a21, a22, a23)、(a31, a32, a33)是三個坐標軸向量在原世界坐標系中坐標。
由上式可得:
由於直角坐標系基向量互為正交向量,所以有:
據此得到Light Space的View轉換矩陣計算函數為:
1 float4x4 LightViewMat(float3 lampPos, float3 lampLookAt) 2 { 3 float3 lampDirt = lampLookAt - lampPos; 4 5 float3 vUp = float3(0.0f, 1.0f, 0.0f); 6 float3 vFront = normalize(lampDirt); 7 float3 vRight = cross(vUp, vFront); 8 vRight = normalize(vRight); 9 vUp = cross(vFront, vRight); 10 vUp = normalize(vUp); 11 12 // get the matrix from I to II 13 float4x4 matTrans = 14 { 15 1, 0, 0, 0, 16 0, 1, 0, 0, 17 0, 0, 1, 0, 18 -lampPos.x, -lampPos.y, -lampPos.z, 1, 19 }; 20 21 float4x4 matView = 22 { 23 vRight.x, vUp.x, vFront.x, 0, 24 vRight.y, vUp.y, vFront.y, 0, 25 vRight.z, vUp.z, vFront.z, 0, 26 0, 0, 0, 1, 27 }; 28 29 float4x4 mView = mul(matTrans, matView); 30 31 return mView; 32 }
2) 計算Light Space的投影矩陣
在設定了視錐體近裁剪平面和遠裁剪平面的值后,根據給定的y方向的視角,就可以計算出投影平面上透視投影區域在y軸上的坐標范圍(top值和bottom值);再根據給出的寬高比(aspect),就可以方便地算出透視投影區域在x軸上的坐標范圍(right值和left值)。透視投影矩陣的目的是將視錐轉換為x∈[-1,1],y∈[-1, 1],z∈[0, 1]長方體(CVV)。經透視投影矩陣處理后的空間坐標,還要再做一個齊次化處理(將x,y,z值分別除以w)。一個處於視錐體內的點經透視變換和齊次化處理后,其坐標值必處於CVV體的范圍內;一個處於視錐體外的點經透視變換和齊次化處理后,其坐標值必處於CVV體范圍之外。這就是依靠CVV體進行裁剪的算法依據。實際上,裁剪操作在經過透視矩陣的轉換后,在做齊次化處理之前就完成了,這樣做可以大大減少運算量。
對於透視變換來說,有了投影平面上的相應點的x、y值,就可以直接畫出物體在透視投影后的形狀。投影平面上的x、y值通過等比關系就可以計算得到。透視變換后所得的點的z值,因為可以體現空間中各對象間的前后遮擋關系,所以也需要計算並保留下來。在實際計算時,由於要將處於視錐體內的各點的坐標范圍轉化到CVV體中,故而要通過 z' = a*z+b這種方式(而不是直接依靠幾何上的等比關系)構造出來。具體過程可以參看這兩篇文章。對於Shadow Map來說,透視投影所得的Z值尤為重要。
1 float4x4 LightProjcetMat() 2 { 3 // get the matrix prjection 4 float yfov = 1.57f; // 90 degree 5 float aspect = ViewPortSize.x/ViewPortSize.y; 6 float n = 6.0f; 7 float f = 100.0f; 8 float dfn = f - n; 9 10 float t = 0.362*n*tan(yfov/2); 11 float b = -t; 12 float r = t*aspect; 13 float l = -r; 14 float drl = r - l; 15 float dtb = t - b; 16 float arl = r + l; 17 float atb = t + b; 18 19 float4x4 matProj = 20 { 21 2*n/drl, 0, 0, 0, 22 0, 2*n/dtb, 0, 0, 23 arl/drl, atb/dtb, f/dfn, 1, 24 0, 0, -f*n/dfn, 0, 25 }; 26 27 28 return matProj; 29 }
第10行在計算t值時,多乘了一個0.362的縮放因子(根據實際情況調整),目的在於減少生成Shadow Map時的計算誤差。第6行將近裁剪平面設為6.0而不是常見的1.0,也可起到同樣的作用。