上一節已經說了 把法線 從模型空間變換大世界空間的兩種方法,然后就可以參與接下來的各種運算了(一般燈光的算等都是在世界空間),但是事情往往沒有想象的那么簡單,我們知道平時在做材質的時候,基本上都會用到法線貼圖。如果我們不用法線貼圖的話,這樣做是可以的,但是想要用法線貼圖,就需要多一些計算。
1.先得到一個“從切線空間到世界空間”的矩陣。
為什么要這個矩陣?我們平時看到的法線貼圖記錄的都是切線空間下的法線信息(實質上是坐標信息),我們想要用這張圖參與接下來的光照計算,其它的如“頂點坐標”““燈光位置”等等都是在世界空間的,自然的這張圖也需要在世界空間才能參與計算。因此需要這個“從切線空間到世界空間”的矩陣,來對這張法線圖進行變換。一般的用的矩陣是:
float3x3 tangentTransform = float3x3(i.tangentDir,i.bitangentDir,i.normalDir);
矩陣的三個參數:切線、副切線、法線,注意三個參數要進行歸一化!
2.對紋理進行采樣
既然是使用紋理,項diffusecolor(basecolor)一樣,需要對紋理進行采樣,用到兩個函數:
TRANSFORM_TEX() //計算貼圖的UV
tex2D()//對紋理進行采樣
當然也可以在一步完成如:
packedNormal = tex2D(_BumpMap,TRANSFORM_TEX(i.uv0,_BumpMap))
3.對“法線貼圖”進行解包(Unpack)操作。
(1)上面說到法線貼圖記錄的是切線空間下的法線信息。而“法線貼圖”本身從名字來看,它本身是張圖,既然是圖,也就是意味著它記錄的是顏色信息(RGB)。所以法線貼圖本身是有一個映射關系的。
法線本身分量是在[-1,1],而法線貼圖(RGB)是以像素為單位的,像素的范圍是[0,1],法線變成法線圖,必然有個映射關系,那就是“pixel = (normal + 1) / 2”。
(2)另外這個公式還可以解釋為什么法線貼圖是淺藍色的,因為法線圖是切線空間下的,而切線空間下的法線基本上都在“頂點的切線空間的Z軸方向”擾動,也就是基本上都是(0,0,1)。帶入公式,像素值=(0.5,0.5,1)
這個值正是淺藍色。
(3)既然法線貼圖要參與下面的計算,就要映射回去,由上面的公式逆推一下,法線=pixel * 2 - 1,帶入公式計算:
//接著上面的代碼:
float3 tangentNormal
tangentNormal.xy = (packedNormal.xy * 2 - 1) * _BumpScale;
tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));
①上面第一句的意思是首先把“法線貼圖”的xy分量按公式映射回法線方向,然后乘以_MumpScale(控制法線強度的值)來得到切線空間下法線的 x y分量。
②第二句意思是:由x y分量,計算Z分量。這是由于法線是單位矢量,所以Z分量可以這么計算。由于我們使用的是切線空間下的法線貼圖,因此可以保證法線的Z分量為正(可以參考下面的圖)
(4)理論上這么做是可以的。但是Unity為了優化貼圖,在進行“反映射”的同時對紋理進行了壓縮優化。具體操作就是把導入的法線貼圖,在其屬性面板中,把選項標記成“NormalMap”這個操作(即使你不標記,Unity也會提醒你的),在shader中的體現就是“UnpackNormal()”函數。這個函數把壓縮貼圖和“反映射”同步進行了,具體的可以參考“UnityCG.cginc”下面的定義:
inline fixed3 UnpackNormalDXT5nm (fixed4 packednormal)
{
fixed3 normal;
normal.xy = packednormal.wy * 2 - 1;
normal.z = sqrt(1 - saturate(dot(normal.xy, normal.xy)));
return normal;
}
inline fixed3 UnpackNormal(fixed4 packednormal)
{
#if defined(UNITY_NO_DXT5nm)
return packednormal.xyz * 2 - 1;
#else
return UnpackNormalDXT5nm(packednormal);
#endif
}
①可以看到“UnpackNormal()”函數包含了“UnpackNormalDXT5nm()函數”,“UnpackNormal()”函數里面只是做了一個簡單的判斷(判斷是否進行紋理壓縮),而真正進行計算的是“UnpackNormalDXT5nm()函數” 其中“DXT5”應該看著很眼熟,它正是壓縮方法的其中一種。
②“具體的細節是:把“w”分量(紋理的a通道)對應法線的 x 分量。g通道對應了法線的y分量,而紋理的r和b通道則會被舍棄,法線的Z分量可以由x y 推導得出。更具體的細節以后再看吧??梢钥吹健癠npackNormalDXT5nm()函數”的輸入參數是“packednormal”,(采樣后的法線貼圖),但是“packednormal”的參數只有RGB,沒有w y 所以可以推斷這個壓縮過程是在外部進行的。而“UnpackNormalDXT5nm()函數”只是對壓縮后的“packednormal”新的RGB進行反映射。
(5)反映射后,如果還想要調節法線的強度,同樣的還需要乘上調節強度的參數(_BumpScale)具體可以這樣寫:
fixed3 tangentNormal;
tangentNormal = UnpackNormal(packedNormal);
tangentNormal.xy *= _BumpScale;
tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));
可以看到只是把(3)手動映射的計算,換成UnpackNormal()操計算。這樣才能保證得到的正確法的法線。
這里還要提一句:不能把得到的“tangentNormal”直接帶入“反映射公式”去計算,如:
tangentNormal = tangentNormal.xyz * 2-1,因為Z分量是由 x y 分量推導出來的。
4.得到反映射的法線后,就可以 用“1”得到的變換矩陣去變換了:
float3 normalDirection = normalize(mul(tangentNormal,tangentTransform));
//用矩陣變換后,執行歸一化
5.到這里,需要用法線貼圖的基本shader的法線變換就講完了。