從零開始手敲次世代遊戲引擎(六十三)
來自專欄高品質遊戲開發29 人贊了文章
本篇我們來實現點光源的動態陰影貼圖。
點光源是360度無死角發射光能的光源,其自身體積可以看作零。我們也可以將點光源看作是視錐角為360度的射燈。
因此,與平行光源不同,我們應該使用和射燈一樣的透視矩陣,而不是正交矩陣來生成點光源的陰影貼圖。
然而,在我們的射燈的實現裡面,我們將陰影貼圖的投影面放在垂直於射燈中心光線方向的地方。點光源沒有指向性,該如何安排投影平面呢?
答案就和如何繪製一幅世界地圖一樣,我們需要某種方法將空間的位置投影到一個平面(陰影貼圖)上。
顯然方法不止一種。可以是球形投影,圓柱投影,圓錐投影,正方體投影等等。
遊戲當中常用的是正方體投影。因為這種方式相對來說在各個方向上的密度比較均衡且在存儲在內存上的時候比較容易定址。在當代的GPU硬體當中也有相關的支持。
我們知道一個正方體有6個面,所以我們可以用6幅正方形的圖像拼成一個完整的正方體投影空間。將點光源放置在這個正方體的中心,場景物體在這個點光源的照射下,投影到6個表面的陰影,就是該場景中該點光源所對應的陰影貼圖。(畫面右上角顯示的就是展開後的陰影貼圖)
點光源實時陰影 https://www.zhihu.com/video/1013446362314211328這個由6個正方形組成的貼圖被稱為cube map,它記錄了在3維空間當中,以點光源為原點的各個方向上光線所能到達的最大深度。從而,對於場景當中的任意一點,我們只需要將其到點光源的距離與陰影貼圖上記錄的該方向上的深度值進行比較,就可以得知其是否處於陰影當中。
點光源實時陰影(移動) https://www.zhihu.com/video/1013447891234402304原理大致就是如此。更為詳細的解釋以及基於opengl的實現方法可以參考參考引用1。我在實現過程當中遇到的問題主要有:
- 如何將cube map展平顯示在右上角的問題。立方體的6個面如果不切分開是無法展開為2x3這種排列的。不是T字形就是十字形。這比較浪費屏幕面積,所以我最後採用的是切開成為6個正方形(36個頂點)渲染;
- 為了生成陰影貼圖當中的6幅圖像,我們實際上是將點光源等效分解為6個90度頂角的聚光燈,每個燈的方向垂直於其中一個表面;很顯然,點光源的陰影貼圖計算是很費的,實際遊戲當中要盡量避免動態的點光源陰影貼圖;
- 另外一方面,在高版本的圖形API當中,可以使用幾何著色器將場景物體在一個繪製pass當中就分別變化到6個不同的坐標系當中,並最終繪製到6個不同的渲染對象(Rendering Target)當中去。具體的做法在參考引用1當中有介紹,我的代碼也是使用的這種方式。然而,這種方式在任何情況下是否一定快,其實是不一定的。因為幾何著色器是在GPU當中生成額外的頂點,而一般來說GPU並不會將這些頂點回寫到內存,而是直接壓入後端的管線。本來,在沒有幾何著色器的情況下,3個頂點形成一個三角面,大多數GPU也是按照這個比例配置的渲染管線。但是有了幾何著色器之後,3個頂點可能可以生成幾十個甚至上百上千個頂點,內部存儲這些頂點的開銷,以及後半段(三角形)與前半段(頂點)的比例失調,可能會造成GPU資源分配上的瓶頸。當然,這些都屬於性能優化方面的內容,我們現在可以不管。我慢慢寫,五年後再討論也不遲(?> <?)
- cube map當中面的順序。這個似乎是有規矩的。右手系當中,按照+X(右)-X(左)+Y(後)-Y(前)+Z(上)-Z(下)的順序。這個順序的意義在於,在採樣cube map的時候,圖形API一般並不需要我們提供面的編號,而是只需要提供一個3維向量,也就是方向。在圖形API當中會根據這個方向換算出所在的面和貼圖坐標(UV)。這裡就有個默認的順序了;
- 陰影貼圖當中的取值為0到1,0為光源所在位置,1則為投影平面的位置,也就是遠裁剪面的位置。注意在之前的射燈和平行光源條件下,我們都是將物體的坐標變換到攝像機坐標系當中,然後比較其Z值和陰影貼圖裡記錄的最大Z值來判斷是否處於陰影內的。然而點光源不存在一個特定的方向,所以我們無法生成一個投影矩陣來代表它。但是,正是因為它沒有指向性,其實我們只需要考慮場景物體到光源的距離,以及方向,而不必將其變換到光源坐標系。
參考引用
https://learnopengl.com/Advanced-Lighting/Shadows/Point-Shadows
推薦閱讀:
※OpenGL使用glm類庫載入OBJ
※OpenGL ES 的精度問題
※用OpenGL寫個3D小遊戲
※在Windows 10系統搭建OpenGL調試環境
※ADS光照模型實例(GLSL實現)