Shader concept

Polar coordinates in shaders: angle and radius

Polar coordinates describe a point by its distance from the centre (radius) and its angle, instead of x and y. In a shader they make flowers, stars, spirals and kaleidoscopes simple.

Running live in your browser. The code below is exactly what draws it.

Converting

From x and y to polar: r = length(p) and a = atan2(p.y, p.x). Back again: p = r * float2(cos(a), sin(a)). In WGSL and GLSL the two-argument arctangent is spelled atan(y, x); in HLSL it is atan2(y, x).

Why it helps

  • Repetition around a point. A pattern that depends on cos(n * a) repeats n times around the circle: the seven petals above.
  • Wedges and kaleidoscopes. Fold the angle with fmod or abs to mirror it.
  • Spirals. Add the radius to the angle (a + r * k) and the pattern winds up.
  • Radial effects. Glows, ripples and vignettes depend only on r.

Common mistakes

  • The seam: the angle jumps from +3.14159 to -3.14159 on the left side. A pattern that is not a whole number of repeats around the circle shows a visible line there. Use whole multiples in cos(n * a).
  • Getting the arguments of atan2 the wrong way round. It takes y first, then x.

The code, in every shader language

The same example for each language family. Open one to see it.

HLSL Unity, Unreal
float4 frag(float2 uv)
{
    float2 p = (uv - 0.5) * float2(aspect, 1.0);
    float r = length(p);                       // distance from the centre
    float a = atan2(p.y, p.x);                 // angle, -3.14159 to 3.14159
    // a petal shape: the radius of the edge changes with the angle
    float edge = 0.22 + 0.08 * cos(7.0 * a + time);
    float shape = 1.0 - smoothstep(edge - 0.004, edge + 0.004, r);
    float rays = 0.5 + 0.5 * cos(a * 14.0 - time * 2.0);
    float3 col = lerp(float3(0.06, 0.07, 0.16) * (0.6 + 0.4 * rays), float3(1.0, 0.65, 0.3), shape);
    return float4(col, 1.0);
}
GLSL Shadertoy, three.js, OpenGL
vec4 frag(vec2 uv)
{
    vec2 p = (uv - 0.5) * vec2(aspect, 1.0);
    float r = length(p);                       // distance from the centre
    float a = atan(p.y, p.x);                 // angle, -3.14159 to 3.14159
    // a petal shape: the radius of the edge changes with the angle
    float edge = 0.22 + 0.08 * cos(7.0 * a + time);
    float shape = 1.0 - smoothstep(edge - 0.004, edge + 0.004, r);
    float rays = 0.5 + 0.5 * cos(a * 14.0 - time * 2.0);
    vec3 col = mix(vec3(0.06, 0.07, 0.16) * (0.6 + 0.4 * rays), vec3(1.0, 0.65, 0.3), shape);
    return vec4(col, 1.0);
}
WGSL WebGPU, Bevy
fn frag(uv: vec2f) -> vec4f
{
    let p = (uv - 0.5) * vec2f(aspect, 1.0);
    let r = length(p);                       // distance from the centre
    let a = atan2(p.y, p.x);                 // angle, -3.14159 to 3.14159
    // a petal shape: the radius of the edge changes with the angle
    let edge = 0.22 + 0.08 * cos(7.0 * a + time);
    let shape = 1.0 - smoothstep(edge - 0.004, edge + 0.004, r);
    let rays = 0.5 + 0.5 * cos(a * 14.0 - time * 2.0);
    let col = mix(vec3f(0.06, 0.07, 0.16) * (0.6 + 0.4 * rays), vec3f(1.0, 0.65, 0.3), shape);
    return vec4f(col, 1.0);
}
Godot Godot 4 shading language
shader_type canvas_item;

vec4 frag(vec2 uv)
{
    vec2 p = (uv - 0.5) * vec2(aspect, 1.0);
    float r = length(p);                       // distance from the centre
    float a = atan(p.y, p.x);                 // angle, -3.14159 to 3.14159
    // a petal shape: the radius of the edge changes with the angle
    float edge = 0.22 + 0.08 * cos(7.0 * a + TIME);
    float shape = 1.0 - smoothstep(edge - 0.004, edge + 0.004, r);
    float rays = 0.5 + 0.5 * cos(a * 14.0 - TIME * 2.0);
    vec3 col = mix(vec3(0.06, 0.07, 0.16) * (0.6 + 0.4 * rays), vec3(1.0, 0.65, 0.3), shape);
    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
float4 frag(float2 uv)
{
    float2 p = (uv - 0.5) * float2(aspect, 1.0);
    float r = length(p);                       // distance from the centre
    float a = atan2(p.y, p.x);                 // angle, -3.14159 to 3.14159
    // a petal shape: the radius of the edge changes with the angle
    float edge = 0.22 + 0.08 * cos(7.0 * a + time);
    float shape = 1.0 - smoothstep(edge - 0.004, edge + 0.004, r);
    float rays = 0.5 + 0.5 * cos(a * 14.0 - time * 2.0);
    float3 col = mix(float3(0.06, 0.07, 0.16) * (0.6 + 0.4 * rays), float3(1.0, 0.65, 0.3), shape);
    return float4(col, 1.0);
}

Practise it in the lessons

Start with "Center, distance & aspect ratio"

Questions

What is atan2?

It is the arctangent that takes both coordinates, so it can tell all four quadrants apart and returns an angle from -pi to pi. The one-argument atan(y / x) cannot.

Why does my pattern show a line on one side?

That is the angle wrapping around from pi to -pi. Make the pattern repeat a whole number of times around the circle so both sides match.

Related concepts