前言
在 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 創作中最有價值的工具之一:
- 公式簡單:
a + b * cos(2π(ct + d)),一行程式碼 - 四個參數:a(基準)、b(振幅)、c(頻率)、d(相位)
- 無限組合:改變參數就能產生完全不同風格的色盤
- 通用性高:可以搭配 SDF、fBm、domain warping 等任何技巧
- 動態變化:加上時間偏移就能產生流動的色彩
我個人的習慣是:先把 shader 的幾何和紋理弄好,最後再花時間調整色盤。好的色盤能讓一個普通的效果變得出色,而壞的色盤能毀掉一個精心設計的場景。
延伸閱讀
- iquilezles.org — Cosine Palette:iq 的原始文章
- Cosine Gradient Generator:iq 的 GraphToy 工具,可以視覺化函數
- Shadertoy — palette:社群使用 cosine palette 的作品
下一篇我們來看如何用 p5.js 和 GLSL shader 互動——透過 uniform 傳遞參數,讓使用者能即時控制 shader 的行為。