前言

如果你看過那些在 Shadertoy 上流動的、像是活生生的有機體一般的紋理,你可能會想:這到底是怎麼做出來的?答案很可能是 Domain Warping

Domain Warping 的概念簡單到令人驚訝:把一個 noise 函數的輸出拿去當另一個 noise 函數的輸入座標。就這樣。但這個遞迴式的操作會產生令人難以置信的複雜、有機、近乎催眠的視覺效果。

這個技巧最早由 Inigo Quilez(iquilezles.org)推廣開來,他的經典文章 “Warping by Noise” 是每個 shader 藝術家必讀的參考資料。今天我們就來深入拆解這個技巧。


Domain Warping 是什麼?

讓我們先釐清「domain」這個詞。在數學中,函數的 domain 就是它的輸入空間。所以 domain warping 就是「扭曲輸入空間」。

正常的 fBm 呼叫:

float f = fbm(p);

最簡單的 domain warping:

float f = fbm(p + fbm(p));

我們先計算 fbm(p) 得到一個值,然後把它加到原始座標 p 上,再丟給 fbm 算一次。第一次的 fbm 扭曲了第二次的座標空間——空間本身被 noise 變形了。


基礎實作

首先需要我們的 noise 和 fbm 函數:

float hash(vec2 p) {
    p = fract(p * vec2(123.34, 456.21));
    p += dot(p, p + 45.32);
    return fract(p.x * p.y);
}

float noise(vec2 p) { vec2 i = floor(p); vec2 f = fract(p); vec2 u = f f (3.0 - 2.0 * f);

return mix( mix(hash(i), hash(i + vec2(1.0, 0.0)), u.x), mix(hash(i + vec2(0.0, 1.0)), hash(i + vec2(1.0, 1.0)), u.x), u.y ); }

const mat2 m = mat2(0.8, 0.6, -0.6, 0.8);

float fbm(vec2 p) { float f = 0.0; float amp = 0.5; for (int i = 0; i < 6; i++) { f += amp * noise(p); p = m p 2.0; amp *= 0.5; } return f; }

第一層 Warp

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

vec2 p = uv * 3.0;

// 一層 domain warping float f = fbm(p + fbm(p + iTime * 0.1));

vec3 col = vec3(f); fragColor = vec4(col, 1.0); }

光是這一層就已經比普通的 fbm 有趣多了——你會看到扭曲的、流動的紋理。


Iq 的經典公式

Inigo Quilez 在他的文章中展示了一個更精緻的版本:

f(p) = fbm( p + fbm( p + fbm(p) ) )

三層嵌套,每一層都進一步扭曲空間。

向量版本(更有趣)

把 fbm 的結果當作 2D 向量而非純量,可以產生更豐富的效果:

// 用不同的偏移來產生兩個獨立的 fbm 值,組成向量
vec2 fbm2(vec2 p) {
    return vec2(
        fbm(p + vec2(1.7, 9.2)),
        fbm(p + vec2(8.3, 2.8))
    );
}

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

vec2 p = uv * 3.0; float t = iTime * 0.1;

// iq 經典公式 vec2 q = fbm2(p + t); vec2 r = fbm2(p + 4.0 q + vec2(1.7, 9.2) + t 0.3);

float f = fbm(p + 4.0 * r);

vec3 col = vec3(f); fragColor = vec4(col, 1.0); }

加上顏色

iq 的原始範例用 qr 的值來混合顏色:

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

vec2 p = uv * 3.0; float t = iTime * 0.1;

vec2 q = vec2( fbm(p + vec2(0.0, 0.0) + t * 0.2), fbm(p + vec2(5.2, 1.3) + t * 0.15) );

vec2 r = vec2( fbm(p + 4.0 q + vec2(1.7, 9.2) + t 0.12), fbm(p + 4.0 q + vec2(8.3, 2.8) + t 0.08) );

float f = fbm(p + 4.0 * r);

// 用 q, r, f 來混合顏色 vec3 col = vec3(0.0); col = mix(vec3(0.102, 0.220, 0.360), // 深藍 vec3(0.667, 0.667, 0.498), // 米黃 clamp(f f 4.0, 0.0, 1.0));

col = mix(col, vec3(0.0, 0.0, 0.165), // 暗藍 clamp(length(q), 0.0, 1.0));

col = mix(col, vec3(0.667, 1.000, 1.000), // 亮青 clamp(length(r.x), 0.0, 1.0));

// 最終調色 col = f f f + 0.6 f f + 0.5 f;

fragColor = vec4(col, 1.0); }

這就是 iq 經典 domain warping demo 的完整程式碼。每次看到它產生的那些像大理石、像木紋、像星雲一般的圖案,我都覺得數學真的是一種藝術。


理解扭曲的幾何意義

為了更直觀地理解 domain warping 在做什麼,我們可以視覺化扭曲場(warp field)本身:

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

vec2 p = uv * 4.0; float t = iTime * 0.2;

// 扭曲向量 vec2 warp = vec2( fbm(p + t), fbm(p + vec2(5.2, 1.3) + t) );

// 視覺化扭曲向量(紅 = x 方向,綠 = y 方向) vec3 col = vec3(warp.x, warp.y, 0.5);

// 或者畫出「扭曲後的格線」 vec2 warped = p + warp * 2.0; float gridX = abs(fract(warped.x) - 0.5); float gridY = abs(fract(warped.y) - 0.5); float grid = min(gridX, gridY); col = vec3(smoothstep(0.0, 0.05, grid));

fragColor = vec4(col, 1.0); }

你會看到原本整齊的格線被 noise 扭曲成波浪狀的有機形狀——這就是 domain warping 的實質。


動態 Domain Warping

加上時間可以讓紋理緩慢流動,非常適合做背景動畫:

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

vec2 p = uv * 3.0; float time = iTime * 0.15;

// 三層 warp,每層用不同的時間速度 vec2 q = vec2( fbm(p + vec2(0.0) + time * 1.0), fbm(p + vec2(5.2, 1.3) + time * 0.8) );

vec2 r = vec2( fbm(p + 3.0 q + vec2(1.7, 9.2) + time 0.5), fbm(p + 3.0 q + vec2(8.3, 2.8) + time 0.3) );

vec2 s = vec2( fbm(p + 2.0 r + vec2(3.1, 4.7) + time 0.2), fbm(p + 2.0 r + vec2(7.5, 6.1) + time 0.1) );

float f = fbm(p + 3.0 * s);

// 深色有機風格 vec3 col = vec3(0.0); col += vec3(0.2, 0.05, 0.1) * smoothstep(0.0, 0.8, f); col += vec3(0.5, 0.2, 0.1) * smoothstep(0.3, 1.0, f); col += vec3(0.8, 0.6, 0.3) * smoothstep(0.6, 1.0, f);

col = 0.8 + 0.2 f;

fragColor = vec4(col, 1.0); }


用 Domain Warping 產生不同風格

木紋

float woodGrain(vec2 p) {
    float warp = fbm(p  2.0)  0.5;
    float grain = sin((p.x + warp)  30.0)  0.5 + 0.5;
    grain = pow(grain, 0.3);
    return grain;
}

// 木頭顏色 vec3 woodColor = mix( vec3(0.4, 0.25, 0.1), // 深色紋路 vec3(0.7, 0.5, 0.25), // 淺色木頭 woodGrain(uv * 3.0) );

大理石紋路

float marble(vec2 p) {
    float warp = fbm(p * 3.0);
    float veins = sin((p.x + p.y)  10.0 + warp  8.0);
    veins = abs(veins);
    veins = pow(veins, 0.4);
    return veins;
}

生物紋路(細胞狀)

float bioPattern(vec2 p, float time) {
    vec2 q = vec2(
        fbm(p + time * 0.1),
        fbm(p + vec2(3.7, 1.2) + time * 0.08)
    );

// 用 sin 產生細胞邊界 float f = fbm(p + 3.0 * q); float cells = sin(f 20.0) 0.5 + 0.5; cells = smoothstep(0.4, 0.6, cells);

return cells; }


效能與控制

控制扭曲強度

用一個乘數來控制 warp 的強度:

float warpStrength = 2.0; // 調整這個值
vec2 q = fbm2(p);
float f = fbm(p + warpStrength * q);

值越大,扭曲越劇烈。通常 1.0 到 4.0 之間效果最好。

效能考量

Domain warping 的計算量不小——每一層 warp 都需要完整計算一次 fbm(6 個 octave),三層 warp 就是 18 次 noise 呼叫。

優化建議:

  • 外層 warp 可以用較少的 octave(3-4 個就夠了)
  • 如果不需要極致的品質,可以用較簡單的 noise 函數
  • 在效能敏感的場景中,減少 warp 的層數
// 外層用快速版本
float fbmFast(vec2 p) {
    float f = 0.0;
    float amp = 0.5;
    for (int i = 0; i < 3; i++) { // 只有 3 層
        f += amp * noise(p);
        p = m  p  2.0;
        amp *= 0.5;
    }
    return f;
}

vec2 q = vec2(fbmFast(p), fbmFast(p + 5.2)); float f = fbm(p + 3.0 * q); // 最終層用完整 fbm


小結

Domain warping 是我個人最喜歡的 shader 技巧之一,因為它完美體現了「簡單規則產生複雜結果」的哲學:

  1. 核心概念:把 noise 的輸出當作另一個 noise 的輸入座標偏移
  2. Iq 公式fbm(p + fbm(p + fbm(p))),多層嵌套產生豐富的有機紋理
  3. 向量版本:用兩個獨立的 fbm 組成 2D 扭曲向量,效果更自然
  4. 應用:木紋、大理石、生物紋路、抽象藝術背景
  5. 顏色映射:用中間變數(q, r)來混合不同的顏色

當你不知道要做什麼 shader 的時候,打開 Shadertoy,隨便寫一個 domain warping,調調參數和顏色——你幾乎一定會得到某個讓你驚嘆的畫面。

延伸閱讀

下一篇我們來聊極座標繪圖——用 atanlength 畫出萬花筒般的對稱圖案。