2.1:CGPROGRAM
前言
經過前面兩個章節的鋪墊,我們對渲染以及Unity Shaderlab相關的知識已經有了大概的認識,接下來將要學習的就是Shader最重要的部分, SL(Shader Language) ,著色器語言。目前主流的著色器語言有HLSL,GLSL,Cg。三者在語法上也有諸多共通之處,選擇一種學習即可。而在Unity中,主流是選擇Cg作為著色器語言。在Shader編寫的過程中,我們會經常穿梭在各個空間中,這裡不對3D數學部分的前置知識作介紹,相關知識可從前面章節推薦的書籍學習。
在Shaderlab中,有三種著色器的書寫方式。一種是Fixed-Function Shader,固定管線著色器。在這個著色器中,我們只能對渲染進行少量的配置,效果也很有限,在Unity 5.x之後的版本,Unity棄用了這種著色器。第二種是Surface Shader,表面著色器,這是Unity為我們提供的一種便於書寫的方式,我們可以通過少量的程式碼,控制光照陰影等繁複的細節由Unity幫我們處理。新建一個Standard Surface Shader,可以看到裡面只有50餘行程式碼,但它包含了所有基礎實現。最後一種,是Vertex/Fragment Shader,頂點/片元著色器,這是實現各種天馬行空想象的最佳場所,當然,它的程式碼量以及複雜度也是最高的。而前兩種shader也會被編譯成對應的Vertex/Fragment Shader。這三種書寫方式,都是在.shader檔案中進行,組織方式上也是極為相似的。
這個系列的重點是Vertex/Fragment Shader。
CGPROGRAM
以之前模板的程式碼作為例子:
Shader "Blog/Start" { Properties { _MainTex ("Base (RGB)", 2D) = "white" {} _Color ("Color", Color) = (1, 1, 1, 1) } SubShader { CGINCLUDE #include "UnityCG.cginc" sampler2D _MainTex; float4 _MainTex_ST; fixed4 _Color; struct a2v { float4 vertex : POSITION; float2 texcoord : TEXCOORD0; }; struct v2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; }; v2f vert(a2v v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.uv = TRANSFORM_TEX(v.texcoord, _MainTex); return o; } fixed4 frag(v2f i) : SV_Target { fixed4 color; color = tex2D(_MainTex, i.uv); color *= _Color; return color; } ENDCG Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag ENDCG } } FallBack "Diffuse" }
在這個Shader中,出現了兩個不同的程式碼塊。首先第一個是CGINCLUDE程式碼塊,它可以被放置在任何位置,甚至是整個Shader程式碼塊的外部。在這個程式碼塊中,我們可以編寫那些需要重用的程式碼(如頂點著色器或片元著色器)。然後是CGPROGRAM程式碼塊。這個程式碼塊需要放在Pass塊內,否則編譯器會把這個Shader當成Surface Shader轉而去檢索 surf()
函式進而引起報錯。這個程式碼塊也是定義Vertex/Fragment Shader的地方。要保證,每個Pass都有且只有一個Vertex Shader和Fragment Shader。這兩個Shader通過 #pragma
編譯命令指定。接著是兩個結構體:
struct a2v { float4 vertex : POSITION; float2 texcoord : TEXCOORD0; }; struct v2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; };
第一個是頂點著色器的輸入結構體,a2v即Application To Vertex(應用階段到頂點著色),在每一個變數的後面都跟了一個冒號說明,冒號後的是這個變數的 Semantic(語義) ,語義是和GPU通訊的橋樑,告訴GPU在這個變數中填充什麼資料。 float4 vertex : POSITION;
告訴GPU,把頂點資料的POSITION(模型空間下的頂點座標)輸入到vertex變數中, float2 texcoord : TEXCOORD0;
的意思是,把紋理座標集0給texcoord變數使用。在著色器之間的資料傳遞都是藝考語義實現的,使用結構體只是為了程式碼組織更有條理。
v2f即Vertex To Fragment,這是頂點著色器的輸出結構體,也是片元著色器的輸入結構體。 float4 pos : SV_POSITION;
:SV指System Value,帶有SV字首的語義在管線中都有特殊的含義,SV_POSITION的含義是裁剪空間下的座標。
為什麼輸出裁剪空間下的頂點座標?
因為這個座標接下來用於片元著色器,片元著色器需要的是光柵化後的座標,也就是裁剪空間的座標。
然後是頂點著色器部分:
v2f vert(a2v v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.uv = TRANSFORM_TEX(v.texcoord, _MainTex); return o; }
這裡只是做裡簡單的空間變換以及紋理對映。既然需要的是裁剪空間的座標,那直接把輸入的頂點座標變換到裁剪空間即可。 UnityObjectToClipPos()
是Unity為我們提供的座標空間轉換函式。
// Tranforms position from object to homogenous space inline float4 UnityObjectToClipPos(in float3 pos) { #if defined(STEREO_CUBEMAP_RENDER_ON) return UnityObjectToClipPosODS(pos); #else // More efficient than computing M*VP matrix product return mul(UNITY_MATRIX_VP, mul(unity_ObjectToWorld, float4(pos, 1.0))); #endif } inline float4 UnityObjectToClipPos(float4 pos) // overload for float4; avoids "implicit truncation" warning for existing shaders { return UnityObjectToClipPos(pos.xyz); }
可以看到,這個函式處理了一些差別並重載了兩個版本,但本質上,都是MVP矩陣右乘頂點座標的列向量形式。然後是 TRANSFORM_TEX
巨集。
// Transforms 2D UV by scale/bias property #define TRANSFORM_TEX(tex,name) (tex.xy * name##_ST.xy + name##_ST.zw)
可以看到這個巨集計算了頂點對應的紋理取樣位置,計算方式也對應了我們之前說到的_ST(Scale & Tiling,紋理縮放和偏移)相關知識點。然後是片元著色器部分:
fixed4 frag(v2f i) : SV_Target { fixed4 color; color = tex2D(_MainTex, i.uv); color *= _Color; return color; }
片元著色器的最終目的是確定片元的畫素顏色,即一個RGBA值。首先注意到的是SV_Target,它的語義是:這個著色器只返回一個值,這個值也就是片元的畫素顏色值。此外,片元著色器還可以返回多個顏色,這時我們需要用到 SV_TargetN
語義,在這個情境下,SV_Target0是對應片元的畫素顏色。例:
struct frag_output { fixed4 color0 : SV_Target0; fixed4 color1 : SV_Target1; fixed4 color2 : SV_Target2; } frag_output frag(v2f i) { frag_output output; // ... return output; }
回到模板的片元著色器內部,首先是 color = tex2D(_MainTex, i.uv);
, tex2D(sampler2D texture, float2 uv);
是Cg為我們提供的一個紋理取樣函式,它將按照輸入的uv取樣輸入的紋理texture,最後返回取樣顏色。然後是 color *= _Color;
這裡只是簡單的把取樣顏色和shader外部給定的顏色做乘法處理(疊加)。在場景中新建一個sphere,把這個shader新增到一個新的material,再把這個material掛到sphere上,不出意外沒有報錯的話,即可得到一個純白色的球(由於沒有任何光照陰影計算,也沒有給紋理賦值)。這是我們第一個生效的shader。