A texture does not interpolate on the page
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 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:
with the depth at one end, the depth at the other, the fraction along the world segment and the fraction along the drawn one.
The direction is worth reading off, because it is the opposite of most people’s first guess. At — the middle of the drawn edge — the world parameter is , 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 : 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 is being halved.
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 is . Differentiating with :
Setting that to one gives , and there
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: , so yes, exactly.
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:
Both the numerator and the denominator are linear in , so both can be stepped by a constant amount per pixel. One division per pixel buys exactness — the gate measures the residual at , which is the noise floor.
Why those two? Because is what a projection makes linear across the page. That is the same fact that the depth buffer stores an affine function of , 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 . The depth ratio is 1, the closed form gives , and the change of parameter is the identity: 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 .
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 the two routes are the same function, and what separates them numerically is the rounding of a division that cancels algebraically — one ulp, . 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.
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.
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.
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 into equal pieces in the world gives each piece a ratio of roughly , so the worst error per piece falls as
For : 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.
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:
with 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 is the interpolated value of — 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.
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.
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.
- What happens behind the eye — both name camera matrix, clip space, demonstration, homogeneous coordinates
- A map along, and a picture across — both name cross ratio, midpoint, projective invariant
- A straight line in a scroll is a hyperbola — both name demonstration, foreshortening, projective invariant
- A carpet and the people on it — both name demonstration, foreshortening
- A centre and a measure are exclusive — both name demonstration, midpoint
- A lens destroys the invariant — both name cross ratio, projective invariant
Named objects
A flat tag is an object no other essay names yet.
Camera matrixClip spaceCross ratioDemonstrationDepth divisionForeshorteningHomogeneous coordinatesMidpointProjective invariantTransversal