深入淺出之切空間

tiancaiKG發表於2019-07-04

  這是我以前在其它地方寫的, 轉到這裡來, 這裡的排版比較好看.

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

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

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

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

我們也可以寫成這樣:

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

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

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

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

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

我們可以用TBN矩陣把所有向量從切線空間轉到世界空間,傳給畫素著色器,然後把取樣得到的法線用TBN矩陣從切線空間變換到世界空間;法線就處於和其他光照變數一樣的空間中了。 
我們用TBN的逆矩陣把所有世界空間的向量轉換到切線空間,使用這個矩陣將除法線以外的所有相關光照變數轉換到切線空間中;這樣法線也能和其他光照變數處於同一空間之中。
我們來看看第一種情況。我們從法線貼圖重取樣得來的法線向量,是以切線空間表達的,儘管其他光照向量是以世界空間表達的。把TBN傳給畫素著色器,我們就能將取樣得來的切線空間的法線乘以這個TBN矩陣,
將法線向量變換到和其他光照向量一樣的參考空間中。這種方式隨後所有光照計算都可以簡單的理解。

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

 

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

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

概念: 首先你想要給一個模型提供法線貼圖, 那麼在每一個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就是一個向量, 它還不能稱為法線,
    注意這裡使用了b(z)來作為法線方向的值.
3. i.tangent, binormal, i.normal 代表的就是當前三角面的空間相對於
    LocalSpace的座標系, 其實就是新座標系的x,z,y軸(想象想象), 這樣乘過去
    就相當於把向量轉到(投影到)切空間裡了. 最終值就是該點的LocalNormal.

 

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向量在切空間的方向就是在切空間各個軸上的投影了...
 
 
 
 
 

 

相關文章