Initial commit: add all skills files
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
380
shader-dev/techniques/color-palette.md
Normal file
380
shader-dev/techniques/color-palette.md
Normal file
@@ -0,0 +1,380 @@
|
||||
# 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
|
||||
```glsl
|
||||
// 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
|
||||
```glsl
|
||||
// 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)
|
||||
```glsl
|
||||
// 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
|
||||
```glsl
|
||||
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
|
||||
```glsl
|
||||
// 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
|
||||
```glsl
|
||||
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
|
||||
```glsl
|
||||
// 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
|
||||
```glsl
|
||||
#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:
|
||||
|
||||
```glsl
|
||||
// === 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)
|
||||
```glsl
|
||||
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
|
||||
```glsl
|
||||
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
|
||||
```glsl
|
||||
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
|
||||
```glsl
|
||||
// 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)
|
||||
```glsl
|
||||
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](../reference/color-palette.md)
|
||||
Reference in New Issue
Block a user