貓都能學會的Unity3D Shader入門指南(一)

原文地址 http://onevcat.com/2013/07/shader-tutorial-1/

動機

本身使用Unity3D也有一段時間了,可是不少時候是流於表面,更多地是把這個引擎簡單地用做腳本控制,而對更深刻一些的層次幾乎沒有瞭解。雖說Unity引擎設計的初衷就是建立簡單的不須要開發者操心的誰都能用的3D引擎,可是隻是膚淺的使用,多是沒法達到爲所欲爲的境地的,所以,這種情況必須改變!從哪裏開始呢,貌似有句話叫作會寫Shader的都是高手,因而,想大概看看從Shader開始能不能使本身到達的層次能再深刻一些吧,再因而,有了這個系列(但願我能堅持寫完它,雖然應該會拖個半年左右)。html

Unity3D的全部渲染工做都離不開着色器(Shader),若是你和我同樣最近開始對Shader編程比較感興趣的話,可能你和我有着一樣的困惑:如何開始?Unity3D提供了一些Shader的手冊和文檔(好比這裏這裏這裏),可是一來內容比較分散,二來學習階梯稍微陡峭了些。這對於像我這樣以前徹底沒有接觸過有關內容的新人來講是至關不友好的。國內外雖然也有一些Shader的介紹和心得,可是也一樣存在內容分散的問題,不少教程前一章就只介紹了基本概念,接下來立刻就搬出一個超複雜的例子,對於不少基本的用法並無解釋。也許對於Shader熟練使用的開發者來講是沒有問題,可是我相信像我這樣的入門者也並不在少數。在多方尋覓無果後,我以爲有必要寫一份教程,來以一個入門者的角度介紹一些Shader開發的基本步驟。其實與其說是教程,倒不如說是一份自我總結,但願可以幫到有須要的人。編程

因此,本「教程」的對象是app

  • 總的來講是新接觸Shader開發的人:也許你知道什麼是Shader,也會使用別人的Shader,可是僅限於知道一些基本的內建Shader名字,歷來沒有打開它們查看其源碼。編輯器

  • 想要更多瞭解Shader和有需求要進行Shader開發的開發者,可是以前並無Shader開發的經驗。ide

固然,由於我自己在Shader開發方面也是一個徹徹底底的大菜鳥,本文不少內容也只是在本身的理解加上一些可能不太靠譜的求證和總結。本文中的示例應該會有更好的方式來實現,所以您是高手而且恰巧路過的話,若是有好的方式來實現某些內容,懇請您不吝留下評論,我會對本文進行不斷更新和維護。函數

一些基本概念

Shader和Material

若是是進行3D遊戲開發的話,想必您對着兩個詞不會陌生。Shader(着色器)實際上就是一小段程序,它負責將輸入的Mesh(網格)以指定的方式和輸入的貼圖或者顏色等組合做用,而後輸出。繪圖單元能夠依據這個輸出來將圖像繪製到屏幕上。輸入的貼圖或者顏色等,加上對應的Shader,以及對Shader的特定的參數設置,將這些內容(Shader及輸入參數)打包存儲在一塊兒,獲得的就是一個Material(材質)。以後,咱們即可以將材質賦予合適的renderer(渲染器)來進行渲染(輸出)了。性能

因此說Shader並無什麼特別神奇的,它只是一段規定好輸入(顏色,貼圖等)和輸出(渲染器可以讀懂的點和顏色的對應關係)的程序。而Shader開發者要作的就是根據輸入,進行計算變換,產生輸出而已。學習

Shader大致上能夠分爲兩類,簡單來講ui

  • 表面着色器(Surface Shader) - 爲你作了大部分的工做,只須要簡單的技巧便可實現不少不錯的效果。類比卡片機,上手之後不太須要不少努力就能拍出不錯的效果。google

  • 片斷着色器(Fragment Shader) - 能夠作的事情更多,可是也比較難寫。使用片斷着色器的主要目的是能夠在比較低的層級上進行更復雜(或者針對目標設備更高效)的開發。

由於是入門文章,因此以後的介紹將主要集中在表面着色器上。

Shader程序的基本結構

由於着色器代碼能夠說專用性很是強,所以人爲地規定了它的基本結構。一個普通的着色器的結構應該是這樣的: 一段Shader程序的結構

首先是一些屬性定義,用來指定這段代碼將有哪些輸入。接下來是一個或者多個的子着色器,在實際運行中,哪個子着色器被使用是由運行的平臺所決定的。子着色器是代碼的主體,每個子着色器中包含一個或者多個的Pass。在計算着色時,平臺先選擇最優先可使用的着色器,而後依次運行其中的Pass,而後獲得輸出的結果。最後指定一個回滾,用來處理全部Subshader都不能運行的狀況(好比目標設備實在太老,全部Subshader中都有其不支持的特性)。

須要提早說明的是,在實際進行表面着色器的開發時,咱們將直接在Subshader這個層次上寫代碼,系統將把咱們的代碼編譯成若干個合適的Pass。廢話到此爲止,下面讓咱們真正實際進入Shader的世界吧。

Hello Shader

百行文檔不如一個實例,下面給出一段簡單的Shader代碼,而後根據代碼來驗證下上面說到的結構和闡述一些基本的Shader語法。由於本文是針對Unity3D來寫Shader的,因此也使用Unity3D來演示吧。首先,新建一個Shader,能夠在Project面板中找到,Create,選擇Shader,而後將其命名爲Diffuse Texture

在Unity3D中新建一個Shader

隨便用個文本編輯器打開剛纔新建的Shader:

Shader "Custom/Diffuse Texture" {  
    Properties {
        _MainTex ("Base (RGB)", 2D) = "white" {}
    }
    SubShader {
        Tags { "RenderType"="Opaque" }
        LOD 200CGPROGRAM#pragma surface surf Lambertsampler2D _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。

在Unity3D中找到剛纔新建的Shader

接下來咱們講逐句講解這個Shader,以期明瞭每個語句的意義。

屬性

Properties{}中定義着色器屬性,在這裏定義的屬性將被做爲輸入提供給全部的子着色器。每一條屬性的定義的語法是這樣的:

_Name("Display Name", type) = defaultValue[{options}]

  • _Name - 屬性的名字,簡單說就是變量名,在以後整個Shader代碼中將使用這個名字來獲取該屬性的內容

  • Display Name - 這個字符串將顯示在Unity的材質編輯器中做爲Shader的使用者可讀的內容

  • type - 這個屬性的類型,可能的type所表示的內容有如下幾種:

    • Color - 一種顏色,由RGBA(紅綠藍和透明度)四個量來定義;

    • 2D - 一張2的階數大小(256,512之類)的貼圖。這張貼圖將在採樣後被轉爲對應基於模型UV的每一個像素的顏色,最終被顯示出來;

    • Rect - 一個非2階數大小的貼圖;

    • Cube - 即Cube map texture(立方體紋理),簡單說就是6張有聯繫的2D貼圖的組合,主要用來作反射效果(好比天空盒和動態反射),也會被轉換爲對應點的採樣;

    • Range(min, max) - 一個介於最小值和最大值之間的浮點數,通常用來看成調整Shader某些特性的參數(好比透明度渲染的截止值能夠是從0至1的值等);

    • Float - 任意一個浮點數;

    • Vector - 一個四維數;

  • defaultValue 定義了這個屬性的默認值,經過輸入一個符合格式的默認值來指定對應屬性的初始值(某些效果可能須要某些特定的參數值來達到須要的效果,雖然這些值能夠在以後在進行調整,可是若是默認就指定爲想要的值的話就省去了一個個調整的時間,方便不少)。

    • Color - 以0~1定義的rgba顏色,好比(1,1,1,1);

    • 2D/Rect/Cube - 對於貼圖來講,默認值能夠爲一個表明默認tint顏色的字符串,能夠是空字符串或者"white","black","gray","bump"中的一個

    • Float,Range - 某個指定的浮點數

    • Vector - 一個4維數,寫爲 (x,y,z,w)

  • 另外還有一個{option},它只對2D,Rect或者Cube貼圖有關,在寫輸入時咱們最少要在貼圖以後寫一對什麼都不含的空白的{},當咱們須要打開特定選項時能夠把其寫在這對花括號內。若是須要同時打開多個選項,可使用空白分隔。可能的選擇有ObjectLinear, EyeLinear, SphereMap, CubeReflect, CubeNormal中的一個,這些都是OpenGL中TexGen的模式,具體的留到後面有機會再說。

因此,一組屬性的申明看起來也許會是這個樣子的

//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

表面着色器能夠被若干的標籤(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 - 最先被調用的渲染,用來渲染天空盒或者背景

  • Geometry - 這是默認值,用來渲染非透明物體(普通狀況下,場景中的絕大多數物體應該是非透明的)

  • AlphaTest - 用來渲染通過Alpha Test的像素,單獨爲AlphaTest設定一個Queue是出於對效率的考慮

  • Transparent - 以從後往前的順序渲染透明物體

  • Overlay - 用來渲染疊加的效果,是渲染的最後階段(好比鏡頭光暈等特效)

這些預約義的值本質上是一組定義整數,Background = 1000, Geometry = 2000, AlphaTest = 2450, Transparent = 3000,最後Overlay = 4000。在咱們實際設置Queue值時,不只能使用上面的幾個預約義值,咱們也能夠指定本身的Queue值,寫成相似這樣:"Queue"="Transparent+100",表示一個在Transparent以後100的Queue上進行調用。經過調整Queue值,咱們能夠確保某些物體必定在另外一些物體以前或者以後渲染,這個技巧有時候頗有用處。

LOD

LOD很簡單,它是Level of Detail的縮寫,在這裏例子裏咱們指定了其爲200(其實這是Unity的內建Diffuse着色器的設定值)。這個數值決定了咱們能用什麼樣的Shader。在Unity的Quality Settings中咱們能夠設定容許的最大LOD,當設定的LOD小於SubShader所指定的LOD時,這個SubShader將不可用。Unity內建Shader定義了一組LOD的數值,咱們在實現本身的Shader的時候能夠將其做爲參考來設定本身的LOD數值,這樣在以後調整根據設備圖形性能來調整畫質時能夠進行比較精確的控制。

  • VertexLit及其系列 = 100

  • Decal, Reflective VertexLit = 150

  • Diffuse = 200

  • Diffuse Detail, Reflective Bumped Unlit, Reflective Bumped VertexLit = 250

  • Bumped, Specular = 300

  • Bumped Specular = 400

  • Parallax = 500

  • Parallax Specular = 600

Shader本體

前面雜項說完了,終於能夠開始看看最主要的部分了,也就是將輸入轉變爲輸出的代碼部分。爲了方便看,請允許我把上面的SubShader的主題部分抄寫一遍

CGPROGRAM  
#pragma surface surf Lambertsampler2D _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]

  • surface - 聲明的是一個表面着色器

  • surfaceFunction - 着色器代碼的方法的名字

  • lightModel - 使用的光照模型。

因此在咱們的例子中,咱們聲明瞭一個表面着色器,實際的代碼在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 variablevec2 coordinate;  
//Define a color variablefloat4 color;  
//Multiply out a colorfloat3 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學習絕對是一個漸進的過程,由於有不少約定和經常使用技巧,多積累和實踐天然會進步並掌握。

在接下來的教程裏,打算經過介紹一些實際例子以及從基礎開始實際逐步動手實現一個複雜一點的例子,讓咱們能看到shader在真正使用中的威力。我但願能儘快寫完這個系列,可是無奈時間確實有限,因此我也不知道何時能出爐...寫好的時候我會更改這段內容並指向新的文章。您要是擔憂錯過的話,也可使用郵件訂閱或者訂閱本站的rss(雖然Google Reader已經關了- -)。

相關文章
相關標籤/搜索