Shadow Mapping 的原理與實踐(一)


        早在上世紀七十年代末,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,也可起到同樣的作用。

 

 

 

 

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM