1. 程式人生 > >【Unity Shaders】Vertex & Fragment Shader入門

【Unity Shaders】Vertex & Fragment Shader入門

寫在前面

三個月以前,在一篇講卡通風格的Shader的最後,我們說到在Surface Shader中實現描邊效果的弊端,也就是隻對錶面平緩的模型有效。這是因為我們是依賴法線和視角的點乘結果來進行描邊判斷的,因此,對於那些平整的表面,它們的法線通常是一個常量或者會發生突變(例如立方體的每個面),這樣就會導致最後的效果並非如我們所願。如下圖所示:

因此,我們有一個更好的方法來實現描邊效果,也就是通過兩個pass進行渲染——首先渲染物件的背面,用黑色略微向外擴充套件一點,就是我們的描邊效果;然後正常渲染正面即可。而我們應該知道,surface shader是不可以使用pass的。

如果我們想要使用上述方法實現描邊,我們就需要寫另一種shader——fragment shader。和surface shader相比,這種shader需要我們編寫更多的程式碼,處理更多的事情,但也可以讓我們更加了解shader是如何工作的。而之前的

一篇文章也分析過,其實surface shader的背後也是生成了對應的vertex&fragment shader。

這篇文章主要參考了Unity Gems裡的一篇文章,但正如文章評論裡所說,有些技術比如求attenuation穩重方法已經“過時”,因此本文會對這類問題以及一些作者沒有說清的問題給予說明。在查資料的時候,發現由於Unity背後做了太多事,定義了很多變數、函式和巨集,而又沒有給出詳盡的使用說明,寫起來實在太頭大了。。。同樣,本篇內容僅供參考。

Vertex & Fragment Shaders

Vertex & Fragment Shaders的工作流程如下圖所示(簡略版,來自

Unity Gems):


所以,看起來也沒那麼難啦~我們只需要編寫兩個函式就可以嘍~

我們來分析下它的流程。首先,vertex program收到系統傳遞給它的模型資料,然後把這些處理成我們後續需要的資料(但至少要包含這些頂點的位置資訊)進行輸出。其他的輸出資料比如有,紋理的UV座標以及其他需要傳遞給fragment program的資料。然後,系統對vertex program輸出的頂點資料進行插值,並將插值結果傳遞給fragment program。最後,fragment program根據這些插值結果計算最後螢幕上的畫素顏色。

在本篇文章,我們首先會學習編寫一個簡單的diffuse & diffuse bumped shader。然後再來具體看如何編寫一個具有多個passes的shader。

Diffuse, Vertex Lit Fragment Shader

開始的開始,我們首先需要在SubShader中使用Pass {}關鍵字定義一個pass。一個Pass可以為該階段定義一系列的tags。例如,我們可以剔除(Cull)背面或者正面,控制是否寫入Z buffer等。我們的diffuser shader將會剔除背面。具體可見官網

下面是我們的Pass定義:

		Pass {
			Tags { "LightMode" = "Vertex" }
			Cull Back
			Lighting OnCGPROGRAM
			
			#pragma vertex vert
			#pragma fragment frag
			
			#pragma multi_compile_fwdbase
			
			#include "UnityCG.cginc"
			
			// More code here
			
			ENDCG
		}

在上面的程式碼裡,我們定義了一個pass,設定LightMode為Vertex,告訴它開啟光源並且剔除背面。然後,我們定義了CG程式的開頭部分,指定了vertex和fragment programs的名字。最後,我們包含了Unity定義的一個檔案,以便在後面的CG程式中可以使用某些函式和變數。

LightMode是個非常重要的選項,因為它將決定該pass中光源的各變數的值。如果一個pass沒有指定任何LightMode tag,那麼我們就會得到上一個物件殘留下來的光照值,這並不是我們想要的。其他各個LightMode的具體含義可以參見官網(很重要,一定要去看,特別是對於每個Pass的細節解釋,一定要點進去看!!!),這裡做一個簡單的解釋。

  • LightMode=Vertex:會設定4個光源,並按亮度從明到暗進行排序,它們的值會儲存在unity_LightColor[n], unity_LightPosition[n], unity_LightAtten[n]這些陣列中。因此,[0]總會得到最亮的光源。
  • LightMode=ForwardBase: _LightColor0將會是主要的directional light的顏色。

  • LightMode=ForwardAdd:和上面一樣, _LightColor0將是該逐畫素光源的顏色。


Vertex Lit是什麼

在我們寫shader的時候有很多選擇——我們可以定義多個passes,其中每一個pass處理一個光源,這樣來處理所有的光源;或者我們選擇逐頂點處理所有的光源(在一個pass裡處理掉),然後再對它們進行插值。很明顯,後面這種方式會快很多,因為它僅僅需要一個pass就可以了,而前一個方式需要更多的passes。

如果我們寫了一個Vertex Lit shader,那麼我們就會按照第二種方式那樣,一次考慮所有的光源對頂點的影響。如果我們寫了一個多passes的shader,那麼它就會被多次呼叫,每次針對一個光源,考慮該光源對模型的影響。

對於Vertex Lit,Unity已經為我們編寫了一些輔助函式,我們會在後面看到。

The Vertex Program

下面,我們正式開始編寫程式碼。首先,我們需要定義vertex program。而它需要得到模型的相關資訊作為輸入,因此,我們定義下面的結構:

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

這個結構定義依賴某些語法,即那些“:XXX”樣子的值。我們的變數叫什麼並不重要,但這些“:XXX”語法則說明系統將使用哪些值去填充它們。這裡,我們通過上述程式碼可以得到了model space中的頂點位置、法線方向以及紋理座標。在fragment shaders裡,空間(spaces)的概念是非常重要的。空間重要是指座標的相對位置。
  • 在model space中,座標是相對於網格的原點(0,0,0)定義的。我們的vertex function需要把這些座標轉換到clip space中,為投影做準備。
  • 在tangent space中,座標是相對於模型的正面定義的——在處理法線紋理時我們使用這個space,這在後面會具體講到。
  • 在world space中,座標是相對於世界的原點(0,0,0)定義的。 
  • 在view space中,座標是相對於攝像機定義的,因此在這個space中,攝像機的位置就是(0,0,0)。
  • 在clip space中,通常圖元會被裁剪,然後再通過螢幕對映投影到螢幕空間中。
如果你讀過一些關於shaders的文章,那麼你大概會見過關於選擇哪個space來照亮模型的理論。初學者往往會有點困惑,這實際上就是選擇你要把光源方向、位置等資料轉換到哪個座標系中來進行相關運算,得到最終的畫素值。希望在本篇的最後,你可以明白這些問題!

那麼,在定義了vertex program的輸入後,我們還需要定義它的輸出。之前我們說過,vertex program的輸出將會被插值用於生成畫素,而這些插值後的值就是fragment program的輸入。

			struct v2f {
				float4 pos : POSITION;
				float2 uv : TEXCOORD0;
				float3 color : TEXCOORD1;
			};

上面就是我們的輸出。在這裡,之前所說的語義就沒有那麼重要了——只有一個是必須的,即用POSITION標識的變數,這是把頂點座標轉換到clip space後的位置。我們輸出的所有值(並且沒有uniform限定詞)都將在fragment program之前被插值。

注意:但對於DX11和Xbox360來說,必須要有語義說明,否則會報錯。即需要為變數指定TEXCOORD1等位置。

出於效能的考慮,很顯然我們應該儘可能在vertex function裡進行更多的運算,這是因為vertex function是逐頂點呼叫的,而fragment function則是逐畫素呼叫的。

下面是真正的vertex function,它把輸入a2v轉換成輸出v2f(也是fragment function的輸入)。

			v2f vert(a2v v) {
				v2f o;
				o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
				o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
				o.color = ShadeVertexLights(v.vertex, v.normal);		
				return o;
			}

第一行,我們定義了輸出v2f的一個例項。然後把頂點的位置和Unity提前定義的一個矩陣UNITY_MATRIX_MVP(在UnityShaderVariables.cginc裡定義)相乘,從而把頂點位置從model space轉換到clip space。我們使用了矩陣乘法操作mul來執行這個步驟。

第二行,我們為給定的紋理計算其uv座標,即根據mesh上的uv座標來計算真正的紋理上對應的位置。我們使用了Unity.CG.cginc中的巨集TRANSFORM_TEX來實現。

注意,要使用巨集TRANSFORM_TEX,我們需要在shader中定義一些額外的變數,即必須定義一個名為_YourTextureName_ST (也就是你的紋理的名字加一個 _ST字尾)。這是因為巨集TRANSFORM_TEX的定義為:#define TRANSFORM_TEX(tex,name) (tex.xy * name##_ST.xy + name##_ST.zw)。這是因為我們的紋理有Tiling和Offset引數,如下圖中面板所示,因此需要對原mesh上的uv進行相應調整才能得到真正的紋理座標。
最後,我們計算得到頂點的初始顏色——即光源對該頂點的影響。在我們的第一個shader中,我們使用一個名為ShadeVertexLights的函式,它的輸入為模型的頂點和法線。這是一個內建的函式,它將考慮4個距離最近(若距離相等則按光源型別排序)的光源以及一個環境光(在Edit->Render Settings->Ambient Light裡設定)。它的實現可以在UnityCG.cginc裡找到。其他輔助函式可以詳見官網

The Fragment Shader

根據上述過程,系統會在每個頂點上呼叫vertex program,並將其輸出在同一個幾何圖元上進行插值。下面,我們根據這些插值後的值來得到對應的畫素值。下面是真正的fragment program:

			float4 frag(v2f i) : COLOR {
				float4 c = tex2D(_MainTex, i.uv);
				c.rgb = c.rgb * i.color * 2;
				return c;
			}

上述程式碼使用了surface shader中也很常見的紋理取樣操作,來得到對應的紋理畫素值。然後,將該紋理顏色和插值後的vertex function輸出的頂點光顏色進行相乘,並把結果乘以2(否則顏色會太暗。)。最後,返回得到的畫素值。

完整程式碼

最後,完整的Vertex Lit Diffuse程式碼如下:

Shader "Custom/VertexLit" {
	Properties {
		_MainTex ("Base (RGB)", 2D) = "white" {}
	}
	SubShader {
		Tags { "RenderType"="Opaque" }
 		LOD 300
 		
 		Pass {
			Tags { "LightMode" = "Vertex" }
			
			Cull Back
			Lighting On
			
			CGPROGRAM
			
			#pragma vertex vert
			#pragma fragment frag
			
			#include "UnityCG.cginc"
			
			sampler _MainTex;
			float4 _MainTex_ST;
			
			struct a2v {
				float4 vertex : POSITION;
				float3 normal : NORMAL;
				float4 texcoord : TEXCOORD0;
			};
			
			struct v2f {
				float4 pos : POSITION;
				float2 uv : TEXCOORD0;
				float3 color : TEXCOORD1;
			};
			
			v2f vert(a2v v) {
				v2f o;o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
				o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
				o.color = ShadeVertexLights(v.vertex, v.normal);
				return o;
			}
			
			float4 frag(v2f i) : COLOR {
				float4 c = tex2D(_MainTex, i.uv);
				c.rgb = c.rgb * i.color * 2;
				return c;
			}
			
			ENDCG
		}
	} 
	FallBack "Diffuse"
}

這樣,我們就完成了第一個vertex & fragment shader。上述效果如果用surface shader可能只要幾句話,但你漸漸會發現,雖然使用vertex & fragment shader會增加更多的程式碼量,但它能做的真是太多了!

上述shader的效果如下(啦啦啦,又是小蘋果+呆萌小怪獸的組合~~~):


Diffuse Normal Map Shader

下面我們要向shader新增一個非常常見的法線紋理(Normal Texture)。

Normal Maps

如果你在Unity裡使用過法線紋理的話,你應該知道在使用之前,你需要先把該紋理的型別設定成Normal,對吧?那麼,到底為什麼要這樣呢?法線紋理跟其他紋理有什麼不一樣呢?

法線紋理具有以下性質

  • 它儲存了模型表面的法線方向。有基於model space(肉眼看起來顏色比較豐富,有紅色藍色等)和基於tangent space(通常都是藍色的)的兩種法線紋理,而Unity常見的是後面一種法線紋理。

  • 由於法線向量中每一維的範圍在(-1,1),因此我們需要把它重新對映到(0,255)。具體做法是把原值除以2再偏移0.5,最後乘以255。

  • 在儲存的時候是壓縮儲存。因為法線紋理都是被正則化的,即是單位向量,模為1,所以實際上只需要儲存該向量的兩個維度就可以了,第三維可以用前兩個推匯出來。

  • 由於上一點,每一個維度佔用16 bits,即每個rgba包含了兩個維度的值。

當使用法線紋理的時候,我們需要在tangent space中處理光照對模型的影響。也就是說,我們需要把和計算光照對畫素的影響的資料都轉換到tangent space中,然後在這個座標系中計算得到最終的顏色。而且,在這裡我們實際上是計算了逐畫素的光照,而不是像前一個shader那樣是逐頂點的。我們選擇在tangent space計算光照是因為這種做法的計算量更少。我們只需要基於每個頂點,把光照資訊(有時還需要觀察點資訊等)轉換到tangent space,再對其進行插值即可。而另一種方式是在world space中處理光照,這意味著我們需要把法線紋理中的每一個法線轉換到world space中,因此我們需要基於每個畫素進行處理。和逐頂點的處理方式相比,這種方法顯然需要更多的計算。在Unity裡轉換到tangent space是比較容易的。下面,我們不會使用逐頂點的光照處理函式ShadeVertexLights,而是逐畫素的處理光照。

照亮我們的模型

下面,我們將使用Lambert光照模型,也就是法線*光照方向*衰減*2。在我們把需要的資料都轉換到tangent space後,處理光照就變得非常簡單了。可以用下圖(來源:Unity Gems)來演示這樣一個過程:

但是,光源在哪裡呢?

Unity為我們提供了那些對模型有影響的光源(按重要度排序,例如距離遠近、光照型別等)的位置、顏色和衰減等資訊。

Unity使用了三個資料來定義頂點光源:unity_LightPosition,unity_LightAtten和unity_LightColor。例如[0]表示最重要的光源。

當我們編寫一個multi-pass的光照模型(正如我們下面寫的那樣)時,我們只需要一次處理一個單獨的光源,這種情況下,Unity同樣定義了一個名為_WorldSpaceLightPos0的值,來幫助我們得到它的位置,並且還提供了一個非常有用的函式ObjSpaceLightDir,它可以計算得到該光源的方向。而為了得到該光源的顏色,我們可以在程式中包含“Lighting.cginc”檔案,然後使用_LightColor0進行訪問。

Forward Lighting(而非Vertex Lit)

在第一個shader裡我們使用了vertex lights,而現在,我們來看下怎麼為光源定義多個passes。那麼,開始吧!

首先,我們需要更改Tags中的LightMode,讓其值為ForwardBase,來讓Unity我們設定光源資料。

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

然後,我們還需要新增#pragma指令:
#pragma multi_compile_fwdbase

這都是為了能讓Unity各種內建資料、巨集定義等可以正常工作。真的是很頭大啊,至今官方也沒有給出詳細的參考資料。。。(Rant!!!)

然後,為了使用法線紋理我們需要定義兩個變數,一個是名為_XXX的sampler2D變數,一個是名為_XXX_ST的float4變數(當然你還需要在Properties中定義一個名為_XXX的新屬性)。

現在我們需要為vertex program定義新的輸入:

			struct a2v {
				float4 vertex : POSITION;
				float3 normal : NORMAL;
				float4 texcoord : TEXCOORD0;
				float4 tangent : TANGENT;
			};

這裡我們添加了一個新的變數,其語義是:TANGENT。我們會在把光源方向轉換到tangent space中時需要這個變數。

Tangent Space轉換

為了把向量從object space轉換到tangent space,我們需要為頂點定義另外兩個向量。通常對一個頂點來說,我們知道它的法線normal,而其中一個向量tangent是和normal正交的,另一個向量binormal則是normal和tangent的叉乘結果。有了這三個向量,我們就可以定義一個矩陣來執行到tangent space的轉換。

幸運地是,UnityCG.cginc裡定義了一個名為TANGENT_SPACE_ROTATION的巨集,它提供了一個名為rotation的矩陣來把object space下的座標轉換到tangent space中。

Vertex到Fragment Programs的輸出

在知道轉換的方法後,我們需要在vertex function裡計算tangent space下的光源方向,然後對其進行插值後傳遞給fragment function。因此,我們需要在vertex function的輸出裡新增新的變數——光源方向。

			struct v2f {
				float4 pos : POSITION;
				float2 uv : TEXCOORD0;
				float2 uv2 : TEXCOORD1;
				float3 lightDirection : TEXCOORD2;
				LIGHTING_COORDS(3,4)
			};

lightDirection將會儲存插值後的光源方向向量。uv2將會儲存法線紋理的紋理座標。最後的LIGHTING_COORDS(3,4)是在AutoLight.cginc裡定義的巨集,它負責建立光源座標,用於某些內建的光照計算。在下面計算光源的attenuation時,我們會需要這些值。

該shader只對directional lights和point lights有效。本例中我們沒有考慮spotlight的角度。

The Vertex Program

			v2f vert(a2v v) {
				v2f o;
				
				TANGENT_SPACE_ROTATION;
				o.lightDirection = mul(rotation, ObjSpaceLightDir(v.vertex));
				o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
				o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
				o.uv2 = TRANSFORM_TEX(v.texcoord, _BumpTex);
				
				TRANSFER_VERTEX_TO_FRAGMENT(o);
				return o;
			}

在vertex program裡,我們使用了巨集TANGENT_SPACE_ROTATION(在UnityCG.cginc裡定義)來建立一個名為rotation的矩陣,並使用它把object space轉換到tangent space中。為了讓這個巨集能夠正確處理我們的輸入,vertex program的輸入必須是一個名為v的結構體,並且它包含了一個名為normal的法線以及一個名為tangent的切線。這都是因為它的巨集定義裡指明瞭變數的名字的緣故。然後,我們使用內建函式ObjSpaceLightDir(v.vertex)計算了在object space中光源(這時指的就是最重要的那個光源)的方向。隨後,我們再把結果和新的rotation矩陣相乘,從而把方向從object space又轉換到了tangent space。

下面幾行,我們計算得到頂點在clip space中的位置以及紋理的uv座標。

最後,我們使用了名為的TRANSFER_VERTEX_TO_FRAGMENT巨集,它同樣在AutoLight.cginc裡定義,和上面v2f中的巨集LIGHTING_COORDS協同工作,它會根據該pass處理的光源型別(spot?point?or directional?)來計算光源座標的具體值,以及進行和shadow相關的計算等。

Directional和Point Lights

Unity把光源的位置儲存在float4型別的_WorldSpaceLightPos0裡,即_WorldSpaceLightPos0包含了4個元素。如果這個光源是directional,那麼xyz就是這個光源的方向,而w(即最後一個元素)則是0;如果這時一個point light,那麼xyz將表示光源的位置,而w則是1。那麼,這些有什麼影響呢?

這其實方便了ObjSpaceLightDir函式的計算過程。它首先將頂點的位置乘以光源位置的w元素,然後再用光源位置減去頂點的位置,來得到光源方向。因此,如果是一個directional light,我們相乘後就會得到0,即返回光源的xyz值(實際上就是光源的方向);如果是一個point light,我們就會得到頂點到光源的一個方向向量。

The Fragment Function

			float4 frag(v2f i) : COLOR {
				float4 c = tex2D(_MainTex, i.uv);
				float3 n = UnpackNormal(tex2D(_BumpTex, i.uv2));
				
				float3 lightColor = UNITY_LIGHTMODEL_AMBIENT.xyz;
				
				float atten = LIGHT_ATTENUATION(i);
				
				// Angle to the light
				float diff = saturate(dot(n, normalize(i.lightDirection)));
				lightColor += _LightColor0.rgb * (diff * atten);
				
				c.rgb = lightColor * c.rgb * 2;

				return c;
			}

在fragment function裡,我們首先從法線紋理裡解壓出法線。然後,我們使用Unity設定的環境光作為初始顏色值。隨後,我們計算了衰減值,即光源距離的遠近。這裡,我們同樣使用了AutoLight.cginc裡的巨集,即LIGHT_ATTENUATION,它同樣會判斷該pass處理的光源型別,然後得到光源的衰減率。

然後,我們把法線和光源方向進行點乘得到漫反射值,再和光源顏色以及衰減值結合起來,疊加到畫素值上。為了得到光源的顏色,我們使用了_LightColor0——這需要我們在shader中包含“Lighting.cginc”檔案。或者,我們也可以在shader中定義一個名為_LightColor0的變數,Unity會自行填充它的值。

uniform float4 _LightColor0;


完整程式碼

最後完整的程式碼如下:

Shader "Custom/DiffuseNormal" {
	Properties {
		_MainTex ("Base (RGB)", 2D) = "white" {}
		_BumpTex ("Bump Texture", 2D) = "white" {}
	}
	SubShader {
		Tags { "RenderType"="Opaque" }
		LOD 300
	
		Pass {
			Tags { "LightMode" = "ForwardBase" }
			
			Cull Back
			Lighting On
			
			CGPROGRAM
			
			#pragma vertex vert
			#pragma fragment frag
			
			#pragma multi_compile_fwdbase
			
			#include "UnityCG.cginc"
			#include "Lighting.cginc"
			#include "AutoLight.cginc"
			
			sampler _MainTex;
			sampler _BumpTex;
			
			float4 _MainTex_ST;
			float4 _BumpTex_ST;
			
			struct a2v {
				float4 vertex : POSITION;
				float3 normal : NORMAL;
				float4 texcoord : TEXCOORD0;
				float4 tangent : TANGENT;
			};
			
			struct v2f {
				float4 pos : POSITION;
				float2 uv : TEXCOORD0;
				float2 uv2 : TEXCOORD1;
				float3 lightDirection : TEXCOORD2;
				LIGHTING_COORDS(3,4)
			};

			v2f vert(a2v v) {
				v2f o;
				
				TANGENT_SPACE_ROTATION;
				o.lightDirection = mul(rotation, ObjSpaceLightDir(v.vertex));
				o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
				o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
				o.uv2 = TRANSFORM_TEX(v.texcoord, _BumpTex);
				
				TRANSFER_VERTEX_TO_FRAGMENT(o);
				return o;
			}
			
			float4 frag(v2f i) : COLOR {
				float4 c = tex2D(_MainTex, i.uv);
				float3 n = UnpackNormal(tex2D(_BumpTex, i.uv2));
				
				float3 lightColor = UNITY_LIGHTMODEL_AMBIENT.xyz;
				
				float atten = LIGHT_ATTENUATION(i);
				
				// Angle to the light
				float diff = saturate(dot(n, normalize(i.lightDirection)));
				lightColor += _LightColor0.rgb * (diff * atten);
				
				c.rgb = lightColor * c.rgb * 2;

				return c;
			}
			
			ENDCG
		}
	} 
	FallBack "Diffuse"
}

Shader效果如下:


在Forward Mode中處理Multiple Lights

通過上面的學習,我們已經學會了如何處理一個光源,但僅僅是一個。要處理多光源,我們就需要編寫另一個pass,並且使用新的tags來告訴Unity我們想要逐個處理光源。

這基本上只需要兩步:

  • 一個pass處理第一個光源,就像我們上面做的那樣

  • 然後定義更多的pass,來處理後續的光源,並把結果新增(add on)到前面的結果上

因此,我們把之前pass的程式碼再貼上一遍,來建立一個新的pass,但要把tag改成:
Tags { "LightMode" = "ForwardAdd" }
並且更改#pragma指令:
#pragma multi_compile_fwdadd


然後新增一個新的命令來告訴Unity怎樣混合前後兩個pass的值:
Blend One One

然後,我們移除掉第二個pass對UNITY_LIGHTMODEL_AMBIENT的處理,因為我們已經在第一個pass中處理過這個值了。我們最後的程式碼如下:
Shader "Custom/DiffuseNormal" {
	Properties {
		_MainTex ("Base (RGB)", 2D) = "white" {}
		_BumpTex ("Bump Texture", 2D) = "white" {}
	}
	SubShader {
		Tags { "RenderType"="Opaque" }
		LOD 300
	
		Pass {
			Tags { "LightMode" = "ForwardBase" }
			
			Cull Back
			Lighting On
			
			CGPROGRAM
			
			#pragma vertex vert
			#pragma fragment frag
			
			#pragma multi_compile_fwdbase
			
			#include "UnityCG.cginc"
			#include "Lighting.cginc"
			#include "AutoLight.cginc"
			
			sampler _MainTex;
			sampler _BumpTex;
			
			float4 _MainTex_ST;
			float4 _BumpTex_ST;
			
			struct a2v {
				float4 vertex : POSITION;
				float3 normal : NORMAL;
				float4 texcoord : TEXCOORD0;
				float4 tangent : TANGENT;
			};
			
			struct v2f {
				float4 pos : POSITION;
				float2 uv : TEXCOORD0;
				float2 uv2 : TEXCOORD1;
				float3 lightDirection : TEXCOORD2;
				LIGHTING_COORDS(3,4)
			};

			v2f vert(a2v v) {
				v2f o;
				
				TANGENT_SPACE_ROTATION;
				o.lightDirection = mul(rotation, ObjSpaceLightDir(v.vertex));
				o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
				o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
				o.uv2 = TRANSFORM_TEX(v.texcoord, _BumpTex);
				
				TRANSFER_VERTEX_TO_FRAGMENT(o);
				return o;
			}
			
			float4 frag(v2f i) : COLOR {
				float4 c = tex2D(_MainTex, i.uv);
				float3 n = UnpackNormal(tex2D(_BumpTex, i.uv2));
				
				float3 lightColor = UNITY_LIGHTMODEL_AMBIENT.xyz;
				
				float atten = LIGHT_ATTENUATION(i);
				
				// Angle to the light
				float diff = saturate(dot(n, normalize(i.lightDirection)));
				lightColor += _LightColor0.rgb * (diff * atten);
				
				c.rgb = lightColor * c.rgb * 2;

				return c;
			}
			
			ENDCG
		}
		
		Pass {
			Tags { "LightMode" = "ForwardAdd" }
			
			Cull Back
			Lighting On
			Blend One One
			
			CGPROGRAM
			
			#pragma vertex vert
			#pragma fragment frag
			
			#pragma multi_compile_fwdadd
			
			#include "UnityCG.cginc"
			#include "Lighting.cginc"
			#include "AutoLight.cginc"
			
			sampler _MainTex;
			sampler _BumpTex;
			
			float4 _MainTex_ST;
			float4 _BumpTex_ST;
			
			struct a2v {
				float4 vertex : POSITION;
				float3 normal : NORMAL;
				float4 texcoord : TEXCOORD0;
				float4 tangent : TANGENT;
			};
			
			struct v2f {
				float4 pos : POSITION;
				float2 uv : TEXCOORD0;
				float2 uv2 : TEXCOORD1;
				float3 lightDirection : TEXCOORD2;
				LIGHTING_COORDS(3,4)
			};

			v2f vert(a2v v) {
				v2f o;
				
				TANGENT_SPACE_ROTATION;
				o.lightDirection = mul(rotation, ObjSpaceLightDir(v.vertex));
				o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
				o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
				o.uv2 = TRANSFORM_TEX(v.texcoord, _BumpTex);
				
				TRANSFER_VERTEX_TO_FRAGMENT(o);
				return o;
			}
			
			float4 frag(v2f i) : COLOR {
				float4 c = tex2D(_MainTex, i.uv);
				float3 n = UnpackNormal(tex2D(_BumpTex, i.uv2));
				
				float3 lightColor = float3(0);
				
				float lengthSq = dot(i.lightDirection, i.lightDirection);
				float atten = LIGHT_ATTENUATION(i);
				
				// Angle to the light
				float diff = saturate(dot(n, normalize(i.lightDirection)));
				lightColor += _LightColor0.rgb * (diff * atten);
				
				c.rgb = lightColor * c.rgb * 2;

				return c;
			}
			
			ENDCG
		}
	} 
	FallBack "Diffuse"
}


我們在場景裡放置兩個光源——一個平行光,用於ForwardBase Pass的計算,一個Point Light,用於ForwardAdd Pass的計算。效果如下:

寫在最後

本文裡對處理光源attenuation的方法和Unity Gems裡的方法不同,按原文裡的做法在Unity 4.5(更早的版本不清楚)是無法得到正確的attenuation的,即把點光源拉進拉遠不會對模型有任何影響,除非拉出了光源範圍,這時會有一個不正常的明暗突變。為了找正確的方法真是麻煩啊。。。Unity關於shader的文件的確需要加強,而且在Unity裡寫Vertex & Fragment Shader絕對比想象中的難,有一條準則就是,如果它提供給裡某些功能的函式(比如這裡計算attenuation的方法,要4個步驟,#pragmamulti_compile_fwdadd LIGHTING_COORDS TRANSFER_VERTEX_TO_FRAGMENT+ LIGHT_ATTENUATION),那麼千萬不要自己嘗試去寫一個函數出來。。。某些內建的變數實在是不知道它們什麼時候工作、怎麼工作。。。

相關推薦

Unity ShadersVertex & Fragment Shader入門

寫在前面三個月以前,在一篇講卡通風格的Shader的最後,我們說到在Surface Shader中實現描邊效果的弊端,也就是隻對錶面平緩的模型有效。這是因為我們是依賴法線和視角的點乘結果來進行描邊判斷的,因此,對於那些平整的表面,它們的法線通常是一個常量或者會發生突變(例如立

Unity ShadersMobile Shader Adjustment—— 什麽是高效的Shader

ria 類型 告訴 菜單 inline 反射 當我 自己的 html http://blog.csdn.net/candycat1992/article/details/38358773 本系列主要參考《Unity Shaders and Effects Cookboo

Unity Shaders學習筆記之Shader簡介(一)

一、Shader簡介  Shader(著色器)實際上就是一小段程式,它負責將輸入的Mesh(網格)以指定的方式和輸入的貼圖或者顏色等組合作用,然後輸出。繪圖單元可以依據這個輸出將影象繪製到螢幕上。輸

Unity Shaders使用CgInclude讓你的Shader模組化——建立CgInclude檔案儲存光照模型

這裡是本書所有的插圖。這裡是本書所需的程式碼和資源(當然你也可以從官網下載)。========================================== 分割線 ==========================================寫在前面瞭解內建的C

Unity ShadersMobile Shader Adjustment —— 為手機定製Shader

這裡是本書所有的插圖。這裡是本書所需的程式碼和資源(當然你也可以從官網下載)。========================================== 分割線 ===============

Unity Shaders使用CgInclude讓你的Shader模組化——Unity內建的CgInclude檔案

這裡是本書所有的插圖。這裡是本書所需的程式碼和資源(當然你也可以從官網下載)。========================================== 分割線 ==========================================寫在前面啦啦啦,又開

Unity ShadersShadowGun系列之二——霧和體積光

依靠 action 圖形學 取值 線性 數學 viewer https 是否 寫在前面體積光,這個名稱是God Rays的中文翻譯,感覺不是非常形象。God Rays事實上是Crepuscular rays在圖形學中的說法,而Crepuscular rays的意思是雲隙光

Unity ShadersLighting Models —— 衣服著色器

Shader "Custom/ClothShader" { Properties { _MainTint ("Global Tint", Color) = (1,1,1,1) _BumpMap ("Normal Map", 2D) = "bump" {} _DetailBump ("Detai

Unity Shaders學習筆記之法線貼圖(七)

 一、簡介   法線貼圖是凸凹貼圖(Bump mapping)的一種常見應用,簡單說就是在不增加模型多邊形數量的前提下,通過渲染暗部和亮部的不同顏色深度,來為原來的貼圖和模型增加視覺細節和真實效果

Unity ShadersDiffuse Shading——漫反射光照改善技巧

這裡是本書所有的插圖。這裡是本書所需的程式碼和資源(當然你也可以從官網下載)。 ========================================== 分割線 =======

Unity 3D學習筆記三十:遊戲元素——遊戲地形

nbsp 3d遊戲 strong 直觀 分辨率 == 摩擦力 fill 世界 遊戲地形 在遊戲的世界中,必然會有非常多豐富多彩的遊戲元素融合當中。它們種類繁多。作用也不大同樣。一般對於遊戲元素可分為兩種:經經常使用。不經經常使用。經常使用的元素是遊戲中比較重要的元素。一

Unity 3D學習筆記四十二:粒子特效

空間 獲得 material package 一個 log 創建 spa mpi 粒子特效 粒子特效的原理是將若幹粒子無規則的組合在一起。來模擬火焰,爆炸。水滴,霧氣等效果。要使用粒子特效首先要創建,在hierarchy視圖中點擊create——particle s

Android開發Fragment與Acitvity通信

對象 p s ets roi mit blog () open findview   上一篇我們講到與Fragment有關的經常使用函數,既然Fragment被稱為是“小Activity”。如今我們來講一下Fragment怎樣與Acitivity通信。

Unity技巧Unity中的優化技術

移動設備 完整 物體 動態 多少 each blank screen text 寫在前面 這一篇是在Digital Tutors的一個系列教程的基礎上總結擴展而得的~Digital Tutors是一個非常棒的教程網站,包含了多媒體領域很多方面的資料,非常酷!除此之外,還

Unity筆記Terrain地形制作坍塌/深坑

unity flatten err target eight tar log 高度 .com Unity的Terrain組件在【set the terrain height】分頁下,height高度為0時,可理解為該地形的海平面高度,此時就不能地形下榻。把height調到1

幹貨Html與CSS入門學習筆記12-14

進度條 tom step char number 視頻容器 復選框 其中 私有 十二、HTML5標記 現代HTML html5新增的元素:header nav footer aside section article time 這些新增元素使頁面結構更清晰,取代<di

tensorflow:Google三、tensorflow入門

als 管理 神經網絡 等價 問題 sign ria init 節點 【一】計算圖模型 節點是計算,邊是數據流, a = tf.constant( [1., 2.] )定義的是節點,節點有屬性 a.graph 取得默認計算圖  g1 = tf.get_default_gr

Python之路第一篇:Python簡介和入門

源碼 world 網絡服務 換行 編程風格 大小寫 utf8 編譯安裝 比較 python簡介: 一、什麽是python Python(英國發音:/ pa θ n/ 美國發音:/ pa θɑ n/),是一種面向對象、直譯式的計算機程序語言。 每一門語言都有自己的哲學: py

python全棧開發第一篇Python簡介以及入門

request lambda sci linu ogl red 控制 ttl 排行 一、python介紹   python的創始人為吉多·範羅蘇姆(Guido van Rossum)。1989年的聖誕節期間,Guido開始寫Python語言的編譯器。Python這個名字,來

unity 3D關於unity中製作繩子(Rope)的總結

    這幾天因為專案需要,一直在搜尋和學習unity中關於繩子製作的資源。於是開始在CSDN、BaiDu、Bilibili上面找各種相關的資源,但是隻是講解了最基本的關於繩子的通用製作方法,即:繩子由若干個Gameobject(可以是Cube、Capusual、Cylinder