C for Graphic:語言(三)
之前我們通過觀察一個最簡單的CG shader,詳細的學習了CG shader關鍵程式碼的執行機制,這一篇我們就要通過unity引擎的shader上下文環境,觀察並理解其他細節程式碼的作用。
首先貼上CG shader程式碼,這一篇主要圍繞這個程式碼進行學習,如下:
Shader "Unlit/TextureUnlitShader" { Properties { _MainTex ("Texture", 2D) = "white" {} } SubShader { Tags { "RenderType"="Opaque" } LOD 100 Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; }; sampler2D _MainTex; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = TRANSFORM_TEX(v.uv, _MainTex); return o; } fixed4 frag (v2f i) : SV_Target { fixed4 col = tex2D(_MainTex, i.uv); return col; } ENDCG } } }
上面的CG shader只完成一個簡單的圖片渲染顯示功能,但是細節很值得學習,接下來我就逐一講解結合unity CG上下文的shader每一句話都是什麼意思。
首先說一下,nvidia是CG的締造者,其他各個三維引擎或者工具都必須按照CG的基本規範去實現自己的CG編譯器以及類似程式設計語法,比如我最愛的unity3d和rendermonkey(我不愛ue4的原因是ue4實在是太慢了,儲存umap慢,編譯構建慢等,之前用ue4的日子旁邊都是放了一本書,凡是ue4在那操作阻塞著,我就看書玩,實在愛不起來)。那麼我們現在用unity寫CG shader,就得按照unity的那一套語法編譯規範寫程式碼了,上面的程式碼就是一個很標準的語法對照了,只要我們完全理解程式碼的每個詞彙,就達到目標了。
so,我們開始咬文嚼字吧:
①.Shader{}包體。
Shader "Unlit/TextureUnlitShader"
{
//do something
}
類似c++或c#的class,將具體的shader程式碼封裝,Shader讓CG編譯器識別這是一個shader程式碼的起始標籤,“Unlit/TextureUnlitShader”就是shader名稱的標識。
②.Properties{}屬性欄位
Properties { _MainTex ("Texture", 2D) = "white" {} }
類似c++的public:修飾字段,如下:
class Texture
{
//這是一個貼圖類
};
class MainTest
{
public:
float cg_float;
Texture* cg_MainTex;
};
這是一段模擬程式碼,和CG程式碼沒有任何關聯,我為什麼用c++/c#作為演示程式碼呢,首先就是我們大概接觸最早的就是c/c++了,因為學校裡面最開始學習就是從c開始,其次我們作為unity開發,c#當然也是很熟悉啦。最後就算我用很詳細的文字講解程式碼意義,有的時候還是不如來一段演示程式碼讓人更能理解其中的含義。
Properties{}就類似Public:,在其中定義我們需要的屬性欄位,可以供外部訪問,比如我們定義一張texture2D。
_MainTex ("Texture", 2D) = "white" {},此時unity CG編譯器在識別出Properties後,緊接著解析其中封裝的屬性欄位,當解析到這段程式碼後,其中程式碼意義分解如下:
①._MainTex標識了該texture2D在shader中的欄位名稱。
②.("Texture", 2D)標識了該texture2D在unity 編輯面板中ui顯示字串"Texture",2D則標識型別為texture2D。
③.= "white" {}標識了該texture2D預設為白色,當然我們可以改成red紅色。
ps:②中我說了unity編輯面板ui顯示,這裡可以通過官網下載的內建著色器中editor中的StandardShaderGUI.cs檔案檢視具體顯示細節。
當然,Properties不僅僅只能定義一個texture2D,還能定義其他的,比如_MainFloat("Float",float) = 0.5,定義一個float值,當然還有其它的,我們以後用到的時候再看。
③.SubShader{}包體
SubShader
{
//do something
}
官方對SubShader的解釋是封裝一系列渲染pass和所有pass通用的標籤和狀態的包體,一個主Shader可以包含多個SubShader,為什麼主Shader可以包含多個SubShader的原因是,前面我們說了Profile的概念,也就是說因為實際GPU硬體種類太多,為了保證該主Shader可以在多種多樣的GPU上執行,所以可能需要寫多個可以執行的SubShader,然後實際runtime執行時按照順序查詢到一個能執行SubShader去執行相應的渲染效果,假如都不能執行,就Fallback "Diffuse",意思就是回滾到最基本的Diffuse渲染,保證至少不出問題。
④.SubShader中標籤和狀態
Tags { "RenderType"="Opaque" }
LOD 100
Cull back
這些標籤和狀態鍵值對,被CG編譯器識別,主要作用是標識後續渲染pass該什麼時候並且怎麼樣被CG runtime渲染。
比如上面三句程式碼含義如下:
①.Tags { "RenderType"="Opaque" } 標識了渲染型別為Opaque,什麼意思呢?就是代表這個渲染型別定義為不透明著色器,unity CG庫提供了多種適用於不同環境的渲染型別,具體的後面會談到。
②.LOD 100,LOD全稱level of detail,,設定一個100是個什麼意思呢?這裡代表一種shader渲染細節技術,假如我們的主Shader包含多個SubShader,每個SubShader的LOD 為不同的值,假如為100、200、300,那麼我們在c#程式碼中通過Shader.globalMaximumLOD = 100;控制全域性LOD最大值,來顯示不同LOD shader的渲染。有興趣的小夥伴可以自己測試下,我就不貼圖了。
③.Cull back,Cull也是渲染剔除的意思,設定back代表背面剔除渲染,那樣的話實際上我們的shader只渲染了一個正面,節省一半的資源,當然我們也可以設定front剔除正面,或者off關閉,直接渲染雙面。
ps:當然還有其他一些不同功能的tag和state,後面具體是用到再講還是直接一次性列出待定,不過這裡我們明白這些tag和state的語法意義就行了。
⑤.Pass{}渲染通道
從這裡開始就是正式開始進入CG渲染了,Pass{}封裝當前SubShader著色器具體的渲染邏輯,我們的頂點函式,片段函式,表面光照函式就是在其中具體實現,下面開始逐一分解每一句程式碼的具體意義:
①.CGPROGRAM開始和ENDCG結束,這個只是CG編譯器的語法規範之一,代表開始編譯或解析CG程式碼到結束,類似#region #endregion程式碼塊。
②.#pragma vertex vert和#pragma fragment frag(當然還包含#pragma surface surf YangLightModel)這裡就是預編譯頂點函式和片段函式(表面光照函式),代表後面的vert和frag為我們要去具體開發實現的頂點和片段函式。
③.#include "UnityCG.cginc"和c包含標頭檔案一樣,這裡引入了UnityCG.cginc這個檔案(這個檔案可以在下載的內建著色器CGIncludes資料夾中找到),因為我們需要用到unity CG runtime給我們提供的GPU硬體資源資料和各種圖形處理函式,都在這個檔案可以看到,這裡我也不貼程式碼了,小夥伴們務必自己開啟看一下。
④.appdata和v2f結構體,如果小夥伴們是按照順序看我的部落格,那麼對這兩個結構體及其包含的語義繫結欄位有詳細的理解。額外要說的另外一個語義TEXCOORD0,前面的TEXCOORD代表了紋理座標,0則代表了第0通道紋理座標,大部分情況下顯示卡支援4套紋理通道給我們開發使用,語義分別是TEXCOORD0,TEXCOORD1,TEXCOORD2,TEXCOORD3,我們可以對紋理座標進行位移操作並儲存到任意0-3的TEXCOORD。SV_POSITION則等價於POSITION,SV_字首代表system value,我查到的解釋是,當使用SV_POSITION繫結vertex頂點函式輸出時,那麼頂點座標在vertex頂點函式return後就被固定了,直接進入光柵處理階段,不可更改。
by the way,順便畫圖描敘一下TEXCOORD作為紋理座標語義輸入時的uv:貼圖座標系,如下圖:
可以認為,小貓貼圖的uv就是從(0,0)到(1,1),(m,n)為其中一個取樣顏色color值,繫結TEXCOORD語義輸入時,float2 uv值就被傳入(0,0)插值到(1,1)。
⑤.sampler2D _MainTex;,這句程式碼要和Properties{}對應,是定義獲取_MainTex這個texture2D物件,當然CG程式碼中型別名為sampler2D,定義好後sampler2D物件後,提供後續函式使用。
⑥.float4 _MainTex_ST;,這句程式碼可能讓小夥伴們迷惑,咱們又沒在Properties{}中定義_MainTex_ST這個外部變數,怎麼突然就蹦出來了這個東西?實際上這是和_MainTex相關的變數,代表了_MainTex的Scale和Translate,這是我從官方看到的解釋,Scale縮放則對應unity編輯器ui面板上的Tiling,Translate位移則對應其Offset,屬於unity CG編譯器為我們提供的texture2D預設附帶的變數欄位。
⑦.vert頂點函式,我直接在程式碼中進行註釋講解 ,如下:
v2f vert (appdata v)
{
v2f o;
//將unity CG底層提供的模型頂點座標源資料變換到裁剪空間
//UnityObjectToClipPos這個cg內建函式相當於mul(UNITY_MATRIX_MVP,v.vertex);
o.vertex = UnityObjectToClipPos(v.vertex);
// Transforms 2D UV by scale/bias property
// #define TRANSFORM_TEX(tex,name) (tex.xy * name##_ST.xy + name##_ST.zw)
//上面是UnityCG.cginc的TRANSFORM_TEX巨集函式定義,實際上使用到了_MainTex_ST欄位,意思就是將_MainTex_ST帶入紋理uv的運算
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}
⑧.frag片段函式,解釋如下:
fixed4 frag (v2f i) : SV_Target
{
//tex2D為取樣函式,具體意思就是根據uv值對一張紋理進行逐點顏色color值獲取,並在片段函式返回並渲染
fixed4 col = tex2D(_MainTex, i.uv);
return col;
}
上面那張小貓圖,其中動點(m,n)進行tex2D取樣的話,就是返回了不同座標下小貓圖的畫素顏色值,並傳遞給col且return渲染。
⑨.Fallback "Diffuse",這就是unity CG編譯器就行的容錯處理了,假如我們的CG shader執行在一個很低端古老的的GPU而顯示不出來,就回滾為Diffuse著色器,反正所有CPU都支援這個。
以上就是對一個unity CG shader非常詳細的講解,目的就是為了大家能輕鬆理解後續的複雜的高階著色器。
最後還要重複羅嗦一句,學習CG shader必須按照圖形流水線上下文來觀察理解,比如流水線頂點到光柵到片段處理順序流程,同時頂點函式和片段函式處理的輸入輸出的GPU硬體資源資料是動態變化的,比如模型頂點源座標和紋理座標等動態變化的資料,我們心裡要有這些概念才能學好CG shader。
so,接下來繼續深入。