1. 程式人生 > 其它 >「UnityShader筆記」07. 在世界空間下基於法線貼圖實現凹凸紋理效果

「UnityShader筆記」07. 在世界空間下基於法線貼圖實現凹凸紋理效果

Part1.程式碼逐段解析

struct a2v{
    float4 vertex : POSITION;  //頂點模型空間位置
    float3 normal : NORMAL;
    float4 tangent : TANGENT;
    float4 texcoord : TEXCOORD0;  //紋理uv
};

在頂點著色器的輸入結構中,我們需要獲取模型空間下頂點的位置、法向量、切向量、紋理uv

struct v2f{
    float4 pos : SV_POSITION; //頂點在裁剪空間的位置
    float4 uv : TEXCOORD0;  //紋理uv
    float4 TtoW0 : TEXCOORD1;
    float4 TtoW1 : TEXCOORD2;
    float4 TtoW2 : TEXCOORD3;
};

在從頂點著色器到片元著色器的通訊結構體中,需要獲取頂點在裁剪空間的位置,經過尺度、偏移變換後的紋理uv座標,以及三組float4型別的值,用來儲存從切線空間到世界空間的變換矩陣,以及頂點在世界空間的位置

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

    o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
    o.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;

    //計算世界空間的頂點座標
    float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;

    //計算世界空間下的法向量
    fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);  

    //計算世界空間下的切向量
    fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz); 

    //法向量與切向量叉乘,再乘以方向因子,得到副切向量
    fixed3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w;

    //將代表x軸的切向量,代表y軸的副切向量,代表z軸的法線方向按列排列,即可得到從切線空間到世界空間的變換矩陣
    //這裡需要將變換矩陣儲存在插值暫存器中,為了節省空間,使用閒置的w分量來儲存世界座標
    o.TtoW0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x);
    o.TtoW1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y);
    o.TtoW2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z);
    return o;
}

在頂點著色器中,我們的主要任務是:

  • 獲取頂點在裁剪空間下的座標,用於片元著色
  • 獲取頂點在世界空間下的座標,用於光照計算
  • 計算紋理uv的尺度變換與偏移
  • 計算從切線空間到世界空間的變換矩陣,用於在片元著色器中將切線空間的法向量變換到世界空間,從而在世界空間下進行光照計算

由於是從切線空間到世界空間的變換矩陣,我們不用做取逆操作,直接將三個座標軸向量按列拼接即可

為了最大化利用插值暫存器的空間,我們將頂點的世界座標的三個分量分別儲存在三個float4型別的w分量中

fixed4 frag(v2f i) : SV_Target{
    //從w分量中提取出世界座標
    float3 worldPos = float3(i.TtoW0.w,i.TtoW1.w,i.TtoW2.w);  

    //使用內建函式獲取世界空間下的光照方向
    fixed3 lightDir = normalize(UnityWorldSpaceLightDir(worldPos));

    //使用內建函式獲取世界空間下的觀察方向
    fixed3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));

    //通過對法線紋理進行取樣,獲取切線空間下的法線
    fixed3 bump = UnpackNormal(tex2D(_BumpMap, i.uv.zw));

    //對法線的x、y分量乘上凹凸程度變換因子
    bump.xy *= _BumpScale;

    //利用法向量是單位向量的特性直接求解出z分量
    bump.z = sqrt(1.0 - saturate(dot(bump.xy, bump.xy)));

    //使用頂點著色器中構造的變換矩陣,將法線從切線空間變換到世界空間
    bump = normalize(half3(dot(i.TtoW0.xyz, bump),dot(i.TtoW1.xyz, bump),dot(i.TtoW2.xyz, bump)));

    //通過世界空間的法向量,進行光照計算,註釋從略(可參照之前的筆記)
    fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb;
    fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
    fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(bump, lightDir));
    fixed3 halfDir = normalize(lightDir + viewDir);
    fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(bump, halfDir)), _Gloss);
    return fixed4(ambient + diffuse + specular, 1.0);
}

在片元著色器中,我們的主要任務是:

  • 通過世界空間的頂點座標,獲取世界空間下的光照方向和觀察方向
  • 對法線紋理進行取樣,得到切線空間下的法向量
  • 通過頂點著色器中構建的變換矩陣,將法向量變換到世界空間
  • 通過法向量進行光照計算

Part2.一些問題

Q1.為什麼要在片元著色器進行法線變換?

因為法線紋理的取樣是必須在片元著色器才能完成的,因此只有在片元著色器才能獲得法線,而必須先獲取法線才能進行法線變換

Q2.為什麼在切線空間實現凹凸貼圖時,沒有通過插值暫存器傳輸變換矩陣?

因為在切線空間中,需要的變換是將光照方向、觀察方向從模型空間變換到切線空間,法線本身就在切線空間,是不需要變換的。而光照方向、觀察方向在頂點著色器就能獲取,因此在頂點著色器構造出變換矩陣後,直接就地對兩向量進行變換

Part3.完整程式碼

Shader "Chapter7/bumpText_World"
{
    Properties
    {
        _Color ("Color Tint", Color) = (1,1,1,1)
        _MainTex ("Main Tex", 2D) = "white" {}
        _BumpMap("Normal Map",2D) = "bump"{}
        _BumpScale("Bump Scale",Float) = 1.0
        _Specular("Specular",Color) = (1,1,1,1)
        _Gloss("Gloss",Range(8.0,256)) = 20
    }

    SubShader
    {
        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;

            struct a2v{
               float4 vertex : POSITION;
               float3 normal : NORMAL;
               float4 tangent : TANGENT;
               float4 texcoord : TEXCOORD0;
            };

            struct v2f{
               float4 pos : SV_POSITION;
               float4 uv : TEXCOORD0;
               float4 TtoW0 : TEXCOORD1;
               float4 TtoW1 : TEXCOORD2;
               float4 TtoW2 : TEXCOORD3;
            };

            v2f vert(a2v v){
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
            
                o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
                o.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;

                float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;

                fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);  

                fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz); 

                fixed3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w;
            
                o.TtoW0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x);
                o.TtoW1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y);
                o.TtoW2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z);
                return o;
            }

            fixed4 frag(v2f i) : SV_Target{
                float3 worldPos = float3(i.TtoW0.w,i.TtoW1.w,i.TtoW2.w);  
                fixed3 lightDir = normalize(UnityWorldSpaceLightDir(worldPos));
                fixed3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));

                fixed3 bump = UnpackNormal(tex2D(_BumpMap, i.uv.zw));
                bump.xy *= _BumpScale;
                bump.z = sqrt(1.0 - saturate(dot(bump.xy, bump.xy)));
                bump = normalize(half3(dot(i.TtoW0.xyz, bump),dot(i.TtoW1.xyz, bump),dot(i.TtoW2.xyz, bump)));

                fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb;
			    fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
			    fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(bump, lightDir));
			    fixed3 halfDir = normalize(lightDir + viewDir);
			    fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(bump, halfDir)), _Gloss);
			    return fixed4(ambient + diffuse + specular, 1.0);
            }

            ENDCG
        }
    }
    FallBack "Diffuse"
}

Part4.效果圖

在切線空間和世界空間下實現的凹凸貼圖,視覺效果是完全一樣的