Math Playground
Back to Graphics

Graphics › Coordinate spaces

Screen space

The last hop: stretch the −1…1 NDC square across the window's pixels — and flip y, because on a screen the top row is row zero.

Coordinate spaces — follow a cube from mesh to pixels
xyz

Model space: the cube as the mesh file stores it — built around its own origin, no idea where it'll end up.

the cube's far-top-right corner here: (0.5, 0.5, 0.5)

The viewport transform

NDC runs from −1 to 1 and knows nothing about your monitor. The viewport transformmaps that square onto an actual rectangle of pixels — typically the whole window, but it can be a sub-rectangle (split-screen, a picture-in-picture minimap). For a viewport at (vx, vy) of size w × h:

x_screen = vx + (x_ndc + 1) / 2 · w
y_screen = vy + (1 − y_ndc) / 2 · h
z_screen = (z_ndc + 1) / 2  (→ the depth buffer, 0…1)

Why y is flipped

Maths puts +y up; screens, inherited from how CRTs scanned (top-left, left-to-right, top-to-bottom), put pixel (0,0) at the top-left with y growing downward. So the viewport transform negates y. Forget this and your scene renders perfectly — upside-down. (Texture coordinates have the same fight: OpenGL UV origin is bottom-left, almost every image file's is top-left.)

Sub-pixel positions and sampling

A vertex usually lands between pixel centres. The rasterizer keeps the fractional position and decides coverage from it — that's the basis of anti-aliasing (MSAA samples several points per pixel; analytic AA computes exact coverage). Rounding too early gives you the jaggies.

Pixels aren't the same as points

On high-DPI screens one CSS “point” is 2 or 3 device pixels, so the framebuffer is bigger than the logical window. The viewport is set in device pixels; UI layout is in points; the devicePixelRatio bridges them. Mixing the two is a classic “why is everything blurry / half-size” bug.