iT邦幫忙

2025 iThome 鐵人賽

DAY 4
0
Software Development

渲染與GPU編程系列 第 4

Day 3|Shader 是什麼?GPU 編程的靈魂

  • 分享至 

  • xImage
  •  

把它想成:在顯示卡上跑的迷你程式。每個頂點、每個像素、甚至每個小工作(工作群組)都同時跑一份,負責把場景「算成顏色」。
你不需要數學很好就能入門——先把它當作「一段會被重複執行很多次的小程式」。


1. 為什麼需要 Shader?(把同一件事做上百萬次)

螢幕是由像素組成的。以 1920×1080 解析度來說,大約有 207 萬個小點。如果一張畫面要有光照、陰影、金屬粗糙感、貼圖花紋,那表示:

  • 每個像素都要做一串相似的計算。
  • 每個頂點(模型的角點)也要被轉換到正確位置。

CPU(中央處理器)內核心不多、擅長複雜決策;GPU(顯示卡)內有大量小核心,擅長把同樣的計算同時做很多次。而 Shader 就是寫給 GPU 執行的這種「重複計算」的程式。

小圖:一段程式被同時跑很多份

你的 Shader 程式:  [程式碼]
同時被套用在:   像素#1 像素#2 像素#3 ... 

2. Shader 在哪裡?(放在渲染管線裡的「可程式」區)

簡化的渲染管線(Day 2 你看過):

CPU 準備資料 → [頂點處理 VS] → [把三角形切成像素 Raster]
                      │
                      → [像素著色 FS]
                            ↓
                       深度/混色 → 畫面

中括號的 VS(Vertex Shader)和 FS(Fragment/Pixel Shader)就是你能寫程式的地方。還有一些額外的類型(等一下介紹)。


3. Shader 有哪些種類?(先懂常用三種)

  1. 頂點著色器 Vertex Shader(VS)

    • 每個「頂點」各跑一次。
    • 把模型位置轉到相機視角、計算要傳給像素階段的資訊(例如 UV、法線)。
    • 你可以想成「把 3D 點放到正確位置」。
  2. 片段/像素著色器 Fragment/Pixel Shader(FS/PS)

    • 每個「被三角形覆蓋到的像素」各跑一次。
    • 計算這個像素的最終顏色(取貼圖、打光、陰影、金屬/粗糙度等)。
    • 你可以想成「把顏色算出來」。
  3. 運算著色器 Compute Shader(CS)

    • 不一定跟畫面有關,像在顯卡上開一個小工廠。
    • 做影像處理、物理模擬、資料排序、粒子更新、後處理特效等。
    • 由你決定每次要開多少「工作」(threads),如何讀寫緩衝/貼圖。

進階還有:Tessellation(曲面細分)、Geometry(幾何增生)、Mesh/Task(更彈性的幾何生成)、以及光線追蹤用的 Ray Generation / Miss / Closest Hit 等。先把 VS / FS / CS 用熟最實在。


4. Shader 長什麼樣?(像 C 語言的迷你版本)

常見方言:

  • GLSL(OpenGL / Vulkan)
  • HLSL(Direct3D;Unity 也多用這家族)
  • WGSL(WebGPU)
  • MSL(Apple Metal)

它們都長得像 C 語言:有函式、有型別(float, vec3)、有輸入/輸出、可以讀貼圖。


5. 你的第一支頂點/像素 Shader(看得懂就會改)

先不用緊張,之後會在Unity實作。

頂點著色器(GLSL 風格,說明版)

// 輸入:每個頂點的屬性(位置、法線、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;          // 丟到投影空間(交給後面切像素)
}

片段/像素著色器(GLSL 風格,說明版)

// 接 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 物件畫到螢幕上,而且會有基本亮/暗變化。


6. Shader 跟資料怎麼連?(輸入/輸出/資源綁定)

別被名詞嚇到,先抓核心:

  • 頂點屬性aPos / aNormal / aUV,一個頂點一份。
  • Uniform / Push Constant:小而常改的參數,例如矩陣、相機位置。
  • Texture(貼圖)+ Sampler:從圖片取顏色,Samper 決定放大縮小怎麼取(模糊 or 銳利)。
  • Varying(VS → FS):在三角形內自動插值的通道(像把三個角的 UV 混合到中間)。

小圖:資料怎麼過關

[頂點資料/貼圖/參數]
        │ (CPU 綁定好)
        ↓
      VS → (插值) → FS → 螢幕

7. Compute Shader:不一定要畫圖,也能用 GPU 幫忙算

如果你想做影像模糊、粒子更新、物理模擬、甚至資料排序,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 差不多,只是用途更自由。


8. 新手最常見的 8 個坑(和解法)

  1. 畫面上下顛倒

    • 原因:不同 API 的座標原點不同。
    • 解法:在頂點或像素階段把 y 取負或調整投影矩陣;Web 與 Vulkan/D3D 習慣不同要特別留意。
  2. 顏色抖動或馬賽克

    • 原因:沒啟用 Mipmap、貼圖採樣方式不當。
    • 解法:启用 Mipmaps,遠處自動選用低解析版本;選 LINEAR 濾波。
  3. 表面陰影方向怪

    • 原因:法線沒轉對座標系,或模型有非等比縮放。
    • 解法:法線用 inverse(transpose(M)) 處理,或在建模階段避免非等比縮放。
  4. 透明物件亂序

    • 原因:半透明顏色必須與背景按順序混合。
    • 解法:由遠到近排序繪製、或使用加權混合等技巧(OIT)。
  5. 效能突降(像素太貴)

    • 原因:解析度高、貼圖取樣多、分支太多。
    • 解法:先降解析度試、減少取樣次數、用 mix/step 取代 if(降低分歧)。
  6. 鋸齒明顯

    • 原因:邊緣只採一個樣本。
    • 解法:開 MSAA、TAA 或在後處理抗鋸齒。
  7. Early-Z 失效

    • 原因:像素階段 discard 或寫入深度,讓 GPU 不能先過濾。
    • 解法:能不丟棄就別丟;或改雙通道做法、alpha-to-coverage。
  8. 顏色偏灰或過亮

    • 原因:Gamma/色彩空間處理不一致。
    • 解法:確保貼圖是 sRGB 輸入,最終輸出做正確的 gamma 變換。

9. 初學者的效能心法(不用看數學也能提升)

  • 少分支:同一群像素若一半走 if 一半走 else,顯卡會分兩段跑(叫「分歧」),吞吐下降。能用 lerp/mix(線性插值)代替就代替。
  • 少取樣:每多一次貼圖取樣就多一次記憶體訪問;能合圖(Texture Atlas)或使用壓縮與 Mipmaps 就用。
  • 重用計算:相同的中間結果存成變數,不要每次重算。
  • 小心暫存器:變數太多會讓硬體無法同時裝更多工作(降低並行度),盡量精簡。
  • 先看瓶頸:把解析度從 4K 降到 1080p,如果速度大幅提升,多半是像素階段太重。

10. 三個「立刻可做」的小實驗(你會立刻有感)

  1. 顏色漸層:在像素著色器用 UV 畫出 vec3(vUV, 0.0) 的彩色漸層,學會「UV 就是坐標」。
  2. 光照 on/off:用 if(開關) 控制是否做光照,體會「算越多越慢」。然後把 if 改成 mix,感受分歧 vs 插值的差別。
  3. Mipmap 效果:關掉 Mipmap 看遠處紋理的抖動,再打開 Mipmap,比對差異。

11. 我該學哪一種語言?(選你現在的目標)

  • 要做瀏覽器WGSL(WebGPU)
  • 要做 Windows / Xbox 等 Direct3DHLSL
  • 要做 Vulkan / OpenGLGLSL(或用工具把 HLSL 轉 GLSL)
  • 要做 Apple / iOS / macOSMSL(Metal)
  • Unity → 語法近 HLSL,外面包一層 ShaderLab

先挑一條路,把 VS/FS 跑起來,心智模型是通用的,換語言只是在換方言。


12. 一句話總結

Shader = 在 GPU 上大量重複執行的小程式
VS 決定「點在哪裡」、FS 決定「像素什麼顏色」、CS 讓你把 GPU 當平行計算工廠。把資料(頂點、貼圖、參數)餵好、把分支與取樣控制好,你就能畫出既漂亮又流暢的畫面。


明日預告

我們要談 GPU 記憶體架構與資料流:如何有效管理資源——弄懂顯存、快取、貼圖壓縮、Mipmap、Uniform/Storage/Push 常數的分工,以及「什麼放哪裡」最好。


上一篇
Day 2|渲染管線簡介:從「頂點」到「像素」的旅程
下一篇
Day 4|GPU 記憶體架構與資料流:如何有效管理資源
系列文
渲染與GPU編程7
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言