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 t goes 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 a and b do not add up. Keep a ≥ b and a + b ≤ 1, or clamp the result.
  • Using a huge t. Cosine of very large numbers loses precision on some graphics cards. Keep t in a modest range, for example by taking frac of 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

Start with "Cosine palettes"

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.

Related concepts