Shader concept
Cosine palettes: beautiful colour gradients in one line
A cosine palette makes a smooth, repeating colour gradient from one formula: a + b * cos(6.28318 * (c * t + d)). Change four vectors and you get a different set of colours.
Running live in your browser. The code below is exactly what draws it.
The formula
color(t) = a + b * cos(2π * (c * t + d)), where a, b, c and d are each a colour (three numbers). Because cosine goes up and down smoothly, so does each colour channel, and the three channels fall out of step with each other, which is what gives the rainbow-like shifts. The picture uses a = b = 0.5, c = 1 and d = (0, 0.33, 0.67), a full rainbow.
What each part does
- a is the middle brightness of each channel.
- b is how far each channel swings up and down (the contrast).
- c is how many times the palette repeats while
tgoes from 0 to 1. - d shifts each channel along the cycle. Offsets that differ between channels are what create the colour change.
Using it
Feed it any number: a noise value, a distance, an angle, or time. A palette turns that single number into a colour, so one function colours noise, distance fields and gradients alike. To get a calm palette, keep the swings small and the offsets close together; for strong rainbow colours use offsets spread a third apart.
Common mistakes
- Forgetting that the output can go below 0 or above 1 when
aandbdo not add up. Keepa ≥ banda + b ≤ 1, or clamp the result. - Using a huge
t. Cosine of very large numbers loses precision on some graphics cards. Keeptin a modest range, for example by takingfracof a time value.
The code, in every shader language
The same example for each language family. Open one to see it.
HLSL Unity, Unreal
float3 palette(float t)
{
return 0.5 + 0.5 * cos(6.28318 * (t + float3(0.0, 0.33, 0.67)));
}
float4 frag(float2 uv)
{
// a strip of the palette at the top, and the same palette used as a pattern below
float t = uv.x + time * 0.05;
float3 strip = palette(t);
float2 p = (uv - 0.5) * float2(aspect, 1.0);
float3 plasma = palette(length(p) * 1.4 - time * 0.2 + 0.2 * sin(p.x * 7.0 + time));
float3 col = lerp(plasma, strip, step(0.85, uv.y));
return float4(col, 1.0);
} GLSL Shadertoy, three.js, OpenGL
vec3 palette(float t)
{
return 0.5 + 0.5 * cos(6.28318 * (t + vec3(0.0, 0.33, 0.67)));
}
vec4 frag(vec2 uv)
{
// a strip of the palette at the top, and the same palette used as a pattern below
float t = uv.x + time * 0.05;
vec3 strip = palette(t);
vec2 p = (uv - 0.5) * vec2(aspect, 1.0);
vec3 plasma = palette(length(p) * 1.4 - time * 0.2 + 0.2 * sin(p.x * 7.0 + time));
vec3 col = mix(plasma, strip, step(0.85, uv.y));
return vec4(col, 1.0);
} WGSL WebGPU, Bevy
fn palette(t: f32) -> vec3f
{
return 0.5 + 0.5 * cos(6.28318 * (t + vec3f(0.0, 0.33, 0.67)));
}
fn frag(uv: vec2f) -> vec4f
{
// a strip of the palette at the top, and the same palette used as a pattern below
let t = uv.x + time * 0.05;
let strip = palette(t);
let p = (uv - 0.5) * vec2f(aspect, 1.0);
let plasma = palette(length(p) * 1.4 - time * 0.2 + 0.2 * sin(p.x * 7.0 + time));
let col = mix(plasma, strip, step(0.85, uv.y));
return vec4f(col, 1.0);
} Godot Godot 4 shading language
shader_type canvas_item;
vec3 palette(float t)
{
return 0.5 + 0.5 * cos(6.28318 * (t + vec3(0.0, 0.33, 0.67)));
}
vec4 frag(vec2 uv)
{
// a strip of the palette at the top, and the same palette used as a pattern below
float t = uv.x + TIME * 0.05;
vec3 strip = palette(t);
vec2 p = (uv - 0.5) * vec2(aspect, 1.0);
vec3 plasma = palette(length(p) * 1.4 - TIME * 0.2 + 0.2 * sin(p.x * 7.0 + TIME));
vec3 col = mix(plasma, strip, step(0.85, uv.y));
return vec4(col, 1.0);
}
void fragment()
{
// Godot's UV starts at the top-left, the lessons use bottom-left
COLOR = frag(vec2(UV.x, 1.0 - UV.y));
} Metal iOS, macOS
float3 palette(float t)
{
return 0.5 + 0.5 * cos(6.28318 * (t + float3(0.0, 0.33, 0.67)));
}
float4 frag(float2 uv)
{
// a strip of the palette at the top, and the same palette used as a pattern below
float t = uv.x + time * 0.05;
float3 strip = palette(t);
float2 p = (uv - 0.5) * float2(aspect, 1.0);
float3 plasma = palette(length(p) * 1.4 - time * 0.2 + 0.2 * sin(p.x * 7.0 + time));
float3 col = mix(plasma, strip, step(0.85, uv.y));
return float4(col, 1.0);
} Practise it in the lessons
- Cosine palettesMake a rainbow that scrolls: t = uv.x + time × 0.2.
- Colors are four numbersMatch the green. The Colors panel shows its hex code — convert it to floats.
Questions
Who invented cosine palettes?
The idea was popularised for shaders by Inigo Quilez in an article on procedural colour palettes. It is widely used on Shadertoy.
How do I pick good values?
Start from a = b = 0.5, c = 1, and try different offsets for d. Small changes in d shift the whole mood, so tweak one channel at a time.