2018年9月20日 星期四

Shader(著色器)新手入門筆記 - 初次接觸

最近一段時間在使用Unity開發一款遊戲,想做一些特別的視覺效果,在網上做了一些資料蒐集後,似乎寫Shader(著色器)是最好的選擇。在兩年前用Unity開發遊戲的時候並沒有需要用到特別的視覺效果,因此並沒有接觸過Shader,所以要從零開始學起。

不過我剛開始打算自學Shader的時候遇到了很大的問題,就是網上的教學很少也很分散(可能是因為這項技術本身比較專門),入門門檻很高。雖然Unity官方有提供一篇入門教學(A Gentle Introduction to Shaders),但看了很久也不太明白,可能是因為這篇文章面向的對象並非完全是新手,同時也沒有清楚解釋一個Shader程式碼內每個部分的意義。

經過一段時間的自學後,總結一下自己對Shader的初步理解:(自己也還只是新手,若有錯誤歡迎指正)

甚麼是Shader?

Shader(著色器)是一種電腦程式,簡單來說主要負責進行圖像處理,一般都在GPU上執行。

如果想講得準確點的話,要把遊戲的各個物件從模型到畫在螢幕上的過程有很多個步驟,這個過程我們叫Rendering Pipeline(繪圖管線)。當中有一些步驟我們可以寫程式控制,有一些步驟我們可以做一些設定,其他步驟則甚麼也不能做(只能交由GPU處理)。可以寫程式控制的那些程式就叫Shader,不同的步驟就是交由不同種類的Shader來負責。不過一般我們最常接觸的是Vertex shader和Fragment shader,下面會講到這兩種shader實際上負責甚麼。

Shader使用甚麼程式語言?

和Unity使用C#作為程式語言不一樣,Shader是使用另外一種專門為圖像處理而設計的程式語言。Unity可以用的Shader程式語言有兩種,分別為Cg/HLSL(DirectX)和GLSL(OpenGL)。一般來說都會使用跨平台的Cg/HLSL(Unity會根據目標平台再幫你轉成DirectX或OpenGL),網上的Shader資源大多也是使用這個語言。

HLSL是微軟的DirectX的所使用的語言,GLSL則是OpenGL所使用的語言,而Cg是Nvidia開發出來的語言,而因為Cg和HLSL有合作,所以兩者的語法非常相似,但又有一些不同。

Cg全名為C for graphics,語法上有點參考了C但又不太一樣,原本由Nvidia負責維護但現在已經不再支援(depreciated)。Unity官方之所以把Unity Shader使用的程式語言會叫Cg/HLSL,是因為它實際上並不完全等同於Cg或HLSL,而較像是它們的變種,因為有些Cg和HLSL支援的語法或函數在Unity中並不支援(你可以理解為Unity在參考了Cg和HLSL後自己又弄了一個很像它們的語言)。但為了簡單起見,我們就把Unity所使用的這個Cg/HLSL變種語言直接簡稱為Cg。

要注意的是,Unity的Shader程式碼中,要到「CGPROGRAM」和「ENDCG」之間的程式碼才是Cg語言,其他的部分則是使用叫ShaderLab的宣告式語言(即是只是定義,而沒有任何程式邏輯的部分)。

Unity的Shader就是一個Shader嗎?

Unity Shader其實指的是Unity內一個附檔名為.shader的檔案,而裡面實際上是把很多不同步驟所涉及的Shader包裝在一起。這個檔案可以附加在材質(Material)上面,然後我們就可以把材質附加在Game Object上,讓其顯示出我們想要的效果。

簡單來說,Unity Shader為了方便你管理,是以Material為單位,讓你寫該Material下所需的各種Shader。

Unity Shader的結構?

一個Unity Shader下會有多個Sub Shader,GPU會視乎你為其加的標籤而選用其中一個Sub Shader。

而Sub Shader裡面可以再定義不同種類的Shader,常見的有兩種Shader組合:

第一種是Vertex Shader + Fragment Shader,算是比較基礎的Shader,也是我一開始學習的組合,會比較容易了解Shader的原理並進行基礎的運算,也容易做Optimization,確保只計算自己需要的效果,不計算其他多餘的東西。

第二種是Surface Shader(表面著色器),是Unity為了方便你而設立的一個簡化版Shader,實際上它還是會再幫你將其轉為Vertex Shader + Fragment Shader。據說比較容易用簡單的程式碼寫出比較好的效果(尤其是涉及光源的效果),但可能當中的函數會幫你計算一些多餘的東西,較難做Optimization或者一些獨特的效果。

除了Sub Shader以外,還有Property(弄一些可以在Unity Editor內調節的variables給Shader用)、Tag(定義Shader的一些特性,供GPU參考)和Fallback(如果所有Sub Shade都用了一些GPU不支援的功能的話就轉用另外一個低級的Shader)等。

甚麼是Vertex Shader和Fragment Shader?

在了解這兩種Shader是甚麼之前,建議先了解整個Rendering Pipeline的流程,這樣你會比較清楚他們當中會擔任甚麼工作。在Vertex Shader和Fragment Shader之間其實還隔了很多個步驟,所以不要以為Vertex Shader的output就是Fragment Shader的input。

Vertex Shader(頂點著色器)會每個Vertex(即是你的遊戲模型的一點)運行一次。網上經常只說Vertex Shader可以讓你針對Vertex的位置或者Normal做調節,但卻經常說漏了最重要的作用,就是做Matrix transformation,把vertex的coordinates從Model Space(模型空間)轉成Clip Space(修改空間),我稱之為空間轉換。

(Model Space又稱Local Space局部空間或Object Space物件空間)

本來每個Game Object都是獨自一個個體,有自己的coordinates,而這個轉換就是要把這些Game Object都放在同一個世界內。你如果在Shader檔案內有看過類似mul(UNITY_MVP, v.vertex)這樣的語法(Unity建議簡化成UnityObjectToClipPos(v.vertex)),它們就是在做空間轉換。

因此我對Vertex Shader的理解是,主要負責做空間轉換,但你也可以選擇針對vertex再加多一點調節,例如要模擬水面的扭曲效果,就是需要把vertex按一定的規律進行移動。

Fragment Shader(片段著色器)(有人叫Pixel Shader但我覺得不太準確),則是每個Fragment(亦即是可能會畫在螢幕上的一點,但Fragment本身包含了較多資訊,Pixel就真的只是在螢幕上的一個點)運行一次。Fragment Shader主要做的就是利用每一粒Fragment中不同的資訊,決定每一粒Pixel上的顏色

你可以想像如果你的螢幕大小是1920x1200,那就可能要運行兩百萬次,而為了確保遊戲能保持60fps的速度,一個Fragment Shader需要在最多8.33納秒(nanoseconds,即10的負九次方)內就執行完畢,因此要使用最快的計算方法以及最少的Memory來做Optimization。因此你會用到half和fixed這種比float使用更少Memory的floating point data type,一般會使用2 Bytes但會根據不同GPU硬件而有所不同。

Shader data types:
https://docs.unity3d.com/Manual/SL-DataTypesAndPrecision.html

有時候如果真的做不到更快速計算的話,可以試試以下方法:

  • 在某些GPU上停用該Shader(通常會設定該Shader的LOD,即Level of details,當總體LOD低於某個值就會自動停用Shader)
  • 調低預設fps(可固定在30或更低)

想自己寫一個Shader

我覺得第一個Shader可以試試自己從頭到尾寫,以了解Shader的結構,不過在之後進行開發的時候多數都是複製一個現成的Shader修改程式碼(現成的無論是自己的還是別人的都可以,只要你大致看得懂),這樣就能省卻許多syntax的問題,專心弄想要的效果。

對於Shader的概念有基本的認識後,想自己動手寫一個Shader試試看的話,我覺得以下的影片是個不錯的入手點:
Shaders 101 - Intro to Shaders

Shader難寫嗎?

簡單的Shader很容易寫,但稍微複雜一點的Shader就會難寫很多。主要是因為不同GPU的種類和能力都差很遠,通常好的Shader需要能夠支援不同等級的GPU,於是往往要定義很多Pass或者Fallback來處理GPU不支援的情況。

此外,相比起Unity本身的程式或者其他程式來說,Shader的網上資源很少,討論不多,就連Documentation也是近乎沒有(對於平時非常依賴Documentation的程式員來簡直說是無法想像),想知道一個function在做甚麼往往都只能在一些分散的Tutorial或者社群文章中找到。

Shader因為會涉及許多數學運算,想做到自己想要的效果往往要自己想一條formula出來,如果數學不好的話大概要惡補一下數學了(尤其是Graph和Matrix)。

參考資料

這篇教學很親切地講解了Unity Shader的結構:
貓都能學會的Unity3D Shader入門指南(一)(二)

也可以看看在巴哈的這篇文章,C4Cat的Colin Leung在留言下解答了許多新手常見的問題:
https://home.gamer.com.tw/creationDetail.php?sn=2867450

另外我也在圖書館借過《遊戲大師天堂路:只有Unity Shader才能超越Unity》這本教材,內容由淺入深且相當詳細,也讓我知道了原來Shader可以做的事情比想像中多,不過要注意當中一些專用術語的中文翻譯前後不一,因此個人還是喜歡用回英文術語