UnityShader入門精要筆記1——頂點/片元着色器結構與BRDF(基本光照模型)——實現漫反射

BRDF(基本光照模型)

基本光照模型一共有四種。分別爲環境光自發光漫反射高光反射,我們先來實現漫反射

實現漫反射

光線強度的計算

在開始實現漫反射之前,我們需要了解一個概念:輻射度

在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

下圖使用蘭伯特模型進行漫反射光計算(並且只進行了漫反射光計算),正面對着光線方向的部分最亮,然後逐漸變淡,

蘭伯特模型
反射光線的強度與表面法線和光源方向之間夾角的餘弦值成正比
(也就是使用我們剛剛的“輻射度”作爲漫反射光的強度)

在這裏插入圖片描述

好現在開始寫Shader

我們按書裏的流程寫一個頂點/片元着色器來實現漫反射

新建Shader

首先新建一個Unity Shader,把原有代碼全部刪除,然後給shader起個名字

Shader "Diffuse-Lambert" {

}

添加一個Properties語義塊

爲Shader添加一個Properties 語義塊,聲明我們需要的屬性。

Properties 語義塊是材質和Unity Shader的橋樑,它包含了一系列屬性,這些屬性會出現在檢查器窗口的材質面板中。【candycat】

Shader "Diffuse-Lambert" {
	Properties {
	//_Color 爲屬性標識符,我們會在稍後的Shader編寫中使用這個名字
	//"Color Tint" 爲在檢查器窗口中顯示的屬性名稱
	//Color 爲屬性的類型
	//等號右邊的是屬性的默認值,在這裏四個1代表爲“白色”
	
		_Color ("Color Tint", Color) = (1, 1, 1, 1) //漫反射底色
	}
}

此時在Unity中隨意新建一個材質,並把我們的Diffuse-Lambert着色器拖到材質上,就可以在檢查器面板看到我們剛剛聲明的屬性了
在這裏插入圖片描述

添加SubShader和Pass。

每一個Unity Shader可以定義多個SubShader,但最少要有一個。當Unity需要加載這個UnityShader時,Unity會掃描所有的SubShader語義塊,並選出當前顯卡能夠支持的第一個SubShader運行。【candycat】

Shader "RefShader" {
	//屬性
	Properties {
	}
	//顯卡A使用的子着色器
	SubShader {
	}
	//顯卡B使用的子着色器
	SubShader {
	}
}

Shader "Diffuse-Lambert"{
	Properties{
		_Color("Color Tint", Color) = (1, 1, 1, 1) //漫反射底色
	}

	SubShader {
		//如果支持的話,當前顯卡會使用這個子着色器
		//不支持就靠 Fallback了
	}
	
}

SubShader中定義了一系列Pass以及可選的狀態和標籤,每個Pass定義了一次完整的渲染流程,所以我們應該儘量使用最小數目的Pass。

Shader "Diffuse-Lambert"{
	Properties{
		_Color("Color Tint", Color) = (1, 1, 1, 1) //漫反射底色
	}

	SubShader {
		Pass {
			//在這裏會進行一次完整的渲染
		}
	}
	
}

在Pass中,我們設置標籤LightModeForwardBase(向前渲染),這是爲了Unity能夠按向前渲染路徑的方式爲我們正確提供各個光照變量

Shader "Diffuse-Lambert"{
	Properties{
		_Color("Color Tint", Color) = (1, 1, 1, 1) //漫反射底色
	}

	SubShader {
		Pass {
			Tags { "LightMode"="ForwardBase" } //向前渲染
		}
	}
	
}

使用CG/HLSL語言來編寫頂點/片元着色器

我們使用 CGPROGRAM作爲CG/HLSL語言的開始符,而ENDCG是結束符。
在CG開始後,我們先來申明vert函數(頂點着色器,逐頂點渲染)和frag函數(片元着色器,逐片元渲染)

Shader "Diffuse-Lambert"{
	Properties{
		_Color("Color Tint", Color) = (1, 1, 1, 1) //漫反射底色
	}

	SubShader {
		Pass {
			Tags { "LightMode"="ForwardBase" } //向前渲染
			
			CGPROGRAM //CG開始
			#pragma vertex vert
			#pragma fragment frag

			//app to vert,頂點着色器的輸入類型,獲取當前頂點的信息(由程序給出的)
			struct a2v {
			};
			
			//vert to frag,片元着色器的輸入類型,獲取當前片元的信息(由頂點着色器輸出的數據再插值得到的)
			struct v2f {
			};
			
			//頂點着色器,逐頂點運行,輸入a2v是當前頂點的信息,輸出v2f給片元着色器
			v2f vert(a2v v) {
			}
			
			//片元着色器,逐片元運行,輸入v2f是當前片元的信息,輸出顏色
			fixed4 frag(v2f i) : SV_Target {
			}

			ENDCG //CG結束


		}
	}
	
}

這個流水線大約是這樣子的:

Created with Raphaël 2.2.0開始程序給出當前頂點信息(a2v)頂點着色器儲存當前頂點信息(v2f)頂點遍歷完了?三角形設置:連接頂點得到三角形網格三角形遍歷:檢查每個像素,判斷是否被一個三角形所覆蓋。如果是的話就由該像素生成一個片元,並使用三角網格的3個頂點信息插值得到該片元的信息。劃重點:片元其實就是一個像素帶着它的信息(我們在v2f中(也就是頂點着色器的返回中)定義需要的信息),這些信息由片元所在的三角形頂點信息插值生成獲取當前片元信息(v2f)片元着色器輸出當前片元顏色到顏色緩衝區片元遍歷完了?結束yesnoyesno

定義a2v和v2f結構體

現在我們的a2v還沒有任何字段,也就是說我們的頂點着色器(vert)什麼輸入都沒有,讓我們使用語義來給它增加一些數據。

Shader "Diffuse-Lambert"{
	Properties{
		_Color("Color Tint", Color) = (1, 1, 1, 1) //漫反射底色
	}

	SubShader {
		Pass {
			Tags { "LightMode"="ForwardBase" } //向前渲染
			
			CGPROGRAM //CG開始
			#pragma vertex vert
			#pragma fragment frag

			struct a2v {
				//POSITION語義告訴Unity,用模型空間中當前頂點的座標填充vertex變量
				float4 vertex : POSITION; 
				//NORMAL語義告訴Unity,用模型空間中當前頂點法的線方向填充normal變量
				float3 normal : NORMAL;
			};

			struct v2f {
			};
			
			v2f vert(a2v v) {
			}
			
			fixed4 frag(v2f i) : SV_Target {
			}

			ENDCG //CG結束
		}
	}
}

接着給我們的v2f定義結構體,以確保我們在稍後的片元着色器中能夠有足夠的數據進行運算

我們回顧一下蘭伯特模型
反射光線的強度表面法線光源方向之間夾角的餘弦值成正比

也就是說,只要得到了片元的表面法線光源方向,我們就能得到該片元漫反射光線的強度

Shader "Diffuse-Lambert"{
	Properties{
		_Color("Color Tint", Color) = (1, 1, 1, 1) //漫反射底色
	}

	SubShader {
		Pass {
			Tags { "LightMode"="ForwardBase" } //向前渲染
			
			CGPROGRAM //CG開始
			#pragma vertex vert
			#pragma fragment frag

			struct a2v {
				float4 vertex : POSITION; 
				float3 normal : NORMAL;
			};

			struct v2f {
				//SV_POSITION語義告訴Unity,pos裏包含了頂點在裁剪空間中的位置信息
				//這也是頂點着色器最重要的一個工作:將頂點座標從模型空間轉換到裁剪空間
				float4 pos : SV_POSITION;
				//TEXCOORD0語義表示worldNormal變量佔用了TEXCOORD0插值寄存器
				//每個插值寄存器可以存儲4個浮點值(float)
				float3 worldNormal : TEXCOORD0; //世界空間下的頂點法線向量
			 	float3 worldLightDir : TEXCOORD1; //世界空間下的光源位置
			};
			
			v2f vert(a2v v) {
			}
			
			fixed4 frag(v2f i) : SV_Target {
			}
			
			ENDCG //CG結束
		}
	}
}

包含頭文件以及聲明屬性變量

在正式開始計算之前,爲了使用一些Unity內置的變量和函數,我們需要包含進內置文件 "UnityCG.cginc"

Shader "Diffuse-Lambert"{
	Properties{
		_Color("Color Tint", Color) = (1, 1, 1, 1) 
	}

	SubShader {
		Pass {
			Tags { "LightMode"="ForwardBase" } 
			
			CGPROGRAM 
			#pragma vertex vert
			#pragma fragment frag
			
			#include "UnityCG.cginc" //包含內置文件"UnityCG.cginc"

			struct a2v {
				float4 vertex : POSITION; 
				float3 normal : NORMAL;
			};

			struct v2f {
				float4 pos : SV_POSITION;
				float3 worldNormal : TEXCOORD0;
				float3 worldLightDir : TEXCOORD1;
			};
			
			v2f vert(a2v v) {
			}
			
			fixed4 frag(v2f i) : SV_Target {
			}
			
			ENDCG
		}
	}
}

而爲了可以使用我們在Properties 語義塊中定義的屬性(那個_Color),我們需要在CG中對屬性進行申明

Shader "Diffuse-Lambert"{
	Properties{
		_Color("Color Tint", Color) = (1, 1, 1, 1) 
	}

	SubShader {
		Pass {
			Tags { "LightMode"="ForwardBase" } 
			
			CGPROGRAM 
			#pragma vertex vert
			#pragma fragment frag
			#include "UnityCG.cginc" 

			fixed4 _Color; //這裏的變量名需要和屬性的名字完全一致
			
			struct a2v {
				float4 vertex : POSITION; 
				float3 normal : NORMAL;
			};

			struct v2f {
				float4 pos : SV_POSITION;
				float3 worldNormal : TEXCOORD0;
				float3 worldLightDir : TEXCOORD1;
			};
			
			v2f vert(a2v v) {
			}
			
			fixed4 frag(v2f i) : SV_Target {
			}
			
			ENDCG
		}
	}
}

編寫頂點着色器

好了,開始寫我們的頂點着色器(vert函數)。首先申明一個v2f類型的變量,對結構體中的字段依次賦值,最後將其返回。

v2f vert(a2v v) {
	//申明返回值v2f
	v2f o;
	
	//這是頂點着色器最重要的一個任務,將頂點座標從模型空間轉換到裁剪空間
	//UnityObjectToClipPos函數接受一個模型空間的座標,返回該座標在裁剪空間的座標
	o.pos = UnityObjectToClipPos(v.vertex);
	
	//UnityObjectToWorldNormal函數接受一個模型空間的法線向量,將其轉換到世界空間中並返回
	o.worldNormal = UnityObjectToWorldNormal(v.normal);
	
	//WorldSpaceLightDir函數接受一個模型空間中的頂點位置,並返回世界空間中從該點到光源的光照方向。未被歸一化。(由於是平行光,任何點的光照方向都是一樣的,參數填fixed(0)都可以)
	o.worldLightDir = WorldSpaceLightDir(v.vertex);
	return o;
}

經過頂點着色器的處理,我們的每個片元中已經包含我們需要的兩個信息:法線向量光源方向。現在,讓我們在片元着色器中爲每個片元計算他們的光線強度

編寫片元着色器

將法線向量和光源方向歸一化(轉爲長度爲1的單位向量)

由於我們稍後要用點積來求得兩向量夾角的cos值,而點積的公式是a·b=|a||b|cosθ(θ爲a和b的夾角),很明顯,只有當a和b均爲單位向量時,點積的結果纔會是我們要的兩向量的cos值。

fixed4 frag(v2f i) : SV_Target
{
	//使用normalize()函數對向量進行歸一化
	fixed3 worldNormal = normalize(i.worldNormal);
	fixed3 worldLightDir = normalize(i.worldLightDir);
}
計算光線強度並返回顏色
fixed4 frag(v2f i) : SV_Target
{
	fixed3 worldNormal = normalize(i.worldNormal);
	fixed3 worldLightDir = normalize(i.worldLightDir);
	//saturate()函數可以將值截取到[0,1],而dot()函數用於計算兩向量間的點積
	//計算出光線強度後,和我們的_Color屬性相乘,就能實現一個亮度更改了
	fixed3 diffuse = saturate(dot(worldNormal, worldLightDir)) * _Color;
	
	//返回計算完成的顏色
	return fixed4(diffuse, 1.0);
}

在這裏插入圖片描述

增加對燈光顏色的考慮

剛剛的計算我們實際上忽略了燈光的顏色,現在我們獲取燈光顏色並加入計算。

包含頭文件

爲了獲取燈光顏色,我們需要先在CG中包含頭文件 "Lighting.cginc"

Shader "Diffuse-Lambert"{

	Properties{
		_Color("Color Tint", Color) = (1, 1, 1, 1)
	}

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

			CGPROGRAM
			
			#pragma vertex vert 
			#pragma fragment frag 
			#include "Lighting.cginc"	 //額外包含一個"Lighting.cginc"
			#include "UnityCG.cginc"
			
			fixed4 _Color;

			struct a2v {
				float4 vertex : POSITION;
				float3 normal : NORMAL;
			};

			struct v2f
			{
				float4 pos : SV_POSITION;
				float3 worldNormal : TEXCOORD0;
				float3 worldLightDir : TEXCOORD1;
			};

			v2f vert(a2v v)
			{
				v2f o;
				o.pos = UnityObjectToClipPos(v.vertex);
				o.worldNormal = UnityObjectToWorldNormal(v.normal);
				o.worldLightDir = WorldSpaceLightDir(v.vertex);
				return o;
			}

			fixed4 frag(v2f i) : SV_Target
			{
				fixed3 worldNormal = normalize(i.worldNormal);
				fixed3 worldLightDir = normalize(i.worldLightDir);
				fixed3 diffuse = saturate(dot(worldNormal, worldLightDir)) * _Color;
				return fixed4(diffuse, 1.0);
			}
			ENDCG
		}
	}
}
修改片元着色器
fixed4 frag(v2f i) : SV_Target
{
	fixed3 worldNormal = normalize(i.worldNormal);
	fixed3 worldLightDir = normalize(i.worldLightDir);
	//直接乘上燈光顏色 _LightColor0
	fixed3 diffuse = saturate(dot(worldNormal, worldLightDir)) * _Color * _LightColor0;
	return fixed4(diffuse, 1.0);
}

在這裏插入圖片描述

另一種實現,半蘭伯特模型

在蘭伯特模型中,光照無法到達的區域,模型的外觀通常是全黑的,沒有任何明暗變化,這會使模型的背光區域看起來就像一個平面一樣,失去了模型細節表現。【candycat】
Valve公司在開發《半條命》時提出了一個新技術,被稱爲半蘭伯特光照模型。在半蘭伯特模型中,我們不將片元上法線向量光源方向的cos值截取到[0, 1],而是爲這個cos值乘上α倍的縮放然後加上一個β大小的位移,通常,α和β都是0.5。也就是說,半蘭伯特模型將法線向量光源方向的cos值從[-1, 1]映射到了[0, 1]。

修改片元着色器

fixed4 frag(v2f i) : SV_Target
{
	fixed3 worldNormal = normalize(i.worldNormal);
	fixed3 worldLightDir = normalize(i.worldLightDir);
	//蘭伯特模型
	//fixed3 diffuse = saturate(dot(worldNormal, worldLightDir)) * _Color * _LightColor0;
	//半蘭伯特模型
	fixed3 diffuse = (dot(worldNormal, worldLightDir) * 0.5 + 0.5) * _Color* _LightColor0;

	return fixed4(diffuse, 1.0);
}

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章