Unity Shader-Phong光照模型與Specular

簡介


學完了蘭伯特光照模型,再來學習一個更加高級一點的光照模型-Phong光照模型。光除了漫反射,還有鏡面反射。一些金屬類型的材質,往往表現出一種高光效果,用蘭伯特模型是模擬不出來的,所以就有了Phong模型。Phong模型主要有三部分構成,第一部分是上一篇中介紹了的Diffuse,也就是漫反射,第二部分是環境光,在非全局光照的情況下,我們一般是通過一個環境光來模擬物體的間接照明,這個值在shader中可以通過一個宏來直接獲取,而第三部分Specular,也就是高光部分的計算,是一種模擬鏡面反射的效果,也是本篇文章重點介紹的內容。


在現實世界中,粗糙的物體一般會是漫反射,而光滑的物體呈現得較多的就是鏡面反射,最明顯的現象就是光線照射的反射方向有一個亮斑。再來複習一下鏡面反射的概念:當平行入射的光線射到這個反射面時,仍會平行地向一個方向反射出來,這種反射就屬於鏡面反射,其反射波的方向與反射平面的法線夾角(反射角),與入射波方向與該反射平面法線的夾角(入射角)相等,且入射波、反射波,及平面法線同處於一個平面內。反射光的亮度不僅與光線的入射角有關,還與觀察者視線和物體表面之間的角度有關。鏡面反射通常會造成物體表面上的“閃爍”和“高光”現象,鏡面反射的強度也與物體的材質有關,無光澤的木材很少會有鏡面反射發生,而高光澤的金屬則會有大量鏡面反射。


Phong光照模型


Phong光照模型中主要的部分就是對高光的計算,首先來看下面這張圖片:

理想情況下,光源射出的光線,通過鏡面反射,正好在反射光方向觀察,觀察者可以接受到的反射光最多,那麼觀察者與反射方向之間的夾角就決定了能夠觀察到高光的多少。夾角越大,高光越小,夾角越小,高光越大。而另一個影響高光大小的因素是表面的光滑程度,表面越光滑,高光越強,表面月粗糙,高光越弱。L代表光源方向,N代表頂點法線方向,V代表觀察者方向,R代表反射光方向。首先需要計算反射光的方向R,反射光方向R可以通過入射光方向和法向量求出,R + L = 2dot(N,L)N,進而推出R = 2dot(N,L)N - L。關於R計算的推導,可以看下面這張圖:


不過在cg中,我們不用這麼麻煩,cg爲我們提供了一個計算反射光方向的函數reflect函數,我們只需要傳入入射光方向(光源方向的反方向)和表面法線方向,就可以計算得出反射光的方向。然後,我們通過dot(R,V)就可以得到反射光方向和觀察者方向之間的夾角餘弦值了。下面給出馮氏反射模型公式:

I(spcular) = I * k * pow(max(0,dot(R,V)), gloss) ,其中I爲入射光顏色向量,k爲鏡面反射係數,gloss爲光滑程度。

通過上面的公式,我們可以看出,鏡面反射強度跟反射向量與觀察向量的餘弦值呈指數關係,指數爲gloss,該係數反映了物體表面的光滑程度,該值越大,表示物體越光滑,反射光越集中,當偏離反射方向時,光線衰減程度越大,只有當視線方向與反射光方向非常接近時才能看到高光現象,鏡面反射光形成的光斑較亮並且較小;該值越小,表示物體越粗糙,反射光越分散,可以觀察到光斑的區域越廣,光斑大並且強度較弱。

Phong光照模型在Unity中的實現


下面看一下馮氏光照模型在Unity中的實現,由於有高光,爲了更好的效果,我們將主要的計算放到了fragment shader中進行。不多說,上代碼:
Shader "ApcShader/SpecularPerPixel"
{
	//屬性
	Properties
	{
		_Diffuse("Diffuse", Color) = (1,1,1,1)
		_Specular("Specular", Color) = (1,1,1,1)
		_Gloss("Gloss", Range(1.0, 255)) = 20
	}

	//子着色器	
	SubShader
	{
		Pass
		{
			//定義Tags
			Tags{ "LightingMode" = "ForwardBase" }

			CGPROGRAM
			//引入頭文件
			#include "Lighting.cginc"

			//定義函數
			#pragma vertex vert
			#pragma fragment frag

			fixed4 _Diffuse;
			fixed4 _Specular;
			float _Gloss;

			//定義結構體:應用階段到vertex shader階段的數據
			struct a2v
			{
				float4 vertex : POSITION;
				float3 normal : NORMAL;
			};

			//定義結構體:vertex shader階段輸出的內容
			struct v2f
			{
				float4 pos : SV_POSITION;
				float3 worldNormal : NORMAL;
				float3 worldPos : TEXCOORD1;
			};

			//頂點shader
			v2f vert(a2v v)
			{
				v2f o;
				o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
				//法線轉化到世界空間
				o.worldNormal = normalize(mul(v.normal, (float3x3)_World2Object));
				//頂點位置轉化到世界空間
				o.worldPos = mul(_Object2World, v.vertex).xyz;
				return o;
			}

			//片元shader
			fixed4 frag(v2f i) : SV_Target
			{
				//環境光
				fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * _Diffuse;
				//歸一化光方向
				fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz);
				//再次歸一化worldNorml
				fixed3 worldNormal = normalize(i.worldNormal);
				//diffuse
				fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLight));
				//計算反射方向R,worldLight表示光源方向(指向光源),入射光線方向爲-worldLight,通過reflect函數(入射方向,法線方向)獲得反射方向
				fixed3 reflectDir = normalize(reflect(-worldLight, worldNormal));
				//計算該像素對應位置(頂點計算過後傳給像素經過插值後)的觀察向量V,相機座標-像素位置
				fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
				//計算高光值,高光值與反射光方向與觀察方向的夾角有關,夾角爲dot(R,V),最後根據反射係數計算的反射值爲pow(dot(R,V),Gloss)
				fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0.0,dot(reflectDir, viewDir)), _Gloss);
				//馮氏模型:Diffuse + Ambient + Specular
				fixed3 color = diffuse + ambient + specular;
				return fixed4(color, 1.0);
			}
			ENDCG
		}
	}

	//前面的Shader失效的話,使用默認的Diffuse
	FallBack "Diffuse"
}

我們找一個球球,使用我們的shader看一下效果:


調整觀察的方向以及光滑程度,可以看見亮斑是隨着觀察的方向改變而變化的:




Blinn-Phong光照模型


Phong光照模型能夠很好地表現高光效果,不過Blinn-Phong光照的缺點就是計算量較大,所以,在1977年,Jim Blinn對Phong光照進行了改進,稱之爲Blinn-Phong光照模型。

關於Blinn-Phong和Phong光照模型的對比,可以參照這張圖片:

Blinn-Phong光照引入了一個概念,半角向量,用H表示。半角向量計算簡單,通過將光源方向L和視線方向V相加後歸一化即可得到半角向量。Phong光照是比較反射方向R和視線方向V之間的夾角,而Blinn-Phong改爲比較半角向量H和法線方向N之間的夾角。半角向量的計算複雜程度要比計算反射光線簡單得多,所以Blinn-Phong的性能要高得多,效果比Phong光照相差不多,所以OpenGL中固定管線的光照模型就是Blinn-Phong光照模型。

BlinnPhong光照模型如下:

I(spcular) = I * k * pow(max(0,dot(N,H)), gloss) ,其中I爲入射光顏色向量,k爲鏡面反射係數,gloss爲光滑程度。

Blinn-Phong光照在Unity中的實現


Shader "ApcShader/BlinnPhongPerPixel"
{
	//屬性
	Properties
	{
		_Diffuse("Diffuse", Color) = (1,1,1,1)
		_Specular("Specular", Color) = (1,1,1,1)
		_Gloss("Gloss", Range(1.0, 256)) = 20
	}

	//子着色器	
	SubShader
	{
		Pass
		{
			//定義Tags
			Tags{ "LightingMode" = "ForwardBase" }

			CGPROGRAM
			//引入頭文件
			#include "Lighting.cginc"

			//定義函數
			#pragma vertex vert
			#pragma fragment frag

			//定義Properties中的變量
			fixed4 _Diffuse;
			fixed4 _Specular;
			float _Gloss;

			//定義結構體:應用階段到vertex shader階段的數據
			struct a2v
			{
				float4 vertex : POSITION;
				float3 normal : NORMAL;
			};

			//定義結構體:vertex shader階段輸出的內容
			struct v2f
			{
				float4 pos : SV_POSITION;
				float3 worldNormal : NORMAL;
				float3 worldPos : TEXCOORD1;
			};

			//頂點shader
			v2f vert(a2v v)
			{
				v2f o;
				o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
				//法線轉化到世界空間
				o.worldNormal = normalize(mul(v.normal, (float3x3)_World2Object));
				//頂點位置轉化到世界空間 
				o.worldPos = mul(_Object2World, v.vertex).xyz;
				return o;
			}

			//片元shader
			fixed4 frag(v2f i) : SV_Target
			{
				//環境光
				fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * _Diffuse;
				//世界空間下光線方向
				fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz);
				//需要再次normalize
				fixed3 worldNormal = normalize(i.worldNormal);
				//計算Diffuse
				fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLight));
				//計算視線方向(相機位置-像素對應位置)
				fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
				//計算半角向量(光線方向 + 視線方向,結果歸一化)
				fixed3 halfDir = normalize(worldLight + viewDir);
				//計算Specular(Blinn-Phong計算的是)
				fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(halfDir, worldNormal)), _Gloss);
				//結果爲diffuse + ambient + specular
				fixed3 color = diffuse + ambient + specular;
				return fixed4(color, 1.0);
			}
				ENDCG
		}
	}
	//前面的Shader失效的話,使用默認的Diffuse
	FallBack "Diffuse"
}

我們再放一個球,使用Blinn-Phong光照,與Phong光照模型進行對比:

換個角度再看一下:

帶紋理的Phong光照Shader


只有兩個沒有紋理的球,是說明不了問題的,下面來看一下帶有紋理的Phong光照Shader。首先,我們要思考一個問題,如果我們要使用Specular類型的Shader,那麼這個物體一般是金屬類型的,這樣,這個物體就會呈現金屬特有的高光屬性。然而,實際上完全是金屬的物體並不是很多,現實世界中漫反射和鏡面反射是共存的,拿一把刀來說,刀身是金屬,刀柄是木頭,那麼,只有刀身適合這種類型的shader。可能我們最簡單的想法是把刀拆成兩個部分,刀身用的是Specular,刀柄用Diffuse;但是這種做法很麻煩,而且一個物體通過了兩個drall call才能渲染出來。所以,聰明的前輩們總是能想到好的辦法,次時代類型遊戲中最簡單的一種貼圖就誕生了---高光貼圖(通道)。

所謂高光貼圖,或者說成高光通道,就是通過在製作貼圖時,把圖片的高光信息存儲在一個灰度圖或者直接存儲在貼圖的通道內,如果不需要Alpha Test的話,可以直接把高光通道放在Diffuse貼圖的Alpha通道。而我們在shader中計算時,通過採樣,就可以獲得這個貼圖中每個像素對應的位置是否是有高光的。這樣,在Fragment Shader中可以直接通過這個Mask值乘以高光從而通過一個材質渲染出同一個模型上的不同質地。比如在通道內,0表示無高光,1(255)表示高光最強,那麼,不需要高光的地方我們就可以在製作這張圖的時候給0,需要高光的,刷上顏色,顏色越接近白色,高光越強。

知道了原理之後,我們就可以找一個人物模型貼圖,然後在PhotoShop中給RGB格式的貼圖增加一個通道,作爲高光通道,然後把需要高光的部分摳出來,其他部分置爲黑色

好吧,作爲一個程序員,一直想說,我.......的美工,實在不怎麼樣。不管圖扣得再怎麼差,原理沒錯就好。下面上shader:
Shader "ApcShader/BlinnPhongWithTex"
{
	//屬性
	Properties
	{
		_Diffuse("Diffuse", Color) = (1,1,1,1)
		_Specular("Specular", Color) = (1,1,1,1)
		_SpecularScale("SpecularScale", Range(0.0, 5.0)) = 1.0
		_Gloss("Gloss", Range(0.0, 1)) = 20
		_MainTex("RGBSpecular", 2D) = "white"{}
	}

	//子着色器	
	SubShader
	{
		Pass
		{
			//定義Tags
			Tags{ "LightingMode" = "ForwardBase" }

			CGPROGRAM
			//引入頭文件
			#include "Lighting.cginc"

			//定義函數
			#pragma vertex vert
			#pragma fragment frag

			//定義Properties中的變量
			fixed4 _Diffuse;
			fixed4 _Specular;
			float _Gloss;
			float _SpecularScale;
			sampler2D _MainTex;
			float4 _MainTex_ST;

			//定義結構體:應用階段到vertex shader階段的數據
			struct a2v
			{
				float4 vertex : POSITION;
				float3 normal : NORMAL;
				float4 texcoord : TEXCOORD0;
			};

			//定義結構體:vertex shader階段輸出的內容
			struct v2f
			{
				float4 pos : SV_POSITION;
				float3 worldNormal : NORMAL;
				float3 worldPos : TEXCOORD0;
				float2 uv : TEXCOORD1;
			};

			//頂點shader
			v2f vert(a2v v)
			{
				v2f o;
				o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
				//法線轉化到世界空間
				o.worldNormal = normalize(mul(v.normal, (float3x3)_World2Object));
				//頂點位置轉化到世界空間 
				o.worldPos = mul(_Object2World, v.vertex).xyz;
				//轉化uv
				o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
				return o;
			}

			//片元shader
			fixed4 frag(v2f i) : SV_Target
			{
				//環境光
				fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
				//世界空間下光線方向
				fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz);
				//需要再次normalize
				fixed3 worldNormal = normalize(i.worldNormal);
				//計算Diffuse
				fixed3 diffuse = _LightColor0.rgb * (dot(worldNormal, worldLight) * 0.5 + 0.5);
				//計算視線方向(相機位置-像素對應位置)
				fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
				//計算半角向量(光線方向 + 視線方向,結果歸一化)
				fixed3 halfDir = normalize(worldLight + viewDir);
				//計算Specular(Blinn-Phong計算的是)
				fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(halfDir, worldNormal)), _Gloss);
				//紋理採樣
				fixed4 tex = tex2D(_MainTex, i.uv);
				//紋理中rgb爲正常顏色,a爲一個高光的mask圖,非高光部分a值爲0,高光部分根據a的值控制高光強弱
				fixed3 color = (diffuse  + ambient + specular * tex.a * _SpecularScale) * tex.rgb;
				return fixed4(color, 1.0);
			}
				ENDCG
		}
	}
	//前面的Shader失效的話,使用默認的Diffuse
	FallBack "Diffuse"
}
看一下最終效果,左側爲使用了高光的效果,右側爲普通diffuse的效果,可以看出,使用了高光貼圖,我們只有上面的刀才表現出了高光,其他部分仍然是正常的。



高光有個很搭配的後處理,就是Bloom效果(全屏泛光),也是我很喜歡的效果之一。虛幻四的Bloom效果非常好,帶有金屬的效果加上泛光,讓人特別舒服,我們在Unity中用一個簡單的Bloom效果,讓金屬效果更好。


最後來一張動圖,Specular的主要特點就在於當觀察者與對象角度有變化時,也就是V向量和R向量之間的角度有變化時,高光效果會有變化,所以只有動起來,才能真正看出Specular與Diffuse的不同。


Shader的優化


一般情況下,fragment shader是性能的瓶頸,所以優化Shader的重要思路之一就是減少逐像素計算,將計算挪到vertex shader部分,然後通過vertex shader向fragment shader中傳遞參數。正常情況下,一個物體在屏幕上,逐頂點計算的量級要遠遠小於物體在屏幕上逐像素計算的量(當然如果物體離相機特別遠,光柵化之後在屏幕上只佔了很小的一部分時,有可能有反過來的情況,但是有LOD之類的技術的話,遠了之後,更換爲低模,也會降低頂點數,所以還是逐像素計算的比較可怕,尤其是分辨率大了之後)。當然,我們也不能把所有計算都放在vertex shader中,上一篇文章中說過,如果將高光計算放在vertex shader中,效果很差,下面就來看一下,效果有多差:
爲什麼會有這樣的結果呢,主要是頂點計算的結果是通過頂點傳遞好的顏色進行高洛德着色,只是一個顏色的插值。而放在像素着色階段,是通過頂點傳遞過來的參數,並且傳遞到像素階段時經過了插值計算得到的信息,逐像素計算光照效果得到最終結果。更加詳細的解釋可以參照上一篇文章。2001年左右第三代modern GPU開始支持vertex shader,而在2003年左右,NVIDIA的GeForce FX和ATI Radeon 9700開始,GPU纔開始支持fragment shader,也就是說fragment更先進,可以得到更好的效果。所以,我們只是將一些不會影響效果的計算放在vertex shader中即可。

上面的blinn-phong shader中,我們在fragment shader中計算了世界空間下的ViewDir,我們可以把這個計算移到vertex shader中進行:
//blinn-phong shader
//puppet_master
//2016.12.11
Shader "ApcShader/BlinnPhongPerPixel"
{
	//屬性
	Properties
	{
		_Diffuse("Diffuse", Color) = (1,1,1,1)
		_Specular("Specular", Color) = (1,1,1,1)
		_Gloss("Gloss", Range(1.0, 256)) = 20
	}

	//子着色器	
	SubShader
	{
		Pass
		{
			//定義Tags
			Tags{ "LightingMode" = "ForwardBase" }

			CGPROGRAM
			//引入頭文件
			#include "Lighting.cginc"

			//定義函數
			#pragma vertex vert
			#pragma fragment frag

			//定義Properties中的變量
			fixed4 _Diffuse;
			fixed4 _Specular;
			float _Gloss;

			//定義結構體:應用階段到vertex shader階段的數據
			struct a2v
			{
				float4 vertex : POSITION;
				float3 normal : NORMAL;
			};

			//定義結構體:vertex shader階段輸出的內容
			struct v2f
			{
				float4 pos : SV_POSITION;
				float3 worldNormal : NORMAL;
				float3 viewDir : TEXCOORD1;
			};

			//頂點shader
			v2f vert(a2v v)
			{
				v2f o;
				o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
				//法線轉化到世界空間
				o.worldNormal = normalize(mul(v.normal, (float3x3)_World2Object));
				//頂點位置轉化到世界空間 
				float3 worldPos = mul(_Object2World, v.vertex).xyz;
				//計算視線方向(相機位置 - 像素對應位置)
				o.viewDir = _WorldSpaceCameraPos - worldPos;
				return o;
			}

			//片元shader
			fixed4 frag(v2f i) : SV_Target
			{
				//環境光
				fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * _Diffuse;
				//世界空間下光線方向
				fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz);
				//需要再次normalize
				fixed3 worldNormal = normalize(i.worldNormal);
				//計算Diffuse
				fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLight));
				//normalize
				fixed3 viewDir = normalize(i.viewDir);
				//計算半角向量(光線方向 + 視線方向,結果歸一化)
				fixed3 halfDir = normalize(worldLight + viewDir);
				//計算Specular(Blinn-Phong計算的是)
				fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(halfDir, worldNormal)), _Gloss);
				//結果爲diffuse + ambient + specular
				fixed3 color = diffuse + ambient + specular;
				return fixed4(color, 1.0);
			}
				ENDCG
		}
	}
	//前面的Shader失效的話,使用默認的Diffuse
	FallBack "Diffuse"
}
在優化前後,沒有特別明顯的變化:









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