1. 程式人生 > >Normal Map中的值, Tangent Space, 求算 Tangent 與 Binormal 與 TBN Matrix

Normal Map中的值, Tangent Space, 求算 Tangent 與 Binormal 與 TBN Matrix

- 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 = u21uv2 - 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

    實際上,由於u21pos2-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/dubinormal = 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換成了再切空間下的用來置換的向量,而非法線。 3.book "pbrt"(2nd edition) section-3.6.2

相關推薦

Normal Map Tangent Space Tangent Binormal TBN Matrix

- Normal Map中的值 - 有沒有想過,Normal Map(法線貼圖)為什麼看上去都是“偏藍色”的?這是因為,在map中儲存的值都是在Tangent Space(切空間)下的。比如,一根正好垂直於表面的法線向量在切空間下是(0,0,1),假如用一個char(注意不是unsigned cha

java List包含List如何新增多個list,Map包含多個list如何新增?

1、List中新增list public class TestList { public static void main(String[] args) { List<List<Integer>> vecvecRes = new ArrayLi

Javamap按照key鍵和value分別排序

參考:https://www.cnblogs.com/zhujiabin/p/6164826.html 1. 按照key排序,可以藉助有序集合TreeMap實現,如下: @Test public void sortByMapKey() { Map<String,Str

JVM記憶體區域劃分Eden SpaceSurvivor SpaceTenured GenPerm Gen

  jvm區域總體分兩類,heap區和非heap區。heap區又分:Eden Space(伊甸園)、Survivor Space(倖存者區)、Tenured Gen(老年代-養老區)。 非heap區又分:Code Cache(程式碼快取區)、Perm Gen(永久代)、Jvm Stack(java虛擬機器棧)

jvm記憶體區域劃分heapEden SpaceSurvivor SpaceSurvivor SpaceCode CachePerm Gen

 jvm區域總體分兩類,heap區和非heap區。heap區又分:Eden Space(伊甸園)、Survivor Space(倖存者區)、Tenured Gen(老年代-養老區)。 非heap區又分:Code Cache(程式碼快取區)、Perm Gen(永久代)、Jvm Stack(

Map四種獲取key和value的方法以及對map的元素排序(轉)

compareto map.entry 排序 區別 sta hashmap 得到 package log 獲取map的值主要有四種方法,這四種方法又分為兩類,一類是調用map.keySet()方法來獲取key和value的值,另一類則是通過map.entrySet()方法來

將一個類的屬性存入MAP(key為屬性名value為屬性

有時候需要將javaBean與Map互轉,寫個方法(只寫了javaBean轉為Map,Map轉為javaBean比較簡單),供大家參考 /** * 獲取obj中的所有方法 * @param obj * @return */ public List<

mybatis傳入參及出參遊標map獲得返回List

      實際開發中,什麼時候會用到儲存過程,像網上說的簡單求和?我覺得不是,那樣簡單的例子根本就不能拿來學習儲存過程,那是一條sql就能搞定的事,沒必要動儲存過程大駕,當我們開發的過程中,遇到很複雜的業務邏輯時,而我們只想傳些引數進去,想動態的得到返回結果,在儲存過程裡

定義Iterator遍歷器取出set的key然後通過key的map取出對應value

//Map介面和HashMap練習 //定義一個Map物件,如下 //Map<String,String> map = new HashMap<String,String>(); //在map中新增鍵值對(“1”,“xiaohong”)、(“2”,”

jQuery使用attributeprop獲取設置input的checked【轉】

attribute 原因 lib size 未定義 software eight pos -h 1、prop方法獲取、設置checked屬性 當input控件checkbox設置了checked屬性時,無論checked=”“或 checked=”checked”,$

均值濾波濾波最大最小濾波

fin proc repeat 效果 mod ava rom static 包含 http://blog.csdn.net/fastbox/article/details/7984721 討論如何使用卷積作為數學工具來處理圖像,實現圖像的濾波,其方法包含以下幾種,均值 濾波

當input的type為file時各瀏覽器的表現形式不同

button tex ati color 不同 ack 需要 標簽 圖片 如果想使各瀏覽器下的表現形式相同,需要對該input元素隱藏,然後再改元素下方添加標簽。其html寫法如下 <div class="input-file"> <input type

快速找出一個數組的兩個數字讓這兩個數字之和等於一個給定的

http 知識 繼續 進一步 repl 有一個 tails 窮舉 too 我覺得寫得很清晰,希望沒有侵犯作者的著作權,原文地址http://blog.csdn.net/hackbuteer1/article/details/6699642 快速找出一個數組中的兩個數字,讓這

WPF多key綁定問題一個key綁定一個界面上的對象

eval vid sha 實現 notify name har 內部實現 arp 問題說明: 當用到dictionary<key,value>來儲存數據的時候,有時候需要在界面上綁定一個key來顯示value,這時候有兩種思路: 一種是寫一個自定義的擴展類,

C++的構造函數拷貝構造函數函數

cpp 區域 操作 兩個 16px size 取值 基於 lan C++中一般創建對象,拷貝或賦值的方式有構造函數,拷貝構造函數,賦值函數這三種方法。下面就詳細比較下三者之間的區別以及它們的具體實現 1.構造函數 構造函數是一種特殊的類成員函數,是當創建一個類的對象時,它被

python 讀取大文件越來越慢(判斷 key 在 map 千萬別用 in keys())

方案 使用 tail 千萬 上傳 true 文件夾 blog alt 背景: 今天樂樂姐寫代碼,讀取一個四五百兆的文件,然後做一串的處理。結果處理了一天還沒有出來結果。問題出在哪裏呢? 解決: 1. 樂樂姐打印了在不同時間點的時間,直接print time() 即可。發

【基礎】結構體重載用 char*作為std::map的key

重載 http 註意 urn .net 參考 article 添加 無法 結構體重載 C++中,結構體是無法進行==,>,<,>=,<=,!=這些操作的,這也帶來了很多不方便的地方,尤其是在使用STL容器的時候,如果我們可以往語句中傳入結構體,一些

全網把Map的hash()分析的最透徹的文章別無二家。

nbsp -i lin 相等 分布 原因 單向鏈表 mas ret 你知道HashMap中hash方法的具體實現嗎?你知道HashTable、ConcurrentHashMap中hash方法的實現以及原因嗎?你知道為什麽要這麽實現嗎?你知道為什麽JDK 7和JDK 8中ha

在i.jsp url地址欄輸入一個參數是整型要求倒著輸出

lang 輸出 spa span url request parameter out getpara <% String k = request.getParameter("k"); int tt = Integer.parseInt(k); out.println(

Java 數組 要求將以上數組的 0 項去掉將不為 0 的存入一個新的數組

pac 數組 system 靜態 args 將不 pub bsp clas package com.xuyigang1234.chp01; public class Demo9 { public static void main(String[] args) {