前言

身為一個偶爾寫 shader 的後端工程師,我對 GitHub Copilot 在 GLSL 上的表現一直很好奇。Shader 跟一般的應用程式碼很不一樣 — 它是高度數學化的、平行運算的、而且 debug 困難到令人崩潰。AI 在這個領域到底能幫多少忙?

過去半年我有意識地在寫 shader 時開著 Copilot,這篇文章是我的完整體驗報告。結論先說:Copilot 在 shader 開發中有用,但它的極限比在一般程式語言中更明顯。

Copilot 在 GLSL 中的基本表現

好的方面:自動補全常見模式

Copilot 對常見的 shader 模式記得很清楚。比如你寫一個 SDF(Signed Distance Function),它幾乎能猜到你要寫什麼:

// 我打了 "float sdSphere",Copilot 自動補全:
float sdSphere(vec3 p, float r) {
    return length(p) - r;
}

// 我打了 "float sdBox",Copilot 自動補全: float sdBox(vec3 p, vec3 b) { vec3 q = abs(p) - b; return length(max(q, 0.0)) + min(max(q.x, max(q.y, q.z)), 0.0); }

// 我打了 "float sdTorus",Copilot 自動補全: float sdTorus(vec3 p, vec2 t) { vec2 q = vec2(length(p.xz) - t.x, p.y); return length(q) - t.y; }

這些都是 iquilezles.org 上的經典公式,Copilot 顯然有訓練過這些內容。精確度很高,我幾乎不需要修改。

好的方面:Noise 函數

各種 noise 函數是 shader 中最常用但又很煩人要手打的東西。Copilot 在這方面很可靠:

// 打了 "vec2 hash22",Copilot 給出:
vec2 hash22(vec2 p) {
    p = vec2(dot(p, vec2(127.1, 311.7)),
             dot(p, vec2(269.5, 183.3)));
    return -1.0 + 2.0  fract(sin(p)  43758.5453123);
}

// 打了 "float perlinNoise",Copilot 給出一個完整的實作: float perlinNoise(vec2 p) { vec2 i = floor(p); vec2 f = fract(p);

// quintic interpolation vec2 u = f f f (f (f * 6.0 - 15.0) + 10.0);

float a = dot(hash22(i + vec2(0.0, 0.0)), f - vec2(0.0, 0.0)); float b = dot(hash22(i + vec2(1.0, 0.0)), f - vec2(1.0, 0.0)); float c = dot(hash22(i + vec2(0.0, 1.0)), f - vec2(0.0, 1.0)); float d = dot(hash22(i + vec2(1.0, 1.0)), f - vec2(1.0, 1.0));

return mix(mix(a, b, u.x), mix(c, d, u.x), u.y); }

好的方面:通用 utility 函數

// 旋轉矩陣 — Copilot 秒給
mat2 rot2D(float a) {
    float c = cos(a), s = sin(a);
    return mat2(c, -s, s, c);
}

// 色彩空間轉換 vec3 rgb2hsv(vec3 c) { vec4 K = vec4(0.0, -1.0/3.0, 2.0/3.0, -1.0); vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g)); vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r)); float d = q.x - min(q.w, q.y); float e = 1.0e-10; return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x); }

// Smooth minimum(SDF 混合用) float smin(float a, float b, float k) { float h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0); return mix(b, a, h) - k h (1.0 - h); }

不好用的場景

複雜的 Raymarching 邏輯

當你開始寫比較複雜的 raymarcher,Copilot 就開始掙扎了:

// 我想寫一個帶有自適應步長和 over-relaxation 的 raymarcher
// Copilot 給的版本:
float rayMarch(vec3 ro, vec3 rd) {
    float t = 0.0;
    for (int i = 0; i < 100; i++) {
        vec3 p = ro + rd * t;
        float d = map(p);
        if (d < 0.001) break;
        t += d;
        if (t > 100.0) break;
    }
    return t;
}

// 但我實際需要的是帶 over-relaxation 的版本(需要自己寫): float rayMarch(vec3 ro, vec3 rd) { float t = 0.0; float omega = 1.2; // over-relaxation 因子 float prev_d = 1e10; float step_size = 0.0;

for (int i = 0; i < 128; i++) { vec3 p = ro + rd * t; float d = map(p);

// 如果 over-relaxation 跳過頭了,退回去 if (d < 0.0 && step_size > 0.0) { t -= step_size; omega = 1.0; // 退回保守步進 continue; }

if (d < 0.0001 * t) break;

// over-relaxation: 步長乘以 omega step_size = d * omega; t += step_size; prev_d = d;

if (t > 200.0) break; } return t; }

Copilot 給的是「教科書版本」,但在實際專案中你幾乎都需要更進階的變體。

自訂的數學推導

這是 Copilot 最弱的地方。當你在實作一個特定的數學效果時,Copilot 往往猜不到你要什麼:

// 我想實作一個根據視角動態調整的 subsurface scattering 近似
// Copilot 完全不知道我在幹嘛,只會給一些通用的 lighting 程式碼

// 最後我自己寫的: vec3 subsurfaceScatter(vec3 p, vec3 n, vec3 rd, vec3 lightDir) { // 光線穿透厚度估算 float thickness = 0.0; for (int i = 0; i < 8; i++) { float fi = float(i) / 8.0; float dist = map(p - n fi 0.5); thickness += max(0.0, -dist); } thickness /= 8.0;

// 穿透光計算 float transmittance = exp(-thickness * 8.0); float forwardScatter = pow(max(dot(rd, lightDir), 0.0), 12.0); float backScatter = max(dot(n, lightDir), 0.0) * 0.3;

vec3 scatterColor = vec3(1.0, 0.2, 0.1); // 皮膚色調 return scatterColor (forwardScatter + backScatter) transmittance; }

效能優化

Shader 的效能優化是非常 low-level 的,Copilot 在這方面幫不上太多忙:

// Copilot 不會幫你做這種優化:

// 優化前(Copilot 可能給的) float fbm(vec2 p) { float f = 0.0; f += 0.5 perlinNoise(p); p = 2.01; f += 0.25 perlinNoise(p); p = 2.01; f += 0.125 perlinNoise(p); p = 2.01; f += 0.0625 * perlinNoise(p); return f; }

// 優化後(需要根據場景手動調整) float fbm(vec2 p) { float f = 0.0; float amp = 0.5; float freq = 1.0;

// 根據螢幕空間的像素大小決定要算幾層 // 這可以避免 aliasing 又省效能 float pixel_size = fwidth(p.x); int octaves = int(clamp(4.0 - log2(pixel_size * freq), 1.0, 4.0));

for (int i = 0; i < 4; i++) { if (i >= octaves) break; f += amp perlinNoise(p freq); amp *= 0.5; freq *= 2.01; } return f; }

與手寫的比較

| 面向 | Copilot 輔助 | 純手寫 |
|——|————-|——–|
| 常見 SDF 函數 | 快 5-10 倍 | 需要查資料 |
| Noise 函數 | 快 10 倍 | 手打容易打錯 |
| 簡單 lighting | 快 3 倍 | 但 Copilot 版本通常夠用 |
| 複雜特效 | 幾乎沒幫助 | 還是要自己推導 |
| 效能優化 | 幾乎沒幫助 | 需要 profiling + 經驗 |
| Debug | 完全沒幫助 | shader debug 還是地獄 |

我的工作流建議

開 Copilot 的好時機

  1. 專案初始階段:建立 utility 函數庫(SDF、noise、色彩轉換等)
  2. 寫 boilerplate:vertex shader、基本的 fragment shader 框架
  3. 常見效果的第一版:bloom、blur、tone mapping 等

關 Copilot 的好時機

  1. 精密的數學推導:Copilot 的建議會打斷你的思路
  2. 效能 profiling:你需要專注在數字上,不是 autocomplete
  3. Debug:shader 的 debug 靠的是視覺觀察,不是程式碼建議

搭配使用的小技巧

// 技巧 1: 用註解引導 Copilot
// 寫清楚你要什麼,Copilot 會更準確

// Calculate fresnel effect using Schlick's approximation // f0 is the reflectance at normal incidence vec3 fresnel(vec3 f0, float cosTheta) { // Copilot 會正確補全 Schlick's approximation return f0 + (1.0 - f0) * pow(1.0 - cosTheta, 5.0); }

// 技巧 2: 先寫函數簽名和註解,讓 Copilot 補全內容 // Compute ambient occlusion using 5-sample hemisphere sampling float ambientOcclusion(vec3 p, vec3 n) { // Copilot 會給出一個合理的 AO 實作 float ao = 0.0; float step = 0.05; for (int i = 0; i < 5; i++) { float fi = float(i + 1) * step; float dist = map(p + n * fi); ao += (fi - dist) / fi; } return 1.0 - ao * 0.2; }

小結

GitHub Copilot 在 shader 開發中是個「不錯但有限」的助手。它在常見模式(SDF、noise、基本 lighting)上表現很好,能幫你省下大量查資料和手打的時間。但一旦進入複雜的數學推導、效能優化、或是客製化的視覺效果,你還是得靠自己。

我的建議是:把 Copilot 當成一個「shader snippet 庫」來用,而不是把它當成一個「shader programmer」。讓它處理重複性的打字工作,把你的腦力留給真正需要創造力的部分。

延伸資源:

  • iquilezles.org — SDF 函數大全
  • Shadertoy — 線上 shader 練功場
  • The Book of Shaders — shader 入門教學
  • GPU Gems 系列 — 進階 shader 技術