What a machine computes

A texture does not interpolate on the page

Walking across a drawn surface at a constant rate walks across the real one at a rate that changes, and the worst gap is a closed form in the depth ratio alone — 0.52 at ten to one, more than half the whole range. It is exactly the error a person makes dividing depth by eye, made by a machine, and the fix is the fourth coordinate the pipeline kept.

Worth reading first: The divide is postponed, not avoided · Dividing depth by eye.

A surface being drawn carries things that vary across it. Where in a texture each point sits, which way the surface faces, what colour was assigned at each corner. A renderer walks the drawn shape pixel by pixel and has to know, at each pixel, what those quantities are.

The obvious way to do it is to step them by a constant amount per pixel, which takes an addition and no divisions. It is also wrong, and the size of the wrongness is not small: for a surface receding from 2 m to 20 m, the worst error is 0.52 of the whole range of whatever is being interpolated. Not five per cent. Fifty-two.

The gap between walking the page and walking the surfaceFor a surface receding from 2 m to 20 m, the departure peaks at 0.5195 of the whole range — over half of it — at s = 0.7597. The closed form is (√k−1)/(√k+1) with k the depth ratio, and the marked point is where it says the peak is.-0.400-0.200000.2000.4000.6000.8001position across the drawn surfacehow far along the real surface, minus how far along the drawn one(√k−1)/(√k+1) = 0.5195at the page's midpoint, 40.9%depth ratio 10 : 1peak 0.5195 at s = 0.760
Fig. 1 The gap between how far along the drawn surface a point is and how far along the real one, for a surface receding 2 m to 20 m. It peaks at (√k−1)/(√k+1) = 0.5195 at s = 0.7597, and the closed form is checked at its own stationary point rather than against a grid — sampling near a peak is a statement about the sample count.

The change of parameter

A straight world segment images as a straight page segment. That much is a projection’s defining property and this site has leaned on it since its foundation.

What does not survive is the parametrisation. Walk the world segment at a constant rate and the drawn point does not move at a constant rate; walk the drawn segment at a constant rate and the world point does not. The relation between the two is one line, and every result in this essay is a consequence of it:

s=t/a(1t)/b+t/a,t=asb(1s)+ass = \frac{t/a}{(1-t)/b + t/a}, \qquad t = \frac{a\,s}{b(1-s) + a\,s}

with aa the depth at one end, bb the depth at the other, tt the fraction along the world segment and ss the fraction along the drawn one.

The direction is worth reading off, because it is the opposite of most people’s first guess. At s=12s = \tfrac12 — the middle of the drawn edge — the world parameter is a/(a+b)a/(a+b), which for a receding edge is less than a half. The middle of the drawn edge is nearer than the middle of the real one. For an edge from 2 m to 20 m, the page’s midpoint is only 9.1% of the way along the world segment, and it sits at world depth 3.64 m.

3.64 m is 2220/222 \cdot 2 \cdot 20/22: the harmonic mean of the two depths. That is the same quantity the depth buffer’s midpoint turned out to be, for the same underlying reason — something linear in 1/z1/z is being halved.

The midpoint of one segment, under both familiesThe parallel projection places it exactly halfway (3e-14 px out). The perspective projection places it 39 px away from halfway, 12% of the drawn length.halfway along the drawn linethe actual midpointcorrect from 26 cm, at 160 mm wide39 px apart
Fig. 2 The site’s oldest version of this fact, from the parallel field. A parallel projection preserves the midpoint of a segment exactly; a perspective projection does not, and the drift is what the whole comparison between the two systems is built on. What a renderer interpolating in screen space does is put a texture’s midpoint where the perspective midpoint is, which is precisely the wrong place.

The closed form, and where its peak is

The error made by assuming t=st = s is t(s)s|t(s) - s|. Differentiating with k=b/ak = b/a:

dtds=k(ks(k1))2\frac{\mathrm{d}t}{\mathrm{d}s} = \frac{k}{\big(k - s(k-1)\big)^2}

Setting that to one gives s=k/(k+1)s^\star = \sqrt{k}/(\sqrt{k}+1), and there

tsmax=k1k+1\big|t - s\big|_{\max} = \frac{\sqrt{k}-1}{\sqrt{k}+1}

The maximum depends on the depth ratio and on nothing else. Not on the focal length, not on the orientation of the surface in the picture, not on how large the surface is drawn. Two surfaces spanning the same depth ratio make the same worst error whatever else is different about them.

depth ratio worst error at
2 : 1 0.1716 s = 0.586
4 : 1 0.3333 s = 0.667
10 : 1 0.5195 s = 0.760
20 : 1 0.6345 s = 0.817

At a depth ratio of four the error is exactly one third, which is a pleasant enough coincidence to be worth checking: (41)/(4+1)=1/3(\sqrt4 - 1)/(\sqrt4+1) = 1/3, so yes, exactly.

A striped surface, interpolated across the surface and across the pageThe quad recedes from 2 m to 20 m. The dark stripe boundaries are at equal intervals along the surface, which is what the surface has; the light ones are at equal intervals across the drawn quad, which is what a renderer without the divide produces. The worst boundary is 101 px out.depth ratio 10 : 1 — worst boundary 101 px outcorrect from 16 cm, at 160 mm widethe two sets meet only at the ends
Fig. 3 The same thing as a picture rather than a curve. The dark stripe boundaries are at equal intervals along the surface; the light ones are at equal intervals across the drawn quad, which is what screen-space interpolation produces. The two sets meet only at the ends, and everything between is the error the curve plots.

The fix, and why the pipeline was already carrying it

The correction is to interpolate two quantities that are linear in screen position, and divide at the very end:

u(s)=(1s)u0/a+su1/b(1s)/a+s/bu(s) = \frac{(1-s)\,u_0/a + s\,u_1/b}{(1-s)/a + s/b}

Both the numerator and the denominator are linear in ss, so both can be stepped by a constant amount per pixel. One division per pixel buys exactness — the gate measures the residual at 101610^{-16}, which is the noise floor.

Why those two? Because 1/z1/z is what a projection makes linear across the page. That is the same fact that the depth buffer stores an affine function of 1/z1/z, that the horizon is the image of the ground plane’s line at infinity, and that a picture with no size–distance signal cannot be read for depth. The reciprocal of depth is the quantity a perspective picture is linear in, and every convenience in this pipeline is somebody exploiting that.

Which is the third reason the divide is postponed. Clipping needs the fourth coordinate’s sign; edge interpolation needs its linearity; and this needs its value, at the very last step, per pixel. The first essay of this field sets out all three together, and this is the one that reaches furthest down the pipeline.

The control, and why the bug survives testing

Set a=ba = b. The depth ratio is 1, the closed form gives (11)/(1+1)=0(\sqrt1 - 1)/(\sqrt1+1) = 0, and the change of parameter is the identity: t=st = s exactly.

So a renderer with no perspective correction at all draws a fronto-parallel surface perfectly. A wall square to the camera, a heads-up display, a piece of user interface, a billboard turned to face the viewer — all exact, to one part in 101610^{16}.

That is not a footnote. It is the reason the bug is famous rather than obvious, and it has a distinctive signature: a system looks entirely correct until somebody turns a surface away from the camera, and then it is dramatically wrong. There is no gentle degradation, because the error is governed by the depth ratio and a surface square to the camera has a depth ratio of exactly one.

The gate asserts this as a control rather than mentioning it, and the assertion had to be weakened once for an honest reason: at a=ba = b the two routes are the same function, and what separates them numerically is the rounding of a division that cancels algebraically — one ulp, 1.1×10161.1 \times 10^{-16}. Writing === 0 passed on the first draft only because the sweep happened to miss the value that rounds, which is a check of the sample list rather than of the claim.

Seven identical spheres across a 84° frameThe outer sphere images 27% wider than the central one. That is what a correct rectilinear projection does, and it vanishes if the picture is viewed from 9 cm.54 px69 px84° across27% wider at the edge
Fig. 4 A related fact from the viewing field, which sharpens what “fronto-parallel” is doing here. A sphere at the edge of a wide frame is stretched, and the stretch is a property of the angle off axis. Depth ratio and angle off axis are different quantities and only the first governs interpolation error: a surface far off axis but square to the camera interpolates exactly, and a surface dead ahead but steeply slanted does not.

The same error, made by a person

This site already has an essay about a human being making exactly this mistake, and it was written three phases before there was any machine in the subject.

Dividing depth by eye measures what happens when somebody spaces the transversals of a receding pavement by judgement rather than by construction. The commonest by-eye method is to space them at a constant ratio, and the essay’s finding is that a constant ratio is not what perspective does — the drift accumulates and the depths implied by the drawn spacings run away from the depths a camera would produce.

The very simplest by-eye method is to space them equally on the page, and that is screen-space interpolation exactly. Same assumption, same error, same closed form. The only difference is that a person doing it produces a slightly wrong drawing of a pavement, and a renderer doing it produces a texture that swims.

Three by-eye methods for spacing a receding row, against the truthThe posts are 1.4 m apart. The nearest by-eye method misplaces one by 11.72 m; the worst by 347.80 m.horizonequal stepshalve thetaper byworst error, in metres of depthequal steps to the horizon11.72 mhalve the remaining gap225.13 mtaper by eye347.80 mcorrect from 26 cm, at 160 mm wide34° across
Fig. 5 The human version. Depths recovered from a row of transversals spaced by three different by-eye recipes, against what the camera actually produces. The equal-on-the-page recipe is the one this essay’s error is: it puts the page’s midpoint at the world’s midpoint, and the world’s midpoint is not there.

There is a genuinely interesting difference in what the two mistakes reveal. The person’s version was, historically, a method — an attempt to get perspective without computing it, and one that is easy to defend because the picture looks nearly right. The machine’s version is a shortcut for speed, and it looks nearly right for the same reason. In both cases the defence is aesthetic and the refutation is a number.

Two versions of the same invariant, one of which measures nothingFour consecutive divisions give the equal-steps method a perfect score. Using the vanishing point as the fourth point rejects it by 14%.error against the value the projection must producefour divisionsthree plus the VPthe projectionexactexactequal stepsexact14%halving3.6%25%tapering0.9%21%green: agrees with the projectiona necessary condition is not a test
Fig. 6 And the refutation, which is the same one in both cases. The cross-ratio of four points, with a vanishing point as the fourth, distinguishes a projected row from an equally-spaced one — the check the wrong field earned. An equally-spaced texture coordinate fails exactly this test, and it is the only test that catches it.

Subdivision, and why it nearly works

Before the divide-at-the-end correction became universal, the standard workaround was subdivision: cut the surface into smaller pieces, interpolate linearly across each one, and rely on each piece spanning a small depth ratio.

It is worth pricing, because the closed form gives the answer immediately and the answer explains both why the technique was used and why it was abandoned.

Splitting a surface spanning ratio kk into nn equal pieces in the world gives each piece a ratio of roughly k1/nk^{1/n}, so the worst error per piece falls as

k1/2n1k1/2n+1\frac{k^{1/2n} - 1}{k^{1/2n} + 1}

For k=10k = 10: one piece has an error of 0.519, two pieces 0.290, four pieces 0.152, eight 0.077, sixteen 0.039. Halving the error costs a doubling of the geometry, for ever.

That is the classic shape of a workaround that is good enough at first and never good enough later. Four subdivisions get the error below a sixth, which on a coarse display is invisible; getting it below a hundredth needs about sixty-four, which is sixty-four times the vertex work to avoid one division per pixel.

And subdivision has a defect the arithmetic does not show. The error does not vanish at the joins — it vanishes at the ends of each piece, so the reconstructed function is continuous and its slope is not. A subdivided surface has a kink at every join, and a moving camera sweeps those kinks across the surface. The visible result is not blur but a faint ripple, which is harder to attribute and easier to argue about than the swimming it replaced.

The gap between walking the page and walking the surfaceFor a surface receding from 2 m to 4 m, the departure peaks at 0.1716 of the whole range — over half of it — at s = 0.5858. The closed form is (√k−1)/(√k+1) with k the depth ratio, and the marked point is where it says the peak is.-0.150-0.100-0.050000.2000.4000.6000.8001position across the drawn surfacehow far along the real surface, minus how far along the drawn one(√k−1)/(√k+1) = 0.1716at the page's midpoint, 16.7%depth ratio 2 : 1peak 0.1716 at s = 0.586
Fig. 7 The subdivision argument as a curve. At a depth ratio of two — which is what each piece of a ten-to-one surface cut in three sees — the worst error is 0.172, a third of what one piece gives. The curve’s shape is why the technique works and why it stops working: the error falls with the ratio and the ratio falls only as a root of the piece count.

What is being interpolated does not matter

One thing worth saying plainly, because the standard name for this — perspective-correct texture mapping — makes it sound like a fact about textures.

Nothing in the derivation mentions what is being carried. It is a statement about the change of parameter between a world segment and its image, so it applies identically to a texture coordinate, a surface normal, a per-vertex colour, a depth value, a light intensity, or any other quantity that varies linearly across the real surface.

That generality is why the fourth coordinate is worth carrying rather than special-casing. One correction, applied to every varying quantity, at the point where they are all being stepped anyway.

And it is why the error’s signature is so recognisable when it appears: everything a slanted surface carries is wrong together, in the same direction, by the same amount — so the surface does not look noisy, it looks like it is sliding.

The barycentric version, and the one thing it adds

Everything so far is about an edge. A surface is a triangle, and the correction across a triangle is the same statement in three terms rather than two:

u=iλiui/ziiλi/ziu = \frac{\sum_i \lambda_i\,u_i / z_i}{\sum_i \lambda_i / z_i}

with λi\lambda_i the barycentric coordinates measured on the page. Set two of the three to zero and it collapses to the edge formula, which is the check that the generalisation is the same object.

One thing is worth extracting from the three-term version that the two-term version hides. The denominator λi/zi\sum \lambda_i / z_i is the interpolated value of 1/z1/z — so a renderer computing perspective-correct anything is, as a side effect, computing the depth at that pixel, in exactly the form the depth buffer wants it. The reciprocal depth is not an extra quantity carried for the correction; it is the same number the depth test needs, and the correction is what makes it available.

That is a satisfying piece of economy and it also explains a historical accident. Early hardware that interpolated depth linearly in screen space and textures linearly in screen space was internally consistent and wrong about both, in the same way, for the same reason — and fixing either one required the other, because they share the divisor.

The code a depth buffer stores, and the code a linear map wouldDepth is stored as an affine function of 1/z, so the codes are spent near the eye: half of them are gone by 0.80 m. The straight line is a linear depth map over the same range, whose midpoint is the arithmetic mean at 500 m.00.2500.5000.75010123distance from the eye — log₁₀ metresfraction of the buffer's codes used uphalf the codes by 0.80 ma linear map, for comparisonnear 0.4 m, far 1000 mharmonic mean 0.80 m against arithmetic 500 m
Fig. 8 The quantity the correction shares with the depth test. A depth buffer stores an affine function of 1/z; perspective-correct interpolation divides by an interpolated 1/z. Both are exploiting the one thing a projection makes linear across the page, and a renderer that computes one has computed the other.

Why the depth ratio is the whole story

It is worth ending on the one substantive surprise here, which is how little the error depends on.

Not on the focal length: a wide lens and a long one make the same error on a surface spanning the same depth ratio. Not on the field position: a slanted surface at the edge of the frame and one in the middle behave identically. Not on the size of the drawn shape: a slanted surface filling the frame and one twenty pixels across make the same fractional error.

The depth ratio, and only the depth ratio. Which is a statement that this is a projective fact rather than an imaging one — it survives any change of intrinsics, any change of viewpoint that preserves the two depths, and any change of what is being carried across the surface. That is the same kind of statement as the cross-ratio surviving a projection, and it is worth having for the same reason: something that depends on almost nothing is something that can be relied on.

Four points on a line, before and after a projectionLength and the ratio of lengths do not survive the projection; the cross-ratio does, agreeing to 0e+0 relative.horizonABCDon the groundin the picturelength AB1.00011.3930ratio AB:CD0.56670.6837cross-ratio1.31681.3168correct from 26 cm, at 160 mm wide34° across
Fig. 9 The quantity that survives, one more time. Four points on a line, projected: their spacings change and their cross-ratio does not. Perspective-correct interpolation is what you get by insisting that the drawn surface and the real one agree about cross-ratios rather than about distances, and the naive version is what you get by insisting on distances.
A striped surface, interpolated across the surface and across the pageThe quad recedes from 2 m to 6 m. The dark stripe boundaries are at equal intervals along the surface, which is what the surface has; the light ones are at equal intervals across the drawn quad, which is what a renderer without the divide produces. The worst boundary is 29 px out.depth ratio 3 : 1 — worst boundary 29 px outcorrect from 16 cm, at 160 mm widethe two sets meet only at the ends
Fig. 10 And at a modest depth ratio, where the error is a third of what it was and still plainly visible. There is no depth ratio above one at which screen-space interpolation is right; there is only a ratio below which nobody complains.

What links here

Computed from the collection, not written here: the essays that point at this one.

Shares its objects with

Essays that name at least two of the same things, and that neither author linked.

Named objects

A flat tag is an object no other essay names yet.

Camera matrixClip spaceCross ratioDemonstrationDepth divisionForeshorteningHomogeneous coordinatesMidpointProjective invariantTransversal