前言

GLSL Shader 大概是程式藝術中最讓人又愛又恨的東西。愛它,是因為 shader 能直接在 GPU 上運行,效能驚人,可以即時渲染出極為精緻的視覺效果。恨它,是因為 shader 的語法和思維方式跟一般程式設計完全不同——你必須用「平行思維」去想像每一個像素同時被處理。

自從大型語言模型(LLM)出現後,我開始嘗試用 AI 來輔助 shader 的撰寫。結果發現 AI 在這個領域有明確的強項和弱項。這篇文章分享我的經驗——怎麼有效地用 Claude 或 ChatGPT 生成 GLSL shader,什麼時候 AI 很好用,什麼時候你得自己來。

Shader 基礎快速回顧

Fragment Shader 的思維模式

Fragment shader(片段著色器)為螢幕上的每一個像素計算顏色。它的核心概念是:

// 你的 shader 對每個像素都會被呼叫一次
// gl_FragCoord.xy 是目前像素的螢幕座標
// 你的任務是算出這個像素的顏色

void main() { vec2 uv = gl_FragCoord.xy / u_resolution.xy; // 正規化到 0-1 vec3 color = vec3(uv.x, uv.y, 0.5); // 用座標算顏色 gl_FragColor = vec4(color, 1.0); // 輸出 RGBA }

常用的 Uniform 變數

uniform vec2 u_resolution;  // 畫布解析度
uniform float u_time;        // 時間(秒)
uniform vec2 u_mouse;        // 滑鼠位置

在哪裡測試 Shader

  • Shadertoy(shadertoy.com):最多人用的線上 shader 平台
  • GLSL Sandbox(glslsandbox.com):更簡單的介面
  • The Book of Shaders Editor(thebookofshaders.com/edit.php):搭配教學
  • p5.js + createShader():如果你習慣 p5.js 環境
  • VS Code + glsl-canvas 擴充:本機開發

用 AI 生成 Shader 的 Prompt 範例

基本幾何圖形

請寫一個 GLSL fragment shader(Shadertoy 格式),效果如下:
  • 畫面中央有一個圓形,邊緣有柔和的光暈效果
  • 圓形的顏色隨時間緩慢變化(在紫色到藍綠色之間)
  • 背景是深黑色,帶有微弱的噪點
  • 使用 smoothstep 做邊緣抗鋸齒

AI 的回覆通常很不錯:

void mainImage(out vec4 fragColor, in vec2 fragCoord) {
    vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;

// 到中心的距離 float d = length(uv);

// 隨時間變色 float hue = sin(iTime 0.3) 0.5 + 0.5; vec3 circleColor = mix( vec3(0.5, 0.0, 0.8), // 紫色 vec3(0.0, 0.7, 0.6), // 藍綠色 hue );

// 圓形 + 光暈 float circle = smoothstep(0.3, 0.28, d); // 實心圓 float glow = exp(-d 3.0) 0.5; // 光暈衰減

// 背景噪點 float noise = fract(sin(dot(fragCoord, vec2(12.9898, 78.233))) * 43758.5453); vec3 bg = vec3(0.02) + vec3(noise * 0.02);

// 合成 vec3 color = bg + circleColor * (circle + glow);

fragColor = vec4(color, 1.0); }

光線行進(Ray Marching)

寫一個 Shadertoy 格式的 ray marching shader:
  • 場景中有一個圓角立方體(rounded box)
  • 立方體緩慢旋轉
  • 使用 Phong 光照模型(環境光+漫反射+高光)
  • 地板有棋盤格紋理
  • 加入柔和陰影

AI 能生成基本的 ray marching 框架:

// AI 生成的 ray marching 框架(簡化版)

float sdRoundBox(vec3 p, vec3 b, float r) { vec3 q = abs(p) - b; return length(max(q, 0.0)) + min(max(q.x, max(q.y, q.z)), 0.0) - r; }

mat2 rot2D(float angle) { float s = sin(angle), c = cos(angle); return mat2(c, -s, s, c); }

float map(vec3 p) { vec3 boxP = p - vec3(0.0, 0.5, 0.0); boxP.xz = rot2D(iTime 0.5); boxP.yz = rot2D(iTime 0.3);

float box = sdRoundBox(boxP, vec3(0.4), 0.05); float plane = p.y + 0.5;

return min(box, plane); }

vec3 calcNormal(vec3 p) { vec2 e = vec2(0.001, 0.0); return normalize(vec3( map(p + e.xyy) - map(p - e.xyy), map(p + e.yxy) - map(p - e.yxy), map(p + e.yyx) - map(p - e.yyx) )); }

void mainImage(out vec4 fragColor, in vec2 fragCoord) { vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;

// Camera vec3 ro = vec3(0.0, 1.0, 3.0); // ray origin vec3 rd = normalize(vec3(uv, -1.5)); // ray direction

// Ray march float t = 0.0; for (int i = 0; i < 80; i++) { vec3 p = ro + rd * t; float d = map(p); if (d < 0.001 || t > 20.0) break; t += d; }

vec3 color = vec3(0.05); // 背景色

if (t < 20.0) { vec3 p = ro + rd * t; vec3 n = calcNormal(p);

// Phong lighting vec3 lightPos = vec3(2.0, 3.0, 2.0); vec3 lightDir = normalize(lightPos - p); float diff = max(dot(n, lightDir), 0.0); float spec = pow(max(dot(reflect(-lightDir, n), -rd), 0.0), 32.0);

// 棋盤格(地板) vec3 matColor; if (p.y < -0.49) { float checker = mod(floor(p.x 2.0) + floor(p.z 2.0), 2.0); matColor = mix(vec3(0.3), vec3(0.8), checker); } else { matColor = vec3(0.8, 0.2, 0.1); }

color = matColor (0.15 + diff 0.7) + vec3(spec * 0.5); }

// Gamma correction color = pow(color, vec3(1.0 / 2.2)); fragColor = vec4(color, 1.0); }

程式化紋理

寫一個 GLSL shader 生成以下紋理:
  • 類似大理石的紋路
  • 使用多層 Perlin noise 疊加(fbm)
  • 色調在白色、灰色和淡金色之間
  • 紋路隨時間微微流動

AI 的強項

1. SDF(Signed Distance Function)

AI 對常見的 SDF 函數記得非常清楚——球體、立方體、圓環、膠囊體、圓錐⋯⋯你只需要說「我要一個圓角立方體的 SDF」,AI 馬上能給你 sdRoundBox 的完整實作。

這大大加速了 ray marching shader 的開發,因為不用每次都去查 Inigo Quilez 的 SDF 公式表(雖然那個公式表本身就是一個藝術品)。

2. 標準數學轉換

座標系轉換、旋轉矩陣、投影計算⋯⋯這些容易出錯但又是固定公式的東西,AI 都處理得很好。

prompt: 寫一個 GLSL 函數,把笛卡爾座標轉換成球座標
vec3 cartesianToSpherical(vec3 p) {
    float r = length(p);
    float theta = acos(p.y / r);       // 極角
    float phi = atan(p.z, p.x);         // 方位角
    return vec3(r, theta, phi);
}

3. 噪音函數

各種噪音函數的實作——Value noise、Perlin noise、Simplex noise、Worley noise——AI 都能準確生成。包括 fBm(fractal Brownian motion)的疊加。

4. 後處理效果

暈影(vignette)、色差(chromatic aberration)、膠片顆粒(film grain)、tone mapping⋯⋯這些後處理效果的 shader 代碼,AI 生成得又快又準。

AI 的弱項

1. 複雜的視覺構圖

「我想要一個像日落時分海面的效果,遠處有雲層,近處有波浪反射」——這種涉及多個視覺層次的複雜場景,AI 很難一次到位。通常需要你分步拆解。

2. 效能最佳化

AI 生成的 shader 經常有冗餘計算。例如在迴圈中重複計算不變的值、使用不必要的分支(branch)、SDF 的步進距離不夠聰明等。

3. 偵錯

Shader 的 bug 很難偵錯,因為沒有 console.log。AI 也無法「看到」你的 shader 跑出來長什麼樣子。你需要截圖或精確描述問題(「左上角有一條不應該出現的對角線」)。

4. 原創性的視覺風格

跟 p5.js 一樣,AI 生成的 shader 傾向於「常見的」效果。如果你看過很多 Shadertoy 的作品,你會發現 AI 的輸出帶有明顯的「Shadertoy 風格」——因為那就是它的訓練資料。

人工微調技巧

色彩微調

AI 給的色彩通常是起點。我習慣的微調流程:

  1. 先把 AI 的色彩值改成 uniform 變數
  2. 用 dat.gui 或類似工具做即時調整
  3. 找到滿意的值後再寫回 shader
// 把硬編碼的顏色改成可調整的
// 原始:vec3 color = vec3(0.8, 0.2, 0.1);
// 改成:
uniform vec3 u_baseColor;  // 從外部控制
vec3 color = u_baseColor;

動態速度調整

AI 生成的動態效果速度常常不對——太快或太慢。把所有 iTime * something 中的 something 抽出來調整:

#define ROTATION_SPEED 0.5
#define WAVE_SPEED 0.3
#define COLOR_CYCLE_SPEED 0.1

// 使用 boxP.xz = rot2D(iTime ROTATION_SPEED); float wave = sin(uv.x 10.0 + iTime WAVE_SPEED); float hue = fract(iTime * COLOR_CYCLE_SPEED);

SDF 的組合技巧

AI 通常會給你基本的 min/max 組合,但更有趣的效果來自平滑組合:

// 硬組合(AI 常給的)
float d = min(d1, d2);

// 平滑組合(手動改進) float smoothMin(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); } float d = smoothMin(d1, d2, 0.3);

平滑組合讓兩個物體之間產生「融合」效果,視覺上好看很多。

增加細節層次

AI 的 shader 通常只有一層細節。手動加上更多層次可以大幅提升質感:

// AI 可能給你簡單的噪音
float n = noise(uv * 5.0);

// 手動加入 fBm 多層細節 float fbm(vec2 p) { float value = 0.0; float amplitude = 0.5; float frequency = 1.0; for (int i = 0; i < 6; i++) { value += amplitude noise(p frequency); frequency *= 2.0; amplitude *= 0.5; } return value; }

在 p5.js 中使用 AI 生成的 Shader

如果你的工作環境是 p5.js,可以這樣整合 AI 生成的 shader:

let myShader;

const fragSrc = precision mediump float; uniform vec2 u_resolution; uniform float u_time; uniform vec2 u_mouse;

void main() { vec2 uv = (gl_FragCoord.xy - 0.5 * u_resolution.xy) / u_resolution.y; float d = length(uv); vec3 col = vec3(0.5 + 0.5 * cos(u_time + uv.xyx + vec3(0, 2, 4))); col *= smoothstep(0.5, 0.48, d); gl_FragColor = vec4(col, 1.0); } ;

const vertSrc = attribute vec3 aPosition; void main() { vec4 positionVec4 = vec4(aPosition, 1.0); positionVec4.xy = positionVec4.xy * 2.0 - 1.0; gl_Position = positionVec4; } ;

function setup() { createCanvas(800, 800, WEBGL); myShader = createShader(vertSrc, fragSrc); }

function draw() { shader(myShader); myShader.setUniform('u_resolution', [width, height]); myShader.setUniform('u_time', millis() / 1000.0); myShader.setUniform('u_mouse', [mouseX / width, mouseY / height]); rect(0, 0, width, height); }

注意事項:

  • Shadertoy 格式和 p5.js 的 GLSL 格式不完全相同
  • Shadertoy 用 mainImage(out vec4 fragColor, in vec2 fragCoord),p5.js 用標準 main() + gl_FragColor
  • Shadertoy 的 iResolutioniTimeiMouse 要改成你自定義的 uniform 名稱

你可以請 AI 幫你做這個格式轉換:「請把這個 Shadertoy shader 轉換成 p5.js 相容的 GLSL 格式」。

小結

AI 讓 shader 的入門門檻大幅降低。以前你可能要花好幾天理解 ray marching 的原理才能寫出第一個 3D 場景,現在你可以先讓 AI 生成一個能動的版本,然後在修改的過程中學習原理。

但我也要誠實地說:shader 是一個「深度」很深的領域。AI 能幫你做到 70 分的效果,但從 70 分到 95 分——那種讓人驚嘆的 Shadertoy 首頁作品——還是需要你自己深入理解每一行程式碼。

延伸閱讀:

  • The Book of Shaders(thebookofshaders.com)— 最好的 GLSL 入門教學
  • Inigo Quilez 的網站(iquilezles.org)— shader 大神的技術文章和公式
  • Shadertoy.com — 瀏覽和學習別人的 shader
  • Ray Marching and Signed Distance Functions — Jamie Wong 的入門文章