把它想成:在顯示卡上跑的迷你程式。每個頂點、每個像素、甚至每個小工作(工作群組)都同時跑一份,負責把場景「算成顏色」。
你不需要數學很好就能入門——先把它當作「一段會被重複執行很多次的小程式」。
螢幕是由像素組成的。以 1920×1080 解析度來說,大約有 207 萬個小點。如果一張畫面要有光照、陰影、金屬粗糙感、貼圖花紋,那表示:
CPU(中央處理器)內核心不多、擅長複雜決策;GPU(顯示卡)內有大量小核心,擅長把同樣的計算同時做很多次。而 Shader 就是寫給 GPU 執行的這種「重複計算」的程式。
小圖:一段程式被同時跑很多份
你的 Shader 程式: [程式碼]
同時被套用在: 像素#1 像素#2 像素#3 ...
簡化的渲染管線(Day 2 你看過):
CPU 準備資料 → [頂點處理 VS] → [把三角形切成像素 Raster]
│
→ [像素著色 FS]
↓
深度/混色 → 畫面
中括號的 VS(Vertex Shader)和 FS(Fragment/Pixel Shader)就是你能寫程式的地方。還有一些額外的類型(等一下介紹)。
頂點著色器 Vertex Shader(VS)
片段/像素著色器 Fragment/Pixel Shader(FS/PS)
運算著色器 Compute Shader(CS)
進階還有:Tessellation(曲面細分)、Geometry(幾何增生)、Mesh/Task(更彈性的幾何生成)、以及光線追蹤用的 Ray Generation / Miss / Closest Hit 等。先把 VS / FS / CS 用熟最實在。
常見方言:
它們都長得像 C 語言:有函式、有型別(float
, vec3
)、有輸入/輸出、可以讀貼圖。
先不用緊張,之後會在Unity實作。
// 輸入:每個頂點的屬性(位置、法線、UV)
layout(location=0) in vec3 aPos;
layout(location=1) in vec3 aNormal;
layout(location=2) in vec2 aUV;
// 常數:模型(M)、相機視圖(V)、投影(P)矩陣
layout(set=0, binding=0) uniform Matrices {
mat4 M, V, P;
};
// 要傳下去給像素著色器用的資料(會被插值)
layout(location=0) out vec2 vUV;
layout(location=1) out vec3 vNormal_ws;
layout(location=2) out vec3 vPos_ws;
void main() {
vec4 world = M * vec4(aPos, 1.0); // 把頂點放到世界座標
vPos_ws = world.xyz;
vNormal_ws = mat3(M) * aNormal; // 法線也要轉到世界
vUV = aUV; // UV 往下傳
gl_Position = P * V * world; // 丟到投影空間(交給後面切像素)
}
// 接 VS 傳來、已被插值後的資料
layout(location=0) in vec2 vUV;
layout(location=1) in vec3 vNormal_ws;
layout(location=2) in vec3 vPos_ws;
// 材質與一些參數
layout(set=1, binding=0) uniform sampler2D uBaseColor;
layout(push_constant) uniform Params {
vec3 camPos;
vec3 lightPos;
} pc;
// 輸出:畫到螢幕的顏色
layout(location=0) out vec4 outColor;
void main() {
vec3 N = normalize(vNormal_ws);
vec3 V = normalize(pc.camPos - vPos_ws);
vec3 L = normalize(pc.lightPos - vPos_ws);
// 超簡化的打光:Lambert 漫反射 + 基礎貼圖
float NdotL = max(dot(N, L), 0.0);
vec3 albedo = texture(uBaseColor, vUV).rgb;
vec3 color = albedo * (0.1 + 0.9 * NdotL); // 0.1 環境光 + 0.9 漫反射
outColor = vec4(color, 1.0);
}
這兩支加起來就能把一個有貼圖的 3D 物件畫到螢幕上,而且會有基本亮/暗變化。
別被名詞嚇到,先抓核心:
aPos / aNormal / aUV
,一個頂點一份。小圖:資料怎麼過關
[頂點資料/貼圖/參數]
│ (CPU 綁定好)
↓
VS → (插值) → FS → 螢幕
如果你想做影像模糊、粒子更新、物理模擬、甚至資料排序,Compute Shader就像開一條獨立產線。你定義「工作群組大小」(一次開幾個小工),把輸入資料(影像或緩衝)丟給它,讓它算完再交回來。
超短例子:把影像亮度調暗(概念邏輯)
// 以每像素為單位工作
layout(local_size_x=16, local_size_y=16) in;
layout(rgba8, set=0,binding=0) uniform readonly image2D src;
layout(rgba8, set=0,binding=1) uniform writeonly image2D dst;
void main(){
ivec2 uv = ivec2(gl_GlobalInvocationID.xy);
vec4 c = imageLoad(src, uv);
c.rgb *= 0.7; // 降低亮度
imageStore(dst, uv, c);
}
你會發現:語法跟 VS/FS 差不多,只是用途更自由。
畫面上下顛倒
y
取負或調整投影矩陣;Web 與 Vulkan/D3D 習慣不同要特別留意。顏色抖動或馬賽克
LINEAR
濾波。表面陰影方向怪
inverse(transpose(M))
處理,或在建模階段避免非等比縮放。透明物件亂序
效能突降(像素太貴)
mix/step
取代 if
(降低分歧)。鋸齒明顯
Early-Z 失效
discard
或寫入深度,讓 GPU 不能先過濾。顏色偏灰或過亮
lerp/mix
(線性插值)代替就代替。vec3(vUV, 0.0)
的彩色漸層,學會「UV 就是坐標」。if(開關)
控制是否做光照,體會「算越多越慢」。然後把 if
改成 mix
,感受分歧 vs 插值的差別。先挑一條路,把 VS/FS 跑起來,心智模型是通用的,換語言只是在換方言。
Shader = 在 GPU 上大量重複執行的小程式。
VS 決定「點在哪裡」、FS 決定「像素什麼顏色」、CS 讓你把 GPU 當平行計算工廠。把資料(頂點、貼圖、參數)餵好、把分支與取樣控制好,你就能畫出既漂亮又流暢的畫面。
我們要談 GPU 記憶體架構與資料流:如何有效管理資源——弄懂顯存、快取、貼圖壓縮、Mipmap、Uniform/Storage/Push 常數的分工,以及「什麼放哪裡」最好。