前言
這是我入門學習筆記的第二篇。這篇開始,我們會逐漸做一些更貼近實際的效果。而不是和上週那個球體球體,隨便輸出點莫名其妙,妙不可言的顏色(✿◕‿◕✿)。
另外,本章節可能還會稍微提及上一章的代碼,但是後續可能會逐漸跳過大量的代碼來篇幅。畢竟我想讓讀者關注到本章更核心的內容,而不是一直重複某些事情。如果讀者想要獲取完整代碼甚至整個場景,可以到我的github上面去下載。
貼圖和lambert光照模型
回到正文,如標題說的,本章分享一下貼圖,以及一個很簡單的光照模型-lambert光照模型。我們還是老套路,先把最終效果砸出來。
可以看到,這是一個簡單的房子(千萬不要和我說醜,說就是一巴掌)。我們第一步,先把房子搭建起來。很簡單,只需要創建6個平面。可以參考我的層次關係以及每個平面的變換。
現在每個平面使用的都是默認材質Default-Material。看起來場景樣子是這樣的。
現在我們先不討論這個默認材質,我們需要創建自己的材質然後替換掉它。
第二步,創建一個Unlit_Shader文件, 命名爲2_DiffuseWall。打開UnityShader文件後,把裏面的內容全刪了,並且複製下面一大段代碼進去保存。這裏除了Cull Off之外用到的都是上一篇講到的操作和代碼,之後的教程,我會盡量跳過這些內容。
Cull Off, 是指禁用掉面剔除。什麼是面剔除,拿一個正方體來說,我們正常能看到的最多是一個正方體的3個面(你可以試下你能不能成爲那個不正常的人),所以剩餘三個面就沒有繪製的必要了。因此我們會把它剔除掉,具體剔除規則是通過整個面的頂點繪製順序來判斷正反面,我們通常把反面剔除掉。而Plane這個默認對象,他只有單面,因此,如果我們到背面去看他,他就會消失不見,原因就是因爲被Unity剔除掉了。
Shader "Unity Shader/C2/2_WallDiffuse" {
Properties {
_MainColor ("color tint", COLOR) = (1.0, 1.0, 1.0, 1.0) // 物體的主顏色
}
SubShader {
Pass {
Cull Off // 把面剔除禁用
CGPROGRAM // CG代碼塊
#pragma vertex vert // 聲明vert函數爲頂點着色器
#pragma fragment frag // frag爲片段着色器
fixed4 _MainColor;
struct a2v { // 頂點着色器輸入結構體
float4 vertex : POSITION; // 頂點物體空間位置
};
struct v2f { // 片段着色器輸入結構體
float4 pos : SV_POSITION; // 頂點裁剪空間位置
};
v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex); // MVP變換
return o;
}
fixed4 frag(v2f i) : SV_Target {
return _MainColor; // 輸出物體主顏色
}
ENDCG
}
}
Fallback "Diffuse" // 備胎
}
爲了應用上我們的Unity Shader,我們還得創建一個材質。每個shader和材質都是一一對應的。後面我說創建一個材質,就意味着創建一個和UnityShader同名的材質,並且選中對應的Shader(上一節已經講過這個操作)。操作完之後,把材質拖到6個plane中,我們可以看到如下效果。
可以看到這個效果非常糟糕,純白色一片,差點亮瞎我家小致的狗眼。我們希望這是一堵磚牆,而不是這樣一個白色平面,毫無細節。因此,我準備了一張磚的圖片(來自Shader入門精要),接下來我們把這張圖片貼到我們的平面上。
第三步,修改着色器代碼,增加一個2D紋理屬性,用來傳入我們的磚牆紋理。
Properties {
_MainColor ("color tint", COLOR) = (1.0, 1.0, 1.0, 1.0)
// 2D就是一個二維紋理變量。
// "white"是紋理的默認值, 表示一張純白色圖片
_MainTex ("main tex", 2D) = "white" {}
}
我們還需要在CG代碼塊中再聲明一次這個變量,供後續着色器代碼使用。你們應該知道聲明在哪吧,知道吧,知道吧~。不知道的參考下_MainColor變量,然後回顧下上篇講到的Shader結構。也可以直接拿github上源碼參考。後面的代碼不知道放哪我就不重複了哈。
// sampler2D是一個二維紋理採樣器。
// sampler2D就是對應上面的2D屬性。
sampler2D _MainTex;
上面提到一個二維紋理採樣器。什麼是採樣器呢。首先採樣是指,我們拿到一個片段後,需要一個映射,從一張紋理裏面找到這個片段所屬的位置,並且把其中的內容讀出來。如果這是一張顏色紋理,像我們現在的磚牆,那他的內容對應的就是顏色。相當於我們通過紋理採樣過程知道了一個片段的顏色。而輔助我們完成這個採樣過程的就是採樣器。
二維紋理採樣原理也很簡單。我們把紋理放在一個二維座標系裏,以左下角爲原點(0,0),右上角爲(1,1)。水平軸稱爲u軸,垂直軸成爲v軸。那我們給定一個片段一個座標(0.5,0.5), 我們就可以把這個座標映射到紋理的中間。
來總結一下,上面提到的座標系就是紋理座標系,也叫UV座標系。而(0.5,0.5)這個座標就是片段的紋理座標,也叫UV座標。如果還是有點不太理解的話,可以看看learnopengl裏面更詳細的解釋。
好了,第四步,我們爲了讓我們的平面對maintex紋理採樣,我們需要給片段着色器輸入紋理座標uv。而這個座標,Unity會傳給我們(模型導入後就會確定下來)。
struct a2v {
float4 vertex : POSITION;
float2 texcoord : TEXCOORD0; // 頂點着色器輸入紋理座標
};
struct v2f {
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0; // 片段着色器輸入紋理座標
};
v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
// 我們先直接把Unity給我們的紋理座標傳給片段着色器
o.uv = v.texcoord;
return o;
}
fixed4 frag(v2f i) : SV_Target {
fixed4 albedo = tex2D(_MainTex, i.uv); // 採樣
return albedo; // 輸出物體主顏色
}
上面核心代碼就是tex2D函數。這個函數第一個參數是紋理採樣器,第二個參數是紋理座標,返回給我們的就是採樣結果。
第五步,我們可以看到修改之後的材質裏面,我們的紋理屬性對應有一個框框。我們把我們的磚牆拖進那個框框裏面。
可以看到現在場景變成這樣!
是不是瞬間就不一樣了。這張顏色紋理爲我們補充了大量的細節。如果我們要用純顏色實現這個,我們需要好多好多個小平面,這就意味更多的人力成本,更多的頂點和更多的運行消耗。
細心的人會發現,我們紋理屬性那裏除了一個紋理框框,還有4個屬性。
這四個屬性是用來調節我們紋理座標的平移縮放的。但是現在大家怎麼改都沒作用。是因爲我們代碼裏面沒用到它,下面我們來完善一下代碼。
首先,增加一個變量聲明_MainTex_ST。注意,我們不需要在屬性塊裏面填入。我們在屬性塊聲明瞭一個2D變量後,Unity就會把這個平移縮放賦值給我們在shader代碼中聲明的紋理名字+_ST的變量。其中S代表縮放,T代表平移。
sampler2D _MainTex;
float4 _MainTex_ST;
然後我們在頂點着色器中,把uv的賦值代碼修改一下,之前是直接賦值,現在我們加上傳入的平移縮放參數。
o.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
// unity的頭文件 UnityCG.cginc裏面提供了TRANSFORM_TEX函數幫助我們完成這個操作
// #include "UnityCG.cginc" // 定義在 #pragma的後面
// o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
// 之後的代碼都會採用這種,因爲內置就是香,會幫我們考慮一些跨平臺問題
好了,現在回到場景裏面,調整一下4個參數,看看會發生什麼吧!
大家應該發現,儘管我們的場景中有一個光源(默認的平行光,如果你沒有,肯定是你多手刪掉了),但是這個光源對我們的房子沒有任何影響。理論上,面對光源的牆應該亮一點,而背對光源的牆應該暗一點。
爲了實現這個效果,接下來第六步我們簡單引入一個光照模型,lambert光照模型。
所謂的光照模型,其實就是一個公式,就是根據這個公式去計算,我們就能獲得一個類似光照的效果。這個公式不一定要很正確(現實的光照計算是很複雜的),只要這個公式能以很高的效率帶給我們相對較好的效果,那麼就是一個好的光照模型。不同情境下,我們會應用不同的光照模型。
先看看lambert光照模型
l = ambient + diffuse;
蘭伯特告訴我們,光照裏面有環境光和漫反射光兩個分量組成。
環境光,大家會發現晚上在一個幾乎看不到光源的地方,還是能看清物體,這就是環境光的影響。事實上,環境光是所有光源在多次反射後,在整個環境都留下了它們的蹤跡。沒錯!就是光污染。
漫發射光,就是光線在照射一個物體表面的時候,會向四面八方均勻的反射光線。而這個反射光線的強度和光線照射平面的角度相關。入射光線和法線的夾角越小,反射光線的強度越大。
反射光強 = 入射光強 * dot(入射光線, 表面法線)。
dot是點乘的意思。當入射光線和表面法線都是單位向量(長度爲1)的時候,點成就代表他們夾角的餘弦值,剛好是我們需要的一個係數。
好了,根據上面簡單提到的理論,我們來實現lambert光照模型。首先我們需要一個片段的法線和對應的入射光線方向。這兩個屬性我們都可以通過unity提供的一些接口或者變量中得到。我們直接看代碼
#include "UnityCG.cginc"
// 光照相關頭文件,可以幫我們計算入射光線
#include "Lighting.cginc"
struct a2v {
float4 vertex : POSITION;
float2 texcoord : TEXCOORD0;
float3 normal : NORMAL; // Unity提供的頂點法線向量
};
struct v2f {
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float3 worldPos : TEXCOORD1; // 世界空間下的片段位置
float3 worldNormal : TEXCOORD2; // 世界空間下的法線向量
};
v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
o.worldPos = mul(UNITY_MATRIX_M, v.vertex.xyz); // 通過模型變換,求出世界空間的頂點位置
o.worldNormal = UnityObjectToWorldNormal(v.normal); // 求出世界空間的法線位置
return o;
}
fixed4 frag(v2f i) : SV_Target {
// 把插值結果單位化,注意一定要變成單位向量,否則後面計算會不完整。
float3 worldNormal = normalize(i.worldNormal);
// 通過UnityWorldSpaceLightDir求出光照方向
float3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos);
fixed4 albedo = tex2D(_MainTex, i.uv);
可以看到上面我們獲得了世界空間下的法線向量和光線的入射方向向量。接下來我們開始光照模型的計算。
// 環境光
// 內置變量UNITY_LIGHTMODEL_AMBIENT爲我們提供了環境光的光強
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT * albedo.rgb;;
// 漫反射光
// saturate可以把輸入參數截斷到[0,1]區間,相當於 min(0, max(1, x))
float lambert = saturate(dot(worldNormal, worldLightDir));
// 內置變量_LightColor0爲我們提供了光線的顏色
fixed3 diffuse = _LightColor0.rgb *albedo.rgb * lambert;
然後我們把得到的兩種光照結果應用在lambert光照模型上來計算最終的結果,並作爲片段着色器的輸出。
這時候我們回到場景看看效果,法線向光面更亮,背光面更暗了!
最後,我覺得地板用磚好像不太好,所以我多創建了一個材質2_FloorDiffuse, 然後在編輯面板裏換了一張水泥貼圖。把材質拖到地板上,就是我們的最終效果啦!ヾ(≧▽≦*)o
結語
第二篇的分享結束了,不知道你們的是不是有很多問號。我還是強調一下,因爲很多理論上的知識我無法展開普及,例如什麼是單位向量,什麼是點乘。又或者光線顏色和片段顏色相乘的意義是什麼等等。
以上這些我都認爲大家是知道的,但如果實在有什麼地方晦澀難懂或者我說的明顯有問題。再或者你真的很萌新,但又很想學好。都可以在評論裏面說一下,畢竟我也是基於入門做分享的,希望大家和我都能走到蓋棺那一步。
好啦,最核心的支持作者環節來啦!!!