深刻淺出之切空間

  這是我之前在其它地方寫的, 轉到這裏來, 這裏的排版比較好看.框架

  添加了新的內容, 好比法線貼圖和切空間的概念等(2019.07.04)spa

----------- 下面首先這是別人寫的切空間的原理, 由於難懂因此我才寫了一個新的版本的在後面 -----------code

法線貼圖中的法線向量在切線空間中,法線永遠指着正z方向。切線空間是位於三角形表面之上的空間:法線相對於單個三角形的本地參考框架。它就像法線貼圖向量的本地空間;
它們都被定義爲指向正z方向,不管最終變換到什麼方向。使用一個特定的矩陣咱們就能將本地/切線空寂中的法線向量轉成世界或視圖座標,使它們轉向到最終的貼圖表面的方向。 咱們能夠說,上個部分那個朝向正y的法線貼圖錯誤的貼到了表面上。法線貼圖被定義在切線空間中,因此一種解決問題的方式是計算出一種矩陣,把法線從切線空間變換到一個不一樣的空間,
這樣它們就能和表面法線方向對齊了:法線向量都會指向正y方向。切線空間的一大好處是咱們能夠爲任何類型的表面計算出一個這樣的矩陣,由此咱們能夠把切線空間的z方向和表面的法線方向對齊。 這種矩陣叫作TBN矩陣這三個字母分別表明tangent、bitangent和normal向量。這是建構這個矩陣所需的向量。要建構這樣一個把切線空間轉變爲不一樣空間的變異矩陣,
咱們須要三個相互垂直的向量,它們沿一個表面的法線貼圖對齊於:上、右、前;已知上向量是表面的法線向量。右和前向量是切線(Tagent)和副切線(Bitangent)向量。
下面的圖片展現了一個表面的三個向量

計算出切線和副切線並不像法線向量那麼容易。從圖中能夠看到法線貼圖的切線和副切線與紋理座標的兩個方向對齊。咱們就是用到這個特性計算每一個表面的切線和副切線的。須要用到一些數學才能獲得它們;請看下圖:

上圖中咱們能夠看到邊紋理座標的不一樣,是一個三角形的邊,這個三角形的另外兩條邊是和,它們與切線向量和副切線向量方向相同。這樣咱們能夠把邊和用切線向量和副切線向量的線性組合表示出來(注意和都是單位長度,在平面中全部點的T,B座標都在0到1之間,
所以能夠進行這樣的組合):

咱們也能夠寫成這樣:

是兩個向量位置的差,和是紋理座標的差。而後咱們獲得兩個未知數(切線T和副切線B)和兩個等式。你可能想起你的代數課了,這是讓咱們去接和。

上面的方程容許咱們把它們寫成另外一種格式:矩陣乘法

嘗試會意一下矩陣乘法,它們確實是同一種等式。把等式寫成矩陣形式的好處是,解和會所以變得很容易。兩邊都乘以的逆矩陣等於:

這樣咱們就能夠解出和了。這須要咱們計算出delta紋理座標矩陣的擬陣。我不打算講解計算逆矩陣的細節,但大體是把它變化爲,1除以矩陣的行列式,再乘以它的共軛矩陣。

有了最後這個等式,咱們就能夠用公式、三角形的兩條邊以及紋理座標計算出切線向量和副切線。

咱們能夠用TBN矩陣把全部向量從切線空間轉到世界空間,傳給像素着色器,而後把採樣獲得的法線用TBN矩陣從切線空間變換到世界空間;法線就處於和其餘光照變量同樣的空間中了。 
咱們用TBN的逆矩陣把全部世界空間的向量轉換到切線空間,使用這個矩陣將除法線之外的全部相關光照變量轉換到切線空間中;這樣法線也能和其餘光照變量處於同一空間之中。
咱們來看看第一種狀況。咱們從法線貼圖重採樣得來的法線向量,是以切線空間表達的,儘管其餘光照向量是以世界空間表達的。把TBN傳給像素着色器,咱們就能將採樣得來的切線空間的法線乘以這個TBN矩陣,
將法線向量變換到和其餘光照向量同樣的參考空間中。這種方式隨後全部光照計算均可以簡單的理解。

以上就是別人寫的攻略, 我表示有看沒有懂, 就本身寫一個吧orm

 

-------------------------- 我是分割線 --------------------------blog

好吧看完我要跪了, 有圖有文, 但是看不懂, 我就來個深刻淺出版本吧:圖片

先說明什麼是法線貼圖 : ip

  法線貼圖就是提供給模型表面做爲其法線的一張貼圖, 或者叫凹凸貼圖, 通常用來提升模型細節, 好比說一個牆壁模型師作成一個簡單Quad, 而後加上凹凸貼圖就能在光照的時候表現出牆面的凹凸效果了.數學

而後什麼是切空間 : it

  法線貼圖提取出來的向量, 它不是一個本地向量, 是基於該點所在的模型的三角面上的, 也就是基於切空間的, 那麼問題來了, 這個法線它的方向是朝向哪裏的? 也就是說這個法線的切空間座標系是哪裏來的呢? 咱們看下圖 : io

  圖中可見, 若是要組成一個在這個面上的座標系, 有無數種, 這些都是切空間座標系.

  咱們按照美術製做的流程 (假) 來解釋比較容易明白, 首先美術作了一個面, 在這個面上要添加凹凸法線, 而後若是他選擇了紅色座標系, 那麼這個點的法線的向量多是(1,2,3), 若是他選擇了綠色的座標系, 這個法線的向量可能就是(2,3,4)了,

他選擇的座標系就叫切空間了. 那麼咱們要怎樣方便快捷地知道切空間呢? 是要美術在模型的每個面上都附加一個座標系信息嗎? 還真是, 請看下圖: 

  一個模型, 它的切空間信息是能夠導入的, 也就是說模型是能夠附帶切空間信息的. 那麼一個切空間信息應該是怎樣的呢? 很簡單, 由於模型的每一個面都是有向量的 (當作Y軸), 那麼咱們再有一個其它的軸 (X軸或Z軸), 而後叉積就能計算出另一個軸了.

因此導入模型的選項中能夠選擇導入Tangents, 這裏的X軸通常被稱爲Tangent軸. Z軸被叫作Bitangent軸. Y軸就是Normal了.

  到這裏你可能就慌了, 知道了切空間的各個軸, 也知道了在切空間中的凹凸法線向量, 那麼怎樣把凹凸法線變換到世界座標系中啊??? 很簡單, 先把凹凸法線轉換到本地座標系, 而後轉換到世界座標系.

好比:

  凹凸法線向量 (r, g, b) 通常使用r對應X軸, g對應Z軸, b對應Y軸, 因此法線貼圖通常偏藍, 就是偏向法線方向.

  切空間各個座標軸向量(切空間相對於本地座標系) : 

    X軸 : (x0, y0, z0) -> Tangent

    Y軸 : (x1, y1, z1) -> Normal

    Z軸 : (x2, y2, z2) -> Bitangent

  那麼轉換到本地座標系就是 localNormal = normalize(Tangent * r + Normal * b + Bitangent * g), 數學理論不用說了, 初中生的知識. 再轉到世界座標系就用 Transform.localToWorldMatrix 計算便可, 很是簡單. 看到這裏就不虛了吧.

  咱們繼續往下看, 原來切空間還能經過計算得出來?

  爲何呢? 若是美術同窗在導出模型的時候沒有導出切空間信息給咱們, 還能經過計算獲得? 計算獲得的跟美術同窗製做時使用的切空間能同樣嗎?

  答案是 : 能夠計算獲得, 計算出來的切空間跟美術製做時使用的是同樣的. 是否是又開始慌了? 不是說一個面上的切空間有無數種嗎? 爲何能逆計算出來呢? 答案就在UV座標中.

前面的文章是假設了T, B兩個三維向量, 使用差值來計算的, 假設有三個點 : 

  P0 (x0, y0, z0) 對應UV(u0, v0)

  P1 (x1, y1, z1) 對應UV(u1, v1)

  P2 (x2, y2, z2) 對應UV(u2, v2)

  那麼假設T,B向量爲正交向量在三角平面上:

  P1-P0 = T * (u1-u0) + B * (v1-v0)

  P2-P0 = T * (u2-u0) + B * (v2-v0)

  根據上面文章的計算, 這個T,B向量是惟一的, 根據現代工程原理, 那麼通常來講美術製做所使用的軟件, 它也是根據模型的頂點位置和UV來給出切空間的, 而後美術同窗就在給出的切空間去作凹凸貼圖, 而不是由他來自定義切空間.

因此切空間是能夠根據逆計算獲得的.

 

下面是從幾何原理來講明切空間: 

先從shader怎樣使用凹凸貼圖開始說, 原理很簡單, 首先你想要給一個模型提供法線貼圖, 那麼在每個Fragment階段都要去取​NormalMap的rgb當作法線來用, 流程以下:

    1. 用uv取出NormalMap相應的rgb做爲tangentNormal, 它的rgb的b值是咱們一般的法線方向. 見圖一
    2. 把這個tangentNormal貼到uv相應的插值點的Local座標位置(圖二), 由於它表現的是這個點的切空間中的法線方向, 必然要轉換到本地座標系, 轉換​以後它就是這個點的LocalNormal了.

   如圖一是tangentNormal的rgb(xyz)方向. 圖二表示這個圖元在模型的一個面上, tangentNormal​在轉換後的方向也​發生了改變.        
    3. 把LocalNormal轉到世界就是該插值點的世界法線了WorldNormal. 完畢.

圖一

 

圖二

 

經過代碼梳理流程, 如下是某老外寫的, 思路很是清晰 :
  1. GetTangentSpaceNormal就是把法線貼圖的向量弄出來
  ​2. 獲取出來的tangentSpaceNormal就是一個向量, 它還不能稱爲法線,    注意這裏使用了rgb的b來做爲法線方向的值.
  3. i.tangent (X軸), binormal (Z軸), i.normal (Y軸) 表明的就是當前三角面的空間相對於, LocalSpace的座標系, 其實就是新座標系的x,z,y軸(想象想象), 這樣跟tangentSpaceNormal的每一個值相乘, 就至關於把向量投影到切空間裏了,

   最終值就是該點的本地座標系的最終法線方向.

 

// 把法線貼圖的向量弄出來
float3 GetTangentSpaceNormal (Interpolators i) { float3 normal
= float3(0, 0, 1); #if defined(_NORMAL_MAP) normal = UnpackScaleNormal(tex2D(_NormalMap, i.uv.xy), _BumpScale); #endif #if defined(_DETAIL_NORMAL_MAP) float3 detailNormal = UnpackScaleNormal( tex2D(_DetailNormalMap, i.uv.zw), _DetailBumpScale ); detailNormal = lerp(float3(0, 0, 1), detailNormal, GetDetailMask(i)); normal = BlendNormals(normal, detailNormal); #endif return normal; }

 

void InitializeFragmentNormal(inout Interpolators i) {

    float3 tangentSpaceNormal = GetTangentSpaceNormal(i);

    #if defined(BINORMAL_PER_FRAGMENT)

        float3 binormal = CreateBinormal(i.normal, i.tangent.xyz, i.tangent.w);

    #else

        float3 binormal = i.binormal;

    #endif

    

    i.normal = normalize(

        tangentSpaceNormal.x * i.tangent +
​        tangentSpaceNormal.z * i.normal + 
        tangentSpaceNormal.y * binormal +       

    );

}

  這裏可能有點沒有說清楚, i.tangent, binormal, i.normal其實都是三角形面上基於LocalSpace座標系的新座標系(切空間), 而它的法線就是i.normal.

 由於NormalMap的b(z)表示的是垂直方向, 因此用tangentSpaceNormal.z * i.normal 來得到在新座標系中法線方向的值. 

FragmentOutput MyFragmentProgram (Interpolators i) {

    float alpha = GetAlpha(i);

    #if defined(_RENDERING_CUTOUT)

        clip(alpha - _AlphaCutoff);

    #endif


    InitializeFragmentNormal(i);

 

  看, 在Fragment中修改法線方向

  在前面的流程梳理中很天然地忽略了一個過程: 怎樣得到Tangent和Bitangent軸.實際上就是得到一個在三角形面上的座標系, 咱們將LocalSpace座標系做爲原始座標系, 而在模型三角形面上的座標系(切空間)就是LocalSpace座標系的子座標系, 
它的每一個軸的描述是用LocalSpace座標系做爲參照的.因此Tangent和Bitangent的計算能夠直接在模型階段就預先計算好, 做爲本地數據存儲便可.

 

  Unity的模型導入就有Tangent計算/導入選項.


  那麼Tangent和Bitangent軸究竟是怎樣計算出來的呢, 以現有數據來看, 咱們只知道三角形面的幾個頂點座標, 以及該面的Normal(法線), 那麼在這個三角形上構建的座標系能夠是無窮多的, 只要符合在面上的兩個正交向量+法線便可,

看下圖 :

  法線(紅)+藍色 或 法線(紅)+綠色 都能構建一個座標系. 法線貼圖獲取的向量在不一樣座標系裏面的方向確定是不一樣的. 要怎樣才能構建惟一正確的切空間座標系呢...回到法線貼圖來,

當把這個貼圖貼在某個模型上時, 好比在下圖中, 噴塗區域貼在了某個三角面上 : 

 

  噴塗區域就是對應的三角面, 那麼就簡單了, 若是咱們把這張2D圖片作成一個3D中的平面的話, 咱們經過拉伸, 平移旋轉等各類方法把對應的三角形區域跟模型上的重疊起來的話,那麼該3D平面的兩個邊就成了Tangent和Bitangent軸了,
理解了的話就能夠 回去看開篇的數學公式了 往下看了. 下圖我把中國地圖貼在了一個三角形上(假設是在模型的本地座標系中), 而後作了一個在這個座標系中的3D平面掛上貼圖.

  我經過各類方法使他們圖片重疊了, 這樣個人3D圖片的兩個邊 ( 固然是UV的正方向)就成爲了切空間的Tangent和Bitangent了(固然計算切空間不可能這樣神手動, 請往下看 ).

  但願這個可以講清楚切空間的邏輯流程...
  PS: 模型每一個頂點都帶有position, uv, 因此計算Tangent這些數據並不依賴於圖片, 不要被上面個人手動誤導了哈

  下來詳細講解數學流程吧...仍是用中國地圖來講: 

 

  在上面的步驟中咱們把地圖的板子跟模型的對應三角形重疊了, 經過手動方式獲取了Tangent向量 (  注意因爲有叉乘的存在, 用Vector3.Cross(Normal, Tangent)就能夠得到Bitangent了, 因此不須要浪費空間去存儲Bitangent,基本不少引擎都不保存Bitangent),
那麼如何經過數學方式快速正確地獲取Tangent呢, 上圖中有幾個變量:
​     T, B 就是Tangent和Bitangent 是咱們要求出來的.
​     P1,P2,P3 就是模型三角形面的三個點了, 他們帶有位置和UV信息.
​    P1{X1, Y1, Z1, U1, V1}
​    P2{X2, Y2, Z2, U2, V2}
​    P3{X3, Y3, Z3, U3, V3}

 

     E1, E2是咱們臨時計算用到的信息, 就是兩點組成的向量
​    E1{P1 - P2} (X, Y, Z)
​    E2{P3 - P2} (X, Y, Z)
​    注意, 這是計算用到的中間變量, 與取哪一個點的前後無關, 與哪一個點的相對位置也無關, 無論怎樣取只要能表現出三角形的任意兩條邊便可.
​     du1, du2, dv1, dv2 分別表示E1, E2表明的向量在uv上的差值
​    注意, 這裏由於要求得的向量只有T,B因此須要兩個行列式便可, 因此上面的數據只取了三角形的任意兩條邊, 以及他們的增量數據du/dv.
​​
​變量就這些, 它已經提供了咱們所需的數據了
​  1. 它有了實際空間中的兩個向量E1, E2
​  2. 它提供了向量增加的方向的參考數據du1, dv1, du2, dv2, 也就是說E1,E2在T,B座標系下是如何增加的(由於UV就是沿着T,B增加的), 反過來也就能夠求出T,B的向量了.
​  PS -- 這裏能夠把T,B座標系當作是有邊界的座標系(UV值就是座標系中的位置所佔的百分比), 以後的計算可以進行全依賴於UV座標是個歸一化數據, 在任何縮放下都不受影響的功勞.

​以後就能夠開始寫等式了​: 

 

  與上圖中的幾何信息徹底相符, 而T, B也寫成向量形式, 由於它被映射到了實際空間裏通過了縮放(參考我手動Tangent的圖), 計算出來的方向是正確的, 最後會取它的歸一化向量.
T, B都是基於LocalSpace空間下的子座標系, 因此能夠用通常向量來表示T, B的軸向 ( 這裏就用上文的轉換公式了 ) : 
 
等式轉爲行列式 : 
 
求T,B向量 : 
而後獲得 : 
 
  到這裏就求出了T(Tx, Ty, Tz) 與B(Bx, By, Bz)的座標軸了, 而NormalMap的向量與Tangent, Bitangent, Normal都同樣屬於LocalSpace座標系, 那麼NormalMap向量在切空間的方向就是在切空間各個軸上的投影了...
 
 以前的文章寫得有點亂, 開始整理一下, 感受腦子被驢踢了(2019.07.04)
相關文章
相關標籤/搜索