轉自:http://www.zwqxin.com/archives/shaderglsl/review-normal-map-bump-map.html
Normal Map法線貼圖,想必每一個學習計算機圖形學的人都不陌生。今天在這裏按個人理解總結一下,做爲複習,也做爲深刻學習吧。——ZwqXin.com
自從看完那本《數學在計算機圖形學上的應用》後,一直想好好地真正實踐一次法線貼圖/凹凸貼圖呢(之前是根據橙書弄了一下罷了)。昨天偶爾看到篇涉及BumpMap的文,正好以爲是個機會,便在網上狂找相關資料——果真,越看越以爲本身還有不少理論的地方須要弄明白呢。
提及Normal Map(法線貼圖),就會想起Bump Map(凹凸貼圖)。Bump Mapping是Blin大師在1978年提出的圖形學算法,目的是以低代價給予計算機幾何體以更豐富的表面信息(高模蓋低模)。30年來,這項技術不斷延展,尤爲是計算機圖形學成熟之後,相繼出現了很多算法變體,90年代末的Normal Map解放了必須自行計算紋理像素法線的痛苦,新世紀以來相繼又出現了Parallax Mapping, Relief Mapping等技術。拋開那些無聊的概念區分,它們的本體仍是Bump Map,目的也是一致的。
1. 傳統的Bump Map
若是你對純淨的Bump Map有興趣,A Practical and Robust Bump-mapping Technique for Today's GPU應該是值得一看的論文。說Today,實際上是GDC 2000的事情了,但對於傳統的Bump Map的理論是很豐富的,我是沒精力看完它啦……
那時候的Bump Map需要咱們計算紋理圖上每一個像素的法線信息,簡單的還可能作到,對複雜的紋理要搞清面光背光分量簡直要命,因而就用Height Map,在一張高度圖上記錄每一個像素對應的紋理位置的高度信息(這個比較容易辦到,NEHE22也是這類)。看上去就是一張地形網格——這樣的話,計算每一個像素點的法線就不那麼難了。XY方向相鄰像素的高度相減就是兩條正交的切向量,叉乘外加左/右手定則就得到法線。或者更精確點,用八鄰域弄個邊緣檢測算子(sobel、拉普拉斯之類 )[圖像處理裏的空間域濾波],或者應用斜坡法([水效果Ⅲ - 抖動波] )來求切線、法線。html
2. 製做NormalMap
可是這樣仍是挺麻煩的,既然都動用額外的貼圖了,何不把這些與實現無關的預處理——做爲結果的法線信息——都放進紋理裏呢?這就是Normal Map的思想起源。可是,誰來作這樣的一張法線圖呢?敲定美工了。每一個像素的RGB分別存儲該像素對應法線的XYZ份量,只要把法線的份量由(-1,1)映射成(0,255)就可了。觀察一張法線圖,以藍色爲主,是由於朝向圖面外的法線(0,0,1)都被編碼成(0,0,127)了(讀入OpenGL後即(0,0,0.5)),而圖上越紅的地方代表法線越向右,越綠的地方代表法線越向上,就能夠理解了。整體來講,就是一張紫藍色的圖。怎麼作這樣的圖呢?固然最好是有一個工具,輸入原圖和高度圖後執行上述的算法得出新圖了,事實上已經有不少這類工具了(譬如比較著名的photoshop的NV插件Normal Map Filter,甚至不用高度channel也可[效果- -]),如下幾篇文章有詳細介紹,有興趣的能夠看一看:
Tutorial On Normal Mapping (PHOTOSHOP [ENGLISH])
怎樣用PhotoShop建立Bump Map圖像 (PHOTOSHOP [CHINESE])
Nvidia Normal Map 插件參數之詳解 (PHOTOSHOP [翻譯])
GIMP normalmap plugin (GIMP [ENG])
關於NormalMap製做的原理,更詳細的可參考此文:Normalmap原理及去除接縫
3. 切線空間(Tangent Space)
其實這個概念前文已經說起了。每一個像素根據高度圖生成的三軸座標系,就是被稱爲切線空間座標系的東西,每一個像素人手一個。可見Normal Map裏面每一個像素的法線就是定義在這個切線空間的。注意,這些法線是屬於像素的,而不是頂點,咱們平時用的法線是頂點法線,是定義在模型座標系的[亂彈OpenGL中的矩陣變換(上)] ,定義於所屬物件的惟一的局部座標系原點之上。而這些像素法線定義於切線座標系,其原點就在該像素上,切線副法線在法線的垂直平面上。算法
(表面依然是平的,但經過攪動法線,使進入咱們眼睛的光線強度不一,模擬出凹凸面漫反射的特色。圖from GDNet)
應用這些像素法線的目的無非是計算出該像素的OutPut顏色:col = baseColor * (amb + diffuse) + specular。這些都應該在像素着色器(fragment shader)裏進行,由於咱們要作的是針對每一個像素的處理[Shader快速複習:Per Pixel Lighting(逐像素光照)] 。其中須要用到像素法線的是diffuse和specular(之前是用經過頂點法線線性插值而來的normal),法線分別與光線向量、半向量做點乘獲得對應因子。這個因子是個夾角cos而已,因此只要知足像素法線與兩個向量單位化並在同一座標系下(而不管是哪一個座標系),夾角就是必定的。這樣看來,兩個選擇:
1. 把像素法線都從各自的切線空間轉到視圖空間來,再點乘;
2.把光線向量、半向量從視圖空間轉到像素各自的切空間來,再點乘。
不少文章一口咬定就是第2種好,緣由是第1種要變換N個量;第2種只變換2個量。仔細分析,其實兩種選擇變換的次數是同樣的,都是2*N。說第2種好,是由於:
第1種必須在fragment shader裏進行,對象是從Normal Map讀出的像素法線和通過線性插值而來的兩個向量,它們不是同一座標系的,按描述應該是各像素法線乘以各自一個的變換矩陣,轉到視圖空間來,但確實沒有其餘的可提供構築這個矩陣的信息了,如有可能應該就是另外的varying變量傳入了;
第2種能夠選擇在vertex shader裏進行,可是能不能就在這裏變換到切線空間呢?假設能夠,那麼獲得的針對頂點的數值在光柵化-線性插值後可否知足呢?
要回答這個問題,還得考慮像素的切線空間和頂點的切線空間之間的關係。是的,頂點法線也能夠變換到切線空間,但這有什麼用呢?一步一步來吧。先考慮切線空間在OpenGL世界裏的次元位置:app
(from paulsprojects)
爲何是緊挨模型座標系呢?其實想一想也能理解,在上面談及切線座標系的時候,並無廣闊的「世界」這個概念。只針對每一個像素/頂點,無疑是比模型座標系更狹隘的「世界觀」,因此那個位置是適合的(箭頭方向無所謂,座標系之間是能夠相互轉換的)。其實對於某個具體的物體上的像素/頂點,你能夠考慮那是把模型空間的原點平移到該像素/頂點上,各模型座標系方向軸向量一塊兒通過旋轉,使Z軸與像素/頂點的法線重合,XY軸分別與像素/頂點的切線副法線重合——這只是一個仿射變換而已,如同模型/世界/視圖空間之間的變換同樣。
若是你記得圖形學書上關於世界/視圖空間的變換矩陣的構建的話,就更容易理解這樣的形式了。從切線空間到模型空間的變換矩陣(TBN矩陣MTBN)爲:工具
其中T,B,N是定義在模型空間的該像素/頂點的「切/副法/法向量」。稍微檢驗一下,考慮某個三角面上的某個頂點,其法線充當切線空間的Z軸,在切線空間中表示爲(0,0,1),在OpenGL裏解釋爲一個列向量(0,0,1)T,用上面的矩陣MTBN左乘該向量,獲得(Nx,Ny,Nz)T,正是該向量在模型空間的表示。其餘兩軸同理。說明該矩陣把切線空間的座標系統轉換到模型空間了(一切變換都是在變換座標系[亂彈OpenGL中的矩陣變換(上)] )。固然這是特例說明,但確實這個矩陣包含仿射矩陣裏的旋轉元素了(它只包含旋轉,不設置平移,是由於咱們只須要它來變換向量,向量是能夠任意平移的,若要弄完整的4X4矩陣,第4列平移列就是該頂點模型座標)。具體推導也不難,隨便Google一下"tangent space"就出來一堆了,並且都是基本同樣的推導過程,推一個:Tangent Space。
其逆變換(矩陣MTBN-1)就能夠把向量從模型空間變換到對應頂點的切線空間了。若是你確保T,B,N兩兩垂直,這個正交矩陣的逆矩陣就是其轉置矩陣,這很理想。但萬一你不確保這點(涉及到具體應用,不少問題的,後面會說),就保證它們大體知足三叉狀,用所謂的Gram-Schmidt 算法矯正:
T′ = T − (N · T)N
B′ = B − (N · B)N − (T′ · B)T′
反正最後獲得的是這樣的形式——用它左乘光源向量和半向量,就獲得對應於該頂點切線空間的光源向量和半向量了:
T′x
B′x
NxT′y
B′y
NyT′z
B′z
Nz學習
爲何是頂點?由於這是你惟一能取得其切線/副法線/法線的東西了。這也是以前說的選擇1不行的緣由,在那張Normal Map裏面已經沒有任何法線副法線的確實信息了(只知道它們在法線垂直平面上),即便能經過別的方法取得(起碼要增長傳入數據),那要在fragment shader裏每像素人手又計算一個矩陣,這就又是一個「計算量」(不是次數)的問題。因此仍是用選擇2吧,也就是上面矩陣MTBN-1的討論。
選擇2的第一個問題如今很清楚了:是能夠的。只要取得頂點的切線/副法線/法線數據就能創建矩陣並變換光源向量和半向量,但結果是針對頂點的,咱們須要的是針對像素的。光柵化線性插值這兩個向量,就是對應像素的值,但這對嗎?直覺上不對,但結果顯示這樣作沒有不妥(或者說不會與真實所須差太多)。通常文章都沒有直接透視這個問題,其實考慮一個矩形平面就露餡了,它四個頂點的TBN一致,變換得的光源向量也該一致,插值後得光源向量也該一致,但NormalMap中的像素有各自不一樣的切線空間系統,光源向量不應一致的呃(雖則同向光源、不一樣法線足夠造成凹凸效果)。因此我對選擇2的第二個問題保持疑問,有道深者請爲鄙人指點迷津!
反正即便計算兩向量夾角的計算可能會有誤差,也不會太離譜,問題到此結束。至於有的文章說起對diffuse的計算,光源向量插值後不須再歸一化的問題(我嘗試過,總體會變暗一點),就不深刻了。注意咱們在vertex shader裏變換到切線空間的是模型空間下的光源向量和視線向量(半向量是它們的和),而通常這兩個向量定義在視圖空間,因此以前還要作一個視圖空間->模型空間的變換(用ModelView矩陣的逆矩陣)。這是不少文章囫圇掉的一點。但若是你能取得視圖空間下的頂點TBN,也不需。由於切線/副法線/法線如果被變換到視圖空間,則上面的TBN矩陣MTBN就是把東西從該頂點的切線空間變換到視圖空間(道理是同樣的),MTBN-1就能把視圖空間下的這兩個向量變換到該頂點的切線空間(參見下篇的代碼)。
最後的問題:怎麼去取得模型空間下的頂點的切線,副法線,法線?連同shader實現代碼一塊兒,我會在下篇談及,請留意了哦。編碼
本文來源於ZwqXin http://www.zwqxin.com/ , 轉載請註明
原文地址:http://www.zwqxin.com/archives/shaderglsl/review-normal-map-bump-map.htmlspa