1. 程式人生 > >UnityShader學習_基本光照模型的實現

UnityShader學習_基本光照模型的實現

最近在學習《Unity Shader入門精要》一書,學到了光照模型,正好自己以前也用過OpenGL實現過標準光照模型(也可以叫Phong光照模型),那麼這次就用Unity Shader實現一下標準光照模型吧。

 那麼首先做一下準備工作:

  1. 準備使用的軟體:這裡用的是unity2018以及vs2015,其中vs2015使用了VSTU,HlSL Tools for  VS,ShaderLabVS三個外掛。(雖然並不是很好用)
  2. 建立一種材質Material以及一個Unlit Shader,把這個Shader賦予給材質。
  3. 在場景中關閉天空盒(skybox),建立一個3D物體(這裡採用的是capsule,能比較好的看出光照效果),把上面的材質賦予給物體,光源就採用預設的平行光,其它光源的計算方式有所不同。在如圖所示的地方關閉天空盒。                                                                                        
  4. 接下來複習一下標準光照模型的知識,其中傳到相機的光線由四個部分組成:環境光自發光漫反射 ,高光反射(也叫鏡面反射)。那麼:最終的效果顏色=環境光顏色+自發光顏色+漫反射顏色+高光反射顏色  。  其中環境光可以通過內建變數獲取 ,自發光可以自身設定,因此我們所要計算的就只有漫反射和高光反射。

            至此,準備工作已經完成,現在開始計算漫反射。漫反射使用蘭伯特(Lambert)光照模型表示,公式為

                                    diffuse = Kd * lightColor * max(dot(N, L), 0)

其中Kd

為材質的漫反射顏色,lightColor為光照顏色,N, L分別為表面法線,指向光源的單位向量,dot為向量點乘,max的作用是保證法線方向和光源方向的點乘不為負值,意義就是防止物體被從後面來的光源照亮。

雖然漫反射公式已經知道,那麼是在頂點著色器計算它的顏色還是在片元著色器計算它的顏色呢?答案自然是兩者都可以。其中在片元中計算就是逐畫素光照,在頂點中計算就是逐頂點光照,對於下面的高光反射的計算也是如此。

逐頂點計算漫反射:

Shader "Unity Shaders Book/DiffuseVertexLevel"
{
	Properties{
	_Diffuse("Diffuse",Color)=(1,1,1,1) //宣告一個Color屬性用來表示漫反射顏色,可以在Inspector面板顯示
	}
	SubShader{
		Pass{
			Tags{"LightMode"="ForwardBase"} //定義該Pass在光照流水線中的角色,只有定義正確才能獲得一些內建光照變數
			CGPROGRAM
			#pragma vertex vert 
			#pragma fragment frag 
			#include "Lighting.cginc" 
			//Lighting.cginc為包含一些內建光照變數的標頭檔案
			fixed4 _Diffuse; //定義一個在Properties中宣告過的變數,這樣才可以使用它

		    struct a2v //頂點著色器輸入結構體
			{
                float4 vertex : POSITION;
                float3 normal : NORMAL;
            };

            struct v2f //頂點著色器輸出結構體,片元著色器輸入結構體
			{
                float4 pos : SV_POSITION;
                fixed3 color : COLOR;
            };

			v2f vert(a2v v)//頂點著色器
			{
				v2f o;
				o.pos=UnityObjectToClipPos(v.vertex);//把頂點座標轉換到裁剪空間
				fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz;//獲取自然光
				fixed3 worldNormal=normalize(mul(v.normal,(float3x3)unity_WorldToObject));//把表面法線轉換到世界座標系並歸一化
				fixed3 worldLight=normalize(_WorldSpaceLightPos0.xyz);//獲取平行光的方向(只適用只有一個平行光源的時候)
				fixed3 diffuse=_LightColor0.rgb*_Diffuse.rgb*saturate(dot(worldNormal,worldLight));//計算漫反射,其中_LightColor0只有正確聲明瞭Tags和包含正確的標頭檔案才可以獲取
				o.color=ambient+diffuse;
				return o;
			}

			fixed4 frag(v2f i):SV_Target//表面著色器
			{
				return fixed4(i.color,1.0);
			}
			ENDCG
		}
	}
	FallBack "Diffuse"
}

       逐畫素計算漫反射:

Shader "Unlit/DiffusePixelLevel"
{
	Properties{
	_Diffuse("Diffuse",Color)=(1,1,1,1)
	}
	SubShader{
		Pass{
			Tags{"LightMode"="ForwardBase"}
			CGPROGRAM
			#pragma vertex vert 
			#pragma fragment frag 
			#include "Lighting.cginc"
			fixed4 _Diffuse;
		    struct a2v {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
            };

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

			v2f vert(a2v v)
			{
				v2f o;
				o.pos=UnityObjectToClipPos(v.vertex);
				o.worldNormal=mul(v.normal,(float3x3)unity_WorldToObject);
				return o;
			}

			fixed4 frag(v2f i):SV_Target{
				fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz;
				fixed3 worldNormal=normalize(i.worldNormal);
				fixed3 worldLgihtDir=normalize(_WorldSpaceLightPos0.xyz);
				fixed3 diffuse=_LightColor0.rgb*_Diffuse.rgb*saturate(dot(worldNormal,worldLgihtDir));
				fixed3 color=ambient+diffuse;
				return fixed4(color,1.0);
			}
			ENDCG
		}
	}
	FallBack "Diffuse"
}

        對比可見,兩種計算方式並沒有太大的區別,區別只是在於顏色的計算位置。逐頂點的顏色計算是在頂點著色器中,得到的是頂點光照顏色,然後片元著色器通過線性插值輸出畫素的顏色,逐片元的是以每個畫素基礎,得到它的法線(這裡是對頂點法線插值得到),然後再計算顏色。

至此,漫反射光照模型已經建立完成,但是由於max函式的作用,物體的背光面會是完全的黑色,這顯然與現實不符合,因此就有了半蘭伯特模型。它使用一個α倍的縮放和一個β倍的偏移來保證其不會為負值,這兩個數絕大部分時候為0.5.

計算公式:

漫反射光照 = (光照顏色與強度 * 漫反射顏色)* (dot(法線方向 ,光照方向) * α + β);

因此使用半蘭伯特模型的逐畫素漫反射為:

Shader "Unlit/DiffuseHalfLambert"
{
	Properties{
	_Diffuse("Diffuse",Color)=(1,1,1,1)
	}
	SubShader{
		Pass{
			Tags{"LightMode"="ForwardBase"}
			CGPROGRAM
			#pragma vertex vert 
			#pragma fragment frag 
			#include "Lighting.cginc"
			fixed4 _Diffuse;
		    struct a2v {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
            };

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

			v2f vert(a2v v)
			{
				v2f o;
				o.pos=UnityObjectToClipPos(v.vertex);
				o.worldNormal=mul(v.normal,(float3x3)unity_WorldToObject);
				return o;
			}

			fixed4 frag(v2f i):SV_Target{
				fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz;
				fixed3 worldNormal=normalize(i.worldNormal);
				fixed3 worldLgihtDir=normalize(_WorldSpaceLightPos0.xyz);
				fixed halfLambert=dot(worldNormal,worldLgihtDir)*0.5+0.5;
				fixed3 diffuse=_LightColor0.rgb*_Diffuse.rgb*halfLambert;
				fixed3 color=ambient+diffuse;
				return fixed4(color,1.0);
			}
			ENDCG
		}
	}
	FallBack "Diffuse"

     最終三種效果為:

             從裡到外分別為逐頂點光照,逐畫素光照,半蘭伯特逐畫素光照,可以看出逐頂點的黑白交界處有著明顯的鋸齒,逐畫素的則更加平滑,半蘭伯特的黑白分別沒有前面兩個的那麼明顯。

             現在,我們來計算一下高光反射模型,也可以說是Phong光照模型,計算公式為:

specular = Ks * lightColor * pow(max(dot(view, reflect), 0), gloss)         

其中Ks 為高光反射顏色, lightColor為光照顏色,view為相機方向,reflect為入射光反射方向,gloss為材質的光澤度,用來控制高光區域的大小,光澤度越大,區域越小。

同樣的,這個光照模型也有兩種方式:逐頂點著色,逐畫素著色。

Phong模型的逐頂點著色(其中反射方向用reflect函式求得)

Shader "Unlit/SpecularVertexLevel"
{
	Properties
	{
		_Diffuse("Diffuse",Color)=(1,1,1,1)
		_Specular("Specular",Color)=(1,1,1,1)
		_Gloss("Gloss",Range(8.0,256))=20
	}
	SubShader
	{
		Pass
		{
		Tags{"LightMode"="ForwardBase"}
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			#include "Lighting.cginc"
			fixed4 _Diffuse;
			fixed4 _Specular;
			float _Gloss;

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

			struct v2f{
			float4 pos:SV_POSITION;
			fixed3 color:COLOR;
			};

			v2f vert(a2v v)
			{
			v2f o;
			o.pos=UnityObjectToClipPos(v.vertex);
			fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz;
			fixed3 worldNormal=normalize(mul(v.normal,(float3x3)unity_WorldToObject));
			fixed3 worldLightDir=normalize(_WorldSpaceLightPos0.xyz);
			fixed3 diffuse=_LightColor0.rgb*_Diffuse.rgb*saturate(dot(worldNormal,worldLightDir));

			fixed3 reflectDir=normalize(reflect(-worldLightDir,worldNormal));
			fixed3 viewDir=normalize(_WorldSpaceCameraPos.xyz-mul(unity_ObjectToWorld,v.vertex).xyz);
			fixed3 specular=_LightColor0.rgb*_Specular.rgb*pow(saturate(dot(reflectDir,viewDir)),_Gloss);
			o.color=ambient+diffuse+specular;
			return o;
			}

			fixed4 frag(v2f i):SV_Target{
			return fixed4(i.color,1.0f);
			}
			ENDCG
		}
	}
	FallBack "Specular"
}

    Phong模型的逐畫素著色(其中反射方向用reflect函式求得)    

Shader "Unlit/SpecularPixelLevel"
{
	Properties
	{
		_Diffuse("Diffuse",Color)=(1,1,1,1)
		_Specular("Specular",Color)=(1,1,1,1)
		_Gloss("Gloss",Range(8.0,256))=20
	}
	SubShader
	{
		Pass
		{
		Tags{"LightMode"="ForwardBase"}
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			#include "Lighting.cginc"
			fixed4 _Diffuse;
			fixed4 _Specular;
			float _Gloss;

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

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

			v2f vert(a2v v)
			{
			v2f o;
			o.pos=UnityObjectToClipPos(v.vertex);
			o.worldNormal=mul(v.normal,(float3x3)unity_WorldToObject);
			o.worldPos=mul(unity_ObjectToWorld,v.vertex).xyz;
			return o;
			}

			fixed4 frag(v2f i):SV_Target
			{
			fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz;
			fixed3 worldNormal=normalize(i.worldNormal);
			fixed3 worldLightDir=normalize(_WorldSpaceLightPos0.xyz);
			fixed3 diffuse=_LightColor0.rgb*_Diffuse.rgb*saturate(dot(worldNormal,worldLightDir));

			fixed3 reflectDir=normalize(reflect(-worldLightDir,worldNormal));
			fixed3 viewDir=normalize(_WorldSpaceCameraPos.xyz-i.worldPos.xyz);
			fixed3 specular=_LightColor0.rgb*_Specular.rgb*pow(saturate(dot(reflectDir,viewDir)),_Gloss);
			return fixed4(ambient+diffuse+specular,1.0);
			}
			ENDCG
		}
	}
	FallBack "Specular"
}

 某些情況下計算更快,更正確的光照模型——Blinn光照模型

這是一種對Phong光照模型進行了修改的模型,並不是Phong模型的改進,它引入了一個新的向量h ,目的是避免計算反射方向reflect,計算方式為:

                                h=normalize(view+light)   

            其中view為相機方向,light為光源入射方向 。

            那麼使用了Blinn光照模型的逐畫素著色   

Shader "Unlit/BlinnPhong"
{
	Properties
	{
		_Diffuse("Diffuse",Color)=(1,1,1,1)
		_Specular("Specular",Color)=(1,1,1,1)
		_Gloss("Gloss",Range(8.0,256))=20
	}
	SubShader
	{
		Pass
		{
		Tags{"LightMode"="ForwardBase"}
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			#include "Lighting.cginc"
			#include "UnityCG.cginc"
			fixed4 _Diffuse;
			fixed4 _Specular;
			float _Gloss;

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

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

			v2f vert(a2v v)
			{
			v2f o;
			o.pos=UnityObjectToClipPos(v.vertex);
			//o.worldNormal=mul(v.normal,(float3x3)_World2Object);
			o.worldNormal=UnityObjectToWorldNormal(v.normal);
			o.worldPos=mul(unity_ObjectToWorld,v.vertex).xyz;
			return o;
			}

			fixed4 frag(v2f i):SV_Target
			{
			fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz;
			fixed3 worldNormal=normalize(i.worldNormal);
			//fixed3 worldLightDir=normalize(_WorldSpaceLightPos0.xyz);
			fixed3 worldLightDir=normalize(UnityWorldSpaceLightDir(i.worldPos));
			fixed3 diffuse=_LightColor0.rgb*_Diffuse.rgb*saturate(dot(worldNormal,worldLightDir));

			fixed3 reflectDir=normalize(reflect(-worldLightDir,worldNormal));
			//fixed3 viewDir=normalize(_WorldSpaceCameraPos.xyz-i.worldPos.xyz);
			fixed3 viewDir=normalize(UnityWorldSpaceViewDir(i.worldPos));
			fixed3 halfDir=normalize(worldLightDir+viewDir);
			fixed3 specular=_LightColor0.rgb*_Specular.rgb*pow(saturate(dot(worldNormal,halfDir)),_Gloss);
			return fixed4(ambient+diffuse+specular,1.0);
			}
			ENDCG
		}
	}
	FallBack "Specular"
}

           最終效果為:                 從裡到外分別為逐頂點著色,逐畫素著色,Bling模型的逐畫素著色,其中漫反射顏色為淺黑色,這是為了更好的看出高光反射效果。由圖可以看出逐頂點著色並不是一個圓形,這是對顏色進行線性插值的結果,但是高光反射的計算是非線性的。而使用Bling-Phong光照模型的高光部分更大更亮。                                                                                                                                             至此一個完整的標準光照模型就此完成,儘管標準光照模型並不是很符合現實,但是其簡單,易於計算,更容易上手。