在3D物體的模型數(shù)據(jù)里,有一種數(shù)據(jù)叫做法線,它有一個(gè)重要特點(diǎn):垂直于頂點(diǎn)所在的切面。結(jié)合3D軟件來看,法線是以下面一種姿態(tài)分布的。

可以看出,每個(gè)頂點(diǎn)都有其對(duì)應(yīng)的法線,法線的一個(gè)最重要作用就是法線紋理,也就是使用一張紋理貼圖來修改模型表面的法線,從而為模型提供更多的凹凸細(xì)節(jié)。
當(dāng)然,這里的修改,并不會(huì)真正修改模型的頂點(diǎn)位置,只是從視覺上讓模型看起來有凹凸細(xì)節(jié)。
首先我們要知道,法線只是一個(gè)矢量,它只能表示一個(gè)方向上的偏移。要正確表示模型上一個(gè)頂點(diǎn)的凹凸細(xì)節(jié),最起碼要有三個(gè)方向上的矢量,這三個(gè)矢量相互垂直。
我們很自然地就想到了三維坐標(biāo),一個(gè)以頂點(diǎn)作為原點(diǎn),法線為Z軸的三維坐標(biāo)軸,將法線轉(zhuǎn)換到模型空間,然后歸一化,就能得到Z軸方向。
o.normal_dir = normalize(mul(float4(v.normal,0.0),unity_WorldToObject).xyz);
那么另外的X軸、Y軸要如何求出呢?
和法線垂直的叫做切線(tangent),在物體的模型數(shù)據(jù)中保存,直接通過unity內(nèi)置的TANGENT變量獲取即可,切線所在的方向就是Y軸。
struct appdata { float4 tangent: TANGENT; // 切線數(shù)據(jù) };
o.tangent_dir =normalize(mul(unity_ObjectToWorld,float4(v.tangent.xyz,0.0) ).xyz);
Y軸確定了,接下來是X軸。垂直于切線和法線的矢量,在圖形學(xué)里有一個(gè)專業(yè)的術(shù)語叫做雙法線(binormal)或者雙切線(bitangent),這個(gè)有待爭(zhēng)議,這里我就使用雙切線(binormal)的叫法。
要得到雙切線,就需要使用到叉積,叉積的一個(gè)重要應(yīng)用就是根據(jù)兩個(gè)互相垂直的矢量計(jì)算得到一個(gè)同時(shí)垂直于兩個(gè)矢量的新矢量。具體使用大家可以參考這篇文章。
o.binormal_dir = normalize(cross(o.normal_dir,o.tangent_dir))*v.tangent.w;
這樣我們就能得到一個(gè)以頂點(diǎn)原點(diǎn)、法線是Z軸、切線是Y軸、雙切線是X軸的三維坐標(biāo)系,通過操作這三個(gè)方向上的偏移值,就能靈活控制法線紋理的凹凸細(xì)節(jié)了。

要靈活控制法線紋理,首先要獲取到法線紋理貼圖,這個(gè)非常簡(jiǎn)單:
half4 normal_map = tex2D(_NormalMap,i.uv);
但需要注意,法線紋理貼圖的Texture Type 需要設(shè)置為Normal Map

接著,再對(duì)對(duì)紋理進(jìn)行解碼操作:
half3 normal_data = UnpackNormal(normal_map); // 解碼
至于為什么要解碼,我們先來看看UnpackNormal這個(gè)函數(shù)的源碼:
inline fixed3 UnpackNormal(fixed4 packednormal) {
#if defined(SHADER_API_GLES) defined(SHADER_API_MOBILE) return packednormal.xyz * 2 - 1;
#else fixed3 normal; normal.xy = packednormal.wy 2 - 1; normal.z = sqrt(1 - normal.xnormal.x - normal.y * normal.y); return normal;
#endif }
這個(gè)函數(shù)的主要作用,就是對(duì)法線貼圖的xyz數(shù)據(jù)做了一個(gè)乘2減1的操作。
我們要知道,對(duì)法線紋理進(jìn)行采樣,就是將模型每條法線的xyz數(shù)據(jù)對(duì)應(yīng)存入到每個(gè)像素的RGB通道中。
但是歸一化的法線,它的每個(gè)分量范圍都是[-1,1],而像素顏色的通道范圍是在[0,255],要實(shí)現(xiàn)一一對(duì)應(yīng),首先法線分量不能為負(fù),并且范圍還要在[-1,1]之間,所以要將法線的每個(gè)分量加1再除以2,這樣就能將法線分量轉(zhuǎn)換到[0,1]的范圍:

而上述操作的逆過程,就是一個(gè)解包的操作,也就是將法線紋理貼圖中的RGB通道轉(zhuǎn)換為法線分量的過程。所以就有了UnpackNormal函數(shù)中乘2減1的操作。

我們獲取到了法線紋理上RGB所對(duì)應(yīng)的法線分量值,再結(jié)合一開始計(jì)算得出的法線、切線和雙法線。將它們意義對(duì)應(yīng)相乘,就能得到具體的凹凸細(xì)節(jié)了。
normaldir = normalize(tangent_dir normal_data.x + binormal_dir normal data.y + normal_dir * normal_data.z);
還有另外一種更簡(jiǎn)單的法線,但基本原理都是相同的,代碼如下:
float3x3 TBN = float3x3(tangent_dir,binormal_dir,normal_dir);
normal_dir = normalize(mul(normal_data.xyz, TBN));
最后的實(shí)現(xiàn)效果如下:
