Cascaded Shadow Maps(CSM)實時陰影的原理與實現 - GetIt01

文章推薦指數: 80 %
投票人數:10人

本文翻譯自:https://developer.download.nvidia.com/SDK/10.5/opengl/src/cascaded_shadow_maps/doc/cascaded_shadow_maps.pdf由於本人才疏學淺,翻譯難免有... 標籤: CascadedShadowMaps(CSM)實時陰影的原理與實現 03-06 本文翻譯自:https://developer.download.nvidia.com/SDK/10.5/opengl/src/cascaded_shadow_maps/doc/cascaded_shadow_maps.pdf由於本人才疏學淺,翻譯難免有誤,望各位不吝惜指正。

陰影貼圖是在遊戲引擎中廣泛使用的實現實時陰影的技術。

基礎的陰影貼圖方法對於大型場景渲染顯得力不從心,很容易出現陰影抖動和鋸齒邊緣現象。

CascadedShadowMaps(CSM)方法根據對象到觀察者的距離提供不同解析度的深度紋理來解決上述問題。

它將相機的視錐體分割成若干部分,然後為分割的每一部分生成獨立的深度貼圖。

CSM通常用來在大型場景模擬太陽投射的陰影。

在一張陰影貼圖中捕捉所有對象需要陰影貼圖具有非常高的解析度。

使用多張陰影貼圖就可以解決這個問題,對於近處的場景使用較高解析度的陰影貼圖,對於遠處的場景使用粗糙的陰影貼圖,在兩張陰影貼圖過渡的地方選擇其中一張使用。

因為遠處的對象只佔畫面的很少一部分像素,而近處的對象佔據了畫面的很大一部分,進行這樣的處理顯然非常合理。

圖1-1演示了CSM的平行分割原理,分割使用的平面和視錐體的近平面和遠平面平行。

因為太陽是方向光源,所以它的光錐體是一個長方體(圖中的紅色和藍色部分)。

CSM方法的基本思路如下: 使用每個光源的光椎體渲染場景的深度值。

從相機位置渲染場景。

根據片段的z值,選擇合適的陰影貼圖查詢片段對應的陰影貼圖中的深度數據,將其和片段在光椎體下的深度值進行比較,根據比較結果決定片段的最終顏色。

圖1-1最右邊的樹被它最近的陰影貼圖覆蓋,其餘兩棵樹被另一個陰影貼圖覆蓋。

從觀察者的角度看,左邊樹的陰影對畫面的貢獻很小。

通過分割使用不同的陰影貼圖,三棵樹的陰影的走樣程度大致相同。

與其它方法比較 還有其它一些流行的方法被人們用來減少陰影在屏幕空間的走樣現象。

下面討論的方法通常應用於整個視錐體,但我們可以對使用CSM方法劃分的視錐體的每一部分使用這些方法。

然而,這樣做對渲染效果的提升並不大,反而提高了演算法實現的難度。

本質上,CSM可以看作是離散的PerspectiveShadowMaps(PSM)。

PerspectiveShadowMaps(PSM)——圖1-1中的光椎體有很大一部分沒有任何遮擋物,並且完全位於相機的視錐體外,顯然,這一部分的陰影貼圖完全被浪費了。

PSM方法通過使用和相機視錐體一致的光椎體避免了陰影貼圖的浪費。

但PSM方法可能會引起光源位置和光源類型的變化,所以在計算機遊戲領域很少被使用。

LightSpacePerspectiveShadowMaps(LiPSM)——這一方法不會引起光源方向的改變。

LiPSM通過使用一個正交於光源方向(平行於陰影貼圖)的射線來構建光椎體。

光椎體的大小恰好包含了相機視錐體和前在的可能投射陰影的遮擋物。

相比於PSM,LiPSM不存在那麼多的特殊情況需要處理,但它會造成陰影貼圖的浪費。

TrapezoidalShadowMaps(TSM)——使用一個不規則的梯形包圍盒(代替LiPSM的光椎體)包裹相機的視錐體,除此之外,它和其它方法相同。

概要 下面的部分使用OpenGL來一步步地實現CascadedShadowMaps(CSM)。

實現的每一個細節都會被詳細說明。

為了讓片段著色器可以方便地訪問所有陰影貼圖,我們將陰影貼圖存儲在了紋理數組中。

陰影貼圖的生成 觀察圖1-1,我們發現光椎體(圖中的長方形)包裹了分割後的相機視錐體。

光椎體就像一個包圍盒。

僅僅包裹分割後的相機視錐體是不夠的,如果在光源和分割後的相機視錐體之間存在可以投射陰影的遮擋物,我們應該擴展光椎體的大小將遮擋物包裹在光椎體中。

比如,光源和分割後的相機視錐體之間有一隻小鳥,如果不擴展光椎體將小鳥渲染到陰影貼圖中,我們就不能在最後渲染出的畫面中看到小鳥投射到地面上的影子了。

圖2-1相機視錐體分割 CSM方法的第一步是計算相機視錐體的分割面在相機視覺空間下的z值。

假設單位像素的陰影貼圖的邊長為。

那麼根據圖2-1,單位像素的陰影貼圖投射在畫面上的陰影大小受被陰影覆蓋的對象的位置和它表面的法線影響。

是一個十分有用的比例,替換分子中的我們得到:式子中的是相機到近平面的距離。

為了使整個畫面的陰影看起來質量一致,不因到相機的距離而發生明顯質量變化,應該讓是一個常數。

那麼,式子中的餘弦項的係數就是一個常數。

為了透視正確,我們可以得到下式:其中。

可以看出的值受圖2-1中的約束,。

利用上面式子,我們可以推導出:式子中的是視錐體分割的個數。

更詳細的推導過程可以參考文獻1。

圖2-2光源位於觀察者上方形成的光椎體通常的值取1到4之間的整數值。

在圖2-2中,我們可以看到光椎體包含了很大一部分不可見的區域,造成了陰影貼圖的大量浪費。

為了改善這個問題,我們加入一個線性項到式子中:其中用來控制校正的程度。

計算出分割使用的z值後,我們可以很容易地使用比例關係計算出分割後的視錐體。

具體細節可以參考文獻3。

圖2-3校正光椎體假設渲染陰影貼圖使用的模型視圖矩陣為M,使用的投影矩陣P為單位矩陣。

則分割後的相機視錐體的4個角的頂點p在光源的齊次空間下的坐標為。

設這4個頂點在每個方向的最小值為,最大值為,由和我們可以得到一個和光椎體(長方體)對齊的包圍盒。

根據這個包圍盒,我們可以使用縮放和平移構造出更加精確的光椎體。

我們將進行的縮放和平移操作使用矩陣C表示,則新的投影矩陣P可以由得到,其中是近平面在處,遠平面在處的正交矩陣。

矩陣C可以由下面得出: 我們確實可以讓光椎體和分割後的相機視錐體完全一致,但這可能會改變光源的方向。

渲染陰影貼圖時可以先使用分割後的相機視錐體進行剔除操作。

重複操作,生成張陰影貼圖。

渲染場景 上面,我們生成了張陰影貼圖,現在讓我們使用它們來進行陰影的渲染。

對於要渲染的每一個片段,首先需要確定使用哪一張陰影貼圖來判斷它是否處在陰影中,這可以通過與我們之前計算的每個分割平面的值進行比較完成。

然後,我們需要計算片段對應的陰影貼圖的坐標,我們首先使用相機的模型視圖矩陣的逆矩陣將片段坐標變換到世界坐標系下,然後將變換後的坐標乘以片段對應的光源矩陣。

這兩步可以通過乘以矩陣一次完成。

接著,我們將坐標從線性縮放到的範圍。

現在,我們就可以使用這個坐標訪問片段對應深度貼圖。

如果坐標的z值比深度貼圖對應的z值小,說明片段被光源照亮,否則,說明片段處於陰影中。

圖2-4一個三角形在多張深度貼圖上投射出陰影 代碼概要 OpenGLSDK的示例中包含了下面這些源代碼: terrain.cpp-包含了載入和渲染場景的函數。

裡面和陰影貼圖有關的只有Draw函數。

Load函數和GetDim函數被用載入和設置場景的包圍盒。

utility.cpp-包含了許多讓代碼更具可讀性的輔助函數。

包括著色器讀取函數,相機處理函數以及菜單,鍵盤,滑鼠處理的函數。

shadowmapping.cpp-包含了和CSM演算法有關的代碼,包括生成陰影貼圖和場景渲染的代碼。

terrain.cpp和utilit.cpp提供了運行示例的基礎框架,其中的display函數就是實際執行渲染的函數,它調用了makeShadowMap函數和renderScene函數進行陰影貼圖的生成和場景的渲染。

voidmakeShadowMap() { /*...*/ //setthelight』sdirection gluLookAt(0,0,0, light_dir[0],light_dir[1],light_dir[2], 1.0f,0.0f,0.0f); /*...*/ //computethez-distancesforeachsplitasseenincameraspace updateSplitDist(f,1.0f,FAR_DIST); //forallshadowmaps: for(inti=0;iDraw(minZ); /*...*/ } /*...*/ } renderScene函數設置了著色器使用的Uniform變數,然後進行了場景的渲染。

CSM相關的代碼主要位於片段著色器中。

uniformsampler2Dtex;//terraintexture uniformvec4far_d;//fardistancesof //everysplit varyingvec4vPos;//fragment』spositionin //viewspace uniformsampler2DArrayShadowstex;//depthtextures floatshadowCoef() { intindex=3; //findtheappropriatedepthmaptolookupin //basedonthedepthofthisfragment if(gl_FragCoord.z



請為這篇文章評分?