學習資料:
為何要用法線貼圖
為了提升模型表現細節而又不增加性能消耗,所以不選擇提高模型的面數,而是給模型的材質Shader中使用上法線貼圖(Normal Map),通過更改模型上的點的法線方向,增加光影凹凸效果,從而提升模型表現細節。使用法線貼圖能使一個三角面(平面)表現出凹凸的視覺效果!
法線貼圖原理
http://www.cnblogs.com/tekkaman/p/3992352.html
上面的文章解釋了很多問題:
- 法線被存儲在切線空間(Tangent Space Normal)中,切線空間以點的法線方向為Z軸,對應了RGB中的B值,所以法線貼圖看上去呈藍色的。如果存儲在世界空間中,則各個方向會表現出不同的顏色值。https://www.zhihu.com/question/23706933/answer/161968056
- 為何不選擇將法線存儲在世界空間中(World Space Normal)或是模型空間中(Object Space Normal)。
- 在使用法線貼圖時,可以將光向量轉換到Tangent Space里做計算,也可以把法線向量轉換到World Space與光向量進行計算,結果是一樣的,但為何選擇使用前一種方法?因為后者每個點都要進行一次空間坐標變換,而由於光向量是平行光,所以前一種方法只需計算一次。
法線貼圖的存儲與使用
法線(Normal)每個軸向的取值范圍為-1到1,而顏色值(Pixel)的取值范圍為0到1。所以在存儲(法線方向存儲為法線貼圖)和使用(在程序中將法線貼圖每個點的顏色轉變為法線方向)時,存在一個簡單的計算轉換過程。
- 存儲法線貼圖 Pixel = ( Normal + 1 ) / 2
- 使用法線貼圖 Normal = Pixel * 2 - 1
因為法線貼圖使用的是切線空間,所以以上轉換也是在切線空間下進行的。使用中還要注意光照方向的空間轉換問題。
例子
現在准備好紋理貼圖和法線貼圖,編寫一個簡單的使用法線貼圖的Shader例子。
Shader "Custom/13-Rock NormalMap" { Properties{ _MainTex("Main Tex", 2D) = "white"{} // 紋理貼圖 _Color("Color", Color) = (1,1,1,1) // 控制紋理貼圖的顏色 _NormalMap("Normal Map", 2D) = "bump"{} // 表示當該位置沒有指定任何法線貼圖時,就使用模型頂點自帶的法線 _BumpScale("Bump Scale", Float) = 1 // 法線貼圖的凹凸參數。為0表示使用模型原來的發現,為1表示使用法線貼圖中的值。大於1則凹凸程度更大。 } SubShader{ Pass { // 只有定義了正確的LightMode才能得到一些Unity的內置光照變量 Tags{"LightMode" = "ForwardBase"} CGPROGRAM // 包含unity的內置的文件,才可以使用Unity內置的一些變量 #include "Lighting.cginc" // 取得第一個直射光的顏色_LightColor0 第一個直射光的位置_WorldSpaceLightPos0(即方向) #pragma vertex vert #pragma fragment frag fixed4 _Color; sampler2D _MainTex; float4 _MainTex_ST; // 命名是固定的貼圖名+后綴"_ST",4個值前兩個xy表示縮放,后兩個zw表示偏移 sampler2D _NormalMap; float4 _NormalMap_ST; // 命名是固定的貼圖名+后綴"_ST",4個值前兩個xy表示縮放,后兩個zw表示偏移 float _BumpScale; struct a2v { float4 vertex : POSITION; // 告訴Unity把模型空間下的頂點坐標填充給vertex屬性 float3 normal : NORMAL; // 不再使用模型自帶的法線。保留該變量是因為切線空間是通過(模型里的)法線和(模型里的)切線確定的。 float4 tangent : TANGENT; // tangent.w用來確定切線空間中坐標軸的方向的。 float4 texcoord : TEXCOORD0; }; struct v2f { float4 position : SV_POSITION; // 聲明用來存儲頂點在裁剪空間下的坐標 //float3 worldNormal : TEXCOORD0; // 不再使用世界空間下的法線方向 float3 lightDir : TEXCOORD0; // 切線空間下,平行光的方向 float3 worldVertex : TEXCOORD1; float4 uv : TEXCOORD2; // xy存儲MainTex的紋理坐標,zw存儲NormalMap的紋理坐標 }; // 計算頂點坐標從模型坐標系轉換到裁剪面坐標系 v2f vert(a2v v) { v2f f; f.position = mul(UNITY_MATRIX_MVP, v.vertex); // UNITY_MATRIX_MVP是內置矩陣。該步驟用來把一個坐標從模型空間轉換到剪裁空間 // 法線方向。把法線方向從模型空間轉換到世界空間 //f.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject); // 反過來相乘就是從模型到世界,否則是從世界到模型 f.worldVertex = mul(v.vertex, unity_WorldToObject).xyz; //f.uv = v.texcoord.xy; // 不使用縮放和偏移 f.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw; // 貼圖的紋理坐標 f.uv.zw = v.texcoord.xy * _NormalMap_ST.xy + _NormalMap_ST.zw; // 法線貼圖的紋理坐標 TANGENT_SPACE_ROTATION; // 調用這個宏會得到一個矩陣rotation,該矩陣用來把模型空間下的方向轉換為切線空間下 //ObjSpaceLightDir(v.vertex); // 得到模型空間下的平行光方向 f.lightDir = mul(rotation, ObjSpaceLightDir(v.vertex)); // 切線空間下,平行光的方向 return f; } // 要把所有跟法線方向有關的運算,都放到切線空間下。因為從法線貼圖中取得的法線方向是在切線空間下的。 fixed4 frag(v2f f) : SV_Target { // 環境光 fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb; // 法線方向。從法線貼圖中獲取。法線貼圖的顏色值 --> 法線方向 //fixed3 normalDir = normalize(f.worldNormal); // 不再使用模型自帶的法線 fixed4 normalColor = tex2D(_NormalMap, f.uv.zw); // 在法線貼圖中的顏色值 //fixed3 tangentNormal = normalize(normalColor.xyz * 2 - 1); // 切線空間下的法線方向,發現計算得到的法線不正確! fixed3 tangentNormal = UnpackNormal(normalColor); // 使用Unity內置的方法,從顏色值得到法線在切線空間的方向 tangentNormal.xy = tangentNormal.xy * _BumpScale; // 控制凹凸程度 tangentNormal = normalize(tangentNormal); // 光照方向。 fixed3 lightDir = normalize(f.lightDir); // 切線空間下的光照方向 // 紋理坐標對應的紋理圖片上的點的顏色 fixed3 texColor = tex2D(_MainTex, f.uv.xy) * _Color.rgb; // 漫反射Diffuse顏色 = 直射光顏色 * max(0, cos(光源方向和法線方向夾角)) * 材質自身色彩(紋理對應位置的點的顏色) fixed3 diffuse = _LightColor0 * max(0, dot(tangentNormal, lightDir)) * texColor; // 顏色融合用乘法 // 最終顏色 = 漫反射 + 環境光 fixed3 tempColor = diffuse + ambient * texColor; // 讓環境光也跟紋理顏色做融合,防止環境光使得紋理效果看起來朦朧 return fixed4(tempColor, 1); // tempColor是float3已經包含了三個數值 } ENDCG } } FallBack "Diffuse" }
效果如下圖,左邊是使用法線貼圖(凹凸參數為1,完全使用法線貼圖中的法線方向),右邊未使用法線貼圖:
注意點:
- TANGENT_SPACE_ROTATION宏的使用。調用這個宏會得到一個矩陣rotation,該矩陣用來把模型空間下的方向轉換為切線空間下。
- ObjSpaceLightDir()方法,得到模型空間下當前點到光源方向的向量,即平行光方向。
- 使用mul(rotation, ObjSpaceLightDir(v.vertex)); 得到切線空間下平行光的方向。
- 在從顏色值轉換為切線空間下的法線方向時,發現用Normal = Pixel * 2 - 1計算得到是法線不正確,效果很奇怪,如下圖。改為使用Unity內置的UnpackNormal()方法來計算。
- 定義了屬性_BumpScale來控制凹凸程度。當該屬性值為0時,使用模型自帶的法線方向;當屬性值為1時,使用完全使用法線貼圖中的法線方向。當值位於0和1之間,則是兩種方向的過度,通過在編輯器監視面板中拖拽修改該屬性值,能夠直觀看到模型的變化效果。當該屬性值大於1,則凹凸程度更加強烈。下圖是當該值設為7時的情況。