前言
身為一個偶爾寫 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 的好時機
- 專案初始階段:建立 utility 函數庫(SDF、noise、色彩轉換等)
- 寫 boilerplate:vertex shader、基本的 fragment shader 框架
- 常見效果的第一版:bloom、blur、tone mapping 等
關 Copilot 的好時機
- 精密的數學推導:Copilot 的建議會打斷你的思路
- 效能 profiling:你需要專注在數字上,不是 autocomplete
- 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 技術