GPU Computing · Graphics Pipeline

Homogeneous Clipping and Why Rasterizers Use a Guard Band

How triangles are clipped against the view volume in clip space before the perspective divide, why clipping only the near plane is usually enough, and what the guard band buys.

Clipping is the step that turns a triangle that may extend far outside the visible region into one or more triangles that lie entirely inside it. It has to happen before rasterization, because the rasterizer’s coverage test is only meaningful on geometry whose projected coordinates are representable, and it has to happen in homogeneous clip space rather than after the perspective divide, because the divide is undefined on the plane where w is zero and numerically violent near it.

Why the divide cannot come first

A vertex leaving the vertex shader carries a four-component position in clip space. The perspective divide maps it to normalised device coordinates by dividing the first three components by the fourth. That operation is a projective transformation: it maps lines to lines and preserves incidence, but it does not preserve the interior of the clip volume in a way that can be tested before the division, because the clip volume itself is defined by inequalities involving w.

The OpenGL clip volume is the set of points satisfying

-w <= x <= w
-w <= y <= w
-w <= z <= w

with the additional requirement that w be positive for a point to be in front of the eye. Each of these six inequalities is a half-space in four-dimensional homogeneous space, and a triangle clipped against a half-space yields a polygon, not a triangle. Clipping in homogeneous coordinates therefore means intersecting a triangle with six half-spaces, which can produce a polygon with up to nine vertices in the worst case before triangulation.

Dividing first and clipping in three dimensions is not equivalent. The divide is a rational map; the image of a straight edge under it is a straight line in three dimensions only because the edge is a projective line, and the set of points with w <= 0 maps to the whole plane at infinity and beyond in a way that has no finite representation. A triangle with one vertex behind the eye has an edge that crosses w = 0, and points on that edge near the crossing project to coordinates of unbounded magnitude with sign flips. Clipping after the divide would require clipping against the plane at infinity, which has no finite normal.

Clipping against the near plane is the case that matters

In practice implementations clip only against the near plane, z = -w in OpenGL convention, and rely on other mechanisms for the other five. The reason is that the near plane is the only clip plane that can move a vertex to the wrong side of w = 0: the side planes constrain x and y relative to w, but a vertex with w > 0 and x far outside the guard band is still in front of the eye and still projects to a finite, if enormous, coordinate. Such a vertex can be handled by rasterizing with a wider coordinate range and discarding fragments outside the viewport, provided the range is bounded.

The near plane has no such escape. A vertex behind the eye has w < 0, and no finite screen coordinate represents it, so it must be clipped and the intersection vertex placed on the plane. A vertex on the plane itself still has a positive w — the near plane sits at the near distance, not at the eye — but a triangle whose vertices are only just in front of it projects to coordinates far outside any rasterizer grid, and edge functions computed from coordinates of that magnitude overflow fixed-point arithmetic. Implementations therefore place the near-plane test at a small epsilon in front of zero rather than at zero exactly. The epsilon is a compromise: too large and geometry very close to the eye is clipped away; too small and the projected coordinates of near-plane vertices overflow.

/* Clip one edge of a triangle against the near plane z + w >= 0.
   Returns 1 if the edge crosses the plane and fills `out` with the
   intersection point whose attributes are linearly interpolated. */
#include <stdbool.h>

struct vertex { float x, y, z, w; float attr[4]; };

static float near_dist(const struct vertex *v)
{
    return v->z + v->w;          /* >= 0 means inside */
}

bool clip_edge_near(const struct vertex *a, const struct vertex *b,
                    struct vertex *out)
{
    float da = near_dist(a), db = near_dist(b);
    if ((da < 0.0f) == (db < 0.0f))
        return false;            /* both inside or both outside: nothing to cut */

    /* Parameter along the edge where the plane is crossed. The
       interpolation is linear in clip space, before the divide. */
    float t = da / (da - db);
    out->x = a->x + t * (b->x - a->x);
    out->y = a->y + t * (b->y - a->y);
    out->z = a->z + t * (b->z - a->z);
    out->w = a->w + t * (b->w - a->w);
    for (int i = 0; i < 4; i++)
        out->attr[i] = a->attr[i] + t * (b->attr[i] - a->attr[i]);
    return true;
}

Interpolation in clip space is the correct place to do it. Because the divide is projective, a linear interpolation in clip space followed by the divide produces the perspective-correct result along the edge; interpolating after the divide would not. This is the same relationship that makes perspective-correct attribute interpolation necessary inside the triangle, applied one dimension up.

What clipping does to the primitive stream

Clipping a triangle against one plane can produce one triangle, two triangles, or nothing. Against several planes the output is a convex polygon, which must be triangulated before it can be rasterized, because the rasterizer’s edge functions assume exactly three edges. The classic algorithm is Sutherland-Hodgman: walk the polygon’s vertices, keep inside vertices, and emit an intersection vertex each time an edge crosses the plane. Its output for a triangle clipped against one plane is at most a quadrilateral, which splits into two triangles.

This changes the primitive count. A single input triangle straddling the near plane can become two, so the number of primitives reaching the rasterizer is not fixed by the number of vertices submitted. Implementations buffer the clipped output and feed the rasterizer from that buffer, and the geometry stage’s throughput is therefore measured in vertices in and primitives out rather than in input triangles.

Clipping also creates vertices that never existed in the vertex shader’s output. Their attributes are interpolated, which is fine for smooth varyings but has a subtle consequence for flat-shaded attributes: a flat attribute takes its value from the provoking vertex, and clipping a triangle does not change which vertex is provoking. An implementation that triangulates a clipped polygon and assigns provoking vertices by position rather than by provenance will produce a different flat value than one that tracks it, and the specification fixes the answer rather than leaving it to the implementation.

The guard band

Even with near-plane clipping, side clipping is expensive: it costs a polygon walk and a retriangulation for every triangle that touches the edge of the screen, and in a typical scene a large fraction of triangles are partially off-screen. The guard band removes most of that work by extending the region in which a triangle is rasterized without clipping.

A guard band is a rectangle in screen space, concentric with the viewport and larger than it, chosen so that any vertex whose projected position lies inside it produces edge functions that do not overflow the rasterizer’s fixed-point arithmetic. Triangles entirely inside the guard band are rasterized normally, with fragments outside the viewport discarded by the scissor rectangle — which is a cheap per-fragment test, not a per-primitive restructure. Only triangles with a vertex outside the guard band are clipped against the side planes.

The size of the guard band is a fixed property of the implementation, not something an application sets. It is bounded by the fixed-point range: if the edge function accumulates a signed 32-bit value over a coordinate span of S subpixel units, then S is limited by the number of bits available for the multiply and accumulate. A guard band a few times the viewport in each direction is typical, and geometry beyond it is clipped exactly. Applications cannot detect the boundary except by measuring where their clipping behaviour changes, and the specification does not require a particular size.

The interaction with clipping is worth stating precisely: a vertex inside the guard band is not clipped even if it is outside the viewport, and a vertex outside the guard band is clipped against the side planes even if the resulting triangle would have been entirely off-screen. So the clipping work is proportional to the number of primitives whose vertices stray far from the viewport, which for a scene with a wide field of view and near geometry is a small fraction.

Edge cases that survive the design

Three cases remain awkward. A triangle with all three vertices behind the eye clips to nothing and is discarded, which is correct but requires the clipper to handle an empty result rather than assuming at least one vertex survives. A triangle with exactly one vertex on the near plane produces a degenerate clipped edge of zero length; the rasterizer sees a triangle with zero area and produces no fragments, so the case is harmless as long as the zero-area test is exact rather than a floating-point comparison against a small threshold.

The third is the case of a triangle whose vertices are all in front of the eye but whose edges pass behind it, which is impossible for a straight edge in homogeneous space — a line segment between two points with positive w has positive w throughout, because w is linear along the segment. So a triangle with all vertices in front of the near plane is entirely in front of it, and the clipper never has to consider an edge that leaves and re-enters. That linearity is the property that makes near-plane clipping a per-edge test rather than a search, and it is why the near plane can be handled by the simple edge walk above while a curved surface would need something else entirely.

The guard band’s own boundary has one failure mode worth naming. A triangle with two vertices inside the guard band and one far outside is clipped against the side plane, and the resulting polygon may have a vertex exactly on the guard band boundary. If that boundary coordinate is not representable in the rasterizer’s subpixel grid, the snap can move the vertex inside by one grid unit, and the sliver of the triangle that lies between the true and snapped positions is lost. The visible result is a one-pixel gap at the extreme edge of the screen on geometry that extends far off-screen, which is why rasterization rules and the top-left fill convention insists that snapping happens on a grid the fill rule can reason about.

References

  1. The OpenGL Graphics System: A Specification, Version 4.6 (Core Profile) — Khronos Group
  2. Clipping (Direct3D 11) — Microsoft Learn
  3. Procedural Elements for Computer Graphics, Second Edition — David F. Rogers, McGraw-Hill 1997
  4. Jim Blinn's Corner: A Trip Down the Graphics Pipeline — Jim Blinn, Morgan Kaufmann 1996