【Unity Shader】(四) ------ 紋理之法線紋理、單張紋理及遮罩紋理的實現
筆者使用的是 Unity 2018.2.0f2 + VS2017,建議讀者使用與 Unity 2018 相近的版本,避免一些因為版本不一致而出現的問題。
【Unity Shader】(三) ------ 光照模型原理及漫反射和高光反射的實現 【Unity Shader】(五) ------ 透明效果之半透明效果的實現及原理
在遊戲中,我們除了能看到遊戲物體的形體輪廓,還能看到物體的一些具體外觀,包括顏色,凹凸等。而實現這一步的就是使用 紋理。與紋理相對應的技術就是 紋理映射技術 ,相當於把一張圖貼在物體表面,然後 逐紋素 地控制顏色。
紋理映射坐標:紋理映射坐標定義了一個頂點在紋理中對應的2D坐標。由於常用 U 來表示橫向坐標, V 來表示縱向坐標,所以紋理映射坐標也是我們常常見到的 UV坐標。
頂點 UV 坐標通常會被歸一化至 【0,1】範圍內。當然紋理采樣時使用的坐標也不一定在這個範圍內。另外值得註意的是,OpenGL 與 DirectX 的二維坐標系是不一樣的,OpenGL中原點位於左下角,DirectX原點位於左上角。當然Unity 會幫我們處理這個差異,同時一般情況下,Unity 采用的紋理空間是符合 OpenGL傳統的。
需要註意的是:本文著重講述紋理采樣的原理,由於實現的shader中的光照模型計算如同上文中,並不完整。所以不能直接運用於項目
一. 單張紋理
先看一下我們要實現的效果
shader 的一些書寫方式本文便不再贅述,同時本文的計算光照的方式都能夠在上一篇文章中找到,如果忘了,可以先復習一下
【Unity Shader】(三) ------ 漫反射和高光反射的實現
1.1. 實現單張紋理
新建一個場景,去掉天空盒子;新建一個 Capsule 與 Material,命名為 SingleTexture;
I. 先定義 Properties 語義塊
其中 _MainTex 的紋理用來表示紋理貼圖,這裏我們用這張紋理貼圖來代替物體的漫反射顏色。
II. 為了控制 Properties 中的屬性,我們在CG代碼片中定義與之相匹配的變量
在Unity中,一般使用 紋理名_ST 來代表某個紋理的屬性
_MainTex_ST 代表 _MainTex 這個紋理的屬性:S(Scale)縮放,T(Translation)平移。
_MainTex_ST.xy 代表 縮放值;_MainTex_ST.zw 代表 偏移值
III. 定義輸入輸出結構體
uv 變量存儲了紋理坐標,以便在片元著色器中進行采樣
IV. 頂點著色器
黃色框中,我們使用了 _MainTex_ST 對頂點紋理坐標進行變換,得到最終的紋理坐標。先使用 _MainTex_ST.xy 對頂點紋理坐標進行縮放,然後使用 _MainTex_ST.zw 進行偏移。而 TRANSFORM_TEX 則是封裝了這個計算方式的內置函數,我們可以在 UnityCG.cginc 中找到它的定義
很顯然,參數一為頂點紋理坐標,參數二為紋理名
V. 片元著色器
此處光照模型使用的是 Blinn-Phong 模型,所以光照計算方面與之前並沒有太大的差異, 如果讀者對光照模型不太了解,可以翻看我的前一篇文章。
這個片元著色器主要使用 Cg 函數 tex2D(_MainTex,i.uv) 對紋理進行了采樣,然後以采樣結果與顏色屬性相乘,乘積結果作為反射率。其余的光照計算基本無異。而關於tex2D 的解釋如下
完整代碼:
1 Shader "Unity/Custom/01-SingleTexture" 2 { 3 Properties 4 { 5 _Color("Color Tint",Color) = (1,1,1,1) 6 _MainTex("Main Tex",2D) = "while"{} 7 _Specular("Specular",Color) = (1,1,1,1) 8 _Gloss("Gloss",Range(8.0,256)) = 20 9 10 } 11 SubShader 12 { 13 Pass 14 { 15 Tags { "LightMode"="ForwardBase" } 16 17 18 CGPROGRAM 19 #pragma vertex vert 20 #pragma fragment frag 21 #include "Lighting.cginc" 22 23 fixed4 _Color; 24 sampler2D _MainTex; 25 float4 _MainTex_ST; 26 fixed4 _Specular; 27 float _Gloss; 28 29 struct a2v{ 30 31 float4 vertex : POSITION; 32 float3 normal : NORMAL; 33 float4 texcoord : TEXCOORD0; 34 }; 35 36 struct v2f{ 37 38 float4 pos : SV_POSITION; 39 float3 worldnormal : TEXCOORD0; 40 float3 worldPos : TEXCOORD1; 41 float2 uv : TEXCOORD2; 42 }; 43 44 v2f vert(a2v v) 45 { 46 v2f o; 47 o.pos = UnityObjectToClipPos(v.vertex); 48 o.worldnormal = UnityObjectToWorldNormal(v.normal); 49 o.worldPos = UnityObjectToClipPos(v.vertex).xyz; 50 51 //o.uv = TRANSFORM_TEX(v.texcoord,_MainTex); 52 o.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw; 53 54 return o; 55 } 56 57 fixed4 frag(v2f i) : SV_Target 58 { 59 60 fixed3 worldnormal = normalize(i.worldnormal); 61 fixed3 worldlight = normalize(UnityWorldSpaceLightDir(i.worldPos)); 62 63 fixed3 albedo = tex2D(_MainTex,i.uv).rgb * _Color.rgb; 64 65 fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo; 66 67 fixed3 diffuse = _LightColor0.rgb * albedo * max(0,dot(worldnormal,worldlight)); 68 69 fixed3 reflectDir = normalize(reflect(-worldlight,worldnormal)); 70 fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos)); 71 //計算得到矢量h 72 fixed3 halfDir = normalize(worldlight + viewDir); 73 74 fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0,dot(worldnormal,halfDir)),_Gloss); 75 76 77 return fixed4(ambient + diffuse + specular,1.0); 78 } 79 ENDCG 80 81 } 82 83 } 84 85 FallBack "Specular" 86 }
保存,進入Unity 查看效果。當然還有附上一張紋理。
二. 凹凸映射
另一種常見的紋理應用就是 凹凸映射 。凹凸映射就是為了使用一張紋理來修改模型表面法線,來為模型提供更多的細節。當然,這並不會真的改變模型的頂點位置,僅僅是使得模型看起來是 “不平滑的” ,更加的真實。
凹凸映射常用的兩種方法:
- 高度映射。使用一張高度紋理來模擬表面位移,然後得到修改後的法線值。
- 法線映射。使用一張法線紋理直接存儲表面法線。
2.1 高度紋理
高度紋理圖存儲是強度值,表示表面局部的海拔,顏色越淺表示越向外凸起,顏色越深表示越向內凹進去。這樣就可以很形象地看出模型的凹凸,不過這樣的計算會更加復雜,在實時計算時並不能直接得到表面法線,而是計算像素的灰度值得到。本文著重講述的是法線紋理,所以高度映射技術便不再贅述了。
2.2 法線紋理
前文已經提到法線紋理存儲的是表面法線方向,法線方向的分量範圍在【-1.1】,而像素分量範圍在【0,1】。所以為了讓兩者一致,我們需要做一個簡單的映射。相必這個方式大家都有學過
那麽可以預知的便是,當我們在shader中對法線紋理采樣後,就必須對其進行反映射,得到原先的法線方向。
需要註意的是,這個方向是有著空間之異的。對於模型自帶的頂點法線,則是定義在模型空間的,這個紋理稱為 模型空間的法線紋理。不過,一般制作法線紋理時,我們一般會采樣 切線空間(tangent space)
切線空間:對於每個頂點,它都有一個屬於自己的切線空間,切線空間原點就是該頂點本身,Z 軸則是頂點法線方向。X 軸為切線方向。Y 軸可以由法線和切線叉積而得。也稱為 副切線 或 副法線。而存儲在切線空間的紋理則稱為 切線空間的紋理。
上圖是一張法線紋理。許多使用過法線紋理但不太了解其原理的朋友或許都有一個疑問:為什麽普通的紋理都是紅顏六色的,但是法線紋理大都像上圖一樣是一片藍色的?
- 模型空間的法線紋理,所有法線的坐標都是在模型空間,每個點存儲的法線方向都是各異的,經過映射之後就變成了RGB(x,y,z) ,而x,y,z並不一致,所以對應著不同的顏色。所以模型空間的法線紋理看起來是五顏六色的。
- 切線空間的法線紋理,所有法線的坐標都是在各自的切線空間,新的法線方向就是 Z 軸,即(0,0,1),經過映射就是(0.5,0.5,1)淺藍色。所以切線空間的法線紋理看上去大部分都是藍色的,這也說明了頂點的大部分法線是和模型本身法線一樣的。
兩種法線紋理的優劣·:
模型空間的法線紋理 :
① 直觀,簡單
② 可以提供平滑的邊界部分。
切線空間的法線紋理 :
① 自由度很高:模型空間的法線紋理是 絕對法線信息 ,即只能用於創建它的那個模型,應用於它處就會出錯。而切線空間的法線紋理 是 相對法線信息 ,應用於不同的網格都可以得到一個不錯的效果
② 可以制作UV動畫:可以通過移動UV來實現一個動畫,而模型空間下的紋理則會完全錯誤。
③ 可以壓縮。切線空間下的紋理,法線 Z 方向總是正方向,所以只存儲XY方向就可以通過推導得到 Z 方向。而模型空間下的紋理則不行
④ 可以重復利用
由於法線方向存儲於切線空間,所以在實際計算光照時會有兩種計算方式:① 把光照方向,視角方向轉換至切線空間,進行光照;② 把采樣得到的法線方向轉換至世界空間,計算光照;從效率角度,① 優於 ② ,從通用性來看,② 優於 ①。
本文會先給出第一種方法的實踐,第二種以後我會補充回來,讀者也可以自行實現。
2.3 切線空間下計算光照
新建一個材質和Capsule,命名為NormalTextureTangentSpace
I. 定義 Properties 語義塊
其中 _BumpMap 表示法線紋理,_BumpScale 控制凹凸程度
II. 為了控制 Properties 中的屬性,我們定義與之相匹配的變量
III. 修改輸入輸出結構體
因為切線空間是由頂點法線與切線構建的,所以在輸入結構體添加一個切線變量,使用 TANGENT 語義。
因為我們是在切線空間下計算光照,所以在輸出結構體中添加兩個變量來存儲轉換空間後的光照方向和視角方向
IV. 定義頂點著色器
我們使用了兩張紋理,所以 uv 變量修改為 float4 類型,其中,xy分量存儲 _MainTex 的紋理坐標,zw分量存儲 _BumpMap 的紋理坐標。然後為了對光照方向和視角方向轉換至切線空間,我們需要一個變換矩陣 rotaion,而 TRANGENT_SPACE_ROTATION 則是Unity內幫我們實現了計算過程的內置宏,它會返回我們所需 rotation,我們可以在UnityCG.cginc 中找到它的定義。
V. 修改片元著色器
我們在頂點著色器中已經對光照方向和視角方向做了轉換空間的工作,所以片元著色器中只需要對法線紋理進行采樣,然後計算光照就可以了。tex2D 函數的定義在前文已經給出。然後使用Unity內置函數 UnpackNormal 得到正確的法線方向。然後對得到的法線向量的 xy 分量乘於 _BumpScale 就可以得到 法線的 xy 分量。再計算出 z 分量,就得到了正確的法線方向。
VI. 保存,查看效果
不同 _BumpScale 下的效果:
需要註意的是:
① 使用法線紋理時,註意其類型是否為 Normal map
如果不是,則要在 shader 裏面進行以下的更改
把
更改為
如果不進行修改,Unity 也提醒你
因為如果法線紋理類型不是 Normal map 時,我們需要手動對采樣結果的 xy 分量進行反映射。而如果是 Normal map 類型,則使用 UnpackNormal 函數。因為,當法線紋理類型設置成 Normal map 時,Unity 會根據平臺的不同而對該法線紋理進行壓縮,此時 _BumpMap 的 rgb 分量已經不是切線空間下的 xyz 分量了。所以此時再進行以上的手動計算就會得到錯誤的結果。
而 UnpackNormal 函數則可以在 UnityCG.cginc 中找到其定義
其中 DXT5nm 是一種壓縮格式
那麽,完整代碼如下:
1 Shader "Unity/Custom/01-NormalTexture-Tangent Space" 2 { 3 Properties 4 { 5 _Color("Color Tint",Color) = (1,1,1,1) 6 _MainTex("Main Tex",2D) = "while"{} 7 _BumpMap("Normal Map",2D) = "bump"{} 8 _BumpScale("Bump Scale",Float) = 1.0 9 _Specular("Specular",Color) = (1,1,1,1) 10 _Gloss("Gloss",Range(8.0,256)) = 20 11 12 } 13 SubShader 14 { 15 Pass 16 { 17 Tags { "LightMode"="ForwardBase" } 18 19 20 CGPROGRAM 21 #pragma vertex vert 22 #pragma fragment frag 23 #include "Lighting.cginc" 24 #include "UnityCG.cginc" 25 26 fixed4 _Color; 27 sampler2D _MainTex; 28 float4 _MainTex_ST; 29 sampler2D _BumpMap; 30 float4 _BumpMap_ST; 31 float _BumpScale; 32 fixed4 _Specular; 33 float _Gloss; 34 35 struct a2v{ 36 37 float4 vertex : POSITION; 38 float3 normal : NORMAL; 39 float4 tangent : TANGENT; 40 float4 texcoord : TEXCOORD0; 41 }; 42 43 struct v2f{ 44 45 float4 pos : SV_POSITION; 46 float4 uv : TEXCOORD0; 47 float3 lightDir : TEXCOORD1; 48 float3 viewDir : TEXCOORD2; 49 }; 50 51 v2f vert(a2v v) 52 { 53 v2f o; 54 o.pos = UnityObjectToClipPos(v.vertex); 55 56 o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw; 57 o.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw; 58 59 TANGENT_SPACE_ROTATION; 60 61 o.lightDir = mul(rotation,ObjSpaceLightDir(v.vertex)).xyz; 62 o.viewDir = mul(rotation,ObjSpaceViewDir(v.vertex)).xyz; 63 64 return o; 65 } 66 67 fixed4 frag(v2f i) : SV_Target 68 { 69 //光源方向歸一化 70 fixed3 tangentLightDir = normalize(i.lightDir); 71 //視角方向歸一化 72 fixed3 tangentViewDir = normalize(i.viewDir); 73 74 //對法線紋理取樣 75 fixed4 packedNormal = tex2D(_BumpMap,i.uv.zw); 76 //切線空間下的法線 77 fixed3 tangentNormal; 78 79 //手動反映射 80 //tangentNormal.xy = (packedNormal.xy * 2 - 1) * _BumpScale; 81 82 tangentNormal = UnpackNormal(packedNormal); 83 tangentNormal.xy *= _BumpScale; 84 85 86 87 tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy,tangentNormal.xy))); 88 89 fixed3 albedo = tex2D(_MainTex,i.uv).rgb * _Color.rgb; 90 91 fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo; 92 93 fixed3 diffuse = _LightColor0.rgb * albedo * max(0,dot(tangentNormal,tangentLightDir)); 94 95 //計算得到矢量h 96 fixed3 halfDir = normalize(tangentLightDir + tangentViewDir); 97 98 fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0,dot(tangentNormal,halfDir)),_Gloss); 99 100 return fixed4(ambient + diffuse + specular,1.0); 101 } 102 ENDCG 103 104 } 105 106 } 107 108 FallBack "Specular" 109 }
三. 遮罩紋理
法線紋理是十分常見且重要的紋理,講完了法線紋理,我們現在講另外一種非常有用的紋理:遮罩紋理
遮罩可以保護某些區域不受修改,比如我們上一篇光照原理中實現的高光反射則是對於所有像素而言的,現在我希望物體某部分更強烈一些,而另一部分則更弱一些,此時我們就可以用到遮罩紋理了。
遮罩紋理的使用流程:① 采樣,得到紋素值 ② 使用其中一個或多個通道的值來與表面屬性相乘 ③ 當通道的值為0時,可以保護表面不受該屬性影響
現在我們來實現,對高光反射進行遮罩。計算在切線空間,代碼與之前相差不多,就不贅述了
完整代碼:
1 Shader "Unity/Custom/01-MaskTexture" 2 { 3 Properties 4 { 5 _Color("Color Tint",Color) = (1,1,1,1) 6 _MainTex("Main Tex",2D) = "while"{} 7 _BumpMap("Normal Map",2D) = "bump"{} 8 _BumpScale("Bump Scale",Float) = 1.0 9 _SpecularMask("Specular Mask",2D) = "while"{} 10 _SpecularScale("Specular Scale",Float) = 1.0 11 _Specular("Specular",Color) = (1,1,1,1) 12 _Gloss("Gloss",Range(8.0,256)) = 20 13 14 } 15 SubShader 16 { 17 Pass 18 { 19 Tags { "LightMode"="ForwardBase" } 20 21 22 CGPROGRAM 23 #pragma vertex vert 24 #pragma fragment frag 25 #include "Lighting.cginc" 26 #include "UnityCG.cginc" 27 28 fixed4 _Color; 29 sampler2D _MainTex; 30 float4 _MainTex_ST; 31 sampler2D _BumpMap; 32 float _BumpScale; 33 sampler2D _SpecularMask; 34 float _SpecularScale; 35 fixed4 _Specular; 36 float _Gloss; 37 38 struct a2v{ 39 40 float4 vertex : POSITION; 41 float3 normal : NORMAL; 42 float4 tangent : TANGENT; 43 float4 texcoord : TEXCOORD0; 44 }; 45 46 struct v2f{ 47 48 float4 pos : SV_POSITION; 49 float2 uv : TEXCOORD0; 50 float3 lightDir : TEXCOORD1; 51 float3 viewDir : TEXCOORD2; 52 }; 53 54 v2f vert(a2v v) 55 { 56 v2f o; 57 //o.pos = UnityObjectToClipPos(v.vertex); 58 o.pos = UnityObjectToClipPos(v.vertex); 59 60 o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw; 61 62 TANGENT_SPACE_ROTATION; 63 64 o.lightDir = mul(rotation,ObjSpaceLightDir(v.vertex)).xyz; 65 o.viewDir = mul(rotation,ObjSpaceViewDir(v.vertex)).xyz; 66 67 return o; 68 } 69 70 fixed4 frag(v2f i) : SV_Target 71 { 72 //光源方向歸一化 73 fixed3 tangentLightDir = normalize(i.lightDir); 74 //視角方向歸一化 75 fixed3 tangentViewDir = normalize(i.viewDir); 76 77 //對法線紋理貼圖取樣 78 fixed4 packedNormal = tex2D(_BumpMap,i.uv); 79 //切線空間下的法線 80 fixed3 tangentNormal; 81 82 tangentNormal = UnpackNormal(packedNormal); 83 tangentNormal.xy *= _BumpScale; 84 //反映射 85 tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy,tangentNormal.xy))); 86 87 88 89 fixed3 albedo = tex2D(_MainTex,i.uv).rgb * _Color.rgb; 90 91 fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo; 92 93 fixed3 diffuse = _LightColor0.rgb * albedo * max(0,dot(tangentNormal,tangentLightDir)); 94 95 //計算得到矢量h 96 fixed3 halfDir = normalize(tangentLightDir + tangentViewDir); 97 98 //高光遮罩 99 fixed3 specularMask = tex2D(_SpecularMask,i.uv).r * _SpecularScale; 100 101 fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0,dot(tangentNormal,halfDir)),_Gloss) * specularMask; 102 103 104 return fixed4(ambient + diffuse + specular,1.0); 105 } 106 ENDCG 107 108 } 109 110 } 111 112 FallBack "Specular" 113 }
這裏需要註意的是:
① 這裏三張紋理共用了 _MainTex_ST ,而不是一張紋理對應一個 _ST 變量。因為隨著紋理越來越多,我們會迅速占滿頂點著色器中可以使用的插值寄存器。而很多時候,我們並不需要對紋理進行平鋪和位移,或者很多紋理使用同一種平鋪,那麽我們就可以對這些紋理使用同一個紋理坐標。
② 這張遮罩圖我們只使用了 r 分量,那麽有很多空間都是浪費了,因為一般遮罩紋理的 rgba 存儲的是不同的表面屬性,善用遮罩紋理,可以創作出高自由度的材質,就可以實現更強的畫面效果。
最後給出三種紋理的對比圖
總結
紋理是十分重要的一環,它可以決定你看到的事物有多細膩逼真。
另外再強調一次,本文實現的 shader 僅供學習,因為光照計算並不完整,所以不能直接運用於項目之中
如果對光照不太了解的朋友,可以去翻看我的前一篇文章【Unity Shader】(三) ------ 漫反射和高光反射的實現
最後,希望本文能對你有所幫助!!!路漫漫其修遠兮 !!!
本文實現的 Shader 和紋理
【Unity Shader】(四) ------ 紋理之法線紋理、單張紋理及遮罩紋理的實現