Ground and sky meet at the horizon without a crack
Worth reading first: The divide is postponed, not avoided · A texture does not interpolate on the page.
A sky is carried as a direction completed a pair of results. A ground drawn to infinity carries its texture as a rate and a weight, a coordinate at its points and a rate along its directions; a sky drawn as triangles whose every vertex is at infinity carries the direction itself. Each is exact for the same reason, that the attribute is linear in the vertex’s homogeneous coordinates. The essay ended at the place the two meet. A ground triangle and a sky triangle sharing an edge along the horizon share two vertices, both directions, and a rasteriser decides which pixels along that edge belong to which triangle from the vertices’ positions on the page — positions that a graphics processor rounds before it decides anything.
The question was whether that meeting is a seam: whether pixels fall into a crack between the two triangles, or are painted by both, and whether the ground’s texture coordinate stays finite on every pixel the ground is given, since its weight goes to zero at the horizon. The answer is that the horizon is an ordinary shared edge, with one exact exception and one sliver that rounding makes, and that the crack renderers are used to seeing there comes from somewhere else.
A strip along the horizon
The scene below is a ground of triangles running from points just in front of the eye up to a row of level directions, and a sky of triangles running from the same directions up to directions sixty degrees above the horizon. The two share every horizon vertex. Each vertex goes through the matrix with no far plane and the divide, the camera is rolled about its axis, and every position is snapped to a fixed-point grid, 1/256 of a pixel here, as a rasteriser’s set-up does before it evaluates a single edge. A pixel belongs to a triangle when its centre lies strictly inside it, or on an edge the top-left rule assigns to that triangle.
Across the whole picture, in a band ten rows either side of the horizon, not one pixel is covered by neither triangle and not one by both. The horizon behaves exactly as the edge between two ground triangles does.
That is what the rasterisation rules were designed for, and it is worth saying why they apply here. A rasteriser tests a pixel against a triangle with three edge functions, each computed from two of the triangle’s vertex positions. Two triangles that share an edge compute that edge’s function from the same two positions, one of them in the opposite order, so the two functions are exact negatives of each other: a centre strictly on one side of the edge is strictly outside the other triangle. A centre exactly on the edge is the only case left, and the top-left rule gives it to exactly one of the two. None of that cares whether the vertices are points or directions. A point at infinity is an ordinary vertex found that the matrix draws a direction by the same multiplication as a point; the rasteriser, which sees only the resulting page positions, cannot tell them apart either.
The marked pixel is the one thing the strip shows that an ordinary shared edge does not. Its centre lies four ten-thousandths of a pixel on the sky’s side of the true horizon, and the ground owns it, because the snapped edge passes just above it. Drag the roll and the strip is redrawn round the horizon at each setting: at 2.5 and 19 degrees another ground centre sits on the sky’s side, at 5.5, 8 and 14.5 none does, and at every stop the count of lost and doubled pixels is nought.
Rolls and snappings
A single roll proves little. The tally below runs the same test over sixty rolls, from half a degree to thirty, at four snappings.
At every snapping, over sixty rolls and 387,341 ground pixels, the count of lost and doubled pixels is zero. That is the guarantee the shared edge gives, and it holds however coarse the grid: a rasteriser that snapped positions to a sixteenth of a pixel would draw a coarse horizon, but not a cracked one.
What the snapping does change is where the edge is. A vertex snapped to a grid moves by up to half a grid step, so the edge between two snapped vertices can lie up to half a step from the true horizon. Pixel centres that fall in that sliver go to whichever side the snapped edge puts them. At a sixteenth of a pixel, 197 ground pixels over the sixty rolls have their centres on the sky’s side of the true horizon; at a 256th, 14; at a 65,536th, none. Each is a pixel of sky painted with ground, or the reverse — the horizon drawn a fraction of a pixel out of place — and each is drawn with the ground’s texture at a weight very close to zero.
The coordinate that grows without bound
The ground carries its texture as a rate and a weight, the second of which falls to zero at the horizon, and the texture coordinate is the rate over the weight. A texture reaches the horizon as a rate found that coordinate exact half a pixel from the horizon. Nearer than that it keeps growing.
The weight falls linearly to zero at the horizon, so the coordinate grows as one over the distance: 1,265 checks divided by the distance in pixels, on this camera, which is 127 checks ten pixels below the horizon and 1,265 at one pixel. Every ground pixel has a positive weight and a finite coordinate. How large the largest one is depends only on how close a pixel centre happens to fall to the edge, and that is set by the roll: at three degrees one centre falls 0.0018 pixels below it and carries 786,000 checks; at 11.3 degrees the stray pixel of the strip carries 4.7 million.
Nothing is wrong with those numbers. They are the true texture coordinates of the ground seen through those pixels’ centres — a centre a thousandth of a pixel below the horizon really does look out along the ground more than a million metres — and a coordinate that large is only a problem if something has to hold it.
What single precision loses, and where
A graphics processor interpolates in single precision, which holds 24 bits of a number. A texture coordinate of a million keeps only about an eighth of a check of fractional precision; one of eight million keeps none.
The comparison that matters is not the rounding against a check but the rounding against the patch of ground a pixel covers. Near the horizon a pixel covers an enormous stretch of ground — the coordinate changes by about itself divided by the distance, per pixel — so the pixel’s footprint grows faster than the coordinate. Single precision rounds the coordinate by a relative 2⁻²³, and so it rounds by at most of the footprint anywhere in the band. A mipmapped lookup reads a level whose texels are as large as the footprint, and never sees the rounding. A point-sampled lookup that close to the horizon reads an arbitrary check whatever the precision, because one pixel spans hundreds of millions of them. A pixel is not a point is the reminder that a pixel is an area; here the area does the work, and precision is lost only where the texture has already been averaged away.
A centre exactly on the horizon
The one case the rasterisation rules do not make harmless is a pixel centre that lies exactly on the horizon edge. Its edge function is exactly zero, the top-left rule gives it to one of the two triangles, and if that triangle is the ground its interpolated weight is exactly zero too.
The figure places a point exactly on each segment of the snapped horizon — the midpoint of two snapped vertices lies on a finer grid, so the edge function there is exactly zero — and asks which triangle owns it. The rule gives a shared edge’s points to the triangle that runs the edge up the page, and the ground and the sky run the horizon in opposite directions. With the camera level or rolled one way, the sky owns every such point. Rolled the other way, the ground owns every one, and there its weight is zero and its texture coordinate is a rate divided by nothing.
In nearly two million ground pixels over three hundred rolls, not one pixel centre fell exactly on the edge. The case needs a centre and an edge to coincide on the fixed-point grid, which a sloping edge rarely allows. It is rare, and it is not impossible — a level horizon snapped to exactly half-way between two rows of centres is the obvious way to meet it — and a renderer that wants to be safe gives the case to the sky explicitly, or clamps the ground’s weight away from zero before it divides. The divide is postponed found that the pipeline’s division by depth happens once and late; this is the one place where the division by the texture’s weight needs a guard of its own.
Where the crack comes from
Renderers that draw a ground and a sky separately are familiar with a seam along the horizon, and the measurement above says it is not the horizon’s. It comes from a ground that does not reach it.
A renderer with a far plane cannot draw a ground to infinity, so it draws it to a large finite distance and puts the sky at the horizon. The ground’s far edge then lies below the horizon by the depression of a point that far away — the camera’s focal length in pixels times its height over the distance — and every pixel between that edge and the horizon belongs to neither triangle. From an eye 1.62 metres up, a ground stopped a kilometre out leaves 1.27 rows per column uncovered; ten kilometres out, an eighth of a row. From thirty metres up, the height of a low drone, the same kilometre leaves 23 rows. The crack follows exactly, and it is filled in practice by extending the ground with a skirt, fogging it into the sky’s colour, or drawing the sky lower than the horizon. One plane is nearly free found that removing the far plane costs the depth buffer almost nothing; this is what it buys at the horizon. Giving the ground’s far vertices — directions, shared with the sky — closes the crack exactly, at every height.
What a renderer should do about the edge
Three things follow for anyone drawing a ground and a sky this way, and none of them costs much.
The first is to share the horizon’s vertices rather than duplicate them. The whole guarantee rests on the two triangles computing their common edge from identical snapped positions; two copies of one direction, transformed separately and rounded separately, can differ in the last bit and open a crack a fraction of a pixel wide that shows as a line of background colour along the horizon. One vertex, referenced by both triangles, cannot disagree with itself.
The second is to guard the division. The ground’s texture coordinate is a rate over a weight, and the weight on the pixels the ground owns is positive in every case but one, the centre exactly on the edge with the camera rolled so that the rule gives it to the ground. Winding cannot prevent it: which triangle a shared edge’s points go to is decided by the direction the edge runs on the page, and that turns over as the camera rolls, whatever order the triangles’ vertices were listed in. A shader that clamps the weight away from zero before dividing — to anything smaller than the weight at the nearest centre that can occur, which for a 256th-of-a-pixel grid is far below a thousandth — turns the one division by zero into a very large finite coordinate, which the mipmapped lookup then reads as the ground’s far average like every other pixel near it.
The third is to leave the sliver alone. The centres that fall between the snapped edge and the true horizon are painted with ground on the sky’s side, or with sky on the ground’s, but the ground’s texture there is the average of millions of checks and its colour is the colour the ground fades to; a pixel of that painted over a sky whose own colour at the horizon is usually the same haze is not a defect anyone can see. A finer snapping grid shrinks the sliver, from 197 such pixels at a sixteenth of a pixel to 14 at a 256th and none at a 65,536th over the sixty rolls, and that is a property of the hardware rather than of the scene.
What none of the three touches is the scene’s geometry. The ground still reaches the horizon, the sky still starts there, and the rasteriser still tiles the meeting as it tiles any shared edge; the guards are about an attribute that is allowed to be zero and a grid that is allowed to be coarse.
What the horizon is to a rasteriser
Where parallel lines meet is the reminder that the horizon is a line in the picture and not an edge in the scene: nothing stands there, and nothing is special about it except that the ground’s directions and the sky’s directions both land on it. The measurements here say the rasteriser agrees. It sees two sets of triangles sharing an edge, and it tiles them as it tiles any mesh — no pixel lost, none doubled, at any snapping.
What the horizon adds is two small things, both about the ground’s attribute rather than its coverage. Rounding lets the ground claim a sliver of centres just past the true horizon, which a finer grid shrinks and never quite removes; and a centre exactly on the edge, given to the ground, divides by a weight of exactly zero. The first is invisible, since those centres’ texture is the far average of the ground. The second is a guard a renderer must write. A texture does not interpolate on the page derived the perspective-correct recipe the whole sequence rests on; it divides by an interpolated quantity, and at the horizon that quantity is allowed to be zero.
What was assumed
The rasteriser follows the top-left rule on snapped positions. Every modern graphics interface specifies such a rule, and the shared-edge guarantee comes from it; a software rasteriser that tests centres with a tolerance, or rounds each triangle’s positions separately, can crack or double along any shared edge, the horizon included.
The shared vertices are one vertex. Both triangles read the horizon vertex from one entry in the vertex buffer, so they snap it identically. A ground and a sky built from separately computed directions — the same direction written twice, with different rounding — can disagree in the last bit and leave a crack of their own.
The camera does not roll about the horizon’s own direction exactly. The roll only decides which side owns centres on the edge and where the sliver falls; a roll of exactly zero with a horizon snapped to a half-row would put a whole row of centres exactly on it, which is the case the owner figure warns about.
Still open: whether a sky and a ground can share their texture as well as their edge
The ground carries a rate and a weight; the sky carries a direction. Along the horizon they meet at the same vertices with different attributes, so a texture that should run continuously from the ground into a sky — a fog band, a haze, a gradient painted across the horizon — has to be computed separately on each side, and the two sides need not agree at the edge.
The measurement that settles it defines one quantity both attributes can deliver — the elevation of the pixel’s ray above the horizon, which the ground’s rate and weight give through the ground’s geometry and the sky’s direction gives directly — computes it on the pixels either side of the shared edge, and asks whether it is continuous across the edge to the format’s precision, or jumps. If it is continuous, a haze that depends only on elevation can be painted across the horizon by two different attributes without a seam; if it jumps, the seam the rasteriser does not make, the attributes do.
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 clip space, homogeneous coordinates, point at infinity, projective map
- A point and a line are one object — both name homogeneous coordinates, point at infinity, projective map
- An angle is a cross-ratio — both name homogeneous coordinates, horizon, point at infinity
- The conic a circle becomes — both name horizon, point at infinity, projective map
- Three conics are one conic and a choice of horizon — both name horizon, point at infinity, projective map
- Two lines at infinity — both name horizon, point at infinity, projective map
Named objects
A flat tag is an object no other essay names yet.
Clip spaceHomogeneous coordinatesHorizonPlane at infinitypoint at infinityProjective mapSampling grid