TShopping

 找回密碼
 註冊
搜索
查看: 371|回復: 0
打印 上一主題 下一主題

[分享] unity shader基礎之——基礎紋理:uv坐標、紋理採樣、紋理屬...

[複製鏈接]
跳轉到指定樓層
1#
發表於 2022-11-30 19:12:38 | 只看該作者 |只看大圖 回帖獎勵 |倒序瀏覽 |閱讀模式
Push to Facebook
紋理最初的目的就是使用一張圖片來控制模型的外觀。使用紋理映射技術可以把一張圖黏在模型表面,逐紋素(名字和像素進行區分)地控制模型的顏色。
美術建模時候通常會在建模軟件中利用紋理展開技術把紋理映射坐標存儲在每個頂點上。紋理映射坐標定義了該頂點在紋理中對應的2D坐標。通常坐標使用一個二維變量(u,v)來表示,其中u是橫向坐標,v是縱向坐標,因此紋理映射坐標也被稱為uv坐標。
紋理大小可以是多種多樣的,例如可以是256X256或者1024X1024,但頂點uv坐標範圍通常都被歸一化到[0,1]範圍內。需要注意,紋理採樣時使用的紋理坐標不一定是在[0,1]範圍內,實際上這種不在[0,1]範圍內的紋理坐標有時會非常有用。與之關係緊密的是紋理的平鋪模式,它決定渲染引擎在遇到不在[0,1]範圍內的紋理坐標時如何進行紋理採樣。
unity使用的紋理空間通常只有一種坐標系,unity使用的紋理空間是符合OpenGL的傳統的,也就是說原點位於紋理的左下角,如下圖:


一、單張紋理
我們通常使用一張紋理來代替物體的漫反射顏色,本節在shader中使用單張紋理作為模擬顏色。


1.1實踐
本節使用Blinn-Phong光照模型來計算光照。
1. 首先定義紋理屬性:
  1. Properties {
  2.                 _Color ("Color Tint", Color) = (1, 1, 1, 1)
  3.                 _MainTex ("Main Tex", 2D) = "white" {}
  4.                 _Specular ("Specular", Color) = (1, 1, 1, 1)
  5.                 _Gloss ("Gloss", Range(8.0, 256)) = 20
  6.         }
複製代碼

2. 然後聲明pass的光照模式、指令、相應變量:
  1. SubShader {               
  2.                 Pass {
  3.                         Tags { "LightMode"="ForwardBase" }
  4.                
  5.                         CGPROGRAM
  6.                         
  7.                         #pragma vertex vert
  8.                         #pragma fragment frag
  9.                         #include "Lighting.cginc"
  10.                         
  11.                         fixed4 _Color;
  12.                         sampler2D _MainTex;
  13.                         float4 _MainTex_ST;
  14.                         fixed4 _Specular;
  15.                         float _Gloss;
複製代碼

與其他屬性類型不同的是,我們還需要為紋理類型的屬性聲明一個float4類型的變量_MainTex_ST。其中_MainTex_ST的名字不是任意起的。在unity中,需要使用紋理名_ST的方式來聲明某個紋理的屬性。其中ST是縮放和平移的縮寫。_MainTex_ST可以讓我們得到我們得到該紋理的縮放和平移(偏移)值,_MainTex_ST.xy存儲的是縮放量,而_MainTex_ST.zw存儲的是偏移值,這些值可以在材質面板的紋理屬性中調節,如下圖:


3. 定義頂點著色器的輸入和輸出結構體:
  1. struct a2v {
  2.                                 float4 vertex : POSITION;
  3.                                 float3 normal : NORMAL;
  4.                                 float4 texcoord : TEXCOORD0;
  5.                         };
  6.                         
  7.                         struct v2f {
  8.                                 float4 pos : SV_POSITION;
  9.                                 float3 worldNormal : TEXCOORD0;
  10.                                 float3 worldPos : TEXCOORD1;
  11.                                 float2 uv : TEXCOORD2;
  12.                         };
複製代碼

首先在a2v結構體中使用TEXCOORD0語義聲明了一個新的變量texcoord,這樣unity就會將模型的第一組紋理坐標存儲到該變量中,然後在v2f結構體中添加了用於存儲紋理坐標的變量uv,以便於在片元著色器中使用該坐標進行紋理採樣。
4. 然後定義頂點著色器:
  1. v2f vert(a2v v) {
  2.                                 v2f o;
  3.                                 o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
  4.                                 
  5.                                 o.worldNormal = UnityObjectToWorldNormal(v.normal);
  6.                                 
  7.                                 o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
  8.                                 
  9.                                 o.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
  10.                                 // Or just call the built-in function
  11.                //o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
  12.                                 
  13.                                 return o;
  14.                         }
複製代碼

在頂點著色器中,我們使用紋理的屬性值_MainTex_ST來對頂點紋理坐標進行變換,得到最終的紋理坐標。計算過程是,首先使用縮放屬性_MainTex_ST.xy對頂點紋理坐標進行縮放,然後在使用偏移屬性_MainTex_ST.zw對結果進行偏移。unity提供了一個內置宏TRANSFORM_TEX來幫助我們計算上述過程,是在unityCG.cginc文件中找到,可以直接使用它。
  1. // Transforms 2D UV by scale/bias property
  2. #define TRANSFORM_TEX(tex,name) (tex.xy * name##_ST.xy + name##_ST.zw)
複製代碼

他接受兩個參數,第一個參數是頂點紋理坐標,第二個參數是紋理名,在它的實現中,將利用紋理名_ST的方式來計算變換後的紋理坐標。
5. 我們還需要實現片元著色器,併計算漫反射時使用紋理坐標的紋素值:
  1. fixed4 frag(v2f i) : SV_Target {
  2.                                 fixed3 worldNormal = normalize(i.worldNormal);
  3.                                 fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
  4.                                 
  5.                                 // Use the texture to sample the diffuse color
  6.                                 fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb;
  7.                                 
  8.                                 fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
  9.                                 
  10.                                 fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir));
  11.                                 
  12.                                 fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
  13.                                 fixed3 halfDir = normalize(worldLightDir + viewDir);
  14.                                 fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);
  15.                                 
  16.                                 return fixed4(ambient + diffuse + specular, 1.0);
  17.                         }
複製代碼

首先計算了世界空間下的法線方向和光照方向,然後使用Cg的tex2D函數對紋理進行採樣,它的第一參數是需要被採樣的紋理,第二個參數是float2類型的紋理坐標,它將計算返回得到的紋素值,我們使用採樣結果和顏色屬性_Color的乘積來作為材質的反射率albedo,並把它和環境光照相乘得到環境光的部分,隨後使用albedo來計算漫反射的光照結果,並和環境光照、高光反射光照相加後返回。
1.2 紋理的屬性
在我們向unity中導入一張紋理資源後,可以在它的材質面板上調整其屬性,如下圖所示:


紋理屬性面板中第一個屬性是紋理類型,我們當前使用的是Texture類型,還有Normal map、Cubemap類型等高級紋理類型。我們之所以要為導入的紋理選擇合適的類型,是因為只有這樣才能讓unity知道我們的意圖,為unity shader傳遞正確的紋理,並在一些情況下可以讓unity對該紋理進行優化。
當把紋理類型設置為default時(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中對頂點紋理坐標進行相應的變換,也就是說代碼中需要包含類似下面的代碼:
  1. o.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
  2.                                 // Or just call the built-in function
  3.                //o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
複製代碼

紋理導入面板的下一個屬性是Filter Mode屬性,他決定了當紋理由於變換而產生拉伸時將會採用哪種濾波模式,Filter Mode支持3種模式:Point、Bilinear、Trilinear。他們得到的圖片濾波效果依次提升,但需要耗費的性能依次增大。紋理濾波會影響放大或縮小紋理時得到的圖片質量,例如我們把一張64X64大小的紋理貼在一個512X512大小的平面上時,就需要放大紋理。
縮小紋理過程比放大復雜一些,此時原紋理中多個像素將會對應一個目標像素,紋理縮小更加複雜的原因在於我們往往需要處理抗鋸齒問題,一個最常用的方法就是使用多級漸遠紋理技術(mipmapping)。這個技術是將原紋理提取用濾波處理來得到很多更小的圖像,形成了一個圖像金字塔,每一層都是對上一層圖像降採樣的結果。這樣在實時運行時,就可以快速得到結果像素,例如當物體遠離攝像機時,可以直接使用較小的紋理。但缺點是需要使用一定的空間用於存儲這些多級漸遠紋理,通常會多佔用33%的內存空間。這是一種典型的空間換取時間的方法。在unity中,我們可以在紋理導入面板中,首先將紋理類型(Texture Type)選擇成Advanced,在勾選Generate Mip Maps即可開啟多級漸遠紋理技術。同時還可以選擇生成多級漸遠紋理時是否使用線性空間(用於伽馬校正)以及採用的濾波器等。
在內部實現上,Point模式使用了最近鄰濾波,在放大或縮小時,它的採樣像素數目通常只有一個,因此圖像會看起來有種像素風格的效果。而Bilinear濾波使用了線性濾波,對於每個目標像素,它會找到4個鄰近像素,然後對它們進行線性插值混合後得到最終像素,因此圖像看起來被模糊了。而Trilinear濾波幾乎是和Bilinear一樣的,只是Trilinear還會在多級漸遠紋理之間進行混合。如果一張紋理沒有使用多級漸遠技術,那麼Trilinear得到的結果是和Bilinear就一樣的。通常我們會選擇Bilinear濾波模式。需要注意的是,有時我們不希望紋理看起來是模糊的,例如對於一些類似棋盤的紋理,我們希望它就是像素風的,這時我們可能會選擇Point模式。
最後我們講一下紋理的最大尺寸和紋理模式,當我們在為不同平台發布遊戲時,需要考慮目標平台的紋理尺寸和質量問題。unity允許我們為不同目標平台選擇不同的分辨率,如下圖所示:


如果導入的紋理大小超過了Max Texture Size中的設置值,那麼unity將會把該紋理縮放為這個最大分辨率。理想情況下,導入的紋理可以是非正方形的,但長寬大小應該是2的冪,如果使用了非2的冪的大小的紋理,那麼這些紋理往往會佔用更多的內存空間,而且GPU讀取該紋理的速度也會有所下降。有一些平台甚至不支持這種NPOT紋理,這時unity在內部會把他縮放成最近的2的冪大小。出於性能和空間的考慮,我們應該盡量使用2的冪大小的紋理。
而Format 決定了unity內部使用哪種格式來存儲該紋理。如果我們將Texture Type設置為Advanced,那麼會有更多的Format供我們選擇。使用的紋理格式精度越高,佔用的內存空間越大,但得到的效果也越好。我們可以從紋理導入面板的最下方看到存儲該紋理需要佔用的內存空間(如果開啟了多級漸遠紋理技術,也會增加紋理的內存佔用)。當遊戲使用了大量Truecolor類型的紋理時,內存可能會迅速增加,因此對於一些不需要使用很高精度的紋理(例如用於漫反射顏色的紋理),我們應該盡量使用壓縮格式。



臉書網友討論
*滑块验证:
您需要登錄後才可以回帖 登錄 | 註冊 |

本版積分規則



Archiver|手機版|小黑屋|免責聲明|TShopping

GMT+8, 2024-5-9 19:10 , Processed in 0.095565 second(s), 25 queries .

本論壇言論純屬發表者個人意見,與 TShopping綜合論壇 立場無關 如有意見侵犯了您的權益 請寫信聯絡我們。

Powered by Discuz! X3.2

© 2001-2013 Comsenz Inc.

快速回復 返回頂部 返回列表