🌐 Translate this post:
Gamma Correction與Gamma Encoding
這篇blog是最近看Digital Images: from File to Screen後稍微整理後的簡短心得, 使用到的範例圖片也是直接從文中轉載的,主要是為了釐清gamma correction到底在做什麼。gamma correction跟gamma encoding之間的關係是什麼的一些筆記。
從Unity Shader到OpenGL的隨手筆記(3)- HDR與Bloom
上一次實作了法線與視差貼圖,也玩了一遍TBN矩陣,這次接著實作HDR與Bloom Advanced Lighting - HDR float point frame buffer 一般的螢幕,每個像素的顏色範圍只能介於0到1之間,這個範圍又稱為LDR(low dynamic range)。 在做光照時的各種計算時,實際上並不期待輸出一定會介於0到1之間,我們需要更大的範圍的值才能表達出不同類型的光照強度(尤其是到了PBR時代)。這時候就會需要讓畫面能在HDR(high dynamic range)的範圍內做計算, 首先要先讓Pixel Shader輸出的值能超過0~1,所以要從frame buffer下手,預設的GL_RGBA,每個channel有8bit,範圍為0-1,將internal format改成GL_RGBA16F或32F就可以將這個frame buffer視為float point framebuffer,這樣用pixel shader渲染到該frame buffer時,就可以儲存HDR範圍的值了, **glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, windowWidth, windowHeight, 0, GL_RGBA, GL_FLOAT, NULL);** 改完之後立刻來實驗看看 : 設定8個光源,各自輸出一個隨機顏色,每個顏色的channel值介於0~3之間 除了前排(左2)之外,幾乎都是白色的,這是因為雖然frame buffer已經改為可以HDR範圍,但是螢幕本身還是LDR的, 單單只是改Frame buffer並不夠,改成float point frame buffer讓pixel shader能在輸出HDR範圍的顏色後,要在顯示到畫面前透過Tone Mapping的方式將整個HDR的畫面重新映射到LDR的範圍內,再顯示到螢幕上。 Tone Mapping Tone Mapping的工作就是把HDR的數值重新映射回0到1的範圍內,映射的公式有很多種。目前這個小專案就是照抄LearnOpenGL的Code, vec3 hdrColor = texture(hdrBuffer, uv).rgb; // exposure tone mapping vec3 mappedColor = vec3(1.0) - exp(-hdrColor * _Exposure); //gamma correction //... 最後拿上面八個光源的場景再測試一次,可以看出現在能夠看出不同光源間各自不同的顏色了。
從Unity Shader到OpenGL的隨手筆記(2) 切線空間
接續上一篇跟著LearnOpenGL學著OpenGL的記錄,這次從Normal Mapping開始實作 Advanced Lighting - Normal Mapping 法線貼圖 這章節介紹現在大家都很熟悉的法線貼圖的原理與實作, 首先法線貼圖一般會偏藍色,因為這種貼圖預設表面的法線方向是朝向Z(B通道)軸,所以RGB中的B通道的值會是最大的。而凹凸的表面會讓法線朝xy方向做不同程度的偏移,於是就會看到如上圖般的貼圖。 而直接將法線貼圖採樣出的值當成world space的法線會造成計算上的錯誤,因為並不是所有表面都朝向world space的Z的(譬如上圖來說,表面其實是朝向Y方向),因此,這個法線貼圖的Z其實並不是world space的Z,而是tagent space(切線空間)的Z。tagent space的Z是模型上每個三角面的法線方向 求TBN矩陣 要將法線貼圖的tagent space正確轉換到world space在計算,就會需要矩陣,這時候要搬出之前實作攝影機的LookAt矩陣時提到的 只要利用三個各自垂直的向量(right, upm front)做出一個矩陣後,將該矩陣乘上當前座標的平移矩陣,我們就可以將該座標轉移到任意空間 要實作的矩陣 - 就是可以將tangent space的座標轉換到world space的LookAt矩陣,因為建立矩陣時需要的三個各自垂直的向量分別是Tangent、Bitangent、Normal,一般就稱為TBN矩陣,原文還有實作手動計算這三個向量的方式,但我太懶了,直接在assimp導入模型的時候把計算模型上每個頂點的Tangent跟Normal的flag打開,然後把normals跟tangents都塞到GL_ARRAY_BUFFER中直接在shader內使用。 Unity的話預設的模型import setting都是讓使用者有tangent跟normal能用的,如果是custom shader的話只要在attribute的定義中有寫出來就能取到,shader graph的話只要有用到相關node就會自己產了。 //Unity Code struct Attributes { float4 positionOS : POSITION; float3 normalOS : NORMAL; float4 tangentOS : TANGENT; float2 texcoord : TEXCOORD0; ......... 因為assimp在import模型時已經得出tangent跟normal,直接用T跟N向量求外積就可以得到Bitangent了 //GLSL vec3 T = normalize(vec3(_model * vec4(aTangent, 0.0))); vec3 N = normalize(vec3(_model * vec4(aNormal, 0.0))); vec3 B = cross(N, T); mat3 TBN = mat3(T, B, N); 在vertex shader內求得TBN矩陣後,在pixel shader內將normal map採樣後得到的值轉換到world space,接下來的lighting model的部分就不需要額外處理了。
從Unity Shader到OpenGL的隨手筆記(1)
![Untitled.png](/assets/img/blog/Untitled (5).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, 偷懶直接上圖 ![Untitled.png](/assets/img/blog/Untitled (1).png) 簡單來說只要利用三個各自垂直的向量(right, upm front)做出一個矩陣後,將該矩陣乘上當前座標的平移矩陣,我們就可以將該座標轉移到任意空間,_非常實用,死死記在腦海裡。_之後實作陰影貼圖會直接套用這個概念。 Lighting 這章節中Unity 中寫cg/hlsl的部分完全無縫轉移,直接依自己喜好弄了個光照模型,值得一提的是有實做了point light跟spot light的衰減函數,寫Unity Shader時,拿到的光源的衰減都已經幫我們算好了,沒什麼機會實作這塊。是個不錯的練習。 Model Loading link_preview 這章節示範怎麼用assimp library載入3D模型檔案,並且在載入的同時將VBO還有VAO設定好, 意外的花了超級多時間在處理,像是載入.obj與.fbx的兩種不同的格式時,.fbx就會需要去考慮到模型骨架的父子關係,obj則不用。另外也實作了直接依據模型內的材質名稱找到資料夾內的對應貼圖…等等,貼圖的種類有albedo, normal map, specular map,光想自動載入貼圖的命名規則就花了不少時間。 中間還有不小心因為無法找到對應的貼圖,呼叫glBindTexture時bind到無效的id,導致後面使用到貼圖的地方整個爛掉.…
開始使用Unity的C# Job System(一)
在上一篇blog,我做了多執行緒程式設計的優缺點以及Job System架構的簡單介紹,這一篇會介紹一下Unity提供的各種不同的Job Interface,以及基本的使用方式。希望看完這篇的人,不只可以寫出多執行緒的程式,也可以寫出一支能夠平行運算的程式。 目錄 專有名詞 第一個Job - 實作IJob 實作IJobFor IJobParallelFor 結果比較 再進一步優化效能-BurstCompiler 用Job system還有平行化運算,就是DOTS嗎? DOTS — data oriented technology stack,是Unity推出的數據導向程式組合包。 包含 : ECS架構、C# Job System、Burst Compiler。 這三者是可以獨立存在的,所以即使專案架構不使用ECS,你仍然可以在任何現有專案使用C# Job System還有burst compiler。 在Unity官方文件中,我們可以看到3種不同的Job Interface— IJob、IJobFor、 IJobParallelFor,這篇blog會在實作一個簡單demo的同時,一一介紹每一個Interface的用法。 雖然這3種都會是非常常用到的Job,但如果稍微翻一下Job Package就會發現上能使用的介面還有好幾種,應該是因為Job還沒正式進入1.0版,所以沒有將所有API納入官方文件中,但裡面有一些介面的功能非常實用,考慮到文長,預計在下一篇介紹。 專有名詞 在正式動手寫code前,快速回顧一下Job system中的一些專有名詞 Job 一個Job就是一個任務的單位,並在woreker thread上執行,就像是一個可以在不同thread上執行的function一樣,不過Job是一個struct。 Job System job system 內有個manager,根據當下硬體建立出對應的worker thread,然後把準備執行的Job放到自己管理的queue內,並且分配到閒置/適合的worker thread去排隊&執行。Job system同時也管理不同Job之間的相依、先後順序。 Safety System *上一篇blog中有提到多執行緒程式的各種缺點,為了避免遇到其中的race condition,再將資料傳遞給Job時,一律都是傳值(value type)*,來避免不同thread中的程式碼透過reference去修改到同一份資料,這種做法消滅了race condtion的可能性。 Native Container Safety system 這種只傳值不傳reference的方式,會讓每個job內都只有data的copy,而不是原本的data。為了突破這個限制,Unity特別定義了一個在不同的Thread間讀取/儲存的共享記憶體 - Native container。