1. 程式人生 > >寫給笨人的法線貼圖原理

寫給笨人的法線貼圖原理

我算個笨人吧.笨人以前弄懂一些東西后,講給笨人聽往往更有效.看之前請自行具備圖形學關於光照的基礎知識.

  >>  world/object space normal map

  我們先講基於世界或模型座標的法線貼圖(world/object space normal map).不常用,但是基礎.

  首先,請無視你之前google到的所謂通過Photoshop生成法線貼圖類似文章,美術除外.那只是一種利用近似hack的手法利用法線貼圖原理.無助於理解真正的過程.不過看完這文章後,你應能理解Photoshop的這種做法的來歷.

  不搞清法線貼圖的生成原理,是無法正確理解之後shader中的計算使用的.法線貼圖的出現,是為了低面數的模型模擬出高面數的模型的"光照資訊

".光照資訊最重要的當然是光入射方向與入射點的法線夾角.法線貼圖本質上就是記錄了這個夾角的相關資訊.光照的計算與某個面上的法線方向息息相關.

  我們知道計算機裡的模型,是通過多個多邊形面組合來近似模擬一個物體的.它不是圓滑的.面數越多,則越接近真實物體.光照到某個面當中的一點時,法線是通過這個面的幾個頂點通過插值得到的.插值其實也是為了模擬這個點"正確"的法線方向,不然整個面所有點的法線一致的話,光照上去,我們看到的模型誇張點就像一面面鏡子拼接起來了.但法線插值不可避免的仍然會失真.模型的面數越高,失真的程度自然越小.要是能無限細分到人眼看不出的地步,根本不用插值了.

  面數高,需要計算的量和記憶體需求就高.前輩找到了法線貼圖(前身是凹凸貼圖)這個辦法,使低模能夠近似享受高模的光照細節資訊.代價是有的,就是需要一個記錄這些資訊的檔案.這是程式中常用的儲存空間換計算時間的做法.3D程式中偏愛使用這個手法.誰叫儲存硬體的單位價格比計算硬體的單位價格降低速度快很多呢.

  顯示卡包括包括與之相輔的圖形api,讀的資料最初來源是圖片.所以記錄這個資訊的檔案就被我們儲存為圖片格式.法線貼圖後邊2個字就這麼來的.很好你已經明白一半了.

  我們再來弄清另一半.

  因為面數少,低模上某個區域的一個面,可能就是高模上相同區域的幾個面.看下圖的高模與低模的對比(為了簡便我們抽象為2維的線段)

  

  上邊凹凹凸凸的曲線表示高模.下邊比較平滑的表示低模.因為高模細節多,所以在某段區域它的方向變化自然比平平板板的低模多.上圖看不懂我表示無能為力.

  看到這圖,一些人應該有所感覺了.沒感覺也不要緊,接下再來.

  不管高模還是低模,反正最後還是要被上色的.假設模型已經被渲染完成有顏色了,現在我們想象用剪刀把模型展開(類似給動物扒皮的過程),得到2張差不多一樣大小的皮,畢竟面積不會差太多.高模的皮當然膚白體嫩精度高,低模的皮就有些糙了.現在再想象這麼個過程:逐漸把高模的皮移到低模的皮上方一定高度直到水平重疊.

  現在這個樣子你有感覺沒有?沒感覺也不要緊,接下再來.

  雖然模型精度不一樣,無論如何,這2張皮每一點都是有顏色了的(插值的功勞).兩張皮上相同一點的顏色,高模這張皮上的更真實,因為在計算最終顏色資訊所依賴的法線,高模上的點比低模上的點更精確.我們如何給低模這張皮美容,使它能夠接近高模的效果呢?換句話說,找到辦法,使土肥圓演變為黑木耳,質變為白富美是不可能的,那得下輩子.

  辦法很暴力.現在再想象你用一根針,從上往下,刺穿高模的皮,再刺到低模的皮.保證針垂直,這樣就刺到同一點了.再想象如果這針有魔力的話,它刺穿高模皮的過程中,盜取了一些資訊,傳送到低模皮上邊.低模皮依靠這些資訊計算,成功蛻變為黑木耳.這些資訊是什麼呢?當然是法線資訊了.現在高模這張皮被密密麻麻插滿了針眼,換句話說,儲存高模洩漏來的資訊,必定是點對點的.即這張皮上的每個點,都得被儲存.所以法線貼圖跟原始的貼圖是一樣大小的,貼圖內每個點都儲存了對應高模某個點的法線資訊.實際的計算,只會關心由貼圖裡得來的法線資訊,低模上的那些法線,被拋棄了.

  現在這個樣子你有感覺沒有?沒感覺也不要緊,接下再來.

  如何賦予這根針魔力呢?宅男們,甘道夫是幫不了你的忙的.偉哥也幫不了你.只有數學,才能拯救世界...

  為什麼我之前強調垂直呢?不只是為針能扎到同一點.現在請把這個過程,想象到上圖中.圖中的箭頭,表示高模上某個點的法線方向.如何記錄這個方向資訊?現在請想象逐漸把高模和低模重疊在一起,為了方便想象,低模小一些被高模包住了.或者你乾脆想象高模的面在低模面的正上方,或一個圓球裡有一個內切的正多面體.再想象有一束光線(針的等價物),從上往下照射,把高模上的法線投射到低模上.

  現在你有感覺了吧.

  前戲大功告成,現在我們來處理稍微細節些的問題了.這是一個投影過程.但是影子是2維的啊?向量是由x,y,z三個分量構成的.高模上某點投影到低模上對應點所在平面,只剩2個分量的投影了.好比我們現在只知道法線在x-y平面的投影方向,那在z軸的方向呢?只要我們確保投影前法線是單位向量,那很簡單z=1-x*x-y*y.這樣我們還可以省下儲存z的空間.其實我們既然已經知道這個法線方向(高模object space內的法線方向),而且被單位化了,直接儲存也是可以的.投影過程只是個思想實驗,實際是不會有什麼光線由上到下投射的.

  到此可以明確了,"正統"的法線貼圖生成,是高模,低模不可缺一的.因為沒有高模就不知道法線方向,沒有低模,就不知道高模上某點的法線對應於低模上哪個點.

  因為某點的法線資訊是被儲存到法線貼圖上對應畫素點的.實際計算是把法線x,y,z方向大小對映到顏色空間rgb裡.就是把x值存在r裡,把y值存在g裡,把z值存在b裡.因為rgb是8位元組為單位的.所以高模的法線資訊儲存到畫素裡是要丟失精度的.而且前面計算高模與低模對應點也不可能完全匹配到,本來就是個模擬過程.自然法線貼圖也不是無敵的.

  現在我們可以回答之前的Photoshop根據diffuse貼圖生成法線貼圖的問題了.實際的diffuse貼圖是根本沒有包含模型上的法線資訊的.因此它根據diffuse貼圖得出的法線貼圖根本就是錯誤的.但為什麼能夠應用呢?請想象高模的精度高的嚇人,高到渲染後把高模皮扒下來後,就成了一張照片.再想象之前高模上的貼圖是佈滿了鐵鏽.於是你就得到了一張鐵鏽照片.Photoshop處理這張鐵鏽照片,其實是根據一些演算法(sobel等等)把顏色值轉化為梯度值,近似模擬了法線.因為我們其實不關心鐵鏽的精確分佈,像那麼一回事就可以了,所以這種情況下如此處理是可以將就的,坑坑窪窪效果最適合如此做法.photoshop這種脫離高模低模的做法容易讓人迷惑,導致新手以為法線是從diffuse貼圖上來的,或者乾脆被阻斷了思路.

  我們上邊計演算法線貼圖所用到的法線,又是從哪裡來的.如果這個法線方向,是處於世界座標中的(world space),那稱為world space normal.如果是處於物體本身區域性座標中的,那稱為object space normal.很容易想象,world space normal一旦從貼圖裡解壓出來後,就可以直接用了,效率很高.但是有個缺點,這個world space normal 是固定了,如果物體沒有保持原來的方向和位置,那原來生成的normal map就作廢了.因此又有人儲存了object space normal.它從貼圖裡解壓,還需要乘以model-view矩陣轉換到世界座標,或者轉換到其他座標取決於計算過程及需求.object space normal生成的貼圖,物體可以被旋轉和位移.基本讓人滿意.但仍有一個缺點.就是一張貼圖只能對應特定的一個模型,模型不能有變形(deform).

>>  tangent space normal map

  為解決適應變形的normal map,我們仍能從這兩種方法中得到啟示.world space normal直接儲存的是世界座標系中的高模法線方向.因此低模取出該點法線就可以直接使用,前提是低模的世界座標系與高模一致,一點旋轉都不能有,不然法線方向就改變了.object space normal儲存的是模型空間座標系中的高模方向,低模取出該點取出來法線,還需要乘以所在的model-view矩陣,轉化為低模的世界座標系中的方向,也就是說低模端還需要做一個運算.因此即使低模任意旋轉也不怕,有model-view矩陣可以把法線貼圖中的值轉換兩者效率由高到低,靈活度由低到高.問題來了,我們是否能找到高模上的另外一個座標系統,使低模變形也時也能較正確的變換法線到世界座標系中?

  我們考察一下object space.當一個低模旋轉時,因為是剛體不變形,相當於每個點都乘以一個旋轉矩陣R,之後各點關係保持不變.實際上,我們保持物體不旋轉,將object space的座標系(x,y,z三個軸)旋轉,得到的結果是一樣的.這個關係相信大家都能理解.換句話說,法線針對於object space是固定不動的,物體保持在object space固定,只管跟隨座標系的移動,旋轉就行了.現在我們想象低模的某個點需要變形時,那原則上也可以通過讓object space座標系乘以某個變形矩陣T來達到.但是不同的點有不同的變形,不可能存在一個矩陣T即適合這個點又適合這個點.因此object space座標系是不能用的.會有哪個單一的座標系能存在一個所有點都共用的變形矩陣嗎?顯然無法想象.

  變形時,頂點關係改變了,即面的形狀,方向改變了.如果面上存在一個固定的座標系,那當物體變形,移動,旋轉時,這個座標系必定跟著面一起運動,那麼在這個座標系裡的某個點或向量(比如我們把高模法線轉換到這個座標系裡),不需要變動.當整個面發生變化時,我們只需要計算面上的座標系到世界座標系的轉換矩陣,那麼定義在這個面上的點或座標(固定的),乘以這個矩陣即可得到在世界中的座標.這個座標如何構造目前對我們不重要,請務必理解這個概念.我們不過是尋求一個區域性座標系,區域性座標系中的點座標,乘以區域性座標系到世界座標系的轉換矩陣(這個矩陣是低模渲染時動態計算的的),得到區域性座標系中的點在世界座標系中的座標.這樣法線貼圖中儲存的固定的值(法線方向),才能進行有意義的計算.

  看到這裡很明顯的,這種做法需要數千個不同的定義在面上的座標系.低模上有多少個面,就得有多少個這樣的座標系.這種方法的計算量自然是比object space normal map要大一些的.在低模的每個面上,要構造出這個座標系.這個座標系術語裡稱為tangent space.

  object space normal map的中,低模的object space座標系與高模中的object space座標系是重合的.所以不需要構建,所以低模上某點才能直接用高模的法線替換自己的法線.座標系重合這個概念很重要.新方法中,低模上的這個tangent space,也必須與高模上的座標系tangent space.因為低模上的一個面,可能對應了高模上的幾個面(精度高),按照新方法每個面都有一個區域性座標系,那對於低模上的每個面,高模因為存在好幾個面,就會出現好幾個區域性座標系,這肯定是不行的.所以高模所用的tangent space,就是低模上的.生成法線貼圖,必定會確認高模上哪些面都對應低模上的哪個面,然後高模上的這幾個面的法線,都會轉換為低模這個面上所構建的tangent space的座標.這樣,當低模變形時,即三角面變化時,它的tangent space也會跟著變化,儲存在貼圖裡的法線乘以低模這個面的tangent space到外部座標系的轉換矩陣即可得到外部座標.順便再提一點,高模儲存的這個法線,是高模上object space裡的法線,看到這裡你該明白這是自然而然的.你搜索文章時可能會看到什麼把光轉換到tangent space裡,確保處於同一個座標系下的話.確實是這樣.但初次接觸卻還是模糊.我以為確保tangent sapce重合及做法,才是讓人頓悟tangent space的訣竅點.

  

  上圖稍微清楚一些了.曲線表示高模,P點處有TBN座標系.線段表示低模,M點處有T'B'M座標系.高模上P點處的法線轉換到TBN座標裡,與N的夾角為NPN'.低模處取出這個法線為N'',與低模的PM(面法線)夾角為PMN''.可以看到這2個夾角是近似的.所以渲染時高模上的法線,是基於低模上的這個座標系運算的.你可能會說,我這的TBN不就是實際的高模面上的左邊嗎?別忘了很可能幾個高模的面擠在一起對應一個低模的面,高模的這個TBN必然是經過一種“插值”或“平均”得來的,實際上也會跟低模有些相關性,以求最匹配的效果。具體如何得來未深究,有高手可否告知否?

  當我自己想到上邊這段話時,tangent space的法線貼圖原理就豁然開朗了.接下來我們構建這個tangent space座標系.

  面在動時,tangent space也得跟著動.面上的垂直法線是跟著動的,因此這個法線N可以作為tangent space的一個座標軸.非常非常需要注意的是,這裡所說的面上垂直法線,不是指插值所得來的法線,那個法線正是是我們需要儲存的內容.N單純就是指垂直於這個面的方向.

   

  我們考察上圖,對於一個三角面,它的邊V2V1,V3V1,V3V2我們總是能夠確定的.邊也定會在變形時跟著動.因此我們可以選擇一條邊作為tangent space的第二個座標軸T.第三個座標軸就簡單了,直接根據叉積來B=T * N.這個座標軸就訂好了.其實,座標軸的選定幾乎可以是任意的,只要你能夠確保每次都能構建出來.比如你可以先選擇V1V3,V1V2作為座標軸,N=V1V3 * V1V2.這裡N恰好和前面一樣方向.但如此一來這個座標系中V1V2,V1V3不是垂直的,不正交的座標基在矩陣運算中是不方便的,還得正交化.因此我們選擇第一種最直觀最清晰最方便的方法.

  既然三個座標軸都確定了,那構建object space到tangent space的矩陣O-TBN就簡單了,我們把T,B,N單位化,分別作為tangent space的x,y,z軸.根據三個座標基我們構造矩陣如下:

  O-TBN =   

  高模上object space內的某點法線(不會是world space的,否則旋轉就露相),乘以這個矩陣,即得到tangent space內的法線方向,再把這個值對映到rgb空間,存為貼圖即可.這個矩陣為什麼是這樣,這是題外話了.我簡略說一下:object space的三個座標軸(1,0,0),(0,1,0),(0,0,1)乘以這個矩陣,必須剛好為tangent space中的座標軸,很自然矩陣就是上邊的樣子.而object space其他點的座標都是x,y,z三個單位座標的線性組合.所以這個矩陣對於其他點必定是正確的.

  實際上在vertex shader中,我們只能知道當前頂點的資訊,三角形的另外兩個頂點我們是不知道的.但現代的shader能夠為頂點提供一個tangent資訊,表示在頂點處的切線.你可以想象一個足球上經過某點的切線.因此我們會把頂點的tangent方向作為上邊的T向量.這也是tangent space叫這個名稱的由來.你會看到很多文章中提到紋理的u,v方向.因為面上某點的u,v是沿各條邊線性插值的,所以u,v方向與邊的方向相同.其實我們現在已經有現成的tangent可以用了.

  現在我們可以分析為什麼tangent space法線貼圖是偏藍色了.因為對於高模上的面來說,因為精度太高(面很小,而且周圍的面相對它的方向很平滑),所以這個面渲染時計算機認為這個面的"彎曲"程度很小,即面上各個點插值得來的法線相互間偏差很小.基本跟整個面的垂直方向不會差太多.因此在tangent space裡,這些法線都跟z軸偏差較小.而z軸是被儲存在貼圖裡的b位元組處(藍色通道)裡.所以貼圖顯示出來的顏色就偏藍了.

   好了,現在高模面上各點的法線值,都轉換為低模上的tangent space座標了.現在我們考慮具體的低模上的渲染計算了.假設在低模上的某個面我們計算出了這個矩陣,並取出了面上某點的對應在法線貼圖裡法線值.現在需要計算光照.我們可以把光向量轉換到tangent space裡做計算.也可以把得到的法向量轉換到world space與光向量進行計算.結果是一樣的.實際考量,你會發現後一種方法不好.因為對於面上的每個點,都要計算一次normal到world space的準換.而前一種方法,對一個面上的所有點,只要計算一次光向量到tangent space的計算.然後再考慮到vertex shader與fragment shader的流程,你會發現剛好我們可以在vertex shader計算光線到tangent space的轉換,在fragment sader取出法線值與前面得到的tangent space裡的光線方向做計算即可.這裡提醒一下,一般verteix shader中我們得到的光線方向是基於world space的,而法線貼圖儲存的是高模的object space內的方向然後再轉換到tangent space,所以在vertex shader中,我們必須先把光線先轉換到object space,再轉換到tangent space.這樣才能保證最終計算時,光線與法線是基於同一個座標系的.這也是你在很多normal map的shader裡,看到類似ToOjectSpaceDir(lightDir)之類函式的原因,正是要把光轉換到object space.

  實際做法可能會有些複雜.比如有些模型是映象對稱,貼圖也是映象對稱的,計算時會省去另一半等等.這時如何處理看具體的法線貼圖生成軟體和處理它的引擎(shader)了.基本原理還是上邊所說的.

  tangent space normal map適應變形的這種能力,使它不僅能夠應用在原來的模型上邊,甚至可以應用在變形嚴重的不同模型上.即法線貼圖有一定的脫離原來模型使用的能力.比如你模擬出了一個高精度的粗糙花崗岩平板表面,得出的法線貼圖可以應用到圓柱模型上邊.類似photoshop的直接根據花崗岩表面照片生成法線貼圖也是能夠使用的.因為目地就是為了微小的擾動法線生成凹凸不平的表面.雖然這個表面並不是正確的還原一個真正存在的花崗岩表面.但圖形學不就是一個模擬過程,能足夠真實的欺騙我們的眼睛就行.其實這種粗糙表面微小擾動只是應用之一.你搜到一些處理的好的圖片,不仔細看你會發現低模的怪物會以假亂真體現出高模才有的平滑彎曲.

  以上是法線貼圖的原理.因為該原理的應用範圍很廣,也能夠串起很多知識點,是非常值得搞清楚的.我當初在學習法線貼圖過程中,沒有看到能夠講的讓我清晰明白整個過程的.遂作此篇.