Shader從入門到跑路:樸實無華的圖形學基礎

遊資網發表於2020-03-09
Shader從入門到跑路:樸實無華的圖形學基礎


前言:

Unity Shader的學習的學習路徑是非常陡峭的,筆者在學習的時候走了不少歪路,在這裡權當分享一下學習過的內容,也是給自己做一個記錄了

準備:

1.        基本的Unity使用經驗

2.        天不怕地不怕的心態

枯燥但必須得搞清楚的圖形學內容

因為筆者的目的主要是想介紹shader,因此書本上的圖形學內容這裡就不深入講了。在傳統的圖形處理中,我們一般需要兩種程式,一個叫Vertex Shader,另一個叫Fragment Shader,他們是一對好夥伴,往後的日子也會時常與他們打交道。

Vertex Shader負責獲取處理的網格(Mesh)資訊然後進行處理,比方說我們想渲染一個正方形,那麼首先就得先獲取這個正方形的位置,紋理等資訊,然後做一些矩陣轉換傳遞給Fragment Shader。(關於矩陣轉換的事情之後再講,大概知道有這麼回事就好)。

Shader從入門到跑路:樸實無華的圖形學基礎

網格中的畫素經過處理之後被儲存在Fragments中被傳給Fragment Shader。當Vertex Shader處理頂點的時候,它會將平均每三個頂點組成一個Fragment,最終整個被處理過的影像被稱作Fragments。這個過程用圖形學的知識來講解頗為複雜,有興趣的讀者可以找馮女神的書看看。Fragment Shader其實就是進行核心計算的部分。

Shader從入門到跑路:樸實無華的圖形學基礎

自問自答

既然Vertex Shader已經將網格的資料做好了空間轉換,那這些資訊不是已經可以用了嗎?為什麼還需要一個Fragment Shader呢不是多此一舉嗎?哎,別急,聽我慢慢解釋。這是因為在影像處理當中,我們並不一定需要用到所有的畫素資料,因此有些資料需要被修改,而有些則直接棄之不用。比如在我們在實作模糊效果時,其中一種思路便是通過把一個畫素周邊的畫素合併成一個相同顏色的畫素實現的,這些內容過於複雜,無法交由CPU來計算,只能由GPU代勞,因此也可以將Vertex Shader看作和CPU打交道的程式,而Fragment則是負責GPU的內容。

Shader從入門到跑路:樸實無華的圖形學基礎

於是,在Fragment Shader中,有些畫素被保留,而另一些則可能被丟掉,接下來Fragment Shader講處理好的內容傳給顏色緩衝區(Color Buffer),結束了它的工作。後面的內容超出了本文的內容,就不延伸講了。

我們已經半隻腳邁進了圖形學,下一步就邁遠一些,直接寫shader了。對於長期和各種程式語言打交道的我來說,從來都是先學點皮毛,然後立馬開寫,就像學英語一樣,會那麼幾個單詞就得衝去外國人堆裡尬聊了。這種方法容錯率最低,但也毫無疑問是最快的學習方法,只有最刻骨銘心的錯誤才能讓人長記性。說的不好不能成為你不說的原因,那麼廢話少說,來看看這段你人生當中最無聊的shader。

正文

  1. Shader "Custom/ShaderLearning"
  2. {
  3.         SubShader
  4.         {
  5.                 Tags
  6.                 {
  7.                         "PreviewType" = "Plane"
  8.                 }
  9.                 Pass
  10.                 {
  11.                         CGPROGRAM
  12.                         #pragma vertex vert
  13.                         #pragma fragment frag

  14.                         #include "UnityCG.cginc"

  15.                         struct appdata
  16.                         {
  17.                                 float4 vertex : POSITION;
  18.                         };

  19.                         struct v2f
  20.                         {
  21.                                 float4 vertex : SV_POSITION;
  22.                         };

  23.                         v2f vert(appdata v)
  24.                         {
  25.                                 v2f o;
  26.                                 o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
  27.                                 return o;
  28.                         }

  29.                         float4 frag(v2f i) : SV_Target
  30.                         {
  31.                                 float4 color = float4(1,1,1,1);
  32.                                 return color;
  33.                         }
  34.                         ENDCG
  35.                 }
  36.         }
複製程式碼

將寫好的shader賦值給material,然後新建一個3D平臺將material拖入就可以看到效果(如果這個步驟都不會趕緊谷歌大法)。這個shader本質上只是輸出白色畫素而已,它並沒有很酷,但為了寫出酷炫的shader,這個階段是必不可少的。

Shader從入門到跑路:樸實無華的圖形學基礎

2.1 ShaderLearning檔案的渲染結果

這裡有很多程式碼,但我們只需要關注涉及到vertex/fragment shader輸入輸出的部分就好了。

首先是結構體appdata,它定義了從每個網格的頂點需要獲取什麼資訊,在這裡是所有網格頂點的空間資訊(POSITION)。

  1. struct appdata
  2. {
  3.         float4 vertex : POSITION;
  4. };
  5. 結構體v2f則定義了需要傳遞什麼資料給fragment shader。

  6. struct v2f
  7. {
  8.         float4 vertex : SV_POSITION;
  9. };
複製程式碼

接下來,我們有一個vert函式,以定義的appdata作為引數然後返回一個v2f型別的引數傳遞給frag函式,最後frag函式返回一個float4型別的顏色。(float4可以看成四個浮點陣列成的四維陣列)

  1. // Vertex Shader
  2. v2f vert(appdata v)
  3. {
  4.         v2f o;
  5.         ...
  6.         return o;
  7. }

  8. // Fragment Shader
  9. float4 frag(v2f i) : SV_Target
  10. {
  11.         return float4(1,1,1,1);
  12. }
複製程式碼

值得一提的是在appdata中定義的資料,是通過語義指揮unity來獲取的,因此當我們定義好之後,在函式中就可以直接使用了。這些都是unity本身做好的封裝,讓我們可以專心思考shader的計算過程。

剛才是走馬觀花,現在我們先拿過來一個放大鏡,仔細瞧瞧兩個函式都做了些什麼

Shader從入門到跑路:樸實無華的圖形學基礎
  1. v2f vert(appdata v)
  2. {
  3.         v2f o;
  4.         o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
  5.         return o;
  6. }
複製程式碼

首先是作為shader裡面打頭陣的vertex shader,由於我們現在的shader是整個宇宙最簡單的shader,因此它從網格上能得到的資訊就只有頂點的位置而已。然後我們初始化了一個v2f叫o。接著用一個矩陣轉換的方法設定o裡面的頂點變數,目前你應該知道的就是這裡使用了一個叫做矩陣乘法的運算,它將網格上的區域性頂點從一個與網格相關的點,轉換成一個螢幕上的點。

  1. float4 frag(v2f i) : SV_Target
  2. {
  3.         float4 color = float4(1,1,1,1);
  4.         return color;
  5. }
複製程式碼

而fragment shader的功能是將畫素轉化成螢幕上的顏色,我們不需要管這個畫素在什麼位置,因為在vertex shader裡面已經處理過了,因此在這一段我們返回實際上就是一個有具體的位置的螢幕顏色。返回顏色是白色,它由四個內容組成,分別是紅色,綠色,藍色和透明通道。你可以試著將這些數字做一些更改,比如返回一個float4(1,0.6, 0,1)則可以獲得橙色的顏色

Shader從入門到跑路:樸實無華的圖形學基礎
2.2 輸出橙色的渲染結果

那麼大概就是這樣了,或許這一部分對很多人來說已經是勸退的點,但其實Unity已經為我們做了很多去複雜化的工序了,如果想要深入瞭解這些背後的原理建議讀者學一學openGL或者directX,你馬上會明白Unity真的做的非常良心。

當然,只是輸出純色實在是太無聊了,我們可以在此基礎上做一個更改,比如先在兩個結構體裡面加入一些東西

  1. struct appdata
  2. {
  3.         float4 vertex : POSITION;
  4.         float2 uv : TEXCOORD0;
  5. };

  6. struct v2f
  7. {
  8.         float4 vertex : SV_POSITION;
  9.         float2 uv : TEXCOORD1;
  10. };
複製程式碼


注意,你能讓unity幫助你從網格那裡拿到的資料是受到unity提供的語義所限制的,可以去這裡看看:

https://docs.unity3d.com/Manual/SL-VertexProgramInputs.htmldocs.unity3d.com

在vertex shader做的事情並沒有什麼魔法,只需要將拿到的uv直接通過v2f傳給frag函式就好了

  1. v2f vert(appdata v)
  2. {
  3.         v2f o;
  4.         o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
  5.         o.uv = v.uv
  6.         return o;
  7. }
複製程式碼

接下來把uv的值填入顏色的返回值中,得到下圖

  1. float4 frag(v2f i) : SV_Target
  2. {
  3.         float4 color = float4(i.uv.r, i.uv.g, 0, 1);
  4.         return color;
  5. }
複製程式碼

Shader從入門到跑路:樸實無華的圖形學基礎
2.3 以uv值作為顏色值的渲染結果

自問自答

之前不是說每個三角形只會執行三次來獲取頂點資料嗎?為什麼2.3的所有點都有渲染到顏色呢?

Shader從入門到跑路:樸實無華的圖形學基礎

2.4 理論上來說一個三角只有三個頂點被計算到了,那麼2.3中那種漸變效果是如何得出的呢?

在上一篇文章我們說過,vertex shader將網格進行處理,然後將資料以fragment為單位輸出到fragment shader中,其實我們可以將fragment看作一個三角形的圖形(從圖形學的角度看這麼想不太對,但對知識進行三維建模還是有必要的)。而得出這一結果是因為shader對兩個相鄰頂點之間的值進行的了線性插值,因此雖然vertex shader只計算了四個頂點(上面三個,下面一個),但最終fragment shader還是會得到所有的值資訊。

課後練習

試試修改返回的顏色值,並得到下面這個效果

Shader從入門到跑路:樸實無華的圖形學基礎

作者:俊銘
專欄地址:https://zhuanlan.zhihu.com/p/85594617

相關文章