Shader着色器訓練營第一期

Shader着色器訓練營第一期

爲什麼要學寫 Shader?

  • 內建 Unity Shader 僅僅是“通用”用例。這些通用的範式基本可以涵蓋60%左右的需求和情況,但是不以滿足你所有的畫面表現需求。
  • 一旦掌握 Shader,可以爲遊戲/應用創造獨一無二的視覺享受。根據實際需求,爲遊戲和應用實現特定功能的 Shader。
  • 僅僅通過少量代碼,就能實現非常有趣的效果。有時候,單單只是需要稍微處理一下頂點或片元函數。
  • 它能大大幫助性能優化,因爲通過 Shader 可以控制渲染什麼以及如何渲染。
  • 撰寫Shader的能力對於遊戲團隊非常重要,掌握 Shader 技能的開發一直是炙手可熱的職位。現在一個不爭的事實就是技術美術永遠是各大廠商的稀缺資源。
  • 如果你已經掌握其他語言的編程,Shader 對你而言不會很複雜。因爲它本身不會比其他編程語言多些內容,主要處理的還是一些變量與方法。

Unity Shader 案例

遊戲案例:爐石傳說(Hearthstone)

特效案例:The Great Transmutator - Realtime VFX Contest Entry(查看相關技術文章

Unity Shader 到底是什麼?一言以蔽之,一個告訴計算機以某種方式描繪物體的程序。

Unity Shaders

如何寫?從編程語言層面來說,Unity Shader 使用 Nvidia CG 語言和微軟 HLSL語言,都包含在 ShaderLab 中間。

image

常用 Shader 類型

image

自Unity 5.x起基本常用的Shader就是這上圖三種類型,原來還有針對一些舊型號GPU的固定函數着色器已經被時代所淘汰。

  1. Vertex & Fragment Shader:頂點/片元着色器。它是最基本,也是非常強大的着色器類型。一般用於2D場景、物體,做一些特效之類的。從上圖左邊部分,大家可以看到它繪製出一個蜘蛛機器人的紋理,但是不受任何光線的影響。
  2. Surface Shader:表面着色器。它擁有更多的光照運算,其實在系統內部它會被編譯成一個比較複雜的頂點/片元着色器,包含了更多的光照的運算。從上圖中間部分,與左邊的比較我們不難發現,這個3D的蜘蛛機器人有明亮的部分,也有陰影,甚至還帶有一定的金屬光澤。性能消耗比較大,移動設備謹慎使用。
  3. Standard Shader:標準着色器。它是表面着色器升級的版本,因爲它使用了Physically Based Rendering(簡稱PBR)技術,即基於物理的渲染技術。所以在這個着色器中開放了更多處理光照與材質的參數。仔細觀察上圖右邊部分的蜘蛛機器人,更多不同質感的部件被表現出來。機器人自帶燈的光照,足部的金屬質的甲片,機殼略微的鏽跡豐富了這個物件的畫面呈現。

如何創建 Shader

image

Unity內建了一些Shader範式模板,開發者可以通過它們去創建所需類型的Shader,以此爲基礎開始撰寫。點擊Create → Shader我們便可以找到它們(不僅限於前面所提到的三種,在以後的系列文章中,將有機會分析其他類型的着色器)。

  1. 如果你想創建一個Vertex/Fragment Shader,可以選擇Unlit Shader(無光照着色器),它是一個不包含光照(但包含霧效)的基本頂點/片元着色器。
  2. 如果你想創建一個Surface Shader,可以選擇Standard Surface Shader(標準表面着色器),它是一個包含了標準光照模型的表面着色器模板。
  3. 如果你想使用Standard Shader,很可惜不能自行撰寫,但是可以通過選擇的方式去使用。找到檢視窗口(Inspector)中所需渲染對象的材質(Material),展開Shader選項卡,第一個Standard就是PBR的Standard Shader;第二個有Specular Setup的就是預製高光運算的Standard Shader。

image

Vertex/Fragment Shader 流程圖

第一步:數據引入

image

在世界三維空間中,一開始傳入Shader處理的數據其實就是網格數據(Mesh Data)。

image

但是一般情況下,光是網格數據不能滿足我們處理畫面的需求,這時就需要引入一些常數屬性數據(Properties)

常數屬性數據(Properties)

image

這些“屬性”就是Shader的變量,可以有資源(Assets)、腳本(Scripts)和動畫數據(Animation Data)來驅動表現效果,甚至是粒子系統(Particle System)也能作用(詳見《Unity粒子遇上着色器》),而這些數據可在頂點(Vertex)函數和片元(Fragment)函數中使用。

屬性的聲明規則如下:
_Name(“Display Name”, type) = defaultValue[{options}]

1._Name 是屬性的名字,也就是變量名,在之後整個Shader代碼中將使用這個名字來獲取該屬性的內容 ,切記要添加下劃線。
2.Display Name 這個字符串將顯示在Unity的Inspector中作爲Shader的使用者可讀的內容 ,即顯示的名稱。
3.type 屬性的類型。常用的有這個幾種:Color顏色,一般爲RGBA的數組;2D紋理,寬高爲2的冪次尺寸;Rect紋理,對應非2的冪次尺寸;Cube立方體,即6張2D紋理組成;Float和Range,都是浮點數,但是Range要求定義最大值和最小值,以Range(min,max)形式顯示;Vector四維數。
4.defaultValue 默認值,與類型直接掛鉤。一開始賦予該屬性的初始值,但是在檢視窗口中調整過屬性值之後,不在有效。Color 以0~1定義的rgba顏色,比如(1,1,1,1);2D/Rect/Cube,對於紋理來說,默認值可以爲一個代表默認tint顏色的字符串,可以是空字符串或者“white”,“black”等中的一個;Float和Range 爲某個指定的浮點數;同樣,Vector的是一個四維數值,寫爲(x,y,z,w)的形式。
5.Options 可選項,它只對2D,Rect或者Cube紋理有關,一般填入OpenGL中TexGen的模式,這篇的內容暫未涉及,就先以{}形式。

這樣我們可以嘗試解讀上圖中的那些屬性聲明的是什麼了。比如_MainTex(“A Texture”, 2D) = “”{},就是聲明瞭一個變量名爲_MainTex的2次冪尺寸紋理,它在檢視窗口中顯示的名稱是A Texture,默認是空的。

網格數據(Mesh Data)

image

第二步:頂點函數

這裏寫圖片描述

image

頂點函數是用來“構建”對象的,輸入的是appdata,即組織好的網格數據。經過一定處理後,輸出的將是頂點到片元結構體,即Vertex to Fragment,一般簡稱v2f。當然,這裏的結構體與用於輸入頂點函數的結構體都可以隨便命名,只不過這裏習慣以這種命名方式。

輸入的頂點數據是需要從對象空間轉換到屏幕空間,而頂點渲染到屏幕空間上就會以上圖右邊的情況顯示出來。在Unity 5早期版本一般使用 mul(UNITY_MATRIX_MVP, IN.vertex)方式去處理,即 Model * View * Projection獲得頂點對應到屏幕上的位置。但是這種方式效率不高,現在使用UnityObjectToClipPos函數方式直接處理頂點(vertex)信息。調用這個方法一般需要引入UnityCG.cginc預定義文件,通過#include “UnityCG.cginc”實現。

第三步:頂點到片元結構體

這裏寫圖片描述

image

這個結構體是中間數據,用於存儲從頂點函數(Vertex Function)輸出到片元函數(Fragment Function)輸入的數據。這個結構體也可以添加其他的變量,比如normalAngle,calculatedLightingColor等。

image

到這裏你們應該會發現,用在Shader中的變量有些特殊,比如float就有float4,float3,float2等。數字就對應的維度數量,像float4代表這是一個四維的浮點數變量,對應的四個值可以分別對應X,Y,Z,W,或者是顏色值R,G,B,A。

image

浮點數也因精度的不同可以設置不同的變量。float是高精度的,一般爲32位;half是中精度的,一般爲16位;fixed是低精度的,一般爲11位。在實際開發中,會根據性能需要選用合適的精度。比如顏色值RGBA,每個值域是0~1,而fixed值域是0~2,因此使用fixed4足夠表現所有顏色值。

第四步:片元函數

這裏寫圖片描述

image

片元函數(Fragment Function),通常用於將對象描繪到屏幕上,它輸入的是v2f結構體數據,而輸出的就是像素點。使用CG方法tex2D,輸入參數紋理及UV座標,就可以獲得每個UV對應點的紋理的顏色。最後我們就可以看到如上圖右邊的機器人效果了。

第五步:對象渲染到屏幕

如上上圖

動手修改第一個 Unity 着色器

image

這是一個最基本的 Vertex/Fragment Shader,純色渲染效果。可以看到上圖中,蜘蛛機器人呈現的就是一片純紅色。現在打開對應的Unity着色器文件具體解讀一下:

Shader "Custom/Examples/Spider Robot Shader"{

    Properties{

        _TintColor("Color", Color) = (1,1,1,1)

    }

    SubShader{

        Tags{ 
            "RenderType"="Opaque"
        }

        Pass{

            CGPROGRAM

            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            // Step 1
            struct appdata{
                float4 vertex : POSITION;
            };

            //Step 3
            struct v2f{
                float4 position : SV_POSITION;
            };


            //Shader Properties, but now in CG!
            float4 _TintColor;


            //Step 2 - Build The Object!
            v2f vert(appdata IN){

                v2f OUT;

                OUT.position = UnityObjectToClipPos(IN.vertex);

                return OUT;

            }

            //Step 4
            fixed4 frag(v2f IN) : SV_Target{

                //Step 5
                return _TintColor;

            }

            ENDCG
        }

    }

    FallBack "Diffuse"
}

文件頭部

image

我們發現一件很有趣的事,這個一對引號裏的Shader命名與地址與實際項目中物理命名與存放該着色器不太一樣,這是爲什麼呢?因爲Unity着色器在引擎內部有個自己的取用地址,而這個地址就是文件開頭在Shader後面引號裏的地址。這個文件的調取可以在檢視窗口中材質組件的Shader選項找到,如下:

image

以後你寫的Unity着色器也是遵從這樣的規則。

屬性(Properties)

image

這裏聲明瞭一個屬性,屬性名爲“_TintColor”,在檢視窗口中顯示爲“Color”,類型爲“顏色”,默認值爲RGBA全1,即白色。查看一下檢視窗口:

image

是有Color這個選項,但是顏色卻是紅色的?因爲這個屬性的默認值會在編輯器狀態下由設置的手動調整變化而變化,即自己設置了這個顏色。

標籤(Tags)

image

形式:Tags { “TagName1” = “Value1” “TagName2” = “Value2” }

作用:控制渲染引擎“何時”、“如何”將子Shader內容進行呈現。

上圖中所表示的是“渲染輸出的是非透明物體”。

還有一個比較常用的,卻容易引起混淆的就是“Queue”渲染隊列。Tags { “Queue” = “Opaque” } 表示的是“指定在渲染非透明物體的順序隊列”。其實這兩者最主要的區別在於“RenderType”表示的是渲染什麼樣的物體,而“Queue”表示的是在什麼樣的實際渲染物體。

PASS的開頭部分

image

CGPROGRAM與下文的ENDCG標記了在兩者之間的是一段CG程序。使用的是NVIDIA的CG語言,一種類似於C的語言,其大多數內容基本與微軟的HLSL語言是相似的。

#pragma的作用是指示編譯對應的着色器函數。#pragma vertex vert 所表示的就是聲明一個名爲vert的頂點函數(Vertex Function),#pragma fragment frag 所表示的就是聲明一個名爲frag的片元函數(Fragment Function)。一點實現了這兩個函數,實際上就是實現了頂點/片元着色器了。

#include “UnityCG.cginc”作用就是導入Unity通用CG預定義文件,後面的UnityObjectToClipPos函數就是在該文件裏定義好的。

輸入頂點函數的結構體(appdata):

image

appdata結構體只有一個參數,聲明瞭一個名爲vertex的四維浮點數,語義爲網格的頂點座標數據。

頂點函數實現(vert):

image

聲明瞭vert函數就需要實現它,這裏一目瞭然主要就是做了一件事:就是使用UnityObjectToClipPos方法,將輸入網格頂點對象空間轉換到屏幕剪裁平面。

頂點輸出到片元輸入的結構體(v2f):

image

v2f結構體中也就是一個參數,即網格頂點對應到屏幕上的座標,而語義上的SV所代表的是System Value(系統值),SV_POSITION對應就是屏幕上的像素位置。

實例化聲明:

image

這裏可以發現這個float4的變量與屬性裏的名字是一致了。這個float4變量是將屬性(Properties)裏的變量在Unity着色器內部進行數據綁定用的,爲了CG 程序正常訪問屬性(Properties)的變量,CG程序中的變量必須使用和之前變量相同的名字進行聲明。

片元函數(frag):

image

SV_Target就是System Value Target,實際就是屏幕的像素,最後frag函數return的就是像素,即RGBA顏色,因此frag返回的類型就是fixed4類型。而這裏return的就是_TintColor,含義就是屏幕上每一個像素點返回的都是_TintColor的顏色。

image

修改Color的顏色,在編輯器非運行時狀態下,就能看到渲染的即刻變換。

如果要顯示蜘蛛機器人的紋理,應該怎麼做呢?

第一步:引入蜘蛛機器人的紋理

在Properties添加以下變量:

image

變量名_MainTex,檢視窗口顯示“Main Texture”,類型是寬高爲2次冪的紋理,默認值爲空。保存下看編輯器裏的變化。

image

在Color下就多了個設置紋理的選項,點擊Texture框內的Select按鈕選擇Bot,即引入了蜘蛛機器人的紋理。

image

第二步:結構體添加網格和紋理的UV值

UV是什麼呢?UV(W) 是紋理空間中的多維座標系,值域 0 到 1。這裏使用 2D 紋理,因此是二維的

image

image

分別在appdata和v2f結構體,添加變量uv0,用於記錄引入紋理的UV座標。

第三步:添加紋理的實例化聲明

_TintColor後面添加如下新的實例化:

image

sampler2D是與紋理綁定的數據容器接口,爲CG/HLSL中 2D貼圖的類型,相應還有sampler1D、sampler3D、samplerCUBE等格式。

第四步:結構體UV賦值

image

在vert函數中,添加上圖中的語句,將獲取到的網格數據上的UV信息(網格平鋪成二維與紋理的一一對應),賦值給v2f結構體中。

第五步:渲染紋理

image

將片元函數做以上修改,使用tex2D方法替代掉原來單純返回顏色。

現在重新回到編輯器界面並運行:

image

蜘蛛機器人就以對應紋理顯示了,從運行狀態下可以發現,紋理與網格是完全一致的。

其他有特色的頂點/片元着色器效果介紹:

雙紋理混合(Texture Blending - Lerp!)

image

做雙紋理混合肯定需要引入兩張不同的紋理,這裏分別聲明瞭Main Texture和Second Texture,然後可以通過一個_Blend_Amount參數來調節兩個紋理的混合比例。接着,在片元函數部分,分別獲取兩個紋理對應UV的像素顏色,通過Lerp函數進行混合。

Lerp的功能是基於權重返回兩個標量或向量的線性差值。具體在CG中的實現如下:

image

調節_Blend_Amount,就可以獲得雙紋理混合的顯示效果:

image

image

顏色漸變(Color Ramp - Texture Sample)

image

在輸入參數部分,可以看到[Header(Color Ramp Sample)],它用於在檢視窗口中添加一個標籤文本。這裏是顯示Color Ramp Sample。一般引入一個2D的紋理,都會有Tiling和Offset顯示,即可以調節紋理的縮放與平移,使用了[NoScaleOffset]就會將這兩個參數設置禁用,僅僅獲得紋理的原始比例與平移。

image

在檢視窗口中的顯示如上圖所示。

參數_ColorRamp_Evaluation其實是獲取這張漸變圖的水平中心像素點的位置,從而得到該位置的像素。將主紋理的顏色與漸變紋理的顏色相乘,即可獲得混合後的顏色。

image

image

顏色與顏色之間可以進行加減乘除進行混合運算。加法可以起到顏色疊加的效果,但是由於顏色值的值域爲0~1,相加很容易達到1,就會顏色會愈發明亮,因此疊加建議使用乘法;減法可以進行反色處理,但是同樣是值域的原因,數值達到0,顏色就很暗淡,因此要做反色建議使用除法。

紋理剔除(Texture Cutout)

image

在片元函數中使用了Clip方法,該方法的功能是當輸入的參數小於等於0時,就會刪除對應位置的像素。這裏使用了一個有不同顏色分佈的紋理作爲剔除紋理,使用CutOut Value作爲剔除參考值,當該紋理像素某位置上RGB分別減去這個CutOut Value有小於等於0的時候,這個蜘蛛機器人就會有鏤空的效果。

image

世界座標-梯度(World Space - Gradient)

image

image

在v2f結構體中,添加了世界座標。而這個座標是通過unity_ObjectToWorld,來獲取每個頂點在世界空間中的座標。在輸入的部分加入了兩個表示高低不同位置的顏色,片元函數中對兩者根據踢動值進行了線性插值處理。最後以相乘的方式進行了像素混合,呈現出圖中的畫面效果。

法線擠壓(Normal Extrusion)

image

可以看到在appdata結構體中引入了法線(Normal)。在頂點函數計算的時候,將發現xyz值乘以擠壓值(Extrusion Amount),而後疊加到頂點的xyz上,這樣就可以根據這個擠壓值對於頂點的對象空間位置進行法線相關的偏移處理,最後可以得到“膨脹變胖”、“擠壓變瘦”的有趣效果。

image

image

時間相關(Time)

image

_Time爲Unity着色器默認載入UnityShaderVariables.cginc的變量,_Time.y表示遊戲自啓動開始後的時間。通過波速、波距和波頻三個參數是將機器人進行波形化處理。

image

漫反射光照(Diffuse Lighting)

image

image

漫反射光照牽涉到一些光照運算的內容,首先就需要添加Tags的LightMode爲ForwardBase,基於前向的光照模式,還引入了UnityLightingCommon.cginc預定義文件來輔助光照的運算。當然,appdata結構體中不會缺少網格的法線數據。在頂點函數中,通過UnityObjectToWorldNormal獲取網格對象在世界座標中的法線值。接着,通過dot方法得到法線值與世界空間光照座標的點積值,作爲漫反射參考值。然後,將這個漫反射參考值與光照顏色相乘獲得光照的漫反射顏色。最後在片元函數中將主紋理顏色與之相乘混合,得到最終的像素顏色。

其他控制Unity着色器的方法

通過動畫控制(Animation Clip)

image

正如上圖所示,該示例沒有使用額外的代碼,僅僅依靠動畫片段(Animation Clip)來控制,着色器的參數值。

image

Unity着色器依附於材質(Material),而材質需要渲染器(Renderer)使之生效。因此通過動畫控制Unity着色器只要找到對應的參數,就可以像製作其他角色動畫一樣,讓策劃或美術調整着色器顯示的效果。

腳本控制(Scripting)

對於程序員而言,有時候通過代碼的手段似乎更爲便利,Unity着色器也給這方面的需求提供了方法。也正如前面在敘述動畫片段控制Unity着色器的參數,只要通過Renderer → Material → Shader就可以訪問到需要的參數。因此,可以通過Get和Set的方式去獲取對應參數的值,或者去修改對應參數的值。

image

image

大家如果意猶未盡,可以下載由Unity版PPT生成的單機應用程序和附帶的部分場景的工程,進行體驗。中間頁面的切換通過鍵盤左右鍵進行,大多數內容使用到了UGUI。有些可滑動的部分使用鼠標拖動,還有一些內部的切換,需要使用鍵盤的T鍵,具體位置詳見前文敘述。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章