一: 陰影映射
- 陰影是光線被阻擋的結果;當一個光源的光線由於其他物體的阻擋不能夠達到一個物體的表面的時候,那么這個物體就在陰影中了。陰影能夠使場景看起來真實得多,並且可以讓觀察者獲得物體之間的空間位置關系。
- 陰影還是比較不好實現的,因為當前實時渲染領域還沒找到一種完美的陰影算法。
- 陰影貼圖(shadow mapping),效果不錯,而且相對容易實現。陰影貼圖並不難以理解,性能也不會太低,而且非常容易擴展成更高級的算法(比如 Omnidirectional Shadow Maps和 Cascaded Shadow Maps)。
- 陰影映射(Shadow Mapping)背后的思路非常簡單:我們以光的位置為視角進行渲染,我們能看到的東西都將被點亮,看不見的一定是在陰影之中了。假設有一個地板,在光源和它之間有一個大盒子。由於光源處向光線方向看去,可以看到這個盒子,但看不到地板的一部分,這部分就應該在陰影中了。
- 在深度測試教程中,在深度緩沖里的一個值是攝像機視角下,對應於一個片元的一個0到1之間的深度值。
- 我們從光源的透視圖來渲染場景,並把深度值的結果儲存到紋理中,就能對光源的透視圖所見的最近的深度值進行采樣。最終,深度值就會顯示從光源的透視圖下見到的第一個片元了。我們管儲存在紋理中的所有這些深度值,叫做深度貼圖(depth map)或陰影貼圖。
- 通過儲存到深度貼圖中的深度值,我們就能找到最近點,用以決定片元是否在陰影中。我們使用一個來自光源的視圖和投影矩陣來渲染場景就能創建一個深度貼圖。這個投影和視圖矩陣結合在一起成為一個T變換,它可以將任何三維位置轉變到光源的可見坐標空間。
- 深度映射由兩個步驟組成:首先,我們渲染深度貼圖,然后我們像往常一樣渲染場景,使用生成的深度貼圖來計算片元是否在陰影之中。
二: 深度貼圖
- 深度貼圖是從光的透視圖里渲染的深度紋理,用它計算陰影。因為我們需要將場景的渲染結果儲存到一個紋理中,我們將再次需要幀緩沖。
- 首先,我們要為渲染的深度貼圖創建一個幀緩沖對象。然后,創建一個2D紋理,提供給幀緩沖的深度緩沖使用。
- 生成深度貼圖不太復雜。因為我們只關心深度值,我們要把紋理格式指定為GL_DEPTH_COMPONENT。我們還要把紋理的高寬設置為1024:這是深度貼圖的解析度。
- 把生成的深度紋理作為幀緩沖的深度緩沖。我們需要的只是在從光的透視圖下渲染場景的時候深度信息,所以顏色緩沖沒有用。然而幀緩沖對象不是完全不包含顏色緩沖的,所以我們需要顯式告訴OpenGL我們不適用任何顏色數據進行渲染。我們通過將調用glDrawBuffer和glReadBuffer把讀和繪制緩沖設置為GL_NONE來做這件事。
// 1. 首選渲染深度貼圖
glViewport(0, 0, SHADOW_WIDTH, SHADOW_HEIGHT);
glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO);
glClear(GL_DEPTH_BUFFER_BIT);
ConfigureShaderAndMatrices();
RenderScene();
glBindFramebuffer(GL_FRAMEBUFFER, 0);
// 2. 像往常一樣渲染場景,但這次使用深度貼圖
glViewport(0, 0, SCR_WIDTH, SCR_HEIGHT);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
ConfigureShaderAndMatrices();
glBindTexture(GL_TEXTURE_2D, depthMap);
RenderScene();```
5. 這里一定要記得調用glViewport。因為陰影貼圖經常和我們原來渲染的場景(通常是窗口解析度)有着不同的解析度,我們需要改變視口(viewport)的參數以適應陰影貼圖的尺寸。如果我們忘了更新視口參數,最后的深度貼圖要么太小要么就不完整。
6. ConfigureShaderAndMatrices函數是用來在第二個步驟確保為每個物體設置了合適的投影和視圖矩陣,以及相關的模型矩陣的。
7. 因為我們使用的是一個所有光線都平行的定向光。出於這個原因,我們將為光源使用正交投影矩陣,透視圖將沒有任何變形:
8. 為了創建一個視圖矩陣來變換每個物體,把它們變換到從光源視角可見的空間中,我們將使用glm::lookAt函數;這次***從光源的位置看向場景中央。***
三: 渲染至深度貼圖
1. 當我們以光的透視圖進行場景渲染的時候,我們會用一個比較簡單的着色器,這個着色器除了把頂點變換到光空間以外,不會做得更多了。這個簡單的着色器叫做simpleDepthShader。
2. 這個頂點着色器將一個單獨模型的一個頂點,使用lightSpaceMatrix變換到光空間中。
3. 由於我們沒有顏色緩沖,最后的片元不需要任何處理,所以我們可以簡單地使用一個空像素着色器:
4. RenderScene函數的參數是一個着色器程序(shader program),它調用所有相關的繪制函數,並在需要的地方設置相應的模型矩陣。
5. 最后,在光的透視圖視角下,很完美地用每個可見片元的最近深度填充了深度緩沖。通過將這個紋理投射到一個2D四邊形上(和我們在幀緩沖一節做的后處理過程類似),就能在屏幕上顯示出來
四: 渲染陰影
1. 正確地生成深度貼圖以后我們就可以開始生成陰影了。這段代碼在像素着色器中執行,用來檢驗一個片元是否在陰影之中,不過我們在頂點着色器中進行光空間的變換。
2. 頂點着色器傳遞一個普通的經變換的世界空間頂點位置vs_out.FragPos和一個光空間的vs_out.FragPosLightSpace給像素着色器。
3. 像素着色器使用Blinn-Phong光照模型渲染場景。我們接着計算出一個shadow值,當fragment在陰影中時是1.0,在陰影外是0.0。然后,diffuse和specular顏色會乘以這個**陰影元素**。由於陰影不會是全黑的(由於散射),我們**把ambient分量從乘法中剔除**。
4. 像素着色器的最后,我們我們把diffuse和specular乘以(1-陰影元素),這表示這個片元有多大成分不在陰影中。這個像素着色器還需要兩個額外輸入,一個是光空間的片元位置和第一個渲染階段得到的深度貼圖。
5. 首先要檢查一個片元是否在陰影中,把光空間片元位置轉換為裁切空間的標准化設備坐標。當我們在頂點着色器輸出一個裁切空間頂點位置到gl_Position時,OpenGL自動進行一個透視除法,將裁切空間坐標的范圍-w到w轉為-1到1,這要將x、y、z元素除以向量的w元素來實現。由於裁切空間的FragPosLightSpace並不會通過gl_Position傳到像素着色器里,我們必須自己做透視除法
6. 因為來自深度貼圖的深度在0到1的范圍,我們也打算使用projCoords從深度貼圖中去采樣,所以我們將NDC坐標變換為0到1的范圍。從第一個渲染階段的projCoords坐標直接對應於變換過的NDC坐標。我們將得到光的位置視野下最近的深度。
7. 為了得到片元的當前深度,我們簡單獲取投影向量的z坐標,它等於來自光的透視視角的片元的深度。
```float ShadowCalculation(vec4 fragPosLightSpace)
{
// 執行透視除法
vec3 projCoords = fragPosLightSpace.xyz / fragPosLightSpace.w;
// 變換到[0,1]的范圍
projCoords = projCoords * 0.5 + 0.5;
// 取得最近點的深度(使用[0,1]范圍下的fragPosLight當坐標)
float closestDepth = texture(shadowMap, projCoords.xy).r;
// 取得當前片元在光源視角下的深度
float currentDepth = projCoords.z;
// 檢查當前片元是否在陰影中
float shadow = currentDepth > closestDepth ? 1.0 : 0.0;
return shadow;
}```
四:陰影貼圖的改進
1. 陰影失真
1.1 放大看會發現明顯的線條樣式:我們可以看到地板四邊形渲染出很大一塊交替黑線。這種陰影貼圖的不真實感叫做陰影失真(Shadow Acne)
1.2 多個片元就會從同一個斜坡的深度紋理像素中采樣,有些在地板上面,有些在地板下面;這樣我們所得到的陰影就有了差異。因為這個,有些片元被認為是在陰影之中,有些不在,由此產生了圖片中的條紋樣式。
1.3 我們可以用一個叫做陰影偏移(shadow bias)的技巧來解決這個問題,我們簡單的對表面的深度(或深度貼圖)應用一個偏移量,這樣片元就不會被錯誤地認為在表面之下了
1.4 使用了偏移量后,所有采樣點都獲得了比表面深度更小的深度值,這樣整個表面就正確地被照亮,沒有任何陰影。我們可以這樣實現這個偏移:
2. 懸浮
2.1 使用陰影偏移的一個缺點是你對物體的實際深度應用了平移。偏移有可能足夠大,以至於可以看出陰影相對實際物體位置的偏移
2.2 這個陰影失真叫做懸浮(Peter Panning),因為物體看起來輕輕懸浮在表面之上。我們可以使用一個叫技巧解決大部分的Peter panning問題:當渲染深度貼圖時候使用正面剔除(front face culling)你也許記得在面剔除教程中OpenGL默認是背面剔除。我們要告訴OpenGL我們要剔除正面。
2.3 因為我們只需要深度貼圖的深度值,對於實體物體無論我們用它們的正面還是背面都沒問題。使用背面深度不會有錯誤,因為陰影在物體內部有錯誤我們也看不見。
2.4 另一個要考慮到的地方是接近陰影的物體仍然會出現不正確的效果。必須考慮到何時使用正面剔除對物體才有意義。不過使用普通的偏移值通常就能避免peter panning。
3. 采樣過多
3.1 還有一個視覺差異,就是光的視錐不可見的區域一律被認為是處於陰影中,不管它真的處於陰影之中。出現這個狀況是因為超出光的視錐的投影坐標比1.0大,這樣采樣的深度紋理就會超出他默認的0到1的范圍。根據紋理環繞方式,我們將會得到不正確的深度結果,它不是基於真實的來自光源的深度值。
3.2 在圖中看到,光照有一個區域,超出該區域就成為了陰影;這個區域實際上代表着深度貼圖的大小,這個貼圖投影到了地板上。發生這種情況的原因是我們之前將深度貼圖的環繞方式設置成了GL_REPEAT。
3.3 我們可以儲存一個邊框顏色,然后把深度貼圖的紋理環繞選項設置為GL_CLAMP_TO_BORDER:
五:PCF
1. PCF(percentage-closer filtering),這是一種多個不同過濾方式的組合,它產生柔和陰影,使它們出現更少的鋸齒塊和硬邊。
2. 核心思想是從深度貼圖中多次采樣,每一次采樣的紋理坐標都稍有不同。每個獨立的樣本可能在也可能不再陰影中。所有的次生結果接着結合在一起,進行平均化,我們就得到了柔和陰影。
3. 一個簡單的PCF的實現是簡單的從紋理像素四周對深度貼圖采樣,然后把結果平均起來: