UnityShader(五):紋理之法線紋理、單張紋理及遮罩紋理的實現(xiàn)

在游戲中,我們除了能看到游戲物體的形體輪廓,還能看到物體的一些具體外觀,包括顏色,凹凸等。而實現(xiàn)這一步的就是使用** 紋理**。與紋理相對應(yīng)的技術(shù)就是 紋理映射技術(shù) ,相當(dāng)于把一張圖貼在物體表面,然后 逐紋素 地控制顏色。

紋理映射坐標:紋理映射坐標定義了一個頂點在紋理中對應(yīng)的2D坐標。由于常用 U 來表示橫向坐標, V 來表示縱向坐標,所以紋理映射坐標也是我們常常見到的 UV坐標。 頂點 UV 坐標通常會被歸一化至 【0,1】范圍內(nèi)。當(dāng)然紋理采樣時使用的坐標也不一定在這個范圍內(nèi)。

另外值得注意的是,OpenGL 與 DirectX 的二維坐標系是不一樣的,OpenGL中原點位于左下角,DirectX原點位于左上角。當(dāng)然Unity 會幫我們處理這個差異,同時一般情況下,Unity 采用的紋理空間是符合 OpenGL傳統(tǒng)的。

image

需要注意的是:本文著重講述紋理采樣的原理,由于實現(xiàn)的shader中的光照模型計算如同上文中,并不完整。所以不能直接運用于項目

一. 單張紋理

先看一下我們要實現(xiàn)的效果

image

shader 的一些書寫方式本文便不再贅述,同時本文的計算光照的方式都能夠在上一篇文章中找到,如果忘了,可以先復(fù)習(xí)一下

Unity Shader(三) ------ 漫反射和高光反射的實現(xiàn)

1.1. 實現(xiàn)單張紋理

新建一個場景,去掉天空盒子;新建一個 Capsule 與 Material,命名為 SingleTexture;

I. 先定義 Properties 語義塊

image

其中 _MainTex 的紋理用來表示紋理貼圖,這里我們用這張紋理貼圖來代替物體的漫反射顏色。

II.為了控制 Properties 中的屬性,我們在CG代碼片中定義與之相匹配的變量

image

在Unity中,一般使用 紋理名_ST來代表某個紋理的屬性

_MainTex_ST 代表 _MainTex 這個紋理的屬性:S(Scale)縮放,T(Translation)平移。

_MainTex_ST.xy 代表 縮放值;_MainTex_ST.zw 代表 偏移值

image

III.定義輸入輸出結(jié)構(gòu)體

image

uv 變量存儲了紋理坐標,以便在片元著色器中進行采樣

IV.頂點著色器

image

黃色框中,我們使用了 _MainTex_ST 對頂點紋理坐標進行變換,得到最終的紋理坐標。先使用 _MainTex_ST.xy對頂點紋理坐標進行縮放,然后使用 _MainTex_ST.zw進行偏移。而 TRANSFORM_TEX則是封裝了這個計算方式的內(nèi)置函數(shù),我們可以在 UnityCG.cginc 中找到它的定義

image

很顯然,參數(shù)一為頂點紋理坐標,參數(shù)二為紋理名

V.片元著色器

image

此處光照模型使用的是 Blinn-Phong 模型,所以光照計算方面與之前并沒有太大的差異, 如果讀者對光照模型不太了解,可以翻看我的前一篇文章。

這個片元著色器主要使用 Cg 函數(shù) tex2D(_MainTex,i.uv) 對紋理進行了采樣,然后以采樣結(jié)果與顏色屬性相乘,乘積結(jié)果作為反射率。其余的光照計算基本無異。而關(guān)于tex2D 的解釋如下

image
image

完整代碼:

Shader "Unity/Custom/01-SingleTexture"
{
    Properties
    {
        _Color("Color Tint",Color) = (1,1,1,1)
        _MainTex("Main Tex",2D) = "while"{}
        _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;
            fixed4 _Specular;
            float _Gloss;

            struct a2v{

                float4 vertex : POSITION;
                float3 normal : NORMAL;
                float4 texcoord : TEXCOORD0;
            };

            struct v2f{

                float4 pos : SV_POSITION;
                float3 worldnormal : TEXCOORD0;
                float3 worldPos : TEXCOORD1;
                float2 uv : TEXCOORD2;
            };

            v2f vert(a2v v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.worldnormal = UnityObjectToWorldNormal(v.normal);
                o.worldPos = UnityObjectToClipPos(v.vertex).xyz;

                //o.uv = TRANSFORM_TEX(v.texcoord,_MainTex);
                o.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;

                return o;
            }

            fixed4 frag(v2f i) : SV_Target
            {

                fixed3 worldnormal = normalize(i.worldnormal);
                fixed3 worldlight = normalize(UnityWorldSpaceLightDir(i.worldPos));

                fixed3 albedo = tex2D(_MainTex,i.uv).rgb * _Color.rgb;

                fixed3 ambient =  UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;

                fixed3 diffuse = _LightColor0.rgb * albedo * max(0,dot(worldnormal,worldlight));

                fixed3 reflectDir = normalize(reflect(-worldlight,worldnormal));
                fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
                //計算得到矢量h
                fixed3 halfDir = normalize(worldlight + viewDir);

                fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0,dot(worldnormal,halfDir)),_Gloss);


                return fixed4(ambient + diffuse + specular,1.0);
            }
            ENDCG

        }

    }

    FallBack "Specular"
}

保存,進入Unity 查看效果。當(dāng)然還有附上一張紋理。

image

二. 凹凸映射

另一種常見的紋理應(yīng)用就是 凹凸映射 。凹凸映射就是為了使用一張紋理來修改模型表面法線,來為模型提供更多的細節(jié)。當(dāng)然,這并不會真的改變模型的頂點位置,僅僅是使得模型看起來是 “不平滑的” ,更加的真實。

凹凸映射常用的兩種方法:

  • 高度映射。使用一張高度紋理來模擬表面位移,然后得到修改后的法線值。
  • 法線映射。使用一張法線紋理直接存儲表面法線。

2.1 高度紋理

高度紋理圖存儲是強度值,表示表面局部的海拔,顏色越淺表示越向外凸起,顏色越深表示越向內(nèi)凹進去。這樣就可以很形象地看出模型的凹凸,不過這樣的計算會更加復(fù)雜,在實時計算時并不能直接得到表面法線,而是計算像素的灰度值得到。本文著重講述的是法線紋理,所以高度映射技術(shù)便不再贅述了。

2.2 法線紋理

前文已經(jīng)提到法線紋理存儲的是表面法線方向,法線方向的分量范圍在【-1.1】,而像素分量范圍在【0,1】。所以為了讓兩者一致,我們需要做一個簡單的映射。相必這個方式大家都有學(xué)過


image.png

那么可以預(yù)知的便是,當(dāng)我們在shader中對法線紋理采樣后,就必須對其進行反映射,得到原先的法線方向。


image.png

需要注意的是,這個方向是有著空間之異的。對于模型自帶的頂點法線,則是定義在模型空間的,這個紋理稱為** 模型空間的法線紋理**。不過,一般制作法線紋理時,我們一般會采樣 切線空間(tangent space)

切線空間:對于每個頂點,它都有一個屬于自己的切線空間,切線空間原點就是該頂點本身,Z 軸則是頂點法線方向。X 軸為切線方向。Y 軸可以由法線和切線叉積而得。也稱為 副切線副法線。而存儲在切線空間的紋理則稱為 切線空間的紋理。

image

上圖是一張法線紋理。許多使用過法線紋理但不太了解其原理的朋友或許都有一個疑問:為什么普通的紋理都是紅顏六色的,但是法線紋理大都像上圖一樣是一片藍色的?

  • 模型空間的法線紋理,所有法線的坐標都是在模型空間,每個點存儲的法線方向都是各異的,經(jīng)過映射之后就變成了RGB(x,y,z) ,而x,y,z并不一致,所以對應(yīng)著不同的顏色。所以模型空間的法線紋理看起來是五顏六色的。
  • 切線空間的法線紋理,所有法線的坐標都是在各自的切線空間,新的法線方向就是 Z 軸,即(0,0,1),經(jīng)過映射就是(0.5,0.5,1)淺藍色。所以切線空間的法線紋理看上去大部分都是藍色的,這也說明了頂點的大部分法線是和模型本身法線一樣的。

兩種法線紋理的優(yōu)劣·:

模型空間的法線紋理 :

① 直觀,簡單

② 可以提供平滑的邊界部分。

切線空間的法線紋理 :

① 自由度很高:模型空間的法線紋理是 **絕對法線信息 **,即只能用于創(chuàng)建它的那個模型,應(yīng)用于它處就會出錯。而切線空間的法線紋理 是 相對法線信息 ,應(yīng)用于不同的網(wǎng)格都可以得到一個不錯的效果

② 可以制作UV動畫:可以通過移動UV來實現(xiàn)一個動畫,而模型空間下的紋理則會完全錯誤。

③ 可以壓縮。切線空間下的紋理,法線 Z 方向總是正方向,所以只存儲XY方向就可以通過推導(dǎo)得到 Z 方向。而模型空間下的紋理則不行

④ 可以重復(fù)利用

由于法線方向存儲于切線空間,所以在實際計算光照時會有兩種計算方式:① 把光照方向,視角方向轉(zhuǎn)換至切線空間,進行光照;② 把采樣得到的法線方向轉(zhuǎn)換至世界空間,計算光照;從效率角度,① 優(yōu)于 ② ,從通用性來看,② 優(yōu)于 ①。

本文會先給出第一種方法的實踐,第二種以后我會補充回來,讀者也可以自行實現(xiàn)。

2.3 切線空間下計算光照

新建一個材質(zhì)和Capsule,命名為NormalTextureTangentSpace

I. 定義 Properties 語義塊

image

其中 _BumpMap 表示法線紋理,_BumpScale 控制凹凸程度

II. 為了控制 Properties 中的屬性,我們定義與之相匹配的變量

image

III. 修改輸入輸出結(jié)構(gòu)體

image
image

因為切線空間是由頂點法線與切線構(gòu)建的,所以在輸入結(jié)構(gòu)體添加一個切線變量,使用 TANGENT 語義。

因為我們是在切線空間下計算光照,所以在輸出結(jié)構(gòu)體中添加兩個變量來存儲轉(zhuǎn)換空間后的光照方向和視角方向

IV. 定義頂點著色器

image

我們使用了兩張紋理,所以 uv 變量修改為 float4 類型,其中,xy分量存儲 _MainTex 的紋理坐標,zw分量存儲 _BumpMap 的紋理坐標。然后為了對光照方向和視角方向轉(zhuǎn)換至切線空間,我們需要一個變換矩陣 rotaion,而 ***TRANGENT_SPACE_ROTATION ***則是Unity內(nèi)幫我們實現(xiàn)了計算過程的內(nèi)置宏,它會返回我們所需 rotation,我們可以在UnityCG.cginc 中找到它的定義。

image

V. 修改片元著色器

image

我們在頂點著色器中已經(jīng)對光照方向和視角方向做了轉(zhuǎn)換空間的工作,所以片元著色器中只需要對法線紋理進行采樣,然后計算光照就可以了。tex2D 函數(shù)的定義在前文已經(jīng)給出。然后使用Unity內(nèi)置函數(shù) UnpackNormal 得到正確的法線方向。然后對得到的法線向量的 xy 分量乘于 _BumpScale 就可以得到 法線的 xy 分量。再計算出 z 分量,就得到了正確的法線方向。

VI. 保存,查看效果

不同 _BumpScale 下的效果:

image

需要注意的是:

使用法線紋理時,注意其類型是否為 Normal map

image

如果不是,則要在 shader 里面進行以下的更改

image

更改為

image

如果不進行修改,Unity 也提醒你

image

因為如果法線紋理類型不是 Normal map 時,我們需要手動對采樣結(jié)果的 xy 分量進行反映射。而如果是 Normal map 類型,則使用 UnpackNormal 函數(shù)。因為,當(dāng)法線紋理類型設(shè)置成 Normal map 時,Unity 會根據(jù)平臺的不同而對該法線紋理進行壓縮,此時 _BumpMap 的 rgb 分量已經(jīng)不是切線空間下的 xyz 分量了。所以此時再進行以上的手動計算就會得到錯誤的結(jié)果。

而 UnpackNormal 函數(shù)則可以在 UnityCG.cginc 中找到其定義

image
image

其中 DXT5nm 是一種壓縮格式

那么,完整代碼如下:

Shader "Unity/Custom/01-NormalTexture-Tangent Space"
{
    Properties
    {
        _Color("Color Tint",Color) = (1,1,1,1)
        _MainTex("Main Tex",2D) = "while"{}
        _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"
            #include "UnityCG.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;
                float3 lightDir : TEXCOORD1;
                float3 viewDir : TEXCOORD2;
            };

            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;

                TANGENT_SPACE_ROTATION;

                o.lightDir = mul(rotation,ObjSpaceLightDir(v.vertex)).xyz;
                o.viewDir = mul(rotation,ObjSpaceViewDir(v.vertex)).xyz;

                return o;
            }

            fixed4 frag(v2f i) : SV_Target
            {
                //光源方向歸一化
                fixed3 tangentLightDir = normalize(i.lightDir);
                //視角方向歸一化
                fixed3 tangentViewDir = normalize(i.viewDir);

                //對法線紋理取樣
                fixed4 packedNormal = tex2D(_BumpMap,i.uv.zw);
                //切線空間下的法線
                fixed3 tangentNormal;

                //手動反映射
                //tangentNormal.xy = (packedNormal.xy * 2 - 1) * _BumpScale;

                tangentNormal = UnpackNormal(packedNormal);
                tangentNormal.xy *= _BumpScale;



                tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy,tangentNormal.xy)));

                fixed3 albedo = tex2D(_MainTex,i.uv).rgb * _Color.rgb;

                fixed3 ambient =  UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;

                fixed3 diffuse = _LightColor0.rgb * albedo * max(0,dot(tangentNormal,tangentLightDir));

                //計算得到矢量h
                fixed3 halfDir = normalize(tangentLightDir + tangentViewDir);

                fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0,dot(tangentNormal,halfDir)),_Gloss);

                return fixed4(ambient + diffuse + specular,1.0);
            }
            ENDCG

        }

    }

    FallBack "Specular"
}

三. 遮罩紋理

法線紋理是十分常見且重要的紋理,講完了法線紋理,我們現(xiàn)在講另外一種非常有用的紋理:遮罩紋理

遮罩可以保護某些區(qū)域不受修改,比如我們上一篇光照原理中實現(xiàn)的高光反射則是對于所有像素而言的,現(xiàn)在我希望物體某部分更強烈一些,而另一部分則更弱一些,此時我們就可以用到遮罩紋理了。

遮罩紋理的使用流程:① 采樣,得到紋素值 ② 使用其中一個或多個通道的值來與表面屬性相乘 ③ 當(dāng)通道的值為0時,可以保護表面不受該屬性影響

現(xiàn)在我們來實現(xiàn),對高光反射進行遮罩。計算在切線空間,代碼與之前相差不多,就不贅述了

完整代碼:

Shader "Unity/Custom/01-MaskTexture"
{
    Properties
    {
        _Color("Color Tint",Color) = (1,1,1,1)
        _MainTex("Main Tex",2D) = "while"{}
        _BumpMap("Normal Map",2D) = "bump"{}
        _BumpScale("Bump Scale",Float) = 1.0
        _SpecularMask("Specular Mask",2D) = "while"{}
        _SpecularScale("Specular 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"
            #include "UnityCG.cginc"

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

            struct a2v{

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

            struct v2f{

                float4 pos : SV_POSITION;
                float2 uv : TEXCOORD0;
                float3 lightDir : TEXCOORD1;
                float3 viewDir : TEXCOORD2;
            };

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

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

                TANGENT_SPACE_ROTATION;

                o.lightDir = mul(rotation,ObjSpaceLightDir(v.vertex)).xyz;
                o.viewDir = mul(rotation,ObjSpaceViewDir(v.vertex)).xyz;

                return o;
            }

            fixed4 frag(v2f i) : SV_Target
            {
                //光源方向歸一化
                fixed3 tangentLightDir = normalize(i.lightDir);
                //視角方向歸一化
                fixed3 tangentViewDir = normalize(i.viewDir);

                //對法線紋理貼圖取樣
                fixed4 packedNormal = tex2D(_BumpMap,i.uv);
                //切線空間下的法線
                fixed3 tangentNormal;

                tangentNormal = UnpackNormal(packedNormal);
                tangentNormal.xy *= _BumpScale;
                //反映射
                tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy,tangentNormal.xy)));



                fixed3 albedo = tex2D(_MainTex,i.uv).rgb * _Color.rgb;

                fixed3 ambient =  UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;

                fixed3 diffuse = _LightColor0.rgb * albedo * max(0,dot(tangentNormal,tangentLightDir));

                //計算得到矢量h
                fixed3 halfDir = normalize(tangentLightDir + tangentViewDir);

                //高光遮罩
                fixed3 specularMask = tex2D(_SpecularMask,i.uv).r * _SpecularScale;

                fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0,dot(tangentNormal,halfDir)),_Gloss) * specularMask;


                return fixed4(ambient + diffuse + specular,1.0);
            }
            ENDCG

        }

    }

    FallBack "Specular"
}

這里需要注意的是:

① 這里三張紋理共用了 _MainTex_ST ,而不是一張紋理對應(yīng)一個 _ST 變量。因為隨著紋理越來越多,我們會迅速占滿頂點著色器中可以使用的插值寄存器。而很多時候,我們并不需要對紋理進行平鋪和位移,或者很多紋理使用同一種平鋪,那么我們就可以對這些紋理使用同一個紋理坐標。

② 這張遮罩圖我們只使用了 r 分量,那么有很多空間都是浪費了,因為一般遮罩紋理的 rgba 存儲的是不同的表面屬性,善用遮罩紋理,可以創(chuàng)作出高自由度的材質(zhì),就可以實現(xiàn)更強的畫面效果。

轉(zhuǎn)載于:https://www.cnblogs.com/BFXYMY/p/9715341.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容