- Normal Map中的值 -
有沒有想過,Normal Map(法線貼圖)為什么看上去都是“偏藍色”的?這是因為,在map中存儲的值都是在Tangent Space(切空間)下的。比如,一根正好垂直於表面的法線向量在切空間下是(0,0,1),假如用一個char(注意不是unsigned char)來表達像素的話,該向量就會被轉換為(0,0,127)。這樣的值無疑是“藍色”。由於大部分的法線都不會偏移這根“標准法線”太遠(比如[0.1, 0.2, 0.8]...)所以大部分像素都是“偏藍”的。用這種方式存儲Normal,可以視為這種法線總是“貼着”模型表面“插”上去的,而不用考慮這根法線到底在世界空間/模型空間的什么地方,又會經過怎么樣的轉換。這樣就可以與各種可能的空間變換操作解耦,而且直觀友好,簡單易懂。

- Tangent Space (切空間) -
那么,什么是切空間?我個人理解,切空間就是針對表面上某個考察的點,以該點的uv二維坐標系表達該點的切線(tangent)和該點的次法線(binormal)所構成的切平面,再加上垂直於它們的法線,就組成了一個可以用來被描述的空間。該空間就是切空間,它的坐標系(三根軸,三個基“basis”)分別是tangent(對應x), binormal(對應y)和normal(對應z)。
這里,比較難理解的是切空間和uv坐標的對應關系。不妨假想現在有一個構造非常復雜的模型,但它的表面(surface)可以像剝鹿皮一樣完整剝開,而它的uv也恰可以以這樣的“比喻”展開。它的法線既處處都垂直於它的表面。現在想象所有的法線都“扎”在這個表面上,然后讓我們把這個表面以展uv的方式剝開並攤平(那么你現在面對的其實是一張uv set)。當你聚焦(focus)到某一個點的法線上時,這根法線正對着你,它的方向就是切空間的z軸。而這張uv set就是“切平面”。連接該點與下一點的坐標u的方向就是該點的切平面的x軸,而連接該點與下一點的坐標v的方向,就是它的切平面的y軸。
出於省事的說法,有的人可能會這樣描述,“切空間下的x軸和y軸就是頂點u,v的坐標” 。如果你聽到某人這么說,你可以揣測他可能是真的是行家,因為這個意思算是“點”對了。但不幸的是,嚴格來說,這種說法是錯誤的。切空間下的x軸方向(切線方向),是該點u坐標指向下一點u坐標的方向;對應地,y軸方向(次法線方向),是該點v坐標指向下一點v坐標的方向。如果你在一張uv set上審視它,也就等於是在一個二維笛卡爾坐標系上審視它,那么得到的x軸方向就是“水平”的,而y軸方向就是“豎直”的。這里想要強調的意思是,無論是切線方向還是次法線方向,它們作為一個方向,勢必是由兩點之間的走向關系決定的。單獨的某點u,v坐標,僅僅是個值而已,單憑一個點的值,既不可能得到切線方向,也不可能得到次法線方向。換成這種說法 “切空間下的x軸和y軸就是頂點所在uv坐標系下的u軸和v軸”, 就對了。
- 求算 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 = dp/du。
同理可以得到binormal = dp/dv。
實際上,由於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)
修改成矩陣形式,就成了下面這樣,
再變換一下,就得到
求解這個式子的細節就不列在這里了。關鍵了解一下如何求一個矩陣的逆矩陣(伴隨矩陣數除其行列式),然后根據矩陣的乘法運算規則就能得到結果。隨着結果的求得,我們也就知道了tangent和binormal的值。
- TBN Matrix (TBN 矩陣)
上節求得了
tangent(簡寫為
T)與
binormal(簡寫為
B),再叉乘一下就得到了
normal(簡寫為
N):即
N =
T X
B。 由此三個向量就可以構成一個空間,[
T, B, N]。

這個矩陣表示的是,切空間(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的作用還不止於此,它的應用很廣泛而且也非常重要。比如做displacement mapping時,基於Vector置換的做法就同樣用到了TBN矩陣。用法和原理與Normal Map都是一樣的,只不過此時Map換成了再切空間下的用來置換的向量,而非法線。
-----------------------
reference :
3.book "pbrt"(2nd edition) section-3.6.2