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)repeatsntimes around the circle: the seven petals above. - Wedges and kaleidoscopes. Fold the angle with
fmodorabsto 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
atan2the 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
- Center, distance & aspect ratioMake the circle truly round by correcting for the aspect ratio.
- Polar coordinatesBuild the spinning pinwheel: wedge pattern sin(a × 6 + time × 2), clipped to a disc of radius 0.4 with a…
- Rotation & matricesSpin the box: rotate p by an angle equal to time (in radians) before computing the distance.
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.