很多人在學習Unity的時候對Shader都是一知半解,作為剛入職半年的新人接觸Shader的時間也並不長,正因為是新人才能體會到學習Shader時候所遇到的困難和迷茫,無奈於資料不好找,網上難得的幾篇教程講的又不夠完善或者太淺太短,所以我一直以來就想寫一系列UnityShader的學習教程,現在終於快過年公司放假了, 抽空准備開始着手寫這個系列的教程了,在Unity這個圈子里我還是一只很菜很菜的菜鳥,感謝這半年來一路上幫助過我的人,書,以及在網上無私奉獻寶貴知識的前輩們,僅以此系列向他們致敬.
這個系列的教程會不定期的更新,由於筆者工作繁忙,有時可能會一段時間不更新,還望各位諒解,本人才疏學淺,可能在大神們面前班門弄斧了,寫這系列教程一來是想幫助那些准備接觸Shader卻不知如何入門的同學們分享自己的經驗,二來是借由自己寫教程來鍛煉自己寫技術文章的文筆和鞏固自己學習的知識,查缺補漏,只有你能把你學到的知識透徹的講給別人聽的時候,你才完全掌握了它.由於我接觸Unity時間並不長,接觸Shader時間更短,難免會有所紕漏和錯誤,還望各位看官不吝賜教,批評指正.
前言
前言部分主要是講述一下個人對學習UnityShader的一些建議,以及描述一下圖形渲染的大概模型和UnityShader的工作方式.
首先要說明一點,要想學好Shader而不只是停留在會用幾個簡單語法的層面,那你就一定不可避免的需要接觸數學,主要是線性代數,深入研究可能會用到微積分和空間幾何,要做好心理准備,總之要學習Shader不難,但要想學好,你真的要花一番功夫.再有一點就是編寫Shader來說目前只有一些有限功能的帶代碼提示的編輯器,還沒有可以調試的工具,一旦Shader出錯Unity給出的提示經常是非常不明確的,這時候你只能耐心的一行行分析了.
先說明的一下什么是Shader,Shader一般被翻譯為"着色器",他並不是指某一種具體語言,他是一種技術,可以讓程序員去通過編寫代碼去參與到GPU渲染圖形的具體流程中去,去制作一些精美的特效和動畫甚至是讓模型發生奇怪的形變,簡單點就是可以把它理解成一種對呈現到屏幕上畫面的一種美化工具,想一想是不是很酷,你竟然可以去參與顯卡的工作.以前寫一輩子Hello world一直和CPU打交道.
而編寫Shader目前比較知名的是基於DirectX的HLSL和基於OpenGL的GLSL,而我們Unity用的ShaderLab是基於Nvida和Microsoft一同開發的CG(C for Graphic)語言,從名字上我們就能知道,只是一門類C的語言,如果你接觸過C語言或者C++,甚至是JAVA,C#那對他的基本語法一定不會陌生,CG語言他是跨平台的,他可以基於OpenGL也可以DirectX來運行,也就是說他是工作在DX和OpenGL上層的抽象語言,這三種語言在某些具體方面可能有性能和擴展性上的區別,但是對於背后的實質技術並無過大區別,我對GLSL和HLSL接觸的不多,HLSL由於是基於DX的可能現在很多高端引擎都是用HLSL,GLSL的話優勢可能在於跨平台吧.
好吧我們只要關注我們的CG就可以了,有興趣的同學可以自己去了解一下另外兩種語言,說明一下由於CG是Nvida和微軟共同開發的,所以CG上的很多標准和HLSL是兼容的,學起來有很多互通的地方.Unity 使用的並不是完完全全的CG語言,如果你看官方的API你會發現他的Shader分為三大類:
(1)Fixed function shader 固定渲染管線 Shader, 現在的使用已經很少了,一般用來做默認處理,以一種固定的模式去處理渲染流程,但是他完全使用ShaderLab語法讓你很簡單的去修改一些參數,功能有限,很多Unity內置的默認效果用的是這個Shader.
(2)Surface Shader 不知道該怎么翻譯,姑且叫表面Shader,這個Shader是Unity官方文檔里用的最多的了,而且在國外很多的UnityShader學習教材里也很多用它來舉例子,怎么說呢,這個Shader其實和第三種已經很像了,只不過你並不用全部把片段着色器代碼自己來寫,unity給你自帶了很多封裝好的,如果你想要自己寫也可以,我個人感覺他和第三種最大的區別在於它的光照模型我們不能確定到底是外面哪個光源,我沒試驗過是否可以通過傳外部參數來讓解決.另外在Surface Shader中的同一個SubShader中不可以使用多個Pass.
(3)Vertex and Fragment Shader 這類是我主要要講的Shader,很好很強大,定點着色器和片段着色器完全由我們自己來控制,這樣子我們就能最大化使用我們掌握的東西,當然這類Shader也是最需要時間學習的.
我們來看一下GPU把3D物體渲染到2D屏幕上的一個簡略的步驟(只是簡單地有個印象),以后具體的地方會具體分析:
1,由於我們導入Unity的模型或者我們再Unity創建的模型,它們自身都有一個屬於自己的坐標系,就像人一樣,無論你站在什么方位,你總是知道自己的前后左右是什么方向,而這個前后左右就建立在你自身的坐標系,所以Unity里每一個模型並不知道其他模型的坐標系,那么為了方便計算和操作我們就要把他們轉換到一個統一的空間坐標系里,前者是物體的模型空間,后者是世界空間,這是第一個空間變換:"模型空間"->"世界空間",然后我們要確定我們在攝像機里具體能看到哪些東西,同樣為了方便計算和處理,我們要再一次把物體從世界空間轉換到攝像機空間,其實就是以攝像機為坐標系原點建立一個三維空間,經過這一步操作就能知道每一個物體從攝像機的位置來看他在什么方位,這是第二個空間變換:"世界空間"->"攝像機空間".現在雖然知道了物體在相機空間的位置,但是還不能立刻進行渲染,為了方便后面要進行的空間裁剪和,屏幕坐標映射當然也是為了方便計算,我們要把攝像機空間內所有物體的坐標再一次轉換到一個坐標范圍是(-1,-1,-1)到(1,1,1)的正方體空間區域中,也就是整個空間變換中最難理解的"投影變換",這是第三個空間變換:"攝像機空間"->"投影空間".接下來就是把投影空間里的物體映射到屏幕坐標上去.第四個空間變換(嚴格來說這不應該叫空間變換):"投影空間"->"窗口空間".以上大概屬於空間變換部分的內容,很多書本上的地方叫法和說法都不盡相同,理解大概意思即可.
2,顯卡來處理圖形的過程中一般有三個最基本的要素:點,線,面(一般指三角面),而在上一步中我們只是針對模型中單個頂點來一一處理,接下來我們就把經過空間變換剩下來的頂點(為什么說剩下來呢,因為上一步有一點沒有提,就是在投影變換的過程中, 會進行一步剔除處理,把一些不在顯示區域內的頂點根據一定規則過濾點,這樣可以減少接下來的運算量)進行組裝,組裝成點,線,面.也就是所謂的圖元裝配,圖元裝配進行完之后,我們需要進行柵格化:大家都知道我們面前的屏幕是由像素矩陣構成的,而我們處理的模型只是由有限的頂點構成的,經過圖元裝配后形成的基本圖元(點線面),我們要把它和屏幕上的像素區域對應起來,也就是進行像素填充,一般模型的每個頂點都會帶有一些基本信息(例如,法線,位置,紋理坐標,顏色等),即一個三角面其實只有三個頂點是有基本屬性的,而其他填充進來的像素區域是並不直接擁有這些基本屬性的,他們的屬性都是通過在頂點間差值計算得來的(其實上面提到的像素應該稱為片元fragment).有些書籍中把圖元裝配也歸到柵格化中,請注意.經過這些處理模型基本在GPU中已經形成了他所要繪制到屏幕上的樣子了.不過這並沒有結束.
3,經過上面的處理我們就仿佛得到了一個預備要繪制到屏幕上的臨時數據區域,不過到底是將這些像素是否最終繪制到屏幕上,怎么繪制到屏幕上,還需要經過一些判斷.制作游戲中最常見的現象,離攝像機近的物體要繪制在前面,會把離攝像機較遠的物體擋住(不考慮半透明物體),如果沒有某些判斷的話,那如果GPU先渲染了離屏幕較近的物體,而后處理了離屏幕較遠的物體,那么近的先畫到屏幕上,遠的后畫就會把之前的覆蓋掉了,這顯然不是我們需要的效果(並不是所有引擎都可以自由的去控制物體的渲染順序,即使可以控制,很多時候也十分麻煩,並不十分常用).所以說我們就需要在繪制的時候進行檢測,上面這個問題使用的是ZTest(還有很多檢測這里只提這一個,其他的以后遇到了再具體說),對於GPU來說擁有兩個最基本的緩沖區:幀緩沖區和Z緩沖區(也叫深度緩沖區),這兩個緩沖區都與屏幕上的每個像素一一對應,其中幀緩沖區對應着屏幕上的像素點的最終顏色,GPU最終都是通過把顏色寫入這個區域來呈現在屏幕上的,而另一個深度緩沖區則存儲着屏幕上每個像素的深度值,所謂的深度值就是離攝像機的遠近(一般都被規范化為一個0~1的數值),數值越大說明離攝像機越遠,剛才我們提到的ZTest就是通過將光柵化后的像素的深度值和當前屏幕上像素對應的深度值進行相應的比較,來絕定是不是把新的像素點覆蓋掉原來的像素點(當然這並不是最終是否寫入幀緩沖區的判斷條件,還有一些其他判斷),至於以什么模式來判斷並無硬性要求,根據你具體的要求來選擇判斷方式.經過這一系列的判斷和處理之后GPU會把最終結果寫入幀緩沖區,我們會在下一幀看到剛剛處理過的最新畫面了
在上面我簡略的說了一下GPU渲染3D物體的大概流程,那我們的Shader是怎么參與進來的呢,開篇已經說過了,這個系列的文章只以Vertex and Fragment Shader來進行距離,這是Unity中最復雜和最強大的Shader,另外兩種大家自己跟着官方文檔了解一下吧.
在一個shader中我們主要通過兩個部分來參與渲染,頂點着色器(vertex)和片段着色器(fragment),他們兩個參與的時機不同,VertexShader是在頂點變換的時候,而FragmentShader是在光柵化到將最終計算完成的像素值寫入幀緩沖區。在VertexShader中我們可以對模型頂點實施一些坐標轉換甚至形變,也可以通過計算和賦值把一些片元默認並不含有的屬性帶入FragmentShader來進行一些需要的操作.而在FragmentShader中我們可以做的就很多了,很多光照處理都是在這里進行的(當然在VertexShader中也可以進行光照計算,不過效果不是十分好),而且也可以做一些比較復雜的色彩變換和處理.好了前言就說到這里,我第一次寫關於Shader的東西,語言組織的不是很好,大家即使理解的不太清楚也沒有關系,有個大概的了解
本篇只是接下來系列教程的一個前言,做一些基本交代,接下來為了大家更好的理解接下來的教程,也為了減少浪費一些不必要的時間在一些過於基礎的語法上,提議大家自己先去網上找一些資料或者跟着官方的文檔寫一寫簡單的Shader,熟悉一下基本語法,記得要寫Vertex and Fragment Shader,另外兩種也可以看看.相信我提前預習才不會看的一頭霧水,本人會不定期更新,希望大家能有所收獲.
再給大家推薦一些參考資料,我學習的很多知識也來自與它們(去網上搜搜吧):
1.《gpu編程與cg語言之陽春白雪下里巴人》,挺通俗易懂的,是一位前輩個人總結的,不妨一看
2.《Cg Programming in Unity》,維基教科書的整理的一些關於Unity圖形渲染的知識,我還沒怎么看,同事介紹的
3.《Cg教程_可編程實時圖形權威指南》,一本比較早的書,書上的編排有些晦澀,並不十分適合UnityShader來入門,語法上是純CG的與Unity略有不同.
好了,在下才疏學淺難免有所紕漏,希望各位能及時指正,大家一起進步,若鄙人這些粗淺的見識能夠幫助到大家,我將深感欣慰.也歡迎大家繼續關注《Esfog_UnityShader教程》系列文章.