Why is my circle an oval? UV coordinates and aspect ratio in shaders
Almost everyone's first shader circle comes out as an oval. Nothing is wrong with the circle code. The problem is the coordinates it was given, and once you see it you will spot the same mistake in other people's shaders too.
What UV actually is
A fragment shader is handed a position for each pixel, usually called uv. It runs from (0, 0) at the bottom left to (1, 1) at the top right. It is a fraction of the picture, not a count of pixels. Halfway across is 0.5 whether the picture is 100 pixels wide or 4,000.
That is exactly why UV is so useful: the same shader works at any resolution. But it hides a catch. Suppose the picture is 400 pixels wide and 300 tall:
- A step of
0.1inuv.xis 40 pixels sideways. - A step of
0.1inuv.yis only 30 pixels upwards.
So one unit of UV is a different physical length on each axis. Ask for "everything within 0.25 of the middle" and you get an area that is 200 pixels wide and 150 tall: an ellipse.
The naive circle
float4 frag(float2 uv)
{
float d = length(uv - 0.5); // distance from the middle
float inside = step(d, 0.25); // 1 inside, 0 outside
return float4(inside, inside, inside, 1.0);
}
On a 4:3 picture this draws a white oval. Subtracting 0.5 correctly moves the origin to the middle, but the two axes still disagree about how long a unit is.
The fix: scale x by the aspect ratio
The aspect ratio is width divided by height: 400 / 300 = 1.333. If we stretch the x coordinate by that amount, one unit means the same number of pixels in both directions:
float4 frag(float2 uv)
{
float2 p = (uv - 0.5) * float2(aspect, 1.0); // centered, and equal units on both axes
float d = length(p);
float inside = step(d, 0.25);
return float4(inside, inside, inside, 1.0);
}
Now p.y runs from -0.5 to 0.5 and p.x runs from -0.667 to 0.667 (on a 4:3 picture). A radius of 0.25 is the same length in every direction, so you get a circle. In this site's lessons aspect is provided for you. In Unity you would compute it from the screen size, and in Shadertoy from iResolution.
Order matters: center first, then scale
A very common slip is to scale first and subtract afterwards:
float2 wrong = uv * float2(aspect, 1.0) - 0.5; // the middle is no longer the middle float2 right = (uv - 0.5) * float2(aspect, 1.0); // the middle stays at (0, 0)
After uv * aspect, the horizontal middle sits at 0.667, not 0.5, so subtracting 0.5 leaves your shape drifting off to one side. Centering first and scaling second keeps the origin in the middle, which is where every symmetric shape (circles, rings, rotations) wants it.
When you should not correct for aspect
Aspect correction is for shapes that must keep their proportions: circles, squares, rotations. For things that should stretch with the picture it is wrong. A sky gradient, a vignette that follows the edges, or a background that fills the frame can use plain uv. A good habit is to keep both: uv for "where in the picture am I", and p for "where am I in a space with equal units".
A second way: work in pixels
Some people divide by the height only. With pixel coordinates fragCoord and resolution res:
float2 p = (fragCoord - 0.5 * res) / res.y;
This gives the same result as the version above: p.y runs from -0.5 to 0.5, and p.x is automatically wider by the aspect ratio. Shadertoy shaders use this form so often that it is worth recognising.
Check which way is up
In these lessons uv.y grows upwards. Some engines and image formats put the origin at the top-left instead, so your picture can appear upside down when you move a shader between tools. If a shape is mirrored vertically, try uv.y = 1.0 - uv.y before anything else. A quick test is to output float4(uv.y, 0, 0, 1): the red end tells you which edge is "top".
Quick checklist
- Circle comes out as an oval: multiply the x coordinate by the aspect ratio.
- Shape is off to one side: you scaled before you centered.
- Background looks wrong after correcting: it should probably use plain
uv. - Picture is upside down: flip
uv.y.
Try it yourself
The lesson UV: where am I? shows the coordinates as colors so you can see them. Center, distance and aspect ratio walks through exactly the oval-to-circle fix above, and Distance fields and antialiasing shows how to make the edge of that circle smooth.
Keep reading
- What is a fragment shader? A plain explanation with code
- HLSL vs GLSL vs WGSL vs Metal: shader languages compared
- How to learn shader programming: a step-by-step roadmap
- What is ray marching? Distance fields explained simply
- Noise in shaders: value noise, fBm and what they are for
- How to make an ocean shader: waves, Fresnel and foam
- step, lerp and smoothstep: the three functions behind most shader effects
- Signed distance functions explained: drawing shapes with math
- Porting a Shadertoy shader to Unity (GLSL to HLSL): a conversion guide
- How to debug a shader when you cannot print anything
- Why is my shader slow? A practical guide to shader performance