Shader學習筆記(二):Vertex/Fragment Shader
阿新 • • 發佈:2019-01-01
這篇文章討論如何寫頂點、片元著色器(Vertex/Fragment Shader)。
概念解釋
先看一個完整例子,關鍵地方我做了標記。先熟悉大致結構,後面我會詳細解釋:
Shader "Unlit/NewUnlitShader"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
// CG程式起始位置
CGPROGRAM
// 編譯指令:表示有頂點著色器
#pragma vertex vert
// 編譯指令:表示有片元著色器
#pragma fragment frag
#pragma multi_compile_fog
#include "UnityCG.cginc"
// 自定義的結構
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
// 自定義的結構
struct v2f
{
float2 uv : TEXCOORD0;
UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
// 頂點著色器函式
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}
// 片元著色器函式
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}
關鍵概念的理解
- 可程式設計渲染管線:與固定渲染管線不同,可以讓程式設計師自己控制渲染管線的一部分,分為Vertex Shader和Fragment Shader。
- Vertex Shader:作用於每個頂點,通常是處理從世界空間到裁剪空間(螢幕座標)的座標轉換,後面緊接的是光柵化。
- Fragment Shader:作用於每個螢幕上的片元(這裡可近似理解為畫素),通常是計算顏色。
- ShaderLab:Unity專有的shader語言,Surface Shader全部由ShaderLab實現,Vertex/Fragment是由ShaderrLab內嵌CG/HLSL實現。
- Shading Language: 三種主流shader語言,均可以在ShaderLab中巢狀使用,官方推薦使用CG/HLSL(這兩種語言由Microsoft和Nvidia達成一致,所以基本等價)。詳見下面列表:
名字 | 全名 | 開發商 | 巢狀關鍵字 |
---|---|---|---|
CG | C for Graphic | Nvidia | CGPROGRAM |
HLSL | High Level Shading Language | Microsoft | CGPROGRAM |
GLSL | OpenGL Shading Language | OpenGL | GLSLPROGRAM |
DirectX在Windows和遊戲領域稱霸,OpenGL則在移動領域勝出。
關於兩者孰優孰劣的討論數不勝數。
這裡總結了DirectX在Windows平臺勝出的一些原因:
1. DirectX更早推出可程式設計渲染管線
2. OpenGL成員太多,新標準推進緩慢
3. DirectX不僅是圖形庫,還包括聲音、網路等一套遊戲開發解決方案
4. DirectX不向後相容,OpenGL為相容性保留了很多過時的設計
Vertex/Fragment語法
- SubShader:針對不同的硬體做不同的處理,執行時依次掃描,都失敗則FallBack。
- Pass:一個SubShader中包含1到多個Pass,通常只需一個Pass。多個Pass可用於定義多渲染路徑(Rendering Path),執行時選擇執行哪個,常見於自定義光照模型中陰影處理。
- Shader Semantics:如
float4 vertex : POSITION
,冒號後面是這個變數的語義,與GPU互動時用到。 - Vertex Shader的輸入輸出
- 輸入:可以是以下三種:
- 由基本語義定義的變數:
POSITION
,NORMAL
,TEXCOORD0
,TEXCOORD1
, …,TANGENT
,COLOR
。 - 內建的結構:
appdata_base
,appdata_tan
,appdata_full
。 - 自定義的結構。
- 由基本語義定義的變數:
- 輸出:固定有
SV_POSITION
,其他需要用到的varying變數。語義大部分情況下不重要,用TEXCOORD0
就好。
- 輸入:可以是以下三種:
- Fragment Shader的輸入輸出
- 輸入:同Vertex Shader的輸出。
- 輸出:
SV_Target
,通常是單個RGBA值。
例項解析
下面看幾個Unity官方manual中的例項,加深理解:
- SimpleUnlitTextureShader:簡單的紋理貼圖,變換頂點座標,按uv找貼圖上的顏色。
Shader "Unlit/SimpleUnlitTexturedShader"
{
Properties
{
// we have removed support for texture tiling/offset,
// so make them not be displayed in material inspector
[NoScaleOffset] _MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Pass
{
CGPROGRAM
// use "vert" function as the vertex shader
#pragma vertex vert
// use "frag" function as the pixel (fragment) shader
#pragma fragment frag
// vertex shader inputs
struct appdata
{
float4 vertex : POSITION; // vertex position
float2 uv : TEXCOORD0; // texture coordinate
};
// vertex shader outputs ("vertex to fragment")
struct v2f
{
float2 uv : TEXCOORD0; // texture coordinate
float4 vertex : SV_POSITION; // clip space position
};
// vertex shader
v2f vert (appdata v)
{
v2f o;
// transform position to clip space
// (multiply with model*view*projection matrix)
o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
// just pass the texture coordinate
o.uv = v.uv;
return o;
}
// texture we will sample
sampler2D _MainTex;
// pixel shader; returns low precision ("fixed4" type)
// color ("SV_Target" semantic)
fixed4 frag (v2f i) : SV_Target
{
// sample texture and return it
fixed4 col = tex2D(_MainTex, i.uv);
return col;
}
ENDCG
}
}
}
- SingleColor:給模型賦予單色。vertex只傳出SV_POSITION,fragment不傳參。CG中直接引用了ShaderLab的Properties引數_Color(ShaderLab和CG的型別存在對應關係)。
Shader "Unlit/SingleColor"
{
Properties
{
// Color property for material inspector, default to white
_Color ("Main Color", Color) = (1,1,1,1)
}
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// vertex shader
// this time instead of using "appdata" struct, just spell inputs manually,
// and instead of returning v2f struct, also just return a single output
// float4 clip position
float4 vert (float4 vertex : POSITION) : SV_POSITION
{
return mul(UNITY_MATRIX_MVP, vertex);
}
// color from the material
fixed4 _Color;
// pixel shader, no inputs needed
fixed4 frag () : SV_Target
{
return _Color; // just return it
}
ENDCG
}
}
}
- WorldSpaceNormal:根據法向量來變色。worldNormal作為varying變數傳給fragment。
Shader "Unlit/WorldSpaceNormals"
{
// no Properties block this time!
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// include file that contains UnityObjectToWorldNormal helper function
#include "UnityCG.cginc"
struct v2f {
// we'll output world space normal as one of regular ("texcoord") interpolators
half3 worldNormal : TEXCOORD0;
float4 pos : SV_POSITION;
};
// vertex shader: takes object space normal as input too
v2f vert (float4 vertex : POSITION, float3 normal : NORMAL)
{
v2f o;
o.pos = UnityObjectToClipPos(vertex);
// UnityCG.cginc file contains function to transform
// normal from object to world space, use that
o.worldNormal = UnityObjectToWorldNormal(normal);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed4 c = 0;
// normal is a 3D vector with xyz components; in -1..1
// range. To display it as color, bring the range into 0..1
// and put into red, green, blue components
c.rgb = i.worldNormal*0.5+0.5;
return c;
}
ENDCG
}
}
}
- SkyReflection:反射環境。對比由Surface Shader的實現,可發現簡潔很多。float4表示點,float3表示向量。
Shader "Unlit/SkyReflection"
{
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct v2f {
half3 worldRefl : TEXCOORD0;
float4 pos : SV_POSITION;
};
v2f vert (float4 vertex : POSITION, float3 normal : NORMAL)
{
v2f o;
o.pos = UnityObjectToClipPos(vertex);
// compute world space position of the vertex
float3 worldPos = mul(_Object2World, vertex).xyz;
// compute world space view direction
float3 worldViewDir = normalize(UnityWorldSpaceViewDir(worldPos));
// world space normal
float3 worldNormal = UnityObjectToWorldNormal(normal);
// world space reflection vector
o.worldRefl = reflect(-worldViewDir, worldNormal);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// sample the default reflection cubemap, using the reflection vector
half4 skyData = UNITY_SAMPLE_TEXCUBE(unity_SpecCube0, i.worldRefl);
// decode cubemap data into actual color
half3 skyColor = DecodeHDR (skyData, unity_SpecCube0_HDR);
// output it!
fixed4 c = 0;
c.rgb = skyColor;
return c;
}
ENDCG
}
}
}
再談與Surface Shader的關係
上一篇說到Surface Shader是簡化版的shader編寫工具,通過封裝減少了程式設計師的重複工作量。
那麼Surface Shader是如何編譯成Vertex/Fragment Shader的呢?它們三者的先後關係又是怎樣?
我的理解是:Surface Shader的surf函式位於fragment shader的起始階段,編譯後生成的是Fragment Shader。這點可用Unity自帶的shader inspector工具檢視。具體可參見知乎上的討論,說得很詳細了,這裡不再展開。