UnityShader - 基礎 - Normal Map 法線貼圖

Normal Map 法線貼圖

爲什麼偏藍?

法線貼圖在很多教程裏都有用到,我們知道法線貼圖裏都儲存了法線,但有沒有想過,Normal Map(法線貼圖)爲什麼看上去都是“偏藍色”的?
Normal map

這是因爲,但在貼圖中存儲的值都是在Tangent Space(切空間)下的。

切線空間

如果各位同學對切線空間不瞭解的話,在這裏簡單介紹一下。爲什麼要這個切線空間呢?假如大家都知道什麼是模型空間和世界空間等,那麼都會知道它們各自有什麼用。顯然它們的出現並不是偶然也不是多餘的,它們都有共同性,就是簡化描述方向或位置的一些參照系,而切線空間的根本目的也就是如此。有幾種座標都保存在切線空間中,如uv和normal等等。

模型中不同的三角形,都有對應的切線空間。而這個切線空間裏有三個參數,頂點法線(Normal)、頂點次法線(binormal)和頂點切線(tangent)。頂點切線是這個空間的X軸,頂點次法線是這個空間的Y軸,頂點法線是Z軸。其中tangent軸和binormal軸分別位於三角形所在平面上,結合三角形面對應的法線,我們稱tangant軸(T)、binormal軸(B)及normal軸(N)所組成的座標系,即切線空間(TBN)。

出於省事的說法,有的人可能會這樣描述,“切空間下的x軸和y軸就是頂點u,v的座標” 。如果你聽到某人這麼說,你可以揣測他可能是真的是行家,因爲這個意思算是“點”對了。但不幸的是,嚴格來說,這種說法是錯誤的。切空間下的x軸方向(切線方向),是該點u座標指向下一點u座標的方向;對應地,y軸方向(次法線方向),是該點v座標指向下一點v座標的方向。如果你在一張uv set上審視它,也就等於是在一個二維笛卡爾座標系上審視它,那麼得到的x軸方向就是“水平”的,而y軸方向就是“豎直”的。這裏想要強調的意思是,無論是切線方向還是次法線方向,它們作爲一個方向,勢必是由兩點之間的走向關係決定的。單獨的某點u,v座標,僅僅是個值而已,單憑一個點的值,既不可能得到切線方向,也不可能得到次法線方向。換成這種說法 “切空間下的x軸和y軸就是頂點所在uv座標系下的u軸和v軸”, 就對了。

爲什麼偏藍?

回頭來看看爲什麼法線貼圖總是藍色的。比如,首先我們知道法線每個分量的值的範圍是-1到1之間,而顏色值只有0到1之間,所以要進行映射才能把-1到1之間的值儲存到0到1的區間裏。使用算式加一除以二就能進行映射了。一根正好垂直於頂點所在三角形表面的法線向量在切空間下是(0,0,1),映射之後就是(0.5,0.5,1)假如用三個字節來表達像素RGB的話,該向量就會被轉換爲(128,128,255),這樣的值無疑對應的顏色就是偏“藍色”。由於大部分的法線都不會偏移這根“標準法線”太遠所以大部分像素都是“偏藍”的。用這種方式存儲Normal,可以視爲這種法線總是“貼着”模型表面“插”上去的,而不用考慮這根法線到底在世界空間/模型空間的什麼地方,又會經過怎麼樣的轉換。這樣就可以與各種可能的空間變換操作解耦,而且直觀友好,簡單易懂。

求算 Tangent 與 Binormal

“切空間”下的切線

再來考慮一個問題。在空間中某點的position和normal是很容易知道的,那麼相應也很容易得到該點的“切平面”。那麼,能否隨意在這個“切平面”上指定一條tangent和一條binormal,作爲該點的切線和次法線?從數學理論上來講這沒問題。隨着隨意構造的tangent,binormal和normal指定好後,一個空間座標系就由此指定了。當然了,根據之前的假設,這個座標系未必是正交的(因爲tangent不一定垂直於binormal),但normal = tangent cross binormal。這樣的空間座標系可以構造出無數個來,但如果要構造出上一節討論過的“切空間”座標系,卻只能有一種構造方法。而“切空間”在行業中是一種普遍承認和使用的空間,因此有必要弄清楚如何求算這個屬於“切空間”下的tangent和binormal向量。(至於將切空間和uv座標系聯繫起來究竟有什麼好處,我還不是太理解。暫時作爲事實接受下來)。注意這裏說求算tangent和binormal向量,是指它們從切空間(tangent space)被“轉換”,或者說“映射(mapping)”到物體座標系下(object space)下的值。

從另一個角度看“轉換(映射)”

在開始正式推導前,爲了能夠使接下來的一個結論看起來“顯而易見”,我想舉一個直觀的例子,從另外一個角度談談我對“轉換”,或者說“映射”的認識。想象有一段路面在某點開始分了叉,一條路是上坡,而另一條路是下坡。一輛汽車行駛在這段路面上並開始上坡,太陽在它身後,將它的影子”映”在了那段下坡的路上。現在,可以這麼理解,汽車和汽車影子分別處於不同的兩個空間內,一個是上坡的空間,另一個是下坡的空間。但它們之間有某種聯繫,那就是當汽車在行駛時,它們都會發生相關聯的變化。假如我知道,當汽車在上坡的空間中處於某點p1時,它的陰影處於下坡空間中的某點s1;當汽車前進到上坡空間中的某點p2時,它的陰影對應地前進到下坡空間中的s2。接下來我想知道,當汽車陰影在下坡的空間中前進了一個單位的長度時,對應地汽車在上坡的空間中前進了多少?

這個問題應該是“顯而易見”的,答案是(p2-p1)/(s2-s1),這也是“除法”的基本含義。同時,當p2無限接近p1(由於關聯性s2也無限接近s1)時,這也變成了“導數”的含義(dp/ds),因爲這個問題實質上就是在問“變化率”的問題。在這個例子中,由於上坡和下坡都是一條直線,所以情況可以簡化爲dp = p2-p1, ds = s2-s1, 於是答案也等於dp/ds。

dp/ds這個值的單位,是處於dp所在的空間中的。回到上面這個例子中來,dp/ds這個表達式,是在說當汽車影子在自己的空間中變化ds個單位時,對應汽車在自己的空間中變化了dp個單位。同時也可以這麼理解,汽車影子在自己空間中運動了ds個單位,“映射”到汽車所在的空間,汽車對應在自己的空間中運動了dp個單位。

這個問題可以概括爲,自變量(x)在自己的空間內變化一個單位,“映射”到因變量(y)所在空間中,因變量會變化dy/dx個單位。

有了這個基礎的理解,可以將其推廣到多維的情況。自變量和因變量都處於各自的空間中,它們的維數還不一定相等。例如,考察一架飛機在單位時間(t)內位移(s)的變化,就是ds/dt。其中ds是在三維空間中,而dt卻處於一維空間(時間)中。這裏它們各自是幾維是不重要的,關鍵在於自變量與因變量之間的”關聯“。正是由於這種”關聯“,使得當自變量只變化微小的一丁點時,因變量也會變化那麼微小的一丁點。

向量分解 ,偏導數與 tangent = dp/du( binormal = dp/dv)

接下來,讓我們回到最初的問題,如何求切空間中的”切線“對應在物體空間中的值。從上一節的討論中,我們可以把uv空間簡單看作爲切空間。現在假設頂點v1的uv座標uv1是(u1,v1),空間位置座標是pos1是(x1,y1,z1);頂點v2的uv座標uv2是(u2,v2),空間位置座標pos2是(x2,y2,z2)。uv空間中v1到v2構成的二維向量爲uv21 =(u2-u1, v2-v1),物體空間中v1到v2構成的三維向量爲pos21 =(x2-x1, y2-y1, z2-z1)。

向量uv21實際上可以分解爲兩個向量,一個是與u軸平行的向量u21 = (u2-u1, 0),另一個是與v軸平行的向量v21 = (0, v2-v1)。 由向量分解定理可知uv21 = u21 + v21。實際上,我們可以在uv空間中找到一點uv^ = (u2, v1),使得uv^ - uv1 = u21,uv2 - uv^ = v21。由於u軸在切空間中就是切線的方向,因此u21平行於切線;同理v21平行於次法線。

既然uv空間中存在一點uv^,那麼”映射“到物體空間中,也會對應存在一點pos^。使得pos21 = (pos^-pos1) + (pos2 - pos^)。這個向量分解的動作和上面uv空間中的向量分解是對應的。只是我們還不知道這個pos^到底處於物體空間中的哪個位置。但是,我們知道,當點在uv空間中從uv1變化到uv^時,對應地頂點在物體空間中則從pos1變化到了pos^。這是多麼熟悉的句式!進而我們假設,如果點在uv空間中沿着uv1->uv^的方向變化一個單位時,”映射“到物體空間中頂點會沿着pos1->pos^的方向變化多少呢?答案就是(pos^-pos1)/(uv^-uv1) == (pos^-pos1)/u21 == dp/du。由於du在uv空間(也就是切空間)中的方向與切線相同,因此dp就是對應在物體空間中切線的方向。dp/du是單位化的值,我們就可以將其看作是物體空間中的切線向量。於是得到

tangent=dpdu

同理可以得到

binormal=dpdv

實際上,由於u21對pos2-pos1的影響已經被分解到了(pos^-pos1)上, u21的v軸分部對其不產生任何影響,所以這裏簡單令u21 = u2-u1(注意這裏沒加粗體,表示它是標量)。可以得到dp/du = (pos^-pos1)/u21。同理有 dp/dv = (pos2-pos^)/v21(v21 = v2 - v1)。這實際上就是”偏導數“(partial derivative)的含義。

總結一下上面的分析,我們可以推導出下面的結論:

已知 u21 = u2-u1 和 v21 = v2-v1,

則有 dp/du = (pos^-pos1)/u21 , dp/dv = (pos2-pos^)/v21,

得到 (pos^-pos1) = dp/du * u21, (pos2-pos^) = dp/dv * v21

於是 pos21 = (pos^-pos1) + (pos2 - pos^) = (dp/du) * u21 + (dp/dv) * v21

等同於 pos2 - pos1 = (dp/du) * (u2-u1) + (dp/dv) * (v2-v1)

擴展閱讀 : pbrt中的 pi = p0 + (dp/du) * u + (dp/dv) * v

(注:如果你沒在看《pbrt》,或者你從沒見過上面這個公式,那麼此節可以略過不看。這節只是源於自問自答的一個想法。曾經爲理解這個公式卡了很久,既然現在稍稍有些心得,那也該本着有始有終的態度全部記錄下來爲好。)

讀過pbrt的同學知道,第3.6.2節講的是如何求得射線與三角形相交的那一點“微面(facet)”的全部信息。其中就包括tangent和binormal。只不過,它將tangent值表達成了dp/du, binormal值表達成了dp/dv。關於這個觀點,在本文的上一節中已經進行了力所能及的理解。只不過,書中在進行推導時,是從下面這個假設起步的,即在由三個點pi(i=1,2,3)組成的三角形中,設p0是三角形所在平面的其中一點,那麼則有:pi = p0 + (dp/du) * u + (dp/dv) * v。

這裏,我就想補充解釋一下爲什麼會有這個結論。

說起來也簡單,只是書中沒有明說,這個p0點對應的uv座標值就是(0,0)。然後我們把這個公式變通一下,就成了:pi - p0 = (dp/du)(u-0) + (dp/dv)(v-0)。這個公式,就很像本文上節最後得到的結論。只不過是把點2換成了點i,點1換成了點0。書中的意思是說,在這個三角形所在的這個平面上,任意一點都可以通過這種公式計算得到,只要給定了uv(0,0)對應的p0點以及欲求點的uv座標值(u,v)即可。

這裏可能讓人感到困惑的一個地方是,uv座標與頂點之間的對應關係,一般是通過人爲指定的。那憑什麼認爲uv座標基點(0,0)所對應的p0,會恰好在pi(i=1,2,3)這個三角形所在的平面上?實際上這裏說的uv座標(0,0)點,並不是我們通常認爲的人爲指定的那個點,而是從數學角度上”推斷“出的一個點。可以這樣理解。既然我們知道三角形的三個頂點的三維空間座標值(xi, yi, zi)(i=1,2,3)與uv座標值(ui, vi)(i=1,2,3),又知道三點能夠確定一個平面這個常識。那麼就可以推斷出在uv空間中的任意一點,在三角形所在的這個平面中必定存在對應的頂點。 那麼也就可以確定在這個平面上找到一點p0,使得它的uv座標值是(0,0)。

開始求算!

有了以上知識理解上的準備,接下來的求算任務基本上就是直截了當的了,沒什麼太需要費腦力的地方。不過需要對線性代數的基礎知識有點了解: 其實只要知道如何求逆矩陣就行了。

切線和次法線永遠總是針對面(face)而言的,單獨考察一個點的切線或是次法線沒有意義。而構成一個面最簡單的方式就是三角形,所以我們就考察如何求算三角形的tangent和binormal。給定一個三角形,已知它的三個物理空間的頂點爲pi(i=1,2,3),對應的uv空間座標爲uvi(i=1,2,3)。由於這是一個三角形平面,因此三個點的tangent值與binormal值都是該面上的值。設 tangent = dp/du, binormal = dp/dv。根據上幾節的推導,我們很容易就可以得到,

p2 - p1 = dp/du * (u2 - u1) + dp/dv * (v2 - v1)

p3 - p1 = dp/du * (u3 - u1) + dp/dv * (v3 - v1)

修改成矩陣形式,就成了下面這樣,

[p2p1p3p1]=[u2u1u3u1v2v1v3v1][dp/dudp/dv]

再變換一下,就得到
[dp/dudp/dv]=[u2u1u3u1v2v1v3v1]1[p2p1p3p1]

求解這個式子的細節就不列在這裏了。關鍵瞭解一下如何求一個矩陣的逆矩陣(伴隨矩陣數除其行列式),然後根據矩陣的乘法運算規則就能得到結果。隨着結果的求得,我們也就知道了tangent和binormal的值。

TBN Matrix (TBN 矩陣)

上節求得了tangent(簡寫爲T)與binormal(簡寫爲B),再叉乘一下就得到了normal(簡寫爲N):即 N = T X B。 由此三個向量就可以構成一個空間,[T, B, N]。

[TBN]=TxTyTzBxByBzNxNyNz

這個矩陣表示的是,切空間(tangent space)中的三個基向量被轉換到當前座標系下所對應的三個基向量構成的空間。任何一個切空間下的向量,通過這個矩陣便可以變換到當前座標系下(比如模型的物體座標系亦或它所在的世界座標系),究竟被轉到什麼座標系,這要看當時求算T,B時利用的Pi點所在的空間:如果Pi是模型的物體座標系,那對應該矩陣也就會把切空間的向量轉到物體座標系下;如果Pi是模型所在的世界座標系,那對應矩陣就會把向量轉到世界座標系下。比如,

normal in object space = [T,B,N] X normal in tangent space

這裏我們關心的問題是,這種轉換的意義是什麼?費這麼大勁做轉換,到底是爲了什麼?答案是,通過這種轉換,就可以把Normal Map中的法線轉換到對應的物體/世界座標系下,與這個空間下的光線進行計算,從而能夠算出對應此點的正確光照。我們知道Normal Map的法線爲什麼存儲在“切空間”下是有道理的(第一節有提到),但它不能直接被使用。經過這樣的轉換,就可以與各種可能的空間座標聯繫起來了。

不過經常用的,還不是把normal值轉換到物體/世界座標系下,而是反過來,把光線“反”轉換到到切空間下,與normal計算出光照值。爲什麼?因爲一個模型可能有非常多的點,對應normal map中的normal值數量必然也是巨大的,如果把每一根normal都做轉換,這種計算量的成本是相當高的。但反過來,光線就那麼幾條,轉換一次光線就能給所有切空間下的normal使用,相對來說這種計算要“便宜”得多,所以反轉光線值這種做法是更爲常見的做法。

light in tangent space = [T,B,N]-1 X light in object space

同樣需要求其逆矩陣。不過如果TBN三向量彼此正交的話(一般來說是這樣),那麼它的逆矩陣就簡單地等於它的轉置矩陣。

[TBN]1=TxBxNxTyByNyTzBzNz

實際上TBN的作用還不止於此,它的應用很廣泛而且也非常重要。比如做displacement mapping時,基於Vector置換的做法就同樣用到了TBN矩陣。用法和原理與Normal Map都是一樣的,只不過此時Map換成了再切空間下的用來置換的向量,而非法線。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章