本文爲翻譯,附上原文連接。html
轉載請註明出處——polobymulberry-博客園。編程
剛開始接觸Unity3D Shader編程時,你會發現有關shader的文檔至關散,這也形成初學者對Unity3D Shader編程望而卻步。該系列教程的第一篇文章(譯者注:即本文,後續還有5篇文章)詳細介紹了Unity3D中的表面着色器(Surface Shader)的,爲學習更復雜的Shader編程打下基礎。數組
動機緩存
若是你是剛剛接觸Shader編程的新手,你可能不知道從何開始踏出Shader編程的第一步。本教程將帶你一步步完成一個表面着色器(Surface Shader)和片斷着色器(Fragment Shader)。本教程也將介紹在Unity3D Shader編程中所使用的一些函數和變量,這些內容可能和你在網上看到的不同哦!less
若是你知足下面的條件,我以爲你應該看看這篇文章:編輯器
本文是該系列教程的第一篇文章,隨後咱們會製做一些更復雜的shader。相比起來,第一篇文章確實很簡單。ide
關於做者wordpress
我也是Shader編程的新手----因此我決定寫這篇教程幫助你們入門——我當初也在入門上遇到不少苦惱。事實上我並非一個Shader編程專家。函數
當我想了解Shader編程時,我曾反覆閱讀官方文檔,可是我最終發現官方文檔講述的順序並不適合我學習shader。因此我以爲我應該寫一篇教程,並分享我所學到的知識。不過寫完教程以後,我發現再次閱讀官方文檔時,以爲明白多了。性能
儘管本教程中的全部例子都能正常運行,可是我相信確定有更好shaders實現這些例子。若是聰明的你對這些例子中的shaders有更好的建議,請在評論區留言!
我之因此學習shader編程是由於我須要在我建立的遊戲世界中建立些東西,但這個遊戲世界建立起來有個麻煩之處,由於它是由不一樣角色組成的,而我必須建立由多個部分組成的一個統一網格(mesh)。因此我只能對每一個角色使用一次繪製調用(draw call)。(譯者注:徹底不知道他在講什麼?因此我把原文放在下面給你們評評理)
My reason for getting into shader programming was to build something that I needed for a world populated with an endless array of different characters. I needed to build a combined mesh out of multiple parts so I only have one draw call per character.
經過打開和關閉角色的穿衣效果,我使用Megafiers(一個變形插件)修改了角色的基本網格(base meshes)。其中的困難在於我只有一個紋理(texture),可是我卻想給每一個角色的皮膚,服飾以及其餘的特徵使用不一樣的顏色。我想到一個方法----對每一個角色使用不一樣的3個4x4紋理,並使用一個shader來給模型上色。我將在整個教程中詳細描述我作的這個shader,可是如今—我想大家已經火燒眉毛地想看我建立的角色表演一段即興的快閃舞(flash mob dance)(譯者注:網上截的圖片)。
着色器和材質(shaders&materials)
一個shader所作的就是將一個模型的網格(mesh)渲染到屏幕上。Shader能夠被定義爲一系列的屬性(譯者注:就像一個函數裏面的參數同樣,你能夠改變函數的不一樣賦值來改變函數的輸出結果),你能夠經過改變這些屬性來改變模型渲染到屏幕上的效果。而這些屬性被存放起來,放到一個叫作材質(material)的地方。
Unity3D Shader有如下幾種
本文中咱們將關注點放在表面着色器上。
學習Shader的資源
若是你要學習Shader編程,我向你推薦下面幾個資源
Shader的流水化工做方式
(譯者注:Shader的工做方式也稱爲shader流水線(pipeline),由於shader工做方式很相似汽車流水線,將模型上一系列頂點數據和其餘各類數據做爲輸入,用這個shader組成的流水線加工下,出來的就成了炫酷的效果了。)
你將在shader流水線中看到不明覺厲的各類術語,我將用我本身的語言儘可能下降理解的難度。
Shader的工做就是輸入一些3D幾何信息,通過shader處理後將其變爲2D的像素呈如今屏幕上。好處是在shader處理過程當中,你只須要改變少數幾個屬性就能夠產生不一樣的效果。對於表面着色器,該工做流程看起來像下面這樣:
(譯者注:簡單講解一下這個流程圖,首先要渲染的物體將本身的幾何信息傳遞到Shader中,而且系統獲得了該物體的頂點信息,而後你能夠選擇經不通過Vertex Function來處理這些頂點信息,隨後通過光柵化(將三維幾何信息映射到二維屏幕上,打個不恰當的比喻,至關於把3D模型拍扁到屏幕上,而後你就能夠專心處理屏幕上的像素了),每一個像素通過你的shader代碼將獲得最終的顏色值)
注意在表面着色器(Surface Shader)中的函數退出以前,像素的顏色尚未計算出來。這意味着你能夠再次以前傳入頂點的法向量來影響光照的計算。
片斷着色器(Fragment Shader)有着一樣的工做流程,但事實上,片斷着色器中必須有Vertex Function(上圖中的Vertex Function部分就是可選的(Optional)),並且須要在像素處理階段作不少的工做才能產生最終的像素。而表面着色器隱藏了這些。(譯者注:給個人感受就是片斷着色器向用戶提供了更多的接口進行更高級的渲染)。
下圖展現了你的代碼如何被調用以及代碼構成
從上圖咱們能夠看到,當你寫一個shader的時候,你可能得有一些屬性值(properties),而且有一個或多個Subshaders。具體使用哪一個Subshader進行處理取決於你的運行平臺。你應該還要指定一個Fallback shader,當你的subshader沒有一個能運行在你的目標設備上,將使用Fallback shader(譯者注:有點像備胎)。
每一個Subshader都至少有一個通道(pass)做爲數據的輸入和輸出。你可使用多個通道(passes)執行不一樣的操做,好比在一個Grab Pass中,你能夠獲取將要呈現到屏幕上的像素值(譯者注:相似於glsl中的fragment buffer)。當你想製做高級的扭曲效果,這很是有用。雖然當你開始學習shader編程時,你可能並不會使用到它。另一個使用多通道(multiple passes)的緣由是在不一樣時刻,你可能須要寫入或者禁止寫入深度緩存的使用。
當你寫表面着色器時,咱們將直接在Subshader這個層次上寫代碼,系統將把咱們的代碼編譯成若干個合適的通道(pass)。
儘管shader最終產生的是二維像素,可是其實這些像素除了保存xy座標外,自己保存着深度值(即每一個像素點上的內容在原先3D場景中離照相機的遠近),這樣距離照相機近的物體就會把距離照相機遠的物體遮擋住,在屏幕上顯示時,就是將其像素值覆蓋。
你能夠控制是否在你的shader中使用深度緩存(Z-buffer)產生一些特效,或者在Pass中使用一些指令決定shader是否能夠寫入Z-buffer:好比使用ZWrite Off時,任何你輸出的東西都不會更新Z-buffer的值,即關閉的Z-Buffer的寫入功能。
你可使用Z-buffer技術在別的物體上掏出一個洞,你能夠先寫入須要打洞區域的深度值,但不輸出打洞區域所屬的像素值,而後在你模型後面的物體的深度值將沒法寫入(由於Z-buffer以爲你的模型已經擋住了後面的物體)(譯者注:這樣你打洞區域顯示的就是一開始使用的背景色,會形成一個洞穿過了這些物體的效果)。
下面是一些shader代碼:
但願你能看出上面代碼是由Properties,SubShader,Fallback三段代碼組成的。
理解Shader代碼
文章剩下的部分將講述上面那段簡單代碼到底作了什麼?真正的乾貨立刻就來了,你必須好好掌握這些內容。
當你進行shader編程時,你必須使用正確的變量名和函數名來調用它們,事實上變量的名稱在某些狀況下能讓人一眼看出它的特定含義。
建立並使用默認Shader
(譯者注:在詳細介紹Shader以前,咱們先簡單介紹下shader如何使用。)
1. 咱們先打開Unity(個人版本是4.6.1),建立新工程,並在Assets文件夾下建立三個目錄,以下:
2. 咱們再建立一個cube。
能夠在Inspector面板看到新建立的cube所使用的Material以下。
3. 打開Material文件夾,咱們在其中建立一個Shader和一個Material。
此時New Material的默認Shader爲Diffuse。
咱們將NewShader拖到New Material上。
能夠看到該材質所使用的Shader變成咱們新建的NewShader了。固然你也能夠直接點擊材質編輯器中Shader下拉框,選擇相應的Shader。
4. 最後將New Material拖到cube上。能夠看到cube所使用的材質和Shader都變成了咱們新建立的材質和Shader了。
Properties(屬性值)簡介
你在shader代碼中的Properties{…}部分定義Shader中的屬性值(屬性值就是用戶傳入給shader的數據,好比紋理之類的,而後shader處理這些紋理,產生特效。能夠理解爲屬性值至關於一種全局變量,而Shader就是那個主函數,Unity的優點在於給這個全局變量賦值能夠在Inspector面板進行)。注意Properties(屬性值)是全部Subshader代碼中的共享的,意味着全部SubShader代碼中均可以使用這些屬性值。
屬性值(property)定義的形式:
_Name(「Displayed Name」,type) = default value[{options}]
總結:打開咱們建立的NewShader。能夠看到_MainTex是在代碼中使用的,而Base (RGB)是在材質編輯器中使用的
來張全家福:
下面舉幾個屬性值寫法的例子:
// 定義了一個半透明(alpha=0.5)效果的紅色做爲默認顏色值
_MainColor(「Main Color」,Color)=(1,0,0,0.5)
// 定義了一個默認值爲白色的紋理
_Texture(「Texture」,2D) =」white」 {}
注意屬性值的定義末尾處不需添加分號。
標籤(Tags)
你的表面着色器能夠用一個或多個標籤(tags)進行修飾。這些標籤的做用是告訴硬件什麼時候去調用你的shader代碼。
在咱們的例子中,咱們使用:Tags {「RenderType」 = 「Opaque」},這意味着當程序去渲染不透明的幾何體時,將調用咱們的shader,Unity定義了一系列這樣的渲染過程。另外一個很容易理解的標籤就是Tags {「RenderType」 = 「Transparent」},意味着咱們的shader只會輸出半透明或透明的像素值。
其它一些有用的標籤,好比「IgnoreProjector」=「True」,意味着你渲染的物體不會受到projectors(投影儀)的影響。
「Queue」=「xxxx」(給shader所屬的對象貼上渲染隊列的標籤)。當渲染的對象類型是透明物體時,Queue標籤能產生一些很是有趣的效果。該標籤決定了物體渲染的順序(譯者注:我猜想它的工做方式是這樣的,一個場景中有不少個物體,當這些物體被渲染時,必須有一個渲染的順序,好比背景應該比其餘物體先渲染出來,不然背景會將以前渲染的物體遮擋住,具體方法是將背景使用的shader中貼上一個「Queue」=「Backfround」標籤,這樣使用該shader的物體將被貼上Background的標籤。總之當渲染整個場景時,unity會根據這些渲染隊列的標籤決定按什麼順序去渲染對應標籤所屬的物體)。
有趣的是你能夠給這些基本的渲染標籤進行加加減減。這些預約義的值本質上是一組定義整數,Background = 1000, Geometry = 2000, AlphaTest = 2450, Transparent = 3000,最後Overlay = 4000。(譯者注:今後處咱們也能夠一窺究竟,貌似數值大的後渲染。)這些預設值這對透明物體有很大影響,好比一個湖水的平面覆蓋了你用廣告牌製做的樹,你能夠對你的樹使用「Queue」=」Transparent-102」,這樣你的樹就會繪製在湖水前面了。
Shader的總體結構
讓咱們回顧下shader代碼的結構。
#pragma surface surf Lambert 這段代碼表示其中surface表示這是一個表面着色器,進行結果輸出的函數名稱爲surf,其使用的光照模型爲Lambert光照模型。
咱們的CG程序使用了一種通過修飾的類C語言 —— CG語言(是Nvidia和微軟共同出品的一種shader語言)。詳見Nvidia的文檔 —— 我在文中也會介紹一些基本的Cg使用方法。
浮點數類型(float)和向量值類型(vec)通常都會在末尾加上2,3,4這些數字(float2,float4,vec3…)表示該類型具體有幾個元素組成。這種定義方式使數值操做變得更方便,你能夠將其當作一個總體使用,或者單獨使用其份量。
//定義一個浮點類型的二維座標
vec2 coordinate;
//定義一個顏色變量(4個浮點值份量的顏色值)
float4 color;
//經過點乘獲得3個浮點值份量的顏色值
float3 multipliedColor = color.rgb * coordinate.x;
你可使用.xyzw或.rgba來代表你使用的變量類型具體的含義,好比.xyzw可能表示的是旋轉四元數,而.xyz表示位置或法向量,.rgba表示顏色。固然,你能夠僅僅使用float做爲單個浮點值類型。其實對.rgba等份量訪問符的使用也稱做swizzle,尤爲是對顏色的處理,好比顏色空間的轉換可能會用到它,好比color=color.abgr;
你將會遇到half(半精度)和double(雙精度)類型,half(通常16bit)即正常float(通常32bit)的一半精度,double(通常64bit)是正常float的兩倍精度(此處的倍數衡量的方式不是指表示的範圍,而是表示可使用的bit位數)。使用half常常是出於性能考慮的緣由。還有一種區別於浮點數的定點數fixed,精度更低。
當你想將顏色值規範到0~1之間時,你可能會想到使用saturate函數(saturate(x)的做用是若是x取值小於0,則返回值爲0。若是x取值大於1,則返回值爲1。若x在0到1之間,則直接返回x的值.),固然saturate也可使用變量的swizzled版本,好比saturate(somecolor.rgb);
你可使用length函數獲得一個向量的長度,好比float size = length(someVec4.xz);
如何從表面着色器輸出信息
咱們的surface function(表面函數)每一個像素調用一次,系統已經事先計算出當前處理的像素的輸入值(準確來講應該是輸入結構體,即Input IN中的Input類型)。 它是根據每一個網格上的面片,並進行插值獲得的結果。
來看看咱們的surf函數
void surf (Input IN, inout SurfaceOutput o) { o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb; }
很明顯咱們能夠看出,咱們返回了o.Albeodo值 – 該值是Unity爲咱們定義的SurfaceOutput結構體中的某個成員。接下來讓咱們看看SurfaceOutput具體定義了哪些成員。該Albedo表示像素的顏色。
struct SurfaceOutput { half3 Albedo; //該像素的顏色值 half3 Normal; //該像素的法向量 half3 Emission; //該像素的輻射光,輻射光是最簡單的一種光,它直接從物體發出而且不受任何光源影響 half Specular; //該像素的鏡面高光 half Gloss; //該像素的發光強度 half Alpha; //該像素的透明度 };
你只要將該結構體中值交給Unity,Unity會自動根據這些值產生最終效果,而不須要你關心其中的細節。
我答應大家的乾貨就在下面
首先看看做爲咱們surf函數的輸入是啥?
咱們定義了一個輸入結構體以下:
struct Input { float2 uv_MainTex; };
經過簡單地建立結構體,咱們告訴系統當咱們每次調用surf函數時,獲取MainTex在該像素的紋理座標。若是咱們有第二個紋理叫作—_OtherTexture,咱們能夠經過在輸入結構體中添加下面代碼獲得它的紋理座標
struct Input { float2 uv_MainTex; float2 uv_OtherTexture; };
若是一個模型還有第二套紋理座標,咱們能夠這樣作:
struct Input { float2 uv_MainTex; float2 uv2_OtherTexture; };
此時對於咱們所使用的全部紋理,咱們的輸入結構體包含一套uv座標或者一套uv2座標。
若是咱們的shader很複雜而且須要知道像素的其餘相關信息,咱們就能夠將如下變量包含在輸入結構體中,以此來查詢其餘的相關變量。
關於INTERNAL_DATA的詳細剖析
爲了更清楚的弄懂INTERNAL_DATA的含義,咱們首先在shader中添加#pragma debug。
而後點擊Show generated code。
咱們查找INTERNAL_DATA,獲得以下代碼。
#define INTERNAL_DATA half3 TtoW0; half3 TtoW1; half3 TtoW2; #define WorldReflectionVector(data,normal) reflect (data.worldRefl, half3(dot(data.TtoW0,normal), dot(data.TtoW1,normal), dot(data.TtoW2,normal))) #define WorldNormalVector(data,normal) fixed3(dot(data.TtoW0,normal), dot(data.TtoW1,normal), dot(data.TtoW2,normal))
咱們發現INTERNAL_DATA其實定義了3個half TtoWi(i=0,1,2)的變量,這三個變量合併在一塊兒是一個3x3的矩陣,表示局部座標系到世界座標系的轉換(Translate To World)。因此咱們看到若是要使用o.Normal從新計算worldRefl和worldNormal,就得使用到INTERNAL_DATA這個內置變量表示的座標系變化矩陣!
該shader實際作了哪些事?
如今咱們還有兩行代碼沒有詳細討論:
Sampler2D _MainTex;
對每個屬性值,咱們定義了屬性值區域(Properties Section),該區域用來定義CG程序中使用的變量。在使用中,咱們必須保證屬性名稱一致。
注意輸入結構體中的uv_MainTex是uv+對應屬性值(文中爲_MainTex,注意前面帶下劃線是CG官方推薦的寫法),若是你使用uv2,那將寫做uv2_MainTex。注意Sampler2D _MainTex中的_MainTex變量是一個Sampler2D(這個Sampler2D,能夠理解爲引用一個2D Texture),它引用了Properties中的_MainTex(譯者注:注意二者同名。解釋通了sampler2D是什麼以後,還須要解釋下爲何在這裏須要一句對_MainTex的聲明,以前咱們不是已經在Properties裏聲明過它是貼圖了麼。答案是咱們用來實例的這個shader實際上是由兩個相對獨立的塊組成的,外層的屬性聲明,回滾等等是Unity能夠直接使用和編譯的ShaderLab;而如今咱們是在CGPROGRAM...ENDCG這樣一個代碼塊中,這是一段CG程序。對於這段CG程序,要想訪問在Properties中所定義的變量的話,必須使用和以前變量相同的名字進行聲明。因而其實sampler2D _MainTex;作的事情就是再次聲明並連接了_MainTex,使得接下來的CG程序可以使用這個變量。),他能夠根據指定的uv座標來提供對應紋理上的像素值,而此處uv_MainTex的做用就是提供紋理_MainTex的uv座標值。
若是咱們定義了一個_Color變量,咱們能夠定義它的屬性爲
float4 _Color;
咱們surf函數中惟一一行代碼
o.Albedo = tex2d( _MainTex, IN.uv_MainTex).rgb;
tex2d的做用是利用IN.uv_MainTex所表明的uv座標(注意咱們上面指定了uv座標產生的方式,因此此處的IN.uv_MainTex是自動生成的)對紋理_MainTex進行採樣。此處,對於o.Albedo咱們只取顏色份量中的rgb三份量,其中alpha值(透明度)目前不須要,至少對於非透明物體alpha值得做用不大。
若是你要設置alpha值的話,能夠像下面這樣賦值
float4 texColor = tex2d( _MainTex, IN.uv_MainTex ); o.Albedo = texColor.rgb; o.Alpha = texColor.a;
總結
你已經瞭解了不少的術語,可是目前咱們所寫的shader還至關有限,可是當學習完第二部分教程後,咱們就能夠作一些很酷炫的shader了,由於第二部分咱們將開始使用多重紋理,法向量等等酷炫技術。