//
請注明出處:http://blog.csdn.net/BonChoix,謝謝~)
切線空間(Tangent Space)
切換空間,同局部空間、世界空間等一樣,是3D圖形學中眾多的坐標系之一。切換空間最重要的用途之一,即法線映射(Normal Mapping)。關於法線映射的細節,將在下一篇文章中詳細介紹。但在學習法線映射之前,深刻地理解切換空間非常重要。因此借這一篇文章來學習下它,以為后面學習法線映射、視差映射(Parallax Mapping)、Displacement Mapping等技術作准備。Parallax mapping、Displacement Mapping都屬於bump mapping范疇,而且都基於Normal Mapping, 但相比Normal Mapping,后兩種方法可以提供更加逼真物體表面的凹凸感。
1. 為什么要有切線空間?
在3D世界中定了如此多的坐標系,每個坐標系當然都有它的用途。比如局部空間,或者叫模型空間,它的目的就是方便我們對3D模型進行建模。在這個空間中,我們不需要考慮該模型在場景中可能出現的位置、朝向等眾多細節,而專注於模型本身。在世界空間中,我們關心的問題是場景中各個物體的位置、朝向,即如何構建場景,而不必關注攝像機的觀察位置及其朝向。可見,一個坐標系的根本用途,即讓我們在處理不同的問題時,能夠以合適的參照系,拋開不相關的因素,從而減小問題的復雜度。
直觀地講,模型頂點中的紋理坐標,就定義於切線空間。普通2維紋理坐標包含U、V兩項,其中U坐標增長的方向, 即切線空間中的tangent軸,V坐標增加的方向,為切線空間中的bitangent軸。模型中不同的三角形,都有對應的切線空間,其tangent軸和bitangent軸分別位於三角形所在平面上,結合三角形面對應的法線,我們稱tangant軸(T)、bitangent軸(B)及法線軸(N)所組成的坐標系,即切線空間(TBN)。
如下圖所示:
在立方體中,每個面都有對應的切線空間,每個面由兩個三角形組成,該兩個三角形中的紋理坐標就基於相應的切線空間。
2. 紋理坐標與位置坐標的關系
紋理坐標與位置坐標,可以通過切線空間聯系起來。如下圖所示:
該圖顯示了一個三角形及其所在的切線空間。已知該三角形三個頂點的位置坐標:V0, V1,V2, 以及對應的紋理坐標:(u0,v0,), (u1, v1), (u2, v2)。 定義三角形的兩條邊為E0 = V1 –V0,E1= V2 – V0,對應的紋理坐標差值:(t1, b1) = (u1 – u0, v1– v0), (t2, b2) = (u2 – u0, v2– v0)。 我們有如下關系式:
E0 =t1T+ b1B
E1 = t2T+ b2B
3. 切線坐標系的求法
有了以上紋理坐標與位置坐標的關系,我們便可以根據已知的信息,自己來求得任一三角形的切線坐標系了。在3D模型文件中,所有頂點的位置坐標、紋理坐標、法線等信息一般都會提供的,但卻缺少切線坐標系相關信息。而在應用Normal Mapping等技術時,切線空間又是必不可少的,因此就需要我們自己手動來獲取切線坐標系了。很多讀取模型的庫都提供了生成切線空間的功能,不過了解一下其是如何生成還是很有必要的。下面我們就來一步步地推導下切線空間的求法:
繼續從上面的紋理坐標與位置坐標的關系公式出發,把它表示成矩陣形式為:
把E0,E1,T,B拆成分量形式,即:
把移到另一邊,有:
根據矩陣知識,對於矩陣, 其逆矩陣為:
因此以上公式可以進一步表示為:
至此,等號右邊的數據都是已知的,因此左邊的矩陣即可求得,從而得到切線空間中的T、B兩軸。N軸即三角形面的法線,很容易求得。
4. 注意
現在我們根據三角形的頂點位置坐標與紋理坐標,求得了該三角形所在的切線空間。但有一點要注意,這里求得的T向量和B向量一般不是標准化的(長度不為1)。這與一般其他的坐標系有所區別。在局部空間、世界空間、視角空間中,其對應的X、Y、Z軸長度都為1,究其原因主要是這幾個空間中的坐標所使用的度量單位都是一樣的。而在切線空間中,針對的是紋理,而紋理坐標與位置坐標顯然使用不同的度量單位,比如對於紋理坐標從0到1的變化,其對應的位置坐標變化是不確定的。
因此,這里求得T和B向量長度一般不為1, 而且對於有紋理坐標變換的情形,T和B兩軸甚至不會相互垂直。
但是,在大多數情況下,我們只需要標准化后和T、B、N向量,而不關心其相應的長度。比如在Normal Mapping中,我們使用TBN坐標系的目的只是為了把從Normal Map中得到的法線從切線空間轉換到世界空間,與紋理坐標無任何關聯,因此這里我們使用的TNB坐標系的三個軸全是准備化的。
5. 頂點的切線空間
上面的方法求到的切線空間是基於單個三角形的,而在3D管線中,我們的處理是基於頂點進行的。因此我們需要獲得頂點對應的切線空間。不過有了每個三角形的切換空間,每個頂點的切線空間就很容易處理了,即對於任一頂點,我們使用其所在的所有三角形所對應的切線空間向量的平均值,作為該頂點的切線空間。熟悉法線求法的可能會發現,這種方法與通過三角形法線求取頂點法線的方法思路是完全一樣的。
切線空間的加入,使得我們的頂點定義也要發現相應的更改了。切線空間包括TBN三個向量,大多數情況下,我們使用的切線空間這三個向量都是准備化且相互垂直的,因此,對於每個頂點,我們只要提供T、N兩個向量即可,在運行時通過向量叉乘臨時地計算B向量即可,這樣也節省了每個頂點的數據量。
現在,我們的頂點的定義如下所示:
struct };
這正是我們在GeometryGens.h文件中生成常見幾何體時統一使用的頂點格式。在之前的程序中,我們從未使用過tangent成員,其實它就是為后面學習Normal Mapping而准備滴~
法線映射
1. 為什么使用法線映射?
在開始正式討論法線映射之前,先來看下以下兩張圖片:
這兩張依然是之前一篇文章中用到的仙劍五前傳中兩張截圖,兩圖中顯示的為同一地點,不同的觀察角度。在左邊的圖中,根據紋理圖,它給人一種很粗糙的岩石壁的感覺,但右邊圖中卻出現了強烈的高光反射。這顯然是有點相互矛盾的,因為強烈的全反射只有在表面比較光滑的表面上才會出現,而從左圖中來看,它應該是凹凸不平的。造成這種現象的原因很簡單:紋理的使用給我們帶來了像素級別上的物體表面的細節,而模型本身是由有限個頂點組成的,這樣在像素着色器中,經過插值計算得到的各像素的法線是平滑過渡的,而不再是各像素本身應該有的法線值。這樣平滑過渡的法線在經過光照計算后,就很容易造成這種比較明顯的高光反射現象。
要修正這種現象,根本問題在於修改像素的法線值,使其與真實法線趨於一致,這樣在光照計算后將會得到與實際逼近的結果。要實現這種效果,有兩種方法:一種是增加模型的細節,即頂點個數,這樣就可以為模型表面指定更多的法線,而不再是在像素着色階段依賴簡單的插值計算得到,以得到更加真實的效果。這種方法是可行的,但有一個缺陷,更多的頂點意味着更大的計算量,因為在頂點着色器中,每個頂點都要經過各自的各種矩陣變換。因此這種方法能夠提供的細節程度是有限的,一般不足以滿足我們的要求。 另一種效率非常高而且效果很好的方法,即這篇文章的主題:Normal Mapping。
2. 法線貼圖 及其 數據格式
在Normal Mapping技術中,需要使到到一張紋理。與普通紋理不同的是,這張紋理中的每個像素(texel)存放的並不是顏色值,而是法線,因此也稱之為法線貼圖(Normal Map)。我們知道,紋理的使用給基於頂點的幾何模型帶來了像素級別上的細節,同樣,法線貼圖的使用,使用我們能夠得到模型表面在像素級別上的法線值,這樣的法線值是直接通知讀取紋理獲得的,而不再是經過插值得到,因此可以根據現實需求由美工們靈活設定,以獲得想要的逼真效果。
在數據存放格式上,法線貼圖與普通貼圖並無差別,依然是RGB格式或者RGBA格式。只是這里的R、G、B、A不再是顏色的不同分量,而是三維法線向量的各分量。R、G、B分別代表法向量的X、Y、Z分量,如果是RGBA格式,則一般可以用A分量來存放高度信息。這個高度信息也是非常有用的,在很多地方需要與法線值配對使用,比如Parallax Mapping(后面介紹)。這里我們主要來關注RGB分量。一般情況下各分量占8個位(無符號),因此取值區間位於[0, 255]。但在實際情況下,經過歸一化的法線向量,總長度為1,因此各分量都位於[-1, 1]之間。因此要想使用8位來存放,需要把[-1, 1]范圍映射到[0, 255]之間。 方法其實很簡單,令x為[-1, 1]中任意值,通過y = (x + 1) /2 * 255,即得到了位於[0, 255]之間的y。相反,對於從貼圖中讀取到的y值,我們可以通過反向變換:x = 2*y / 255 - 1,得到我們想要的范圍區間內的值。
在HLSL中,通過內置函數 Sample(與之前讀取紋理的函數一樣),我們可以直接得到位於[0, 1]之間的數據,因此我們只需要進行2*x - 1的變換即可。如下所示:
normal = g_normalMap.Sample(samplerTex, pin.tex).rgb;
- normal = 2 * normal - 1;
3. 切線空間 到 世界空間
通過Sample函數,我們得到了任意像素上對應的法線值,下一步就可以利用這個法線來進行光照計算了。但實際上,這時得到的法線是不能直接用於光照計算的,而需要先進行相應的空間變換。這就是上一節中提到的“切線空間”的用途了。在上面提到的法線貼圖中,里面所存放的法線值正是位於切線空間內,而場景中所提供的光源位於世界空間。要想進行正確的光照計算,需要把光源和法線轉換到同一個空間中進行,要么統一位於切線空間,要么統一位於世界空間。(這里我們統一在世界空間進行光照計算)
關於在切線空間定義法線的目的,在這里我再進行一下補充。如果對切線空間還不是很理解,按我的經驗,可以這樣來理解,即把切線空間類比為3D世界中的局部空間。之所以要有局部空間,就是方便在制作模型時能夠只專注於模型本身,而不必考慮模型在場景中可能出現的各種位置及朝向。在不同的位置、朝向下,模型中針對同一個頂點而言,其位置等信息是不一樣的,如果沒有局部空間,就需要為每種情況制作不同的模型,這樣顯然會很麻煩,甚至不可能,也很浪費,因為這些不同位置、朝向處的模型本質上是同一種。如果使用局部空間來定義模型,而通過為場景中不同模型指定各自的世界變換,就很容易地能夠實現單個模型的重復利用。 同樣,法線貼圖也是一個道理。一個模型的不同部位,甚至多個模型之間,可能會具有同樣特點的表面,但顯然由於其位置、朝向的不同,這些表面針對同一處的法線也是不一樣的。比如一個正方體的六個表面,可以具有完全類似的特點,但各個面朝向不同,對應的法線也不再一樣。沒有切線空間,將不得不對每個表面制定單獨的法線貼圖,很浪費。如果把法線定義在切線空間,而針對每個面,都有其相應的切線空間,這樣將可以使用同一張法線圖來用於六個面。
那么,對於任意像素,從哪里可以獲取其對應的切線空間呢?這時就要用到新的頂點格式,切線空間的信息正是通過在輸入階段由頂點傳過來的,在像素着色階段,每個像素對應的切線空間通過其所在三角形的三個頂點的切線空間進行插值得到。新的頂點格式即上節最后給出的:
struct
- float3 normal : NORMAL;
- float2 tex : TEXCOORD;
- };
這里每個頂點除了位置坐標、紋理坐標外,還存放了法線、切線向量。而切線空間TBN需要三個向量。不過這里為了節省資源占用,只提供了切線與法線信息,另外一維bitangent可以在運行時通過該兩向量的叉乘得到。相應的HLSL代碼如下所示:
//Get TBN space float3 B = cross(N, T);
注意這里的法線不再是我們后面進行光照計算用到的法線,光照計算所用到的所有法線都是從法線圖中獲取的,這里的法線只是用來代表該頂點所在切線空間。
代碼中第一行直接獲取切線空間的法線部分(N),注意前提是要保證法線是已經經過歸一化的。
第二行的目的是獲取切線空間的切線部分(T),這里使用了一點小技巧,主要是為了保證切線與法線的相互垂直關系。因為在經過頂點着色器中的世界變換后,原本相互垂直的T與N可能由於精度的關系而不再垂直,這里需要來對它們進行一下修正,以相互滿足垂直關系。方法即如上:normalize(pin.tangent - N * pin.tangent * N);
第三行通過對N、T進行叉乘,從而得到了切線空間的bitangent向量。注意進行叉乘的N和T的先后順序!絕對不能是T x N!
有了切線空間的三個向量,我們也就得到了從切線空間到世界空間的轉換矩陣了,即:
有了這個矩陣,我們繼而可以很方便把從法線圖讀取到的法線轉換到世界空間中了:
float3x3 T2W = float3x3(T, B, N);
- normal = g_normalMap.Sample(samplerTex, pin.tex).rgb;
- normal = normalize(mul(normal, T2W));
第一行通過切線空間T、B、N向量直接得到從切線空間到世界空間的變換矩陣。float3x3類型有接受三個三維行向量的構造函數,以利用三個行向量獲得一個3x3矩陣。
第二行即之前介紹的把法線的每一維從[0, 1]區間轉換到[-1, 1]區間。
第三行通過剛得到的切線空間到世界空間的矩陣T2W(Tangent to World),把讀取到的法線轉換到世界空間。
4. 小結 及 完整的pixel shader
好了,到這步為止,我們夢寐以求的法線得到了,法線映射的所有工作也就此結束了~
之后所有的像素着色器代碼與之前的完全一樣。本質上講,上面介紹到的所有這些內容,其實特等效於以前代碼中的:
float3 normal = normalize(pin.normal);
后面要做的即使用這個法線進行光照計算、紋理處理、霧效等過程了。不同之處僅僅是這里通過從紋理中讀取法線來代替了之前直接從像素中獲取經過插值的法線。
為了更清晰地展示法線映射在像素着色器中的應用,這里給出完整的pixel shader:
注意:這段代碼中還有一些尚未進行介紹的內容,比如parallax mapping(法線映射的進階),shadow mapping(生成陰影的常用算法之一)。暫時可以把這些內容忽略,以更好的關注normal mapping部分。
//Pixel shader numLights,
- uniform useTexture,
- alphaClipEnable,
- uniform useNormalMap,
- useParallaxMapping,
- uniform useShadowMap,
- pcfShadowEnable,
- uniform useReflection,
- fogEnable
- ): SV_TARGET
- dist = length(toEye);
- float3 normal = normalize(pin.normal);
- (useParallaxMapping)
- height = g_normalMap.Sample(samplerTex,pin.tex).a;
- float3x3 W2T = transpose(float3x3(T,B,N));
- float2 offset = toEyeTangent.xy * height;
- (useNormalMap)
- normal = normalize(mul(normal, T2W));
- (useTexture)
- (numLights > 0)
- {
- float3 shadowFactor = {1.f, 1.f, 1.f};
- (useShadowMap)
- {
- (pcfShadowEnable)
- ( i=0; i<numLights; ++i)
- {
- color = color * (ambient + diffuse) + specular;
- (fogEnable)
- factor = saturate((dist - g_fogStart) / g_fogRange);
- (useReflection)
- {
- color;
- }
5. 示例程序
好了,法線映射的基礎就介紹到這兒,最后是附帶的一個簡單示例程序,用於展示法線映射的效果。 該示例程序中場景極其簡單,僅僅是一個地面,加一個可以自由行走的照相機。可以通過按鍵1 -> 6來開啟、關閉不同的效果,從而對使用法線映射與不使用法線映射的差別有更直觀的感受。(按鍵‘3’ 和 ‘6’分別針對parallax mapping,暫時可以不管)。 以下是幾張運行截圖,懶得下載代碼的話可以從這兒的圖中來感受下區別:
1. 僅僅光照計算下的一個平面地板:(按鍵 ‘1’)
2. 光照計算 + 法線貼圖: (按鍵‘2’)
3. 光照計算 + 紋理, 不使用法線貼圖: (按鍵 ‘4’)
4. 光照計算 + 紋理 + 法線貼圖: (按鍵 ‘5’)
http://m.blog.csdn.net/Game_jqd/article/details/74858146