第六章 基礎紋理(1)

@函數

基礎紋理

紋理最初的目的就是使用一張圖片來控制模型的外觀。使用紋理映射(texture mapping)技術,咱們能夠把一張圖「黏」在模型表面,逐紋素(texel)(紋素的名字是爲和像素進行區分)地控制模型的顏色。
在美術人員建模的時候,一般會在建模軟件中利用紋理展開技術把紋理映射座標(texture-mapping coordinates)存儲在每一個頂點上。紋理映射座標定義了該頂點在紋理中對應的2D座標。一般,這些座標使用一個二維變量(u,v)來表示,其中u是橫向座標,而v是縱向座標。所以,紋理映射座標也被稱爲uv座標。
儘管紋理的大小能夠是多種多樣的,例如能夠是256×256或者1024×1024,但頂點UV座標的範圍一般都被歸一化到[0,1]範圍內。須要注意的是,紋理採樣時使用的紋理座標不必定是在[0,1]範圍內。實際上這種不在[0,1]範圍內的紋理座標有時會很是有用。與之關係緊密的是紋理的平鋪模式,它將決定渲染引擎遇到不在[0,1]範圍內的紋理座標時如何進行紋理採樣。
Unity使用的紋理空間是符合OpenGl傳統的,也就是說,原點位於紋理左下角,以下圖所示:
在這裏插入圖片描述
須要提醒讀者注意的是,本章着重描述紋理採樣的原理,所以實現的Shader每每並不能直接應用到實際的項目中(直接使用的話會缺乏陰影、光照衰減等效果)。咱們會在後面給出包含了紋理採樣和完整光照模型的可真正使用的UnityShader。性能

1. 單張紋理

咱們一般會使用一張紋理來代替物體的漫反射顏色。本節中,咱們將學習如何在Unity Shader中使用單張紋理來模擬顏色。在學習完本節後,咱們會獲得相似於下圖的效果:
在這裏插入圖片描述學習

1.1 實踐

(1)爲了使用紋理,咱們須要在Properties語義塊中添加一個紋理屬性:優化

Properties{
_Color("Color Tint",Color)= (1,1,1,1)
_MainTex("Main Tex",2D) = "white" {}
_Specular("Specular", Color) = (1,1,1,1)
_Gloss("Gloss",Range(8.0,256)) = 20
}

上面的代碼聲明瞭一個名爲_MainTex的紋理,在之前,咱們已經知道2D是紋理的聲明方式。咱們使用一個字符串後跟一個花括號做爲它的初始值。「white」是內置的紋理的名字,也就是一個全白的紋理。爲了控制物體的總體色調,咱們還聲明瞭一個_Color屬性。
(2)而後,咱們在SubShader語義塊中定義了一個Pass語義塊。並且咱們在Pass的第一行指明瞭該Pass的光照模式:ui

SubShader{
Pass{
Tags{"LightMode"="ForwardBase"}
}
}

LightMode標籤是Pass標籤中的一種,它用於定義該Pass在Unity的光照流水線中的角色。
(3)接着咱們使用CGPROGRAM和ENDCG來包圍住Cg代碼片斷,以定義最重要的頂點着色器和片元着色器代碼。首先,咱們使用#pragma指令來告訴Unity,咱們定義的頂點着色器和片元着色器叫什麼名字,在本例中,它們的名字分別是vert和frag:3d

CGPROGRAM
#pragma vertex vert
#pragma fragment frag

(4)爲了使用Unity內置的一些變量,如_LightColor0,還須要包含進Unity的內置文件Lighting.cginc:rest

#include "Lighting.cginc"

(5)咱們須要在Cg代碼片斷中聲明和上述屬性相匹配的變量,以便和材質面板中的屬性創建聯繫:code

fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
fixed4 _Specular;
float _Gloss;

與其它屬性類型不一樣的是,咱們還須要爲紋理類型的屬性聲明一個float4類型的變量_MainTex_ST。其中,_MainTex_ST的名字不是任意起的。在Unity中,咱們須要使用紋理名_ST的方式來聲明某個紋理的屬性。其中ST是縮放(scale)和平移(translation)的縮寫。_MainTex_ST可讓咱們獲得該紋理的縮放和平移(偏移)值,_MainTex_ST.XY存儲的是縮放值,而_MainTex_ST.zw存儲的是偏移值。這些值能夠在材質面板的紋理屬性中調節,以下圖所示:
在這裏插入圖片描述
(6)接下來,咱們須要定義頂點着色器的輸入和輸出結構體:orm

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;
};

在上面代碼中,咱們首先在a2v結構體中使用TEXCOORD0語義聲明瞭一個新的變量texcoord,這樣Unity就會將模型的第一組紋理座標存儲到該變量中。而後,咱們在v2f結構體中添加了用於存儲紋理座標的uv,以便在片元着色器中使用該座標進行紋理採樣。
(7)而後咱們定義了頂點着色器:

v2f vert(a2v v){
v2f o;
o.pos = mul(UNITY_MATRIX_MVP,v.vertex);
o.worldNormal=UnityObjectToWorldNormal(v.normal);
o.worldPos=mul(_Object2World,v.vertex).xyz;
o.uv = v.texcoord.xy*_MainTex_ST.xy+_MainTex_ST.zw;
//Or just call the built-in function
//o.uv = TRANSFORM_TEX(v.texcoord,_MainTex);
return o;
}

在頂點着色器中,咱們使用紋理屬性值_MainTex_ST來對頂點紋理座標進行變換,獲得最終的紋理座標。計算過程是,首先使用縮放屬性_MainTex_ST.xy對頂點紋理座標進行縮放,而後再使用偏移屬性_MainTex_ST.zw對結果進行偏移。Unity提供了一個內置宏TRANSFORM_TEX來幫咱們計算上述過程。TRANSFORM_TEX是在UnityCG.cginc中定義的:

//Transform 2D UV by scale/bias property
#define TRANSFORM_TEX(tex,name) (tex.xy*name##__ST.xy+name##_ST.zw)

它接受兩個參數,第一個參數是頂點紋理座標,第二個參數是紋理名,在它的實現中,將利用紋理名_ST的方式來計算變換後的紋理座標。
(8)咱們還須要實現片元着色器,並計算漫反射時使用紋理中的紋素值:

fixed4 frag(v2f i):SV_Target{
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
//Use the texture to sample the diffuse color
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,worldLightDir));
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
fixed3 halfDir = normalize(worldLightDir+viewDir);
fixed3 specular = _LightColor0.rgb*_Specular.rgb*pow(max(0,dot(worldNormal,halfDir)),_Gloss);
return fixed4(ambient+diffuse+specular,1.0);
}

上面的代碼首先計算了世界空間下的法線方向和光照方向。而後,使用Cg的tex2D函數對紋理進行採樣。它的第一個參數是須要被採樣的紋理,第二個參數是一個float2類型的紋理座標,它將返回計算獲得的紋素值。咱們使用採樣結果和顏色屬性_Color的乘積來做爲材質的反射率albedo,並把它和環境光相乘獲得環境光部分。隨後,咱們使用albedo來計算漫反射光照的結果,並和環境光照、高光反射光照相加後返回。
(9)最後,咱們爲該Shader設置了合適的Fallback:

Fallback"Specular"

1.2 紋理屬性

雖然不少資料把Unity的紋理映射描述的很簡單——聲明一個紋理變量,再使用tex2D函數採樣。實際上,在渲染流水線中,紋理映射的實現遠比咱們想象的複雜。本文不會過多的涉及一些具體的實現細節,但要解釋一些咱們認爲讀者必需要知道的事情。在本節中,咱們將關注Unity中的紋理屬性。
在咱們向Unity中導入一張紋理資源後,能夠在它的材質面板上調整其屬性,以下圖所示。
在這裏插入圖片描述
紋理面板中的第一個屬性是紋理類型。在本節中,咱們使用的是Texture屬性,在下面的法線紋理一節中,咱們會使用Normal map類型。而在後面的章節中,咱們還會看到Cubemap等高級紋理類型。咱們之因此要爲導入的紋理選擇合適的類型,是由於只有這樣才能讓Unity知道咱們的意圖,爲Unity Shader傳遞正確的紋理,並在一些狀況下可讓Unity對該紋理進行優化。
當把紋理類型設置爲Texture後,下面會有一個Alpha from Grayscale 複選框。若是勾選了它,那麼透明通道的值將會由每一個像素的灰度值生成。關於透明效果,咱們會在下一章講到,在這裏咱們不須要勾選它。
下面一個屬性很是重要——Wrap Mode。它決定了當紋理座標超過[0,1]範圍後將如何平鋪。Wrap Mode有兩種模式:一種是Repeat,在這種模式下,若是紋理座標超過了1,那麼它的整數部分將會被捨棄,而直接使用小數部分進行採樣,這樣的結果是紋理將會不斷重複;另外一種是Clamp,在這種模式下,若是紋理座標大於1,那麼將會截取到1,若是小於0,那麼將會截取到0。下圖給出了兩種模式下平鋪一張紋理的效果。
在這裏插入圖片描述
上圖展現了在紋理的平鋪(Tiling)屬性爲(3,3)時分別使用兩種Wrap Mode的結果。做圖使用了Repeat模式,在這種模式下紋理將會不斷重複;右圖使用了Clamp模式,在這種模式下超過範圍的部分將會截取到邊界值,造成一個條形結構。
須要注意的是,想要讓紋理獲得這樣的效果,咱們必須使用紋理的屬性(例如上面的_MainTex_ST變量)在Unity Shader中對頂點座標進行相應的變換。也就是說,代碼中須要包含相似下面的代碼:

o.uv = v.texcoord.xy*_MainTex_ST.xy+_MainTex_ST.zw;
//Or just call the bulit-in function
o,uv = TRANSFORM_TEX(v.texcoord,_MainTex);

咱們還能夠在材質面板中調整紋理的偏移量,下圖給出了兩種模式下調整紋理偏移量的一個例子:
在這裏插入圖片描述
上圖展現了在紋理的偏移屬性爲(0.2,0.6)時分別使用兩種Wrap Mode的結果,左圖使用了Repeat模式,右圖使用了Clamp模式。
紋理導入面板的下一個屬性是Fliter Mode屬性,它決定了當紋理因爲變換而產生拉伸時將會採用哪一種濾波模式。Fliter Mode支持3種模式:Point,Bilinear以及Trilinear。它們獲得的圖片濾波效果依次提高,但須要耗費的性能也依次增大。紋理濾波會影響放大或縮小紋理時獲得的圖片質量。例如,當咱們把一張64×64大小的紋理貼在一個512×512大小的平面上時,就須要放大紋理。下圖給出了3種濾波模式下的放大結果。
在這裏插入圖片描述
紋理縮小的過程比放大更加複雜一些,此時原紋理中的多個像素會對應一個目標像素。紋理縮小更加複雜的緣由在於咱們每每須要處理抗鋸齒問題,一個最經常使用的方法就是多級漸遠紋理(mipmapping)技術,其中「mip」是拉丁文「multum in parvo」的縮寫,它的意思是在一個小空間有許多東西。如同它的名字,多級漸遠紋理技術將原紋理提早用濾波處理來獲得不少更小的圖像,造成了一個圖像金字塔,每一層都是對上一層圖像降採樣的結果。這樣在實時運行時,就能夠快速獲得結果像素,例如當物體遠離攝像機時,就能夠直接使用較小的紋理。但缺點是須要使用必定的空間用於存儲這些多級漸遠紋理,一般會多佔用33%的內存空間,這是一種典型的空間換取時間的方法。在Unity中,咱們能夠在紋理導入面板時,首先將紋理類型(Texture Type)選擇成Advanced,再勾選Generate Mip Maps便可開啓多級漸遠紋理技術。同時,咱們也能夠選擇生成多級漸遠紋理時是否使用線性空間(用於伽瑪校訂)以及採用的濾波器等。以下圖所示:
在這裏插入圖片描述下圖給出了從一個傾斜的角度觀察一個網格結構的地板時,使用不一樣Filter Mode(同時也使用了多級漸遠紋理技術)獲得的效果。
在這裏插入圖片描述
在內部實現上,Point模式使用了最近鄰(nearest neighbor)濾波,再放大或縮小時,它的採樣像素數目一般只有一個,一次圖像看起來會有種像素風格的效果。而Bilinear濾波則使用了線性濾波,對於每一個目標像素,他都會找到4個臨近像素,而後對它們進行線性插值混合後獲得最終像素,所以圖像看起來被模糊了。而Trilinear濾波幾乎是和Bilinear同樣的,只是Trilinear還會在多級漸遠紋理之間進行混合。若是一張紋理沒有使用多級漸遠紋理技術,那麼Trilinear獲得的結果是和Bilinear就是同樣的。一般咱們會選擇Bilinear濾波模式。須要注意的是,有時咱們不但願紋理看起來是模糊的,例如對於一些相似棋盤的紋理,咱們但願它就是像素風的,這時咱們可能會選擇Point模式。
最後,咱們來說一下紋理的最大尺寸和紋理模式。當咱們在爲不一樣平臺發佈遊戲時,須要考慮目標平臺的紋理尺寸和質量問題。Unity容許咱們爲不一樣目標平臺選擇不一樣的分辨率,以下圖所示:
在這裏插入圖片描述
若是導入的紋理大小超過了Max Texture Size中的設置值,那麼Unity將會把改紋理縮放爲這個最大分辨率。理想狀況下,導入的紋理能夠是非正方形的,但長款的大小應該是2的冪,例如2,4,8,16,32,64等。若是使用了非2的冪大小(Non Power ofTwo,NPOT)的紋理,那麼這些紋理每每會佔用更多的內存空間,並且GPU讀取改紋理的速度也會有所降低。有些平臺甚至不支持這種NPOT紋理,這時Unity在內部會把它縮放成最近的2的冪的大小。出於性能和空間考慮,咱們應儘可能使用2的冪大小的紋理。
而Format決定了Unity內部使用哪一種格式來存儲該紋理。若是咱們將Texture Type設置爲Advanced,那麼會有更多的Format供咱們選擇。這裏再也不依次介紹每種紋理模式,但須要知道的是,使用的紋理格式精度越高(例如使用Turecolor),佔用的內存空間越大,獲得的效果也越好。咱們能夠從紋理導入面板的最下方看到存儲該紋理須要佔用的內存空間(若是開啓了多級漸遠紋理技術,也會增長紋理的內存佔用)。當遊戲中使用了大量Truecolor類型的紋理時,內存可能會迅速增長,所以對於一些不須要使用很高精度的紋理(例如用於漫反射顏色的紋理),咱們應該儘可能使用壓縮格式。

.

相關文章
相關標籤/搜索