【Unity Shader】六、使用法線貼圖(Normal Map)的Shader


學習資料:


為何要用法線貼圖

為了提升模型表現細節而又不增加性能消耗,所以不選擇提高模型的面數,而是給模型的材質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時的情況。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM