.png)
為何要寫OpenGL
在Unity的內建管線(BiRP)或是URP上寫Shader,非常方便的一點就是有非常多MACRO、內建函式以及變數可以使用,可以讓使用者忽略掉很多計算上的細節,比如說一個三維座標URP中在不同座標系統間轉換通常會有一個TrasnformNNNToMMM、方向向量則有TrasnformNNNToMMMDDir…計算螢幕座標也只需要呼叫ComputeScreenPos等內建函式,非常方便。但就是因為這些方便的函式,讓我一直沒有去徹底的理解內部的各種運算,
為了釐清許多小細節,我想回去翻OpenGL,對我來說是最踏實的作法。 目前就是照著LearnOpenGL網站來寫一個Renderer,一邊看少女漫畫一邊練習已經寫到ShadowMap + CSM的部分了,雖然還沒寫甚麼炫砲的效果而且中途還確診中斷了一周,但仍然是有不少收穫,這篇blog就是紀錄這過程中釐清一些在寫Unity Shader時不是非常清楚的部分,與過程中的心得。
Getting Started
環境
專案是c++,環境設定上雖然花了點時間但沒啥問題,主要是實在太久沒碰c++還有visual studio了,需要的library只要cmake順利build出來,剩下就是把檔案放對地方跟linker設定好就行,
(如果對c++語法不熟可以去YT看The Cherno的c++系列)
整個Getting Started大章節在座標系統之前都沒啥問題,或者應該說最大重點就是座標系統+實作攝影機,

座標系統
一個座標的生命
這章節主要就是講一個3D座標如何在渲染管線內從物件空間一陸轉換成螢幕上的像素。
Vertex Shader : 模型空間座標→乘上Model矩陣→世界空間座標→ 乘上View矩陣→觀察(攝影機)空間座標→乘上Projection矩陣→剪裁空間座標→ Vertex Shader輸出gl_position進入Viewport Transform階段。
(gl_position是頂點座標在剪裁空間下的值,對照Unity的custom shader就是平常使用的_TransformObjectToHClip(v.vertex);)_
Viewport transform : 將剪裁空間座標除以gl_position.w(perspective devide)得到NDC座標,最終將NDC再轉為螢幕空間。
Pixel Shader : 營幕空間對每個pixel上色
過去我一直都對NDC跟剪裁空間(clip space)座標的差別有點不太清楚,NDC座標其實就是gl_position在Viewport transform階段做透視除法後的結果,而正交投影(Orthographic)時沒有近大遠小的透視效果,所以gl_position.w會是1,在這種情況下NDC會等於剪裁空間座標。
攝影機
攝影機的部分比較值得提的是自訂一個LookAt矩陣,而不是使用glm內建的LookAt, 偷懶直接上圖
.png)
簡單來說只要利用三個各自垂直的向量(right, upm front)做出一個矩陣後,將該矩陣乘上當前座標的平移矩陣,我們就可以將該座標轉移到任意空間,_非常實用,死死記在腦海裡。_之後實作陰影貼圖會直接套用這個概念。
Lighting

這章節中Unity 中寫cg/hlsl的部分完全無縫轉移,直接依自己喜好弄了個光照模型,值得一提的是有實做了point light跟spot light的衰減函數,寫Unity Shader時,拿到的光源的衰減都已經幫我們算好了,沒什麼機會實作這塊。是個不錯的練習。
Model Loading
這章節示範怎麼用assimp library載入3D模型檔案,並且在載入的同時將VBO還有VAO設定好, 意外的花了超級多時間在處理,像是載入.obj與.fbx的兩種不同的格式時,.fbx就會需要去考慮到模型骨架的父子關係,obj則不用。另外也實作了直接依據模型內的材質名稱找到資料夾內的對應貼圖…等等,貼圖的種類有albedo, normal map, specular map,光想自動載入貼圖的命名規則就花了不少時間。
中間還有不小心因為無法找到對應的貼圖,呼叫glBindTexture時bind到無效的id,導致後面使用到貼圖的地方整個爛掉.…
另外OpenGL也可以自己決定mesh資料在VBO對應的GL_ARRAY_BUFFER內的排列方式,可以將讀取的mesh的每個頂點資料排列如 position/normal/uv | position/normal/uv | position/normal/uv | 也可以排列成 position/position/position | normal/normal/normal | uv/uv/uv | 我實作的是後者。
Advanced OpenGL
這一大章節就是實作了很多在Unity Shader Lab中常見的指令,像是半透明的blending、Stencil buffer、深度測試、剃除…等等,
這部分就會牽扯到很多渲染指令的順序問題,像是半透明物體的需要最後才畫,畫skybox時的深度測試,還有場景上半透明物體的排序問題等等,有在URP寫過renderer feature或者實作過自訂SRP的話這一塊都滿容易理解的。
.png)
frame buffer object
除了上述那些不同的指令與透明度混和,這章節最重要的練習是FBO,bind指定的Frame buffer object與對應的貼圖後,就可以將畫念渲染到指定FBO中,Unity中對應的類別就是RThandle或render texture了。概念上很好理解但是因為對OpenGL語法不熟悉,花了不少時間在做實驗XD。
CubeMaps - OpenGL上下顛倒 @
這章節裡面也有透過cube maps來實現environment reflection,比較特別的是一開始我總覺得哪裡怪怪的,原來是openGL的cubemaps在我的電腦上方向是顛倒的。在Unity中寫shader內部有很多macro幫使用者處理掉這塊真是幸福。
Advanced Lighting
blinn-phong
第一章介紹blinn phong,一開始的教學實作的是phong模型,但我一開始就寫blinn phong了所以這part跳過。
Gamma Correction

這部分來回看了好多次,以下是目前對gamma correction的理解 :
- 電腦螢幕輸出畫面時,會自動將畫面轉為gamma空間的顏色(上圖中的gamma曲線),會偏暗,在gamma空間下,我們肉眼所看到的顏色其實跟線性空間的數值是無法對齊的,肉眼從螢幕上看到的半黑,在線性空間下數值不是0.5。
- shader中的各種光照計算、顏色的計算,尤其是基於物理的光照,都是在線性(Linear)空間,0是全黑的顏色,0.5是數值上半黑的顏色,1則是白色
- 美術人員製作貼圖時,是透過螢幕,來做出肉眼覺得正確的顏色,而這些顏色因為是透過螢幕,所以都是gamma空間下的。直接將這些貼圖放到shader中計算都會錯誤,需先轉為sRGB,在shader中採樣出的值才會是線性的。
- 程式生成的貼圖如normal map,height map並不是透過螢幕↔肉眼這層關係製作的,所以這種貼圖本身就是線性空間的,就不需要特別轉換為sRGB。
- Unity中的import setting是否要勾sRGB就是第3點與第4點來判斷。
- 因為shader做計算時都是在線性空間下,而最終輸出螢幕又會自動apply一個gamma曲線,所以輸出之前先將shader結果做一層gamma correction曲線(上圖中紫紅色虛線),這樣最後跟螢幕的gamma曲線互相抵銷後,就可以呈現出現線性空間的值,
Shadows
實作陰影需要應用到FBO跟矩陣運算。 從光源的位置繪製一次場景的深度,這個深度圖即為陰影貼圖(Shadow Map),並且將這一個光源空間的P * V矩陣傳進shader中,在繪製物體時將 世界座標乘上光源空間的VP矩陣即可得到Shadow coordinate,利用這個Shadow coordinate採樣陰影貼圖可以得到該pixel在光源空間的深度,將光源空間深度與當前深度做比對即可判斷該pixel當下是否在陰影之中。
在座標系統的章節中,有學到只要準備一個lookat矩陣,其實就可以將任何座標轉到指定的空間下,所以只要準備一個direactional light方向的lookat矩陣,就可以得到光源空間下的V矩陣了,P矩陣的部分則是用正交投影,正交投影的大小則可以自訂,因為後面有實作CSM所以我這邊是把view frustum的每個corner算出來後來決定正交投影的尺寸。
Cascaded Shadow Mapping

因為之後打算在Unity實作CSM,這裡我想就先在OpenGL實作CSM看看好了,LearnOpenGL中的教學是利用Geometry Shader渲染不同層的Shadow map,我則是用多個FBO+GL_TEXTURE_2D_ARRAY來實作,在render loop的時候根據cascade的index來綁定對應的FBO並將深度畫到對應的2d_array的layer中。
這中間因為對OpenGL語法不熟也是採了不少坑,比如glGenFramebuffers第一個參數要看有幾層cascade,還有綁定2d_array中指定的layer到FBO需要呼叫glFramebufferTextureLayer…..等等,不過最終有被我兜出來,可喜可賀。
.png)
目前OpenGL的練習進度就到這邊而已,之後比較大的項目就剩下延遲渲染還有實作PBR了,心得整理好之後再發~。