前言

在 shader 程式設計中,「上色」往往是決定作品成敗的關鍵一步。你可能已經寫出了很厲害的 SDF 場景或漂亮的 noise 紋理,但如果顏色選得不好,整個作品就會顯得平淡無奇。

Inigo Quilez 提出了一個優雅到令人嘆服的解決方案:Cosine Color Palette。用一個簡單的餘弦公式,配上四個參數向量,就能產生無窮多種色彩漸變。這個技巧被整個 Shadertoy 社群廣泛使用,幾乎是每個 shader 作品中都會出現的基本工具。


公式

整個色盤的公式只有一行:

vec3 palette(float t, vec3 a, vec3 b, vec3 c, vec3 d) {
    return a + b  cos(6.28318  (c * t + d));
}

其中:

  • t:輸入值,通常在 0.0 到 1.0 之間(但可以超出範圍,因為 cos 是週期函數)
  • a:色盤的中心亮度/色彩偏移
  • b:色彩振幅(各通道的變化幅度)
  • c:頻率(各通道經歷多少個完整週期)
  • d:相位偏移(各通道的起始位置)

直覺理解

把公式拆開來看:

R = a.r + b.r  cos(2π  (c.r * t + d.r))
G = a.g + b.g  cos(2π  (c.g * t + d.g))
B = a.b + b.b  cos(2π  (c.b * t + d.b))

每個顏色通道都是一個餘弦波。a 控制基準值,b 控制波的振幅,c 控制波的頻率,d 控制波的起始相位。因為 RGB 三個通道可以有不同的頻率和相位,所以能產生豐富的色彩變化。


基本使用

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

// 色盤參數 vec3 a = vec3(0.5, 0.5, 0.5); vec3 b = vec3(0.5, 0.5, 0.5); vec3 c = vec3(1.0, 1.0, 1.0); vec3 d = vec3(0.00, 0.33, 0.67);

// 用水平座標當作 t vec3 col = palette(uv.x, a, b, c, d);

fragColor = vec4(col, 1.0); }

這個特定的參數組合(d = (0, 0.33, 0.67))會產生一個經典的彩虹漸變——因為 RGB 三個通道的相位剛好相隔 120 度(1/3 週期)。


經典色盤

iq 在他的文章中列出了幾組經典的參數:

彩虹

vec3 a = vec3(0.5, 0.5, 0.5);
vec3 b = vec3(0.5, 0.5, 0.5);
vec3 c = vec3(1.0, 1.0, 1.0);
vec3 d = vec3(0.00, 0.33, 0.67);

暖色調(日落)

vec3 a = vec3(0.5, 0.5, 0.5);
vec3 b = vec3(0.5, 0.5, 0.5);
vec3 c = vec3(1.0, 1.0, 1.0);
vec3 d = vec3(0.00, 0.10, 0.20);

冷色調(海洋)

vec3 a = vec3(0.5, 0.5, 0.5);
vec3 b = vec3(0.5, 0.5, 0.5);
vec3 c = vec3(1.0, 1.0, 0.5);
vec3 d = vec3(0.80, 0.90, 0.30);

紫粉色(霓虹)

vec3 a = vec3(0.5, 0.5, 0.5);
vec3 b = vec3(0.5, 0.5, 0.5);
vec3 c = vec3(2.0, 1.0, 0.0);
vec3 d = vec3(0.50, 0.20, 0.25);

大地色

vec3 a = vec3(0.5, 0.5, 0.5);
vec3 b = vec3(0.5, 0.5, 0.5);
vec3 c = vec3(1.0, 0.7, 0.4);
vec3 d = vec3(0.00, 0.15, 0.20);

黑白高對比

vec3 a = vec3(0.5);
vec3 b = vec3(0.5);
vec3 c = vec3(1.0);
vec3 d = vec3(0.0);

視覺化色盤

為了直觀地看到色盤的效果,我們可以寫一個色盤展示器:

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

// 參數(可自由調整) vec3 a = vec3(0.5, 0.5, 0.5); vec3 b = vec3(0.5, 0.5, 0.5); vec3 c = vec3(1.0, 1.0, 1.0); vec3 d = vec3(0.00, 0.33, 0.67);

// 色帶 vec3 col = palette(uv.x, a, b, c, d);

// 在下方畫出各通道的波形 if (uv.y < 0.3) { vec3 wave = a + b cos(6.28318 (c * uv.x + d)); float lineWidth = 0.005;

col = vec3(0.1);

// R 通道 if (abs(uv.y / 0.3 - wave.r) < lineWidth * 3.0) col = vec3(1.0, 0.0, 0.0); // G 通道 if (abs(uv.y / 0.3 - wave.g) < lineWidth * 3.0) col = vec3(0.0, 1.0, 0.0); // B 通道 if (abs(uv.y / 0.3 - wave.b) < lineWidth * 3.0) col = vec3(0.3, 0.3, 1.0); }

fragColor = vec4(col, 1.0); }


搭配 SDF 使用

Cosine palette 最常見的用法之一是用 SDF 的距離值來上色:

基本距離上色

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

// 圓形 SDF float d = length(uv) - 0.5;

// 用距離值當作色盤的 t vec3 col = palette(d 3.0 + iTime 0.5, vec3(0.5, 0.5, 0.5), vec3(0.5, 0.5, 0.5), vec3(1.0, 1.0, 1.0), vec3(0.00, 0.33, 0.67) );

// 乘以距離的衰減 col *= smoothstep(0.01, 0.02, abs(d));

fragColor = vec4(col, 1.0); }

等距線 + 色盤

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

// 場景 SDF float d = sdBox(uv, vec2(0.3)); d = min(d, sdCircle(uv - vec2(0.5, 0.0), 0.25));

// 等距線 float contour = fract(d * 10.0); float line = smoothstep(0.0, 0.05, contour) * smoothstep(0.15, 0.1, contour);

// 用距離上色 vec3 col = palette(d + iTime * 0.2, vec3(0.5, 0.5, 0.5), vec3(0.5, 0.5, 0.5), vec3(2.0, 1.0, 0.0), vec3(0.50, 0.20, 0.25) );

col = mix(col, vec3(1.0), line * 0.5);

fragColor = vec4(col, 1.0); }


動態色盤

讓色盤隨時間變化是很常見的做法:

方法一:偏移 t 值

vec3 col = palette(t + iTime * 0.1, a, b, c, d);

整個色盤會沿著 t 軸移動,產生流動的色彩效果。

方法二:動態調整參數

vec3 d_param = vec3(
    0.0 + 0.1  sin(iTime  0.3),
    0.33 + 0.1  sin(iTime  0.4),
    0.67 + 0.1  sin(iTime  0.5)
);
vec3 col = palette(t, a, b, c, d_param);

相位緩慢變化,色盤會在不同的色調之間平滑過渡。

方法三:在多個色盤之間插值

vec3 palette1 = palette(t, a1, b1, c1, d1);
vec3 palette2 = palette(t, a2, b2, c2, d2);
float blend = sin(iTime  0.2)  0.5 + 0.5;
vec3 col = mix(palette1, palette2, blend);

搭配 fBm 和 Domain Warping

色盤 + fBm 是非常常見的組合:

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.1;

// Domain warping vec2 q = vec2(fbm(p + t), fbm(p + vec2(5.2, 1.3) + t)); float f = fbm(p + 4.0 * q);

// 用 fBm 值當作色盤的 t vec3 col = palette(f * 2.0 + t, vec3(0.5, 0.5, 0.5), vec3(0.5, 0.5, 0.5), vec3(1.0, 0.7, 0.4), vec3(0.00, 0.15, 0.20) );

// 用 q 的長度調制亮度 col = 0.5 + 0.5 f;

fragColor = vec4(col, 1.0); }


設計自己的色盤

要設計自己的色盤,有幾個思路:

思路一:從目標色反推

如果你想要色盤在 t=0 時是顏色 C0,在 t=0.5 時是顏色 C1:

C0 = a + b    (因為 cos(0) = 1)
C1 = a - b    (因為 cos(π) = -1,前提是 c = 1)

所以: a = (C0 + C1) / 2 b = (C0 - C1) / 2

思路二:互動調整

寫一個互動式的色盤編輯器:

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

// 用滑鼠的 x 位置當作 d 的偏移 vec2 mouse = iMouse.xy / iResolution.xy;

vec3 a = vec3(0.5); vec3 b = vec3(0.5); vec3 c = vec3(1.0, 1.0, 1.0); vec3 d = vec3(mouse.x, mouse.x + 0.33, mouse.x + 0.67);

vec3 col = palette(uv.x, a, b, c, d);

// 顯示色帶 if (uv.y > 0.8) { col = palette(uv.x, a, b, c, d); }

fragColor = vec4(col, 1.0); }

思路三:iq 的線上工具

iq 提供了一個互動式的 cosine palette 編輯器(在他的網站上),你可以直觀地拖動參數看效果。


進階技巧

多重色盤疊加

// 用不同的頻率疊加多個色盤
vec3 col = vec3(0.0);
col += palette(d, a1, b1, c1, d1) * 0.5;
col += palette(d  2.0, a2, b2, c2, d2)  0.3;
col += palette(d  4.0, a3, b3, c3, d3)  0.2;

限定色域(Clamping)

有時候色盤可能產生超出 [0,1] 範圍的值:

vec3 col = clamp(palette(t, a, b, c, d), 0.0, 1.0);

搭配 pow 調整對比

vec3 col = palette(t, a, b, c, d);
col = pow(col, vec3(1.5)); // 增加對比度、壓暗暗部

用 palette 產生漸變背景

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

// 用距離中心的值 + 角度來產生漸變 vec2 center = uv - 0.5; float dist = length(center); float angle = atan(center.y, center.x) / 6.28 + 0.5;

float t = dist 2.0 + angle 0.3 + iTime * 0.1;

vec3 col = palette(t, vec3(0.5, 0.5, 0.5), vec3(0.5, 0.5, 0.5), vec3(1.0, 1.0, 0.5), vec3(0.80, 0.90, 0.30) );

fragColor = vec4(col, 1.0); }


完整範例:SDF 場景 + Cosine Palette

vec3 palette(float t) {
    vec3 a = vec3(0.5, 0.5, 0.5);
    vec3 b = vec3(0.5, 0.5, 0.5);
    vec3 c = vec3(1.0, 1.0, 1.0);
    vec3 d = vec3(0.263, 0.416, 0.557);
    return a + b  cos(6.28318  (c * t + d));
}

void mainImage(out vec4 fragColor, in vec2 fragCoord) { vec2 uv = (fragCoord * 2.0 - iResolution.xy) / iResolution.y; vec2 uv0 = uv; vec3 finalColor = vec3(0.0);

for (float i = 0.0; i < 4.0; i++) { uv = fract(uv * 1.5) - 0.5;

float d = length(uv) * exp(-length(uv0));

vec3 col = palette(length(uv0) + i 0.4 + iTime 0.4);

d = sin(d * 8.0 + iTime) / 8.0; d = abs(d); d = pow(0.01 / d, 1.2);

finalColor += col * d; }

fragColor = vec4(finalColor, 1.0); }

這段程式碼是 Shadertoy 上非常受歡迎的一個範例——多層 fract + SDF + cosine palette,只有短短幾行,卻能產生令人驚嘆的彩色碎形效果。


小結

Cosine color palette 是 shader 創作中最有價值的工具之一:

  1. 公式簡單a + b * cos(2π(ct + d)),一行程式碼
  2. 四個參數:a(基準)、b(振幅)、c(頻率)、d(相位)
  3. 無限組合:改變參數就能產生完全不同風格的色盤
  4. 通用性高:可以搭配 SDF、fBm、domain warping 等任何技巧
  5. 動態變化:加上時間偏移就能產生流動的色彩

我個人的習慣是:先把 shader 的幾何和紋理弄好,最後再花時間調整色盤。好的色盤能讓一個普通的效果變得出色,而壞的色盤能毀掉一個精心設計的場景。

延伸閱讀

下一篇我們來看如何用 p5.js 和 GLSL shader 互動——透過 uniform 傳遞參數,讓使用者能即時控制 shader 的行為。