Skip to content
Fundamental · Color mathBeginner

Palettes & ramps

Turn one 0–1 value into a color with multi-stop ramps, cosine palettes or a gradient strip texture.

How it works

Most shaders compute a single number first, a distance, a noise value, a height, a dot product, and only then decide what color it should be. A palette is that last step: a function from a value t between 0 and 1 to a color. Keeping it separate means one pattern can be recolored a dozen ways, and the color choices stay in one place.

Two stops and more

The smallest palette is a mix between two colors. For more stops, split the 0–1 range into segments and mix within each one. Three stops take one choice:

vec3 ramp3(vec3 a, vec3 b, vec3 c, float t) {
    return t < 0.5 ? mix(a, b, t * 2.0) : mix(b, c, t * 2.0 - 1.0);
}

Any number of evenly spaced stops fit in an array. Scale t to the number of segments, take the whole part as the segment and the fractional part as the position inside it:

const vec3 STOPS[4] = vec3[4](
    vec3(0.02, 0.01, 0.05),  // night
    vec3(0.45, 0.05, 0.20),  // plum
    vec3(0.95, 0.40, 0.10),  // ember
    vec3(1.00, 0.90, 0.60)); // flame

vec3 ramp(float t) {
    float x = clamp(t, 0.0, 1.0) * 3.0;   // 3 segments between 4 stops
    int i = int(min(floor(x), 2.0));       // last segment includes t = 1
    return mix(STOPS[i], STOPS[i + 1], x - float(i));
}

Swap the mix for a smoothstep of the position to soften the corners where segments meet, or for step to get flat bands, a cheap posterized look.

Cosine palettes

A cosine palette, a form popularized by Inigo Quilez, describes a whole color scheme with four vec3 parameters and no stops at all. Each channel follows its own cosine wave over t:

vec3 cosinePalette(float t, vec3 offset, vec3 amp, vec3 freq, vec3 phase) {
    return offset + amp * cos(6.2831853 * (freq * t + phase));
}

Read the parameters per channel, as a wave from the periodic-functions page: offset is the center the channel swings around, amp how far it swings, freq how many cycles it makes as t goes from 0 to 1, and phase where in its cycle it starts. Giving the three channels different values is what produces hues; with the same four values on every channel they move together and the result stays gray.

// a sunset ramp: deep red through orange to pale yellow
vec3 sunset = cosinePalette(t, vec3(0.70, 0.485, 0.325), vec3(0.30, 0.465, 0.275),
                               vec3(0.5), vec3(0.50, 0.45, 0.40));

Keep offset ± amp inside 0–1 on every channel and the palette never clips. Because the waves repeat, t can run past 1: feed in a distance or a scrolling t + time * 0.1 and the colors cycle smoothly forever. A palette with freq of 1 on every channel loops seamlessly over each unit of t.

Gradient strips

When an artist has already painted the gradient, store it as a texture one texel tall and read it with t as the horizontal coordinate:

// at file scope:
uniform sampler2D uRamp; // N×1 texels, clamp to edge
// inside main():
vec3 color = texture(uRamp, vec2(t, 0.5)).rgb;

Linear filtering blends between texels, and nearest filtering gives hard bands, which suits palettes for pixel art. Clamp to edge keeps the two ends from blending into each other. With linear filtering the first and last half texels are flat; to spread the stops evenly from end to end, read at (t * (N - 1.0) + 0.5) / N instead. A strip costs a texture read but can hold any curve, and a taller texture can store several palettes, one per row, picked by a second coordinate.

Which values to pick

Palette colors are usually chosen by eye, as you would in a paint program, so they are encoded sRGB values. In a shader that works in linear values, convert them before use (the gamma page shows how), or every color on the ramp, the ends included, comes out lighter and more washed out than the one you picked. The cards on this site do that for color controls. Mixing in OKLab instead of RGB keeps the brightness of a ramp even along its length; the color-spaces page covers that conversion.

Further reading