NormalMap 贴图 [转]
說起Normal Map(法線貼圖),就會想起B(yǎng)ump Map(凹凸貼圖)。Bump Mapping是Blin大師在1978年提出的圖形學算法,目的是以低代價給予計算機幾何體以更豐富的表面信息(高模蓋低模)。30年來,這項技術(shù)不斷延展,尤其是計算機圖形學成熟以后,相繼出現(xiàn)了不少算法變體,90年代末的Normal Map解放了必須自行計算紋理像素法線的痛苦,新世紀以來相繼又出現(xiàn)了Parallax Mapping, Relief Mapping等技術(shù)。拋開那些無聊的概念區(qū)分,它們的本體還是Bump Map,目的也是一致的。
1. 傳統(tǒng)的Bump Map
如果你對純凈的Bump Map有興趣,A Practical and Robust Bump-mapping Technique for Today's GPU應該是值得一看的論文。說Today,其實是GDC 2000的事情了,但對于傳統(tǒng)的Bump Map的理論是很豐富的,我是沒精力看完它啦……
那時候的Bump Map須要我們計算紋理圖上每個像素的法線信息,簡單的還可能做到,對復雜的紋理要搞清面光背光份量簡直要命,于是就用Height Map,在一張高度圖上記錄每個像素對應的紋理位置的高度信息(這個比較容易辦到,NEHE22也是這類)。看上去就是一張地形網(wǎng)格——這樣的話,計算每 個像素點的法線就不那么難了。XY方向相鄰像素的高度相減就是兩條正交的切向量,叉乘外加左/右手定則就獲得法線。或者更精確點,用八鄰域弄個邊緣檢測算 子(sobel、拉普拉斯之類 )[圖像處理里的空間域濾波],或者應用斜坡法([水效果Ⅲ - 抖動波] )來求切線、法線。
?2. 制作NormalMap
但是這樣還是挺麻煩的,既然都動用額外的貼圖了,何不把這些與實現(xiàn)無關(guān)的預處理——作為結(jié)果的法線信息——都放進紋理里呢?這就是Normal Map的思想起源。但是,誰來做這樣的一張法線圖呢?敲定美工了。每個像素的RGB分別存儲該像素對應法線的XYZ分量,只要把法線的分量由(-1,1) 映射成(0,255)就可了。觀察一張法線圖,以藍色為主,是因為朝向圖面外的法線(0,0,1)都被編碼成(0,0,127)了(讀入OpenGL后即 (0,0,0.5)),而圖上越紅的地方表明法線越向右,越綠的地方表明法線越向上,就可以理解了。總體來說,就是一張紫藍色的圖。怎么做這樣的圖呢?當 然最好是有一個工具,輸入原圖和高度圖后執(zhí)行上述的算法得出新圖了,事實上已經(jīng)有很多這類工具了(譬如比較著名的photoshop的NV插件 Normal Map Filter,甚至不用高度channel也可[效果- -]),以下幾篇文章有詳細介紹,有興趣的可以看一看:
Tutorial On Normal Mapping?(PHOTOSHOP [ENGLISH])
怎樣用PhotoShop創(chuàng)建Bump Map圖像?(PHOTOSHOP [CHINESE])
Nvidia Normal Map 插件參數(shù)之詳解?(PHOTOSHOP [翻譯])
GIMP normalmap plugin?? (GIMP?? [ENG])
關(guān)于NormalMap制作的原理,更詳細的可參考此文:Normalmap原理及去除接縫
?3. 切線空間(Tangent Space)
其實這個概念前文已經(jīng)提及了。每個像素根據(jù)高度圖生成的三軸坐標系,就是被稱為切線空間坐標系的東西,每個像素人手一個。可見Normal Map里面每個像素的法線就是定義在這個切線空間的。注意,這些法線是屬于像素的,而不是頂點,我們平時用的法線是頂點法線,是定義在模型坐標系的[亂彈OpenGL中的矩陣變換(上)] ,定義于所屬物件的唯一的局部坐標系原點之上。而這些像素法線定義于切線坐標系,其原點就在該像素上,切線副法線在法線的垂直平面上。
(表面依然是平的,但通過攪動法線,使進入我們眼睛的光線強度不一,模擬出凹凸面漫反射的特點。圖from GDNet)
應用這些像素法線的目的無非是計算出該像素的OutPut顏色:col = baseColor * (amb + diffuse) + specular。這些都應該在像素著色器(fragment shader)里進行,因為我們要做的是針對每個像素的處理[Shader快速復習:Per Pixel Lighting(逐像素光照)] 。其中需要用到像素法線的是diffuse和specular(以前是用通過頂點法線線性插值而來的normal),法線分別與光線向量、半向量作點乘得 到對應因子。這個因子是個夾角cos而已,所以只要滿足像素法線與兩個向量單位化并在同一坐標系下(而無論是哪個坐標系),夾角就是一定的。這樣看來,兩 個選擇:
1. 把像素法線都從各自的切線空間轉(zhuǎn)到視圖空間來,再點乘;
2.把光線向量、半向量從視圖空間轉(zhuǎn)到像素各自的切空間來,再點乘。
很多文章一口咬定就是第2種好,原因是第1種要變換N個量;第2種只變換2個量。仔細分析,其實兩種選擇變換的次數(shù)是一樣的,都是2*N。說第2種好,是因為:
第1種必須在fragment shader里進行,對象是從Normal Map讀出的像素法線和經(jīng)過線性插值而來的兩個向量,它們不是同一坐標系的,按描述應該是各像素法線乘以各自一個的變換矩陣,轉(zhuǎn)到視圖空間來,但確實沒有其他的可提供構(gòu)筑這個矩陣的信息了,若有可能應該就是另外的varying變量傳入了;
第2種可以選擇在vertex shader里進行,但是能不能就在這里變換到切線空間呢?假設(shè)可以,那么得到的針對頂點的數(shù)值在光柵化-線性插值后能否滿足呢?
要回答這個問題,還得考慮像素的切線空間和頂點的切線空間之間的關(guān)系。是的,頂點法線也可以變換到切線空間,但這有什么用呢?一步一步來吧。先考慮切線空間在OpenGL世界里的次元位置:
(from paulsprojects)
為什么是緊挨模型坐標系呢?其實想想也能理解,在上面談及切線坐標系的時候,并沒有廣闊的“世界”這個概念。只針對每個像素/頂點,無疑是比模型坐標系更 狹隘的“世界觀”,所以那個位置是適合的(箭頭方向無所謂,坐標系之間是可以相互轉(zhuǎn)換的)。其實對于某個具體的物體上的像素/頂點,你可以考慮那是把模型 空間的原點平移到該像素/頂點上,各模型坐標系方向軸向量一起經(jīng)過旋轉(zhuǎn),使Z軸與像素/頂點的法線重合,XY軸分別與像素/頂點的切線副法線重合——這只 是一個仿射變換而已,如同模型/世界/視圖空間之間的變換一樣。
如果你記得圖形學書上關(guān)于世界/視圖空間的變換矩陣的構(gòu)建的話,就更容易理解這樣的形式了。從切線空間到模型空間的變換矩陣(TBN矩陣MTBN)為:
?其中T,B,N是定義在模型空間的該像素/頂點的“切/副法/法向量”。稍微檢驗一下,考慮某個三角面上的某個頂點,其法線充當切線空間的Z軸,在切線空間中表示為(0,0,1),在OpenGL里解釋為一個列向量(0,0,1)T,用上面的矩陣MTBN左乘該向量,得到(Nx,Ny,Nz)T,正是該向量在模型空間的表示。其他兩軸同理。說明該矩陣把切線空間的坐標系統(tǒng)轉(zhuǎn)換到模型空間了(一切變換都是在變換坐標系[亂彈OpenGL中的矩陣變換(上)] )。當然這是特例說明,但確實這個矩陣包含仿射矩陣里的旋轉(zhuǎn)元素了(它只包含旋轉(zhuǎn),不設(shè)置平移,是因為我們只需要它來變換向量,向量是可以任意平移的,若要弄完整的4X4矩陣,第4列平移列就是該頂點模型坐標)。具體推導也不難,隨便Google一下"tangent space"就出來一堆了,而且都是基本一樣的推導過程,推一個:Tangent Space。
其逆變換(矩陣MTBN-1)就可以把向量從模型空間變換到對應頂點的切線空間了。如果你確保T,B,N兩兩垂直,這個正交矩陣的逆矩陣就是其轉(zhuǎn)置矩陣,這很理想。但萬一你不確保這點(涉及到具體應用,很多問題的,后面會說),就保證它們大致滿足三叉狀,用所謂的Gram-Schmidt 算法矯正:
T′?=?T?? (N?·?T)N
B′?=?B?? (N?·?B)N?? (T′?·?B)T′
反正最后得到的是這樣的形式——用它左乘光源向量和半向量,就得到對應于該頂點切線空間的光源向量和半向量了:
?
| T′x B′x Nx | T′y B′y Ny | T′z B′z Nz | ? |
?
為什么是頂點?因為這是你唯一能取得其切線/副法線/法線的東西了。這也是之前說的選擇1不行的原因,在那張Normal Map里面已經(jīng)沒有任何法線副法線的確實信息了(只知道它們在法線垂直平面上),即使能通過別的方法取得(起碼要增加傳入數(shù)據(jù)),那要在fragment shader里每像素人手又計算一個矩陣,這就又是一個“計算量”(不是次數(shù))的問題。所以還是用選擇2吧,也就是上面矩陣MTBN-1的討論。
選擇2的第一個問題現(xiàn)在很清楚了:是可以的。只要取得頂點的切線/副法線/法線數(shù)據(jù)就能建立矩陣并變換光源向量和半向量,但結(jié)果是針對頂點的,我們需要的 是針對像素的。光柵化線性插值這兩個向量,就是對應像素的值,但這對嗎?直覺上不對,但結(jié)果顯示這樣做沒有不妥(或者說不會與真實所須差太多)。一般文章 都沒有直接透視這個問題,其實考慮一個矩形平面就露餡了,它四個頂點的TBN一致,變換得的光源向量也該一致,插值后得光源向量也該一致,但 NormalMap中的像素有各自不同的切線空間系統(tǒng),光源向量不該一致的呃(雖則同向光源、不同法線足夠形成凹凸效果)。所以我對選擇2的第二個問題保 持疑問,有道深者請為鄙人指點迷津!
反正即使計算兩向量夾角的計算可能會有偏差,也不會太離譜,問題到此結(jié)束。至于有的文章提及對diffuse的計算,光源向量插值后不須再歸一化的問題 (我嘗試過,整體會變暗一點),就不深入了。注意我們在vertex shader里變換到切線空間的是模型空間下的光源向量和視線向量(半向量是它們的和),而一般這兩個向量定義在視圖空間,所以之前還要做一個視圖空間 ->模型空間的變換(用ModelView矩陣的逆矩陣)。這是很多文章囫圇掉的一點。但如果你能取得視圖空間下的頂點TBN,也不需。因為切線/ 副法線/法線若是被變換到視圖空間,則上面的TBN矩陣MTBN就是把東西從該頂點的切線空間變換到視圖空間(道理是一樣的),MTBN-1就能把視圖空間下的這兩個向量變換到該頂點的切線空間(參見下篇的代碼)。
?最后的問題:怎么去取得模型空間下的頂點的切線,副法線,法線?
轉(zhuǎn)載:?http://www.zwqxin.com/archives/shaderglsl/review-normal-map-bump-map-2.html
?
1. 怎樣獲得頂點的TBN
其實我覺得這個是實踐部分最麻煩的地方。OpenGL提供了諸如glNormal、normal-vbo之類的接口設(shè)置頂點的法線,然后在shader中 以gl_Normal等方式取得頂點法線數(shù)據(jù),但是沒有提供切線和副法線的。當然兩者只要其一就足夠了(另一者可通過叉乘和左/右手定則獲得)。因為要把 TBN導入shader,干脆就設(shè)置attribute變量,記錄每個頂點的切線。切線一般就是相鄰頂點的差向量了(其實這有時候是非常繁重的工作)。
如果是通常的3DS模型的話,頂點法線是共頂點的面的面法線的加權(quán),這樣法線就不一定垂直于某個面,即與切線不垂直。但只要它們還是近似垂直的,上篇提及的Gram-Schmidt 算法應該可以處理。或者在shader中,把法線與切線叉乘出副法線,再用法線與副法線叉乘得新的切線,也能確保兩兩垂直。這樣之前的TBN矩陣的轉(zhuǎn)置矩陣就能直接作為其逆矩陣,完成向量從模型坐標系往切線空間坐標系的變換了。
問題不只這樣。對于一些模型,共享頂點的三角面片面法線差角太大,這時候計算出的該頂點法線和切線就可能帶來麻煩。在橙書(OpenGL Shading Language)中,談及了切線必須是一致的(consistently),面片相鄰的頂點切線不應該差距太大。但若相鄰面片夾角太大,得到的該頂點法 線就可能與“共享該頂點的面片”上的其他頂點的法線差異很大,從而切線也會相差很大,直接導致光向量等在這兩頂點的切線空間差異很大,插值的各個針對像素 的光向量方向差異很大,與像素法線點乘的cos也會差異得很明顯(而現(xiàn)實中一般的凹凸面漫反射光線不會有太大方向差異)。解決方法是把該出了問題的頂點拆 成兩個(原地拷貝,3DS模型就不用了- -),一個面片用一個,其法線只受所屬的面片的面法線決定(這樣最后會形成突出的邊緣,但夾角大的面片之間實際上就應該會是有這樣的效果吧)。
另一個問題,我們向shader傳入頂點法線切線,希望副法線由兩者叉乘得出。但既然叉乘就有個方向問題(結(jié)果可以有兩個方向,AXB與BXA是不一樣的,我以前弄shadow volume就曾被它這種特性作弄過)。AXB改成BXA實際上會導致凹凸感反向,原來凹的變凸了,原來凸的變凹了(要仔細比對,不然會有首因效應)。一般就用N X T吧,因為基本上都是這個順序的,結(jié)果也符合原Normal Map。
2. GLSL?1.2?Shader實現(xiàn)代碼
沒什么好說的,就是前面算法翻譯成GLSL。
Vertex Shader:
?
傳入的lightPos,eyePos,gl_Vertex,gl_Normal,rm_Tangent是其模型坐標系下的坐標、向量,乘以 ModelView矩陣(法線切線乘以ModelView矩陣的轉(zhuǎn)置逆矩陣)到了視圖空間(vlightPos,veyePos,pos,norm, vtangent);在視圖空間它們已經(jīng)有了“世界”的概念了,因此可以平等地相互影響(在各自封閉的模型空間是享受不了的),可以作各種點乘叉乘加減乘 除計算。
注意,lightPos,eyePos雖說是在其各自模型坐標系下定義的,但不對它們弄什么平移旋轉(zhuǎn)縮放操作的話,其模型矩陣就是一單位陣,此時其“世界坐標 == 模型坐標”。所以這時我可以當它是在世界空間定義的坐標(實際上一般我們都會在世界空間定義這兩個點)。(注意,前提是不對它們做模型變換。)
從以上量得到光源向量、視線向量后(它們在視圖空間),N、T叉乘得B(注意它們現(xiàn)在都在視圖空間),通過TBN矩陣逆矩陣把兩向量變換到當前頂點的切線空間,交給光柵去插值。?
對以上有不理解的朋友,可能是沒看上篇:[shader復習與深入:Normal Map(法線貼圖)Ⅰ]
fragment shader:
?
注意把normal map里的normal由(0,1)映射回(-1,1)。baseCol得到的是基底紋理的像素顏色。其余部分就是per pixel lighting的東西了。
轉(zhuǎn)載于:https://www.cnblogs.com/mazhenyu/p/4481149.html
總結(jié)
以上是生活随笔為你收集整理的NormalMap 贴图 [转]的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Objective -C Memory
- 下一篇: C# Lambda表达式 基础