在游戲中,我們除了能看到游戲物體的形體輪廓,還能看到物體的一些具體外觀,包括顏色,凹凸等。而實現(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)的效果
imageshader 的一些書寫方式本文便不再贅述,同時本文的計算光照的方式都能夠在上一篇文章中找到,如果忘了,可以先復(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 代表 偏移值
imageIII.定義輸入輸出結(jié)構(gòu)體
imageuv 變量存儲了紋理坐標,以便在片元著色器中進行采樣
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 的解釋如下
imageimage完整代碼:
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 中的屬性,我們定義與之相匹配的變量
imageIII. 修改輸入輸出結(jié)構(gòu)體
imageimage因為切線空間是由頂點法線與切線構(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 中找到它的定義。
imageV. 修改片元著色器
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 中找到其定義
imageimage其中 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)更強的畫面效果。




























