Skip to content
Fundamental · CoordinatesBeginner

Polar coordinates

Describe a point by its angle and its distance from a center, for anything radial.

How it works

UV coordinates describe a point by how far right and how far up it is. Polar coordinates describe the same point by how far it is from a center and in which direction: a radius and an angle. Anything that repeats around a center, such as petals, rays, a clock hand, a swirl or a radar sweep, is easy to write in polar form and awkward in x and y.

From x, y to angle and radius

Start from a centered, aspect-corrected coordinate p, so the center is (0, 0). The radius is the vector’s length, and the angle comes from atan with two arguments:

float r = length(p);      // 0 at the center
float a = atan(p.y, p.x); // -PI..PI, 0 pointing right (+x), counterclockwise

The two-argument atan(y, x) looks at the signs of both components, so it knows which quarter of the circle the point is in and returns the full range from -π to π. The one-argument atan(p.y / p.x) only sees the ratio, can’t tell opposite directions apart, returns -π/2 to π/2, and divides by zero on the vertical axis. HLSL calls the two-argument form atan2, with the same order: y first. At the exact center, where both are zero, the GLSL result is undefined; it rarely matters, because the radius there is zero anyway.

An angle in radians is hard to feed into other functions, so remap it to 0..1:

const float PI = 3.14159265;
float t = a / (2.0 * PI) + 0.5; // 0..1 once around, starting on the -x axis

Shapes from angle and radius

In polar form, a circle is “radius below a constant”. Let the limit depend on the angle and the circle grows petals:

#version 300 es
precision highp float;

uniform vec2 uResolution;
out vec4 fragColor;

void main() {
    vec2 p = (2.0 * gl_FragCoord.xy - uResolution) / uResolution.y;
    float r = length(p);
    float a = atan(p.y, p.x);

    float edge = 0.55 + 0.2 * cos(5.0 * a);          // five petals
    float flower = 1.0 - smoothstep(edge - 0.01, edge + 0.01, r);
    fragColor = vec4(vec3(flower), 1.0);
}

The whole number in cos(5.0 * a) sets the petal count. It has to be whole: the angle jumps from π to -π on the left side, and only a whole number of repeats lines up across that jump. A fractional count leaves a visible seam there.

Back to x and y

Converting back is r * vec2(cos(a), sin(a)). The useful pattern is: convert, change the angle or the radius, convert back, and use the result as a new UV. Adding an amount to the angle that grows toward the center twists the image into a swirl:

float twist = 2.0 * (1.0 - smoothstep(0.0, 1.0, r)); // strongest at the center
vec2 q = r * vec2(cos(a + twist), sin(a + twist));   // sample or draw with q instead of p

The seam

The angle is discontinuous on the -x axis, where it jumps from π to -π (or t from 1 to 0). Anything that depends on the angle directly, like a texture lookup with t as a coordinate, can show a line there. Repeating patterns with a whole number of repeats hide it, as above. Texture lookups along the seam need more care, because the GPU estimates how far the coordinate moves between neighboring pixels, and the jump makes that estimate huge; the screen-space derivatives fundamental explains why and how to work around it.

Further reading