本身使用Unity3D也有一段時間了,可是不少時候是流於表面,更多地是把這個引擎簡單地用做腳本控制,而對更深刻一些的層次幾乎沒有瞭解。雖說Unity引擎設計的初衷就是建立簡單的不須要開發者操心的誰都能用的3D引擎,可是隻是膚淺的使用,多是沒法達到爲所欲爲的境地的,所以,這種情況必須改變!從哪裏開始呢,貌似有句話叫作會寫Shader的都是高手,因而,想大概看看從Shader開始能不能使本身到達的層次能再深刻一些吧,再因而,有了這個系列(但願我能堅持寫完它,雖然應該會拖個半年左右)。html
Unity3D的全部渲染工做都離不開着色器(Shader),若是你和我同樣最近開始對Shader編程比較感興趣的話,可能你和我有着一樣的困惑:如何開始?Unity3D提供了一些Shader的手冊和文檔(好比這裏,這裏和這裏),可是一來內容比較分散,二來學習階梯稍微陡峭了些。這對於像我這樣以前徹底沒有接觸過有關內容的新人來講是至關不友好的。國內外雖然也有一些Shader的介紹和心得,可是也一樣存在內容分散的問題,不少教程前一章就只介紹了基本概念,接下來立刻就搬出一個超複雜的例子,對於不少基本的用法並無解釋。也許對於Shader熟練使用的開發者來講是沒有問題,可是我相信像我這樣的入門者也並不在少數。在多方尋覓無果後,我以爲有必要寫一份教程,來以一個入門者的角度介紹一些Shader開發的基本步驟。其實與其說是教程,倒不如說是一份自我總結,但願可以幫到有須要的人。編程
因此,本「教程」的對象是app
固然,由於我自己在Shader開發方面也是一個徹徹底底的大菜鳥,本文不少內容也只是在本身的理解加上一些可能不太靠譜的求證和總結。本文中的示例應該會有更好的方式來實現,所以您是高手而且恰巧路過的話,若是有好的方式來實現某些內容,懇請您不吝留下評論,我會對本文進行不斷更新和維護。編輯器
若是是進行3D遊戲開發的話,想必您對着兩個詞不會陌生。Shader(着色器)實際上就是一小段程序,它負責將輸入的Mesh(網格)以指定的方式和輸入的貼圖或者顏色等組合做用,而後輸出。繪圖單元能夠依據這個輸出來將圖像繪製到屏幕上。輸入的貼圖或者顏色等,加上對應的Shader,以及對Shader的特定的參數設置,將這些內容(Shader及輸入參數)打包存儲在一塊兒,獲得的就是一個Material(材質)。以後,咱們即可以將材質賦予合適的renderer(渲染器)來進行渲染(輸出)了。ide
因此說Shader並無什麼特別神奇的,它只是一段規定好輸入(顏色,貼圖等)和輸出(渲染器可以讀懂的點和顏色的對應關係)的程序。而Shader開發者要作的就是根據輸入,進行計算變換,產生輸出而已。函數
Shader大致上能夠分爲兩類,簡單來講性能
由於是入門文章,因此以後的介紹將主要集中在表面着色器上。學習
由於着色器代碼能夠說專用性很是強,所以人爲地規定了它的基本結構。一個普通的着色器的結構應該是這樣的: ui
首先是一些屬性定義,用來指定這段代碼將有哪些輸入。接下來是一個或者多個的子着色器,在實際運行中,哪個子着色器被使用是由運行的平臺所決定的。子着色器是代碼的主體,每個子着色器中包含一個或者多個的Pass。在計算着色時,平臺先選擇最優先可使用的着色器,而後依次運行其中的Pass,而後獲得輸出的結果。最後指定一個回滾,用來處理全部Subshader都不能運行的狀況(好比目標設備實在太老,全部Subshader中都有其不支持的特性)。google
須要提早說明的是,在實際進行表面着色器的開發時,咱們將直接在Subshader這個層次上寫代碼,系統將把咱們的代碼編譯成若干個合適的Pass。廢話到此爲止,下面讓咱們真正實際進入Shader的世界吧。
百行文檔不如一個實例,下面給出一段簡單的Shader代碼,而後根據代碼來驗證下上面說到的結構和闡述一些基本的Shader語法。由於本文是針對Unity3D來寫Shader的,因此也使用Unity3D來演示吧。首先,新建一個Shader,能夠在Project面板中找到,Create,選擇Shader,而後將其命名爲Diffuse Texture
:
隨便用個文本編輯器打開剛纔新建的Shader:
Shader "Custom/Diffuse Texture" { Properties { _MainTex ("Base (RGB)", 2D) = "white" {} } SubShader { Tags { "RenderType"="Opaque" } LOD 200 CGPROGRAM #pragma surface surf Lambert sampler2D _MainTex; struct Input { float2 uv_MainTex; }; void surf (Input IN, inout SurfaceOutput o) { half4 c = tex2D (_MainTex, IN.uv_MainTex); o.Albedo = c.rgb; o.Alpha = c.a; } ENDCG } FallBack "Diffuse" }
若是您以前沒怎麼看過Shader代碼的話,估計細節上會看不太懂。可是有了上面基本結構的介紹,您應該能夠識別出這個Shader的構成,好比一個Properties部分,一個SubShader,以及一個FallBack。另外,第一行只是這個Shader的聲明併爲其指定了一個名字,好比咱們的實例Shader,你能夠在材質面板選擇Shader時在對應的位置找到這個Shader。
接下來咱們講逐句講解這個Shader,以期明瞭每個語句的意義。
在Properties{}
中定義着色器屬性,在這裏定義的屬性將被做爲輸入提供給全部的子着色器。每一條屬性的定義的語法是這樣的:
_Name("Display Name", type) = defaultValue[{options}]
因此,一組屬性的申明看起來也許會是這個樣子的
//Define a color with a default value of semi-transparent blue _MainColor ("Main Color", Color) = (0,0,1,0.5) //Define a texture with a default of white _Texture ("Texture", 2D) = "white" {}
如今看懂上面那段Shader(以及其餘全部Shader)的Properties部分應該不會有任何問題了。接下來就是SubShader部分了。
表面着色器能夠被若干的標籤(tags)所修飾,而硬件將經過斷定這些標籤來決定何時調用該着色器。好比咱們的例子中SubShader的第一句
Tags { "RenderType"="Opaque" }
告訴了系統應該在渲染非透明物體時調用咱們。Unity定義了一些列這樣的渲染過程,與RenderType是Opaque相對應的顯而易見的是"RenderType" = "Transparent"
,表示渲染含有透明效果的物體時調用。在這裏Tags其實暗示了你的Shader輸出的是什麼,若是輸出中都是非透明物體,那寫在Opaque裏;若是想渲染透明或者半透明的像素,那應該寫在Transparent中。
另外比較有用的標籤還有"IgnoreProjector"="True"
(不被Projectors影響),"ForceNoShadowCasting"="True"
(從不產生陰影)以及"Queue"="xxx"
(指定渲染順序隊列)。這裏想要着重說一下的是Queue這個標籤,若是你使用Unity作過一些透明和不透明物體的混合的話,極可能已經遇到過不透明物體沒法呈如今透明物體以後的狀況。這種狀況極可能是因爲Shader的渲染順序不正確致使的。Queue指定了物體的渲染順序,預約義的Queue有:
這些預約義的值本質上是一組定義整數,Background = 1000, Geometry = 2000, AlphaTest = 2450, Transparent = 3000,最後Overlay = 4000。在咱們實際設置Queue值時,不只能使用上面的幾個預約義值,咱們也能夠指定本身的Queue值,寫成相似這樣:"Queue"="Transparent+100"
,表示一個在Transparent以後100的Queue上進行調用。經過調整Queue值,咱們能夠確保某些物體必定在另外一些物體以前或者以後渲染,這個技巧有時候頗有用處。
LOD很簡單,它是Level of Detail的縮寫,在這裏例子裏咱們指定了其爲200(其實這是Unity的內建Diffuse着色器的設定值)。這個數值決定了咱們能用什麼樣的Shader。在Unity的Quality Settings中咱們能夠設定容許的最大LOD,當設定的LOD小於SubShader所指定的LOD時,這個SubShader將不可用。Unity內建Shader定義了一組LOD的數值,咱們在實現本身的Shader的時候能夠將其做爲參考來設定本身的LOD數值,這樣在以後調整根據設備圖形性能來調整畫質時能夠進行比較精確的控制。
前面雜項說完了,終於能夠開始看看最主要的部分了,也就是將輸入轉變爲輸出的代碼部分。爲了方便看,請允許我把上面的SubShader的主題部分抄寫一遍
CGPROGRAM
#pragma surface surf Lambert sampler2D _MainTex; struct Input { float2 uv_MainTex; }; void surf (Input IN, inout SurfaceOutput o) { half4 c = tex2D (_MainTex, IN.uv_MainTex); o.Albedo = c.rgb; o.Alpha = c.a; } ENDCG
仍是逐行來看,首先是CGPROGRAM。這是一個開始標記,代表從這裏開始是一段CG程序(咱們在寫Unity的Shader時用的是Cg/HLSL語言)。最後一行的ENDCG與CGPROGRAM是對應的,代表CG程序到此結束。
接下來是是一個編譯指令:#pragma surface surf Lambert
,它聲明瞭咱們要寫一個表面Shader,並指定了光照模型。它的寫法是這樣的
#pragma surface surfaceFunction lightModel [optionalparams]
因此在咱們的例子中,咱們聲明瞭一個表面着色器,實際的代碼在surf函數中(在下面能找到該函數),使用Lambert(也就是普通的diffuse)做爲光照模型。
接下來一句sampler2D _MainTex;
,sampler2D是個啥?其實在CG中,sampler2D就是和texture所綁定的一個數據容器接口。等等..這個說法仍是太複雜了,簡單理解的話,所謂加載之後的texture(貼圖)說白了不過是一塊內存存儲的,使用了RGB(也許還有A)通道,且每一個通道8bits的數據。而具體地想知道像素與座標的對應關係,以及獲取這些數據,咱們總不能一次一次去本身計算內存地址或者偏移,所以能夠經過sampler2D來對貼圖進行操做。更簡單地理解,sampler2D就是GLSL中的2D貼圖的類型,相應的,還有sampler1D,sampler3D,samplerCube等等格式。
解釋通了sampler2D是什麼以後,還須要解釋下爲何在這裏須要一句對_MainTex
的聲明,以前咱們不是已經在Properties
裏聲明過它是貼圖了麼。答案是咱們用來實例的這個shader實際上是由兩個相對獨立的塊組成的,外層的屬性聲明,回滾等等是Unity能夠直接使用和編譯的ShaderLab;而如今咱們是在CGPROGRAM...ENDCG
這樣一個代碼塊中,這是一段CG程序。對於這段CG程序,要想訪問在Properties
中所定義的變量的話,必須使用和以前變量相同的名字進行聲明。因而其實sampler2D _MainTex;
作的事情就是再次聲明並連接了_MainTex,使得接下來的CG程序可以使用這個變量。
終於能夠繼續了。接下來是一個struct結構體。相信你們對於結構體已經很熟悉了,咱們先跳過之,直接看下面的的surf函數。上面的#pragma段已經指出了咱們的着色器代碼的方法的名字叫作surf,那沒跑兒了,就是這段代碼是咱們的着色器的工做核心。咱們已經說過不止一次,着色器就是給定了輸入,而後給出輸出進行着色的代碼。CG規定了聲明爲表面着色器的方法(就是咱們這裏的surf)的參數類型和名字,所以咱們沒有權利決定surf的輸入輸出參數的類型,只能按照規定寫。這個規定就是第一個參數是一個Input結構,第二個參數是一個inout的SurfaceOutput結構。
它們分別是什麼呢?Input實際上是須要咱們去定義的結構,這給咱們提供了一個機會,能夠把所須要參與計算的數據都放到這個Input結構中,傳入surf函數使用;SurfaceOutput是已經定義好了裏面類型輸出結構,可是一開始的時候內容暫時是空白的,咱們須要向裏面填寫輸出,這樣就能夠完成着色了。先仔細看看INPUT吧,如今能夠跳回來看上面定義的INPUT結構體了:
struct Input { float2 uv_MainTex; };
做爲輸入的結構體必須命名爲Input,這個結構體中定義了一個float2的變量…你沒看錯我也沒打錯,就是float2,表示浮點數的float後面緊跟一個數字2,這又是什麼意思呢?其實沒什麼魔法,float和vec均可以在以後加入一個2到4的數字,來表示被打包在一塊兒的2到4個同類型數。好比下面的這些定義:
//Define a 2d vector variable vec2 coordinate; //Define a color variable float4 color; //Multiply out a color float3 multipliedColor = color.rgb * coordinate.x;
在訪問這些值時,咱們便可以只使用名稱來得到整組值,也可使用下標的方式(好比.xyzw,.rgba或它們的部分好比.x等等)來得到某個值。在這個例子裏,咱們聲明瞭一個叫作uv_MainTex
的包含兩個浮點數的變量。
若是你對3D開發稍有耳聞的話,必定不會對uv這兩個字母感到陌生。UV mapping的做用是將一個2D貼圖上的點按照必定規則映射到3D模型上,是3D渲染中最多見的一種頂點處理手段。在CG程序中,咱們有這樣的約定,在一個貼圖變量(在咱們例子中是_MainTex
)以前加上uv兩個字母,就表明提取它的uv值(其實就是兩個表明貼圖上點的二維座標 )。咱們以後就能夠在surf程序中直接經過訪問uv_MainTex來取得這張貼圖當前須要計算的點的座標值了。
若是你堅持看到這裏了,那要恭喜你,由於離最後成功讀完一個Shader只有一步之遙。咱們回到surf函數,它的兩有參數,第一個是Input,咱們已經明白了:在計算輸出時Shader會屢次調用surf函數,每次給入一個貼圖上的點座標,來計算輸出。第二個參數是一個可寫的SurfaceOutput,SurfaceOutput是預約義的輸出結構,咱們的surf函數的目標就是根據輸入把這個輸出結構填上。SurfaceOutput結構體的定義以下
struct SurfaceOutput { half3 Albedo; //像素的顏色 half3 Normal; //像素的法向值 half3 Emission; //像素的發散顏色 half Specular; //像素的鏡面高光 half Gloss; //像素的發光強度 half Alpha; //像素的透明度 };
這裏的half和咱們常見float與double相似,都表示浮點數,只不過精度不同。也許你很熟悉單精度浮點數(float或者single)和雙精度浮點數(double),這裏的half指的是半精度浮點數,精度最低,運算性能相對比高精度浮點數高一些,所以被大量使用。
在例子中,咱們作的事情很是簡單:
half4 c = tex2D (_MainTex, IN.uv_MainTex); o.Albedo = c.rgb; o.Alpha = c.a;
這裏用到了一個tex2d
函數,這是CG程序中用來在一張貼圖中對一個點進行採樣的方法,返回一個float4。這裏對_MainTex在輸入點上進行了採樣,並將其顏色的rbg值賦予了輸出的像素顏色,將a值賦予透明度。因而,着色器就明白了應當怎樣工做:即找到貼圖上對應的uv點,直接使用顏色信息來進行着色,over。
我想如今你已經能讀懂一些最簡單的Shader了,接下來我推薦的是參考Unity的Surface Shader Examples多接觸一些各類各樣的基本Shader。在這篇教程的基礎上,配合一些google的工做,徹底看懂這個shader示例頁面應該不成問題。若是能作到無壓力看懂,那說明你已經有良好的基礎能夠前進到Shader的更深的層次了(也許等不到個人下一篇教程就能夠本身開始動手寫些效果了);若是暫時仍是有困難,那也沒有關係,Shader學習絕對是一個漸進的過程,由於有不少約定和經常使用技巧,多積累和實踐天然會進步並掌握。