切線空間
法線紋理用來呈現物體表面的凹凸細節,模型頂點自身的法線定義於模型空間(Object Space)中,模型的法線紋理一般儲存在模型頂點的切線空間(Tangent Space)中,一般的,頂點本身為切線空間原點,選擇頂點法線方向\(n\)為切線空間的正方向\(z\),法線貼圖的\(u\)方向為切線方向\(t\),法線貼圖的\(v\)方向為副切線方向\(b\)。
可以透過三角形頂點的位置和其UV座標推匯出切線方向\(t\)和副切線方向\(b\),如下圖,\(P_i\)為三角形的頂點,\((u_i,v_i)\)為UV座標,其中
\(du_1=u_1-u_0\)
\(du_2=u_2-u_0\)
\(dv_1=v_1-v_0\)
\(dv_2=v_2-v_0\)
\(Q_1=P_1-P_0\)
\(Q_2=P_2-P_0\)
由此可見:
\(Q_1=du_1t+dv_1b\)
\(Q_2=du_2t+dv_2b\)
將上式寫為矩陣形式:
\(\begin{bmatrix} Q{_1}_x&Q{_1}_y&Q{_1}_z\\ Q{_2}_x&Q{_2}_y&Q{_2}_z \end{bmatrix}= \begin{bmatrix} du_1&dv_1\\ du_2&dv_2 \end{bmatrix} \begin{bmatrix} T_x&T_y&T_z\\ B_x&B_y&B_z \end{bmatrix}\)
對\(\begin{bmatrix} du_1&dv_1\\ du_2&dv_2 \end{bmatrix}\)求逆,可得
\(\begin{bmatrix} T_x&T_y&T_z\\ B_x&B_y&B_z \end{bmatrix}= \frac{1}{du_1dv_2-dv_1du_2} \begin{bmatrix} dv_2&-dv_1\\ -du_2&du_1 \end{bmatrix} \begin{bmatrix} Q{_1}_x&Q{_1}_y&Q{_1}_z\\ Q{_2}_x&Q{_2}_y&Q{_2}_z \end{bmatrix}\)
透過對\(T\)和\(B\)規範化後得到切線方向\(t\)和副切線方向\(b\)。
因為三角面是一個平面,只需要計算一個三角面的切線和副切線即可,它們對於三角形的頂點來說都是一樣的,被多個三角面共享的頂點只需要將多個結果平均化即可。
取樣計算
儲存與取樣
紋理顏色\(c\)各分量的範圍為\([0,1]\),而法線向量\(n\)各分量的範圍為\([-1,1]\),故在儲存時需要做\(c=\frac{n+1}{2}\)的對映,在紋理取樣時做\(n=2c-1\)的對映。
法線非統一縮放問題
在模型變換時,跟頂點相關的資訊也需要一併變換,其中包括法線與切線等,由切線空間部分可知頂點的切線透過頂點之間的差值計算得來,根據線性變換的性質,用於頂點變換的矩陣\(M\)(矩陣\(M\)不包含平移)同樣可以用於變換切線向量,但如果\(M\)不是正交矩陣,對變換後的空間可能在某些方向上產生“擠壓”或“拉伸”,所以無法保證其變換的法線向量與原來的表面垂直。
因為切線向量\(t\)與法線向量\(n\)始終保持平行,所以對於同一個頂點來說滿足\(n\cdot t=0\),變換後的\(t'\)和\(n'\)也應滿足該等式,假設頂點的變換矩陣為\(M\),法線向量的變換矩陣為\(G\),則
\(n'\cdot t'=(Gn)\cdot (Mt)=0\)
將\((Gn)\cdot (Mt)\)展開可得
\(\begin{bmatrix} G{_i}_x&G{_j}_x&G{_k}_x\\ G{_i}_y&G{_j}_y&G{_k}_y\\ G{_i}_z&G{_j}_z&G{_k}_z \end{bmatrix} \begin{bmatrix} n_x\\n_y\\n_z \end{bmatrix} \cdot \begin{bmatrix} M{_i}_x&M{_j}_x&M{_k}_x\\ M{_i}_y&M{_j}_y&M{_k}_y\\ M{_i}_z&M{_j}_z&M{_k}_z \end{bmatrix} \begin{bmatrix} t_x\\t_y\\t_z \end{bmatrix}= \begin{bmatrix} n_xG{_i}_x+n_yG{_j}_x+n_zG{_k}_x\\ n_xG{_i}_y+n_yG{_j}_y+n_zG{_k}_y\\ n_xG{_i}_z+n_yG{_j}_z+n_zG{_k}_z \end{bmatrix} \cdot \begin{bmatrix} t_xM{_i}_x+t_yM{_j}_x+t_zM{_k}_x\\ t_xM{_i}_y+t_yM{_j}_y+t_zM{_k}_y\\ t_xM{_i}_z+t_yM{_j}_z+t_zM{_k}_z \end{bmatrix}\)
由此可見
\((Gn)\cdot (Mt)=(Gn)^T(Mt)=n^TG^TMT\)
由於\(n^Tt=n\cdot t=0\),如果\(G^TM=I\),則\(n^TG^TMT=0\),由此可知\(G=(M^{-1})^T\)。
若\(M\)為正交矩陣,則\(M^{-1}=M^T\),可知\((M^{-1})^T=M\),此時可避免求逆和轉置的操作。
在世界空間中取樣計演算法線
在頂點著色器中計算世界空間下的法線向量normalWS
、切線向量tangentWS
和副切線向量bitangentWS
,在計算normalWS
時,變換頂點到世界空間的矩陣\(M_{object\rightarrow world }\)其逆矩陣\(M_{object\rightarrow world }^{-1}\)為unity_WorldToObject
,使用模型空間的法線向量normalOS
左乘\(M_{object\rightarrow world }^{-1}\),相當於右乘其轉置矩陣\((M_{object\rightarrow world }^{-1})^T\)。
half3 normalWS = normalize(mul(input.normalOS, (float3x3)unity_WorldToObject));
half3 tangentWS = normalize(mul((float3x3)UNITY_MATRIX_M, input.tangentOS.xyz));
half3 bitangentWS = cross(normalWS, tangentWS) * input.tangentOS.w;
在片元著色器中取樣切線空間下的法線向量normalTS
,由於紋理格式存在差異,使用Shader Lab中內建函式UnpackNormal()
對儲存的法線進行反對映,然後使用頂點著色器傳入的切線向量tangentWS
、副切線向量bitangentWS
和法線向量構成變換矩陣\(G_{tangent\rightarrow world}\),也即構成新的座標空間,這裡注意half3x3(v1,v2,v3)
是以行向量構建矩陣,故將切線空間的法線向量normalTS
變換至世界空間時,需要使用normalTS
左乘矩陣\(G_{tangent\rightarrow world}\)。
half3 normalTS = UnpackNormal(tex2D(_NormalMap, input.uv));
half3x3 tangentToWorld = half3x3(input.tangentWS.xyz, input.bitangentWS.xyz, input.normalWS.xyz);
half3 normalWS = mul(normalTS.xyz, tangentToWorld);
Unity中的切線設定
Unity預設使用 MikkTSpace 計算切線,在Model Import Settings中可進行設定。
切線最常用於凹凸貼圖著色器中。切線是單位長度的向量,它順著網格表面沿水平 (U) 紋理方向。Unity 中的切線表示為 Vector4, 其“x,y,z”分量定義向量,而 w 用於在需要時翻轉副法線。Unity 計算另一個表面向量(副法線)的方法是獲取法線與切線之間的叉積,然後將結果乘以切線的 w。因此,w 應始終為 1 或 -1。
參考
《3D遊戲與計算機圖形學中的數學方法》
《Unity Shader入門精要》
法線貼圖