Files
skills/shader-dev/techniques/color-palette.md
shihao 6487becf60 Initial commit: add all skills files
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 16:52:49 +08:00

13 KiB

Color Palette & Color Space

Use Cases

  • Mapping scalar values (distance, temperature, time, iteration count) to continuous color ramps
  • Perceptually uniform color interpolation/gradients
  • HDR rendering with linear-space workflow (sRGB decode -> shading -> tone mapping -> sRGB encode)
  • Physically realistic glow/flame/blackbody radiation colors

Core Principles

Core: map a scalar t in [0,1] to an RGB vec3.

Cosine Palette

color(t) = a + b * cos(2pi * (c * t + d))
  • a = brightness offset (~0.5), b = amplitude (~0.5), c = frequency, d = phase (the key parameter controlling color style)

HSV/HSL Branchless Conversion

rgb = clamp(abs(mod(H*6 + vec3(0,4,2), 6) - 3) - 1, 0, 1)

Uses piecewise linear functions to approximate RGB variation with hue. C1 continuity can be achieved via rgb*rgb*(3-2*rgb).

CIE Lab/Lch Perceptually Uniform Interpolation

RGB -> XYZ -> Lab -> Lch pipeline; interpolate in perceptually uniform space to avoid brightness discontinuities in RGB/HSV.

Blackbody Radiation Palette

Temperature T -> Planckian locus approximation -> CIE chromaticity -> XYZ -> RGB, with Stefan-Boltzmann (T^4) controlling brightness.

Implementation

Cosine Palette

// a: offset, b: amplitude, c: frequency, d: phase, t: input scalar
vec3 palette(float t, vec3 a, vec3 b, vec3 c, vec3 d) {
    return a + b * cos(6.28318 * (c * t + d));
}

Classic Preset Parameters

// Rainbow: d=(0.0, 0.33, 0.67)
// Warm: d=(0.0, 0.10, 0.20)
// Blue-purple to orange: c=(1,0.7,0.4) d=(0.0, 0.15, 0.20)
// Warm-cool mix: a=(.8,.5,.4) b=(.2,.4,.2) c=(2,1,1) d=(0.0, 0.25, 0.25)

// Simplified version: fixed a/b/c, only adjust d
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));
}

HSV -> RGB (Standard + Smooth)

// Standard HSV -> RGB (branchless)
vec3 hsv2rgb(vec3 c) {
    vec3 rgb = clamp(abs(mod(c.x * 6.0 + vec3(0.0, 4.0, 2.0), 6.0) - 3.0) - 1.0, 0.0, 1.0);
    return c.z * mix(vec3(1.0), rgb, c.y);
}

// Smooth version (C1 continuous)
vec3 hsv2rgb_smooth(vec3 c) {
    vec3 rgb = clamp(abs(mod(c.x * 6.0 + vec3(0.0, 4.0, 2.0), 6.0) - 3.0) - 1.0, 0.0, 1.0);
    rgb = rgb * rgb * (3.0 - 2.0 * rgb); // Hermite smoothing
    return c.z * mix(vec3(1.0), rgb, c.y);
}

HSL -> RGB

vec3 hue2rgb(float h) {
    return clamp(abs(mod(h * 6.0 + vec3(0.0, 4.0, 2.0), 6.0) - 3.0) - 1.0, 0.0, 1.0);
}

vec3 hsl2rgb(float h, float s, float l) {
    vec3 rgb = hue2rgb(h);
    return l + s * (rgb - 0.5) * (1.0 - abs(2.0 * l - 1.0));
}

RGB -> HSV

// Sam Hocevar branchless method
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);
}

CIE Lab/Lch Conversion Pipeline

float xyzF(float t) { return mix(pow(t, 1.0/3.0), 7.787037 * t + 0.139731, step(t, 0.00885645)); }
float xyzR(float t) { return mix(t * t * t, 0.1284185 * (t - 0.139731), step(t, 0.20689655)); }

vec3 rgb2lch(vec3 c) {
    c *= mat3(0.4124, 0.3576, 0.1805,
              0.2126, 0.7152, 0.0722,
              0.0193, 0.1192, 0.9505);
    c = vec3(xyzF(c.x), xyzF(c.y), xyzF(c.z));
    vec3 lab = vec3(max(0.0, 116.0 * c.y - 16.0),
                    500.0 * (c.x - c.y),
                    200.0 * (c.y - c.z));
    return vec3(lab.x, length(lab.yz), atan(lab.z, lab.y));
}

vec3 lch2rgb(vec3 c) {
    c = vec3(c.x, cos(c.z) * c.y, sin(c.z) * c.y);
    float lg = (1.0 / 116.0) * (c.x + 16.0);
    vec3 xyz = vec3(xyzR(lg + 0.002 * c.y),
                    xyzR(lg),
                    xyzR(lg - 0.005 * c.z));
    return xyz * mat3( 3.2406, -1.5372, -0.4986,
                      -0.9689,  1.8758,  0.0415,
                       0.0557, -0.2040,  1.0570);
}

// Circular hue interpolation
float lerpAngle(float a, float b, float x) {
    float ang = mod(mod((a - b), 6.28318) + 9.42477, 6.28318) - 3.14159;
    return ang * x + b;
}

vec3 lerpLch(vec3 a, vec3 b, float x) {
    return vec3(mix(b.xy, a.xy, x), lerpAngle(a.z, b.z, x));
}

sRGB Gamma & Tone Mapping

// Precise sRGB encoding
float sRGB_encode(float t) {
    return mix(1.055 * pow(t, 1.0/2.4) - 0.055, 12.92 * t, step(t, 0.0031308));
}
vec3 sRGB_encode(vec3 c) {
    return vec3(sRGB_encode(c.x), sRGB_encode(c.y), sRGB_encode(c.z));
}

// Fast approximation: pow(color, vec3(2.2)) / pow(color, vec3(1.0/2.2))

// Reinhard tone mapping
vec3 tonemap_reinhard(vec3 col) {
    return col / (1.0 + col);
}

Blackbody Radiation Palette

#define TEMP_MAX 4000.0 // Tunable: maximum temperature (K)
vec3 blackbodyPalette(float t) {
    t *= TEMP_MAX;
    float cx = (0.860117757 + 1.54118254e-4 * t + 1.28641212e-7 * t * t)
             / (1.0 + 8.42420235e-4 * t + 7.08145163e-7 * t * t);
    float cy = (0.317398726 + 4.22806245e-5 * t + 4.20481691e-8 * t * t)
             / (1.0 - 2.89741816e-5 * t + 1.61456053e-7 * t * t);
    float d = 2.0 * cx - 8.0 * cy + 4.0;
    vec3 XYZ = vec3(3.0 * cx / d, 2.0 * cy / d, 1.0 - (3.0 * cx + 2.0 * cy) / d);
    vec3 RGB = mat3(3.240479, -0.969256, 0.055648,
                   -1.537150,  1.875992, -0.204043,
                   -0.498535,  0.041556,  1.057311) * vec3(XYZ.x / XYZ.y, 1.0, XYZ.z / XYZ.y);
    return max(RGB, 0.0) * pow(t * 0.0004, 4.0);
}

Complete Code Template

A ShaderToy shader demonstrating all core techniques:

// === Procedural Color Palette Showcase ===
#define PI  3.14159265
#define TAU 6.28318530

// ============ Tunable Parameters ============
#define PALETTE_A vec3(0.5, 0.5, 0.5)   // Offset: increase = brighter overall
#define PALETTE_B vec3(0.5, 0.5, 0.5)   // Amplitude: increase = more contrast
#define PALETTE_C vec3(1.0, 1.0, 1.0)   // Frequency: increase = denser color variation
#define PALETTE_D vec3(0.0, 0.33, 0.67) // Phase: change = completely different hues
#define TEMP_MAX 4000.0                  // Blackbody max temperature (K)
#define NUM_ITER 4                       // Fractal iteration count

// ============ Color Functions ============

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

vec3 palette(float t) {
    return cosinePalette(t, PALETTE_A, PALETTE_B, PALETTE_C, PALETTE_D);
}

vec3 hsv2rgb(vec3 c) {
    vec3 rgb = clamp(abs(mod(c.x * 6.0 + vec3(0.0, 4.0, 2.0), 6.0) - 3.0) - 1.0, 0.0, 1.0);
    rgb = rgb * rgb * (3.0 - 2.0 * rgb);
    return c.z * mix(vec3(1.0), rgb, c.y);
}

vec3 hsl2rgb(float h, float s, float l) {
    vec3 rgb = clamp(abs(mod(h * 6.0 + vec3(0.0, 4.0, 2.0), 6.0) - 3.0) - 1.0, 0.0, 1.0);
    return l + s * (rgb - 0.5) * (1.0 - abs(2.0 * l - 1.0));
}

vec3 blackbodyPalette(float t) {
    t *= TEMP_MAX;
    float cx = (0.860117757 + 1.54118254e-4*t + 1.28641212e-7*t*t)
             / (1.0 + 8.42420235e-4*t + 7.08145163e-7*t*t);
    float cy = (0.317398726 + 4.22806245e-5*t + 4.20481691e-8*t*t)
             / (1.0 - 2.89741816e-5*t + 1.61456053e-7*t*t);
    float d = 2.0*cx - 8.0*cy + 4.0;
    vec3 XYZ = vec3(3.0*cx/d, 2.0*cy/d, 1.0 - (3.0*cx + 2.0*cy)/d);
    vec3 RGB = mat3(3.240479, -0.969256, 0.055648,
                   -1.537150,  1.875992, -0.204043,
                   -0.498535,  0.041556,  1.057311) * vec3(XYZ.x/XYZ.y, 1.0, XYZ.z/XYZ.y);
    return max(RGB, 0.0) * pow(t * 0.0004, 4.0);
}

vec3 sRGB(vec3 c) { return pow(clamp(c, 0.0, 1.0), vec3(1.0/2.2)); }
vec3 tonemap(vec3 c) { return c / (1.0 + c); }

// ============ Main ============

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

    if (band < 0.2) {
        // Cosine Palette
        float t = fragCoord.x / iResolution.x + iTime * 0.1;
        col = palette(t);
    } else if (band < 0.4) {
        // HSV color wheel
        float h = fragCoord.x / iResolution.x;
        float v = (band - 0.2) / 0.2;
        col = hsv2rgb(vec3(h + iTime * 0.05, 1.0, v));
    } else if (band < 0.6) {
        // HSL color wheel
        float h = fragCoord.x / iResolution.x;
        float l = (band - 0.4) / 0.2;
        col = hsl2rgb(h + iTime * 0.05, 1.0, l);
    } else if (band < 0.8) {
        // Blackbody radiation
        float t = fragCoord.x / iResolution.x;
        col = tonemap(blackbodyPalette(t));
    } else {
        // Cosine Palette fractal glow
        vec2 p = uv;
        vec3 finalColor = vec3(0.0);
        for (int i = 0; i < NUM_ITER; i++) {
            p = fract(p * 1.5) - 0.5;
            float d = length(p) * exp(-length(uv0));
            vec3 c = palette(length(uv0) + float(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 += c * d;
        }
        col = tonemap(finalColor);
    }

    // Band separator lines
    float bandLine = smoothstep(0.003, 0.0, abs(fract(band * 5.0) - 0.5) - 0.49);
    col *= 1.0 - bandLine * 0.8;
    col = sRGB(col);
    fragColor = vec4(col, 1.0);
}

Common Variants

Multi-Harmonic Cosine Palette (Anti-Aliased)

vec3 fcos(vec3 x) {
    vec3 w = fwidth(x);
    return cos(x) * smoothstep(TAU, 0.0, w);
}

vec3 getColor(float t) {
    vec3 col = vec3(0.4);
    col += 0.12 * fcos(TAU * t *   1.0 + vec3(0.0, 0.8, 1.1));
    col += 0.11 * fcos(TAU * t *   3.1 + vec3(0.3, 0.4, 0.1));
    col += 0.10 * fcos(TAU * t *   5.1 + vec3(0.1, 0.7, 1.1));
    col += 0.09 * fcos(TAU * t *   9.1 + vec3(0.2, 0.8, 1.4));
    col += 0.08 * fcos(TAU * t *  17.1 + vec3(0.2, 0.6, 0.7));
    col += 0.07 * fcos(TAU * t *  31.1 + vec3(0.1, 0.6, 0.7));
    col += 0.06 * fcos(TAU * t *  65.1 + vec3(0.0, 0.5, 0.8));
    col += 0.06 * fcos(TAU * t * 115.1 + vec3(0.1, 0.4, 0.7));
    col += 0.09 * fcos(TAU * t * 265.1 + vec3(1.1, 1.4, 2.7));
    return col;
}

Hash-Driven Per-Tile Color

float hash12(vec2 p) {
    vec3 p3 = fract(vec3(p.xyx) * 0.1031);
    p3 += dot(p3, p3.yzx + 33.33);
    return fract((p3.x + p3.y) * p3.z);
}

vec2 tileId = floor(uv);
vec3 tileColor = palette(hash12(tileId));

Saturation-Preserving Improved RGB Interpolation

float getsat(vec3 c) {
    float mi = min(min(c.x, c.y), c.z);
    float ma = max(max(c.x, c.y), c.z);
    return (ma - mi) / (ma + 1e-7);
}

vec3 iLerp(vec3 a, vec3 b, float x) {
    vec3 ic = mix(a, b, x) + vec3(1e-6, 0.0, 0.0);
    float sd = abs(getsat(ic) - mix(getsat(a), getsat(b), x));
    vec3 dir = normalize(vec3(2.0*ic.x - ic.y - ic.z,
                              2.0*ic.y - ic.x - ic.z,
                              2.0*ic.z - ic.y - ic.x));
    float lgt = dot(vec3(1.0), ic);
    float ff = dot(dir, normalize(ic));
    ic += 1.5 * dir * sd * ff * lgt;
    return clamp(ic, 0.0, 1.0);
}

Circular Hue Interpolation

// HSV space (hue [0,1])
vec3 lerpHSV(vec3 a, vec3 b, float x) {
    float hue = (mod(mod((b.x - a.x), 1.0) + 1.5, 1.0) - 0.5) * x + a.x;
    return vec3(hue, mix(a.yz, b.yz, x));
}

// Lch space (hue [0, 2pi])
float lerpAngle(float a, float b, float x) {
    float ang = mod(mod((a - b), TAU) + PI * 3.0, TAU) - PI;
    return ang * x + b;
}

Additive Color Stacking (Glow/HDR)

vec3 finalColor = vec3(0.0);
for (int i = 0; i < 4; i++) {
    vec3 col = palette(length(uv) + float(i) * 0.4 + iTime * 0.4);
    float glow = pow(0.01 / abs(sdfValue), 1.2);
    finalColor += col * glow;
}
finalColor = finalColor / (1.0 + finalColor); // Reinhard tonemap

Performance & Composition

Performance tips:

  • Cosine Palette: ~3-4 clock cycles (1 MAD + 1 COS + 1 MAD)
  • HSV/HSL conversion: fully branchless using mod/abs/clamp vectorization
  • Multi-harmonic band-limited filtering: fwidth() + smoothstep adds ~2 extra instructions to eliminate aliasing
  • Lch pipeline ~57 instructions; if you only need "slightly better than RGB", use iLerp (~15 instructions) instead
  • sRGB approximation pow(c, 2.2) has <0.4% error and optimizes better in the compiler

Common combinations:

  • Cosine Palette + SDF Raymarching: normals/distance/attributes as t input
  • HSL/HSV + Data Visualization: iteration count -> hue, saturation/brightness encode other dimensions
  • Cosine Palette + Fractals/Noise: length(uv) or fbm(p) + iTime driving dynamic colors
  • Blackbody + Volume Rendering/Fire: temperature field -> blackbodyPalette() -> physically plausible colors
  • Linear space workflow: sRGB decode -> linear shading -> tonemap -> sRGB encode
  • Hash + Palette + Tiling: hash(tileID) as palette input for unified color harmony

Further Reading

For complete step-by-step tutorials, mathematical derivations, and advanced usage, see reference