【UnityShader】凹凸映射之高度貼圖和法線貼圖

基礎知識

  • 紋理的另一種場景的應用就是凹凸映射。凹凸映射的目的是使用一張紋理來修改模型表面的法線,以便爲模型提供更多的細節。這種方法不會真的改變模型的頂點位置,只是讓模型看起來好像是"凹凸不平"的,可以從模型的輪廓處看出“破綻”。

  • 有兩種主要的方法可以用來進行凹凸映射:

    1. 使用一張高度紋理來模擬表面位移,然後得到一個修改後的法線,這種被稱爲高度映射;
    2. 使用一張法線紋理來直接存儲表面法線,這種被稱爲法線映射;
  • 注意:

    • 凹凸映射,從紋理中得到的法線,只會影響光照模型。
    • float4 tangent:TANGENT; // 切線,float4 類型,用tangent.w 分量來決定切線空間中第三個軸——副切線y的方向。
  • 採樣獲取法線

fixed3 bump = UnpackNormal(tex2D(_BumpMap, v.uv.zw));

//上計算等價於
fixed4 packedNormal = tex2D(_BumpMap, v.uv.zw);
fixed3 tangentNormal;
tangentNormal.xy = (packedNormal.xy * 2 - 1) *_BumpScale;
tangentNormal.z = sqrt(1-saturate(dot(tangentNormal.xy, tangentNormal.xy))); 

原因:紋理座標中只記錄 xy,z 需要計算得到。而且 xy 是經過映射的 pixed = (normal + 1) / 2,需要首先進行反映射,然後求 z。

  • 使用 UnpackNormal 函數對法線進行採樣和解碼時,需要把紋理格式標識爲 Normal map。
  • 在 Unity5.x 中,所有的內置 Unity shader 都是用世界空間來進行光照計算。

高度紋理

  • 高度圖中存儲的是強度值,它用來表示模型表面局部的海拔高度。顏色越淺表明該位置的表面越向外凸起,顏色越深表明該位置越向裏凹;這種方法的好處是直觀,但缺點是計算更加複雜,在實時計算時不能直接得到表面法線,而是需要由像素的灰度值來計算而得,因此需要消耗更多的性能。
  • 高度圖通常會和法線映射一起使用,用於給出表面凹凸的額外信息,也就是說,我們通常會使用法線映射來修改光照。

Unity 中的法線紋理類型

  • 當使用包含了法線映射的內置的UnityShader時,必須把使用的法線紋理標識成Normalmap纔能有正確結果。這是因爲UnityShader都是用來內置的UnpackNormal函數來採樣法線方向。
  • 當把紋理類型設置爲Normalmap時,Unity根據不同平臺進行壓縮,再通過UnpackNormal函數來針對不同的壓縮格式對法線紋理進行正確的採樣。可以通過源碼查看
		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;
		}

		// Unpack normal as DXT5nm (1, y, 1, x) or BC5 (x, y, 0, 1)
		// Note neutral texture like "bump" is (0, 0, 1, 1) to work with both plain RGB normal and DXT5nm/BC5
		fixed3 UnpackNormalmapRGorAG(fixed4 packednormal)
		{
		    // This do the trick
		   packednormal.x *= packednormal.w;

		    fixed3 normal;
		    normal.xy = packednormal.xy * 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 UnpackNormalmapRGorAG(packednormal);
		#endif
		}

從代碼中可以看到,在 DXT5nm 格式的法線紋理中,紋素爲 (1, y, 1, x);在 BC5 格式中則爲(x, y, 0, 1) 。

  • 法線紋理座標中只記錄xy,因爲它只有兩個通道是真正必不可少的,第三個通道的值可以用另外兩個推導出來(法線是單位向量,並且切線空間下的法線方向的z分量始終爲證),使用這種壓縮可以減少法線紋理佔用的存儲空間。
  • 當把紋理類型設置爲 Normal map 後,還有一個複選框 Create from Grayscale,這個複選框作用是從高度圖中生成法線紋理。高度圖本身記錄的是相對高度,是一張灰度圖。勾選複選框後,就可以把該紋理和切線空間下的法線紋理同等對待了。
    勾選複選框後
    • Bumpiness 用於控制凹凸程度
    • Filtering 決定使用哪種方式來計算凹凸程度

法線紋理

  • 法線紋理中存儲的就是表面的法線方向。由於法線方向的分量範圍在[-1,1],而像素的分量範圍爲[0, 1],因此需要做一個映射,通常使用的映射就是:

    pixed = (normal + 1) / 2

    這就要求,在 Shader 中對法線紋理進行紋理採樣後,還需要對結果進行一次反映射的過程,已得到原先的法線方向。
    normal = pixed * 2 - 1

  • 模型空間的法線紋理和切線空間的法線紋理

    • 模型空間的法線紋理的優點:
    1. 實現簡單,更加直觀。我們甚至不需要模型原始的法線和切線等信息,計算更少。生成它也非常簡單,而如果要生成切線空間下的法線紋理,由於模型的切線一般是和UV方向相同,因此想要得到效果較好的法線映射就要求紋理映射也是連續的。
    2. 在紋理座標的縫合處和尖銳的邊角部分,可見的突變(縫隙)較少,即可以提供平滑的邊界。這是因爲模型空間下的法線紋理存儲是同一座標系下的法線信息,因此在邊界處通過插值得到的法線可以平滑變換。而切線空間下的法線紋理中的法線信息是依靠紋理座標系的方向得到的結果,可能會在邊緣處或尖銳的部分造成更多可見的縫隙。
    • 切線空間的法線紋理的優點:
    1. 自由度很高。模型空間下的法線紋理記錄的是絕對法線信息,僅可用於創建它時的那個模型,而應用到其他模型上效果就完全錯誤 。而切線空間下的法線紋理記錄的是相對法線信息,這意味着,即便把該紋理應用到一個完全不同的網格上,也可以得到一個合理的效果。
    2. 可以進行UV動畫。比如,我們可以移動一個紋理的UV座標來實現一個凹凸移動效果。
    3. 可以重用法線紋理。比如,一個磚塊,我們可以使用一張法線紋理就可以用到所有的6個面。
    4. 可壓縮。由於切線空間下的法線紋理中發現的Z方向總是正方向,因此我們可以僅存儲XY方向,而推導得到Z方向。而模型空間下的法線紋理由於每個方向都是可能的,因此必須存儲3個方向的值,不可壓縮。
  • 計算光照模型,需要統一各個方向矢量所在的座標空間。由於法線紋理中存儲的法線是切線空間下的方向,所以有兩種選擇:

    1. 在切線空間進行光照計算,把視角方向、光照方向切換到切線空間下;
    2. 在世界空間進行光照計算,把採樣得到的法線方向變換到世界空間下,在和世界空間下的光照方向和視角方向進行計算;
    3. 比較
      • 從效率上說,第一種方法優於第二種方法,因爲可以在頂點着色器就完成對光照和視角方向的變換,而第二種方法由於先對法線紋理進行採樣,所以變換過程必須在片元着色器中實現,這意味着需要在片元着色器中進行一次矩陣操作。
      • 從通用性來說,第二種方法優於第一種方法,還需要在世界空間進行其他計算。
  • 在切線空間下計算

    1. 思路
      在片元着色器中通過紋理採樣得到切線空間下的法線,然後在與切線空間下的視角方向、光照方向等進行計算,得到最終的光照結果。
    2. 實現
      首先需要在頂點着色器中把視角方向和光照方向從模型空間變換到切線空間中,即需要知道從模型空間到切線空間的變換矩陣。這個矩陣的逆矩陣,從切線空間到模型空間的變換矩陣,在頂點着色器中按切線(x軸)、副切線(y軸)、法線(z軸)的順序按列排列即可得到。如果一個變換中僅存在平移和旋轉變換,那麼這個變換的逆矩陣就等於它的轉置矩陣,而從切線空間到模型空間的變換正式符合這樣要求的變換。因此,,從模型空間到切線空間的變換矩陣就是從切線空間到模型空間的變換矩陣的轉置矩陣,把切線(x軸)、副切線(y軸)、法線(z軸)的順序按行排列即可得到。
Shader "Custom/s7_2"
{
    Properties
    {
        // 紋理貼圖代替漫反射
        _Color("Color",color)=(1,1,1,1)
        _MainTex("Main Tex",2D)="white"{}
        // 高光反射
        _Specular("Specular",color)=(1,1,1,1)
        _Gloss("Gloss",Range(0,20))=20
        // 凹凸映射
        _BumpMap("Bump Map",2D)="bump"{} // bump是Unity內置的法線紋理,當沒有提供任何法線紋理時,bump對應了模型自帶的法線信息
        _BumpScale("Bump Scale",Range(0,1))=0.5 // 控制凹凸程度,爲0時,意味着該法線紋理不會對光照產生任何影響
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 200
        Pass
        {
            Tags {"LightMode"="ForwardBase"}
            CGPROGRAM
            #pragma vertex vert 
            #pragma fragment frag 
            #include "Lighting.cginc"

            fixed4 _Color;
            sampler2D _MainTex;
            float4 _MainTex_ST;
            fixed4 _Specular;
            float _Gloss;
            sampler2D _BumpMap;
            float4 _BumpMap_ST;
            float _BumpScale;

            /*
            * 切線空間下計算,需要把視角方向、光照方向變換到切線空間下
            * 已知模型空間下視角方向、光照方向,
            * 已知切線空間的三個軸在模型空間的表示x軸(切線)、z軸(法線),可以叉乘求得y軸(副切線)
            * 從模型空間變換到切線空間,躺着,即按行展開
            */
            struct a2v
            {
                float4 position:POSITION;
                float3 normal:NORMAL; // 法線
                float4 tangent:TANGENT; // 切線
                float3 texcoord:TEXCOORD0; // 第一組紋理座標
            };
            struct v2f
            {
                float4 pos:SV_POSITION; // 頂點座標變換
                float3 tangLightDir:TEXCOORD0; // 光照方向,從模型空間變換到切線空間
                float3 tangViewDir:TEXCOORD1; // 視角方向
                float4 uv:TEXCOORD2;
            };

            v2f vert(a2v v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.position);

                o.uv.xy = TRANSFORM_TEX(v.texcoord, _MainTex);
                o.uv.zw = TRANSFORM_TEX(v.texcoord, _BumpMap);
                // 法線,切線得到y
                float3 y = cross(normalize(v.normal), normalize(v.tangent.xyz)) * v.tangent.w; 
                // 構建從模型空間到切線空間的矩陣
                fixed3x3 trans = fixed3x3(v.tangent.xyz, y, v.normal);
                // 空間變換
                o.tangLightDir = mul(trans, ObjSpaceLightDir(v.position));
                o.tangViewDir = mul(trans, ObjSpaceViewDir(v.position));
                return o;
            }

            fixed4 frag(v2f v):SV_Target
            {
                // 歸一化
                fixed3 tangLightDir = normalize(v.tangLightDir);
                fixed3 tangViewDir = normalize(v.tangViewDir);

                // 紋理採樣
                fixed4 packedNormal = tex2D(_BumpMap, v.uv.zw);
                fixed3 tangentNormal;
                // 如果紋理圖不是 normal map
                // tangentNormal.xy = (packedNormal.xy * 2 - 1) *_BumpScale;
                // tangentNormal.z = sqrt(1-saturate(dot(tangentNormal.xy, tangentNormal.xy))); 
                // 或者標識爲 Normal map
                tangentNormal = UnpackNormal(packedNormal);
                // 如果沒有 _BumpScale, 下面兩步可以不執行
                tangentNormal.xy *= _BumpScale;
                tangentNormal.z = sqrt(1-saturate(dot(tangentNormal.xy, tangentNormal.xy))); 

                // 反射率
                fixed3 albedo = tex2D(_MainTex, v.uv).rgb * _Color.rgb; 
                // 環境光
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
                // 漫反射
                fixed3 diffuse = _LightColor0.rgb * albedo * saturate(dot(tangentNormal, tangLightDir));
                // 高光反射
                fixed3 halfDir = normalize(tangViewDir + tangLightDir);
                fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(tangentNormal, halfDir)), _Gloss);
                return fixed4(ambient + diffuse + specular, 1);
            }

            ENDCG
        }
    }
    FallBack "Diffuse"
}
  • 世界空間下計算光照模型,需要在片元着色器中把法線方向從切線空間變換到世界空間下。基本思路:在頂點着色器中計算從切線空間到世界空間的變化矩陣,並把他們傳遞給片元着色器。變換矩陣的計算可以由頂點的切線、副切線和法線在世界空間下的表示來得到。
Shader "Custom/s7_2_w"
{
    Properties
    {
        _Color("Color",color)=(1,1,1,1)
        _MainTex("Main Tex",2D)="white"{}
        _BumpMap("Bump Map",2D)="bump"{}
        _BumpScale("Bump Scale", Range(0, 1)) = 1
        _Specular("Specular",color)=(1,1,1,1)
        _Gloss("Gloss",Range(8,255))=50
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 200
        Pass
        {
            Tags{"LightMode"="ForwardBase"}
            CGPROGRAM
            #pragma vertex vert 
            #pragma fragment frag 
            #include "Lighting.cginc"

            fixed4 _Color;
            sampler2D _MainTex;
            float4 _MainTex_ST;
            
            sampler2D _BumpMap;
            float4 _BumpMap_ST;
            float _BumpScale;

            fixed4 _Specular;
            float _Gloss;
            /*
            * 切線空間中的法線映射
            * 在世界空間中計算光照模型,視角方向、光照方向易得
            * 需要把採樣得到的切線空間中的法線變換到世界空間,已知模型空間下切線空間的 x軸(切線)、z軸(法線),可得 y 軸(副切線),將它們變換到世界空間下
            * 即可得到在世界空間中切線空間的3個軸,從切線空間到法線空間,需要站着,即按列展開
            */
            struct a2v
            {
                float4 position:POSITION;
                float3 normal:NORMAL;
                float4 tangent:TANGENT;
                float3 texcoord:TEXCOORD0;
            };

            struct v2f
            {
                float4 pos:SV_POSITION;
                float4 uv:TEXCOORD0;
                float4 tangx:TEXCOORD2;
                float4 tangy:TEXCOORD3;
                float4 tangz:TEXCOORD4;
            };
            // 從切線空間變換到世界空間,已知在模型空間中的 x軸 切線,z軸 法線
            v2f vert(a2v v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.position);
                o.uv.xy = TRANSFORM_TEX(v.texcoord, _MainTex); // 代替漫反射
                o.uv.zw = TRANSFORM_TEX(v.texcoord, _BumpMap); // 法線

                float3 worldPos = mul(unity_ObjectToWorld, v.position).xyz;
                fixed3 worldtang = UnityObjectToWorldDir(v.tangent.xyz);
                fixed3 worldnormal = UnityObjectToWorldNormal(v.normal);
                fixed3 worldbinormal = cross(worldnormal, worldtang) * v.tangent.w;// w分量控制方向

                // 充分利用插值寄存器的存儲空間,把世界空間下的頂點位置存儲在變量的 w 分量中
                o.tangx = float4(worldtang.x, worldbinormal.x, worldnormal.x, worldPos.x);
                o.tangy = float4(worldtang.y, worldbinormal.y, worldnormal.y, worldPos.y);
                o.tangz = float4(worldtang.z, worldbinormal.z, worldnormal.z, worldPos.z);
                return o;
            }

            fixed4 frag(v2f v):SV_Target
            {
                float3 worldPos = float3(v.tangx.w, v.tangy.w, v.tangz.w);
                // 光照方向
                fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(worldPos));
                // 視角方向
                fixed3 worldViewDir = normalize(UnityWorldSpaceViewDir(worldPos));

                // 計算法線,從切線空間變換到世界空間
                fixed3 bump = UnpackNormal(tex2D(_BumpMap, v.uv.zw));
                bump.xy *= _BumpScale;
                bump.z = sqrt(1 - saturate(dot(bump.xy, bump.xy)));
                bump = normalize(half3(dot(v.tangx.xyz, bump), dot(v.tangy, bump), dot(v.tangz, bump)));

                // 替代漫反射的紋理採樣
                fixed3 albedo = tex2D(_MainTex, v.uv).rgb * _Color.rgb;
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
                fixed3 diffuse = _LightColor0.rgb * albedo * saturate(dot(bump, worldLightDir));

                // 高光反射
                fixed3 halfDir = normalize(worldLightDir + worldViewDir);
                fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(bump, halfDir)), _Gloss);
                return fixed4(ambient + diffuse + specular, 1);
            }
            ENDCG
        }
    }
    FallBack "Diffuse"
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章