What a machine computes

A ground and a sky share an elevation, not a distance

Read from its own attribute — the ground's rate and weight, the sky's direction — each side of a rasterised horizon reports the elevation of its pixels' rays to 2.2 millionths of a radian, in 32-bit arithmetic as in 64, so a haze painted by elevation crosses the edge without a seam. A fog by distance cannot, and should not: the ground a row below a walker's horizon is 1.2 kilometres away, and at thirty kilometres of visibility the fog steps by 0.85 in one row. A camera's pixel, averaging its area, records a step of 0.69.

Worth reading first: The divide is postponed, not avoided · A texture does not interpolate on the page.

Ground and sky meet at the horizon without a crack put a ground of triangles and a sky of triangles against one another along a row of shared horizon vertices, rasterised them as a graphics processor does — positions snapped to a fixed-point grid, every pixel given to the one triangle whose edges hold its centre — and found no pixel lost and none claimed twice, at any roll and any snapping. The two sides met exactly along their edge. What they carried did not match: the ground carries its texture as a rate and a weight, the second falling to zero at the horizon, and the sky carries a direction. A texture meant to run continuously from one into the other — a haze, a fog band, a gradient painted across the horizon — has to be computed separately on each side.

The essay ended by asking whether the two sides could at least agree on something they both know how to compute. Both know the elevation of a pixel’s ray above the horizon: the ground through its own geometry, the sky directly. If that agrees across the edge, a haze that depends only on elevation can be painted across the horizon by two different attributes without a seam.

It agrees, to the snapping. And the quantity a fog is usually written in, distance, turns out not to be one the two sides could ever share.

The elevation each side reports

The ground’s attribute at a pixel is a rate and a weight, interpolated over the triangle in the way that makes their ratio the ground point the pixel sees — the divide is postponed, and a texture does not interpolate on the page unless it is carried this way. The ray through that pixel meets the ground at a distance, and its elevation is a depression: the negative of the angle whose tangent is the eye’s height over that distance. Written that way it divides by the distance, which grows without bound near the horizon; written with the weight on top — the eye’s height times the weight, over the length of the rate — it goes smoothly to zero as the weight does, and needs no special case at the edge. The sky’s attribute is a direction, carried the same way over the triangle, and its elevation is simply the angle of that direction above the level.

The scene is the earlier essay’s: an eye 1.62 metres up, 740 pixels of focal length, the camera rolled three degrees, the horizon a row of level directions shared by fourteen ground quads and fourteen sky quads, every position snapped to 1/256 of a pixel. For every pixel within six rows of the horizon, the figure takes the elevation its owner’s attribute reports and subtracts the true elevation of the ray through that pixel’s centre.

Ground and sky each report the elevation of their pixels' rays to 2.19 µrad at 1/256 px snapping — in 32 bits as in 64 — so a haze painted by elevation crosses the horizon without a seamEvery pixel within six rows of the horizon, the camera rolled 3°, positions snapped to 1/256 px: the elevation of its ray above the horizon as its owner's own attribute gives it — the ground's from the rate and weight it carries, as the depression of the ground point, with the weight written on top so that it goes to zero rather than dividing; the sky's from the direction it carries — less the true elevation of the ray through the pixel's centre. Ground pixels dark, sky pixels light. The worst difference is 2.187 µrad in 64-bit arithmetic and 2.189 µrad in 32-bit, against a pixel's 1352 µrad; the dashed band is the half-diagonal of the snapping grid over the focal length, 3.73 µrad. The two sides agree at the edge to the snapping, which moved the vertices, and not to the arithmetic. The slider changes the snapping.-4-2024-505the pixel's centre above (−) or below (+) the true horizon, pxelevation from the owner's attribute less the ray's (µrad)the snapping's boundskygroundroll 3°, snapped to 1/256 px, every tenth pixel drawnµrad; a pixel is 1,350
Fig. 1 Every pixel within six rows of the horizon: the elevation its owner reports, less its ray’s, in µrad. Ground dark, sky light. At 1/256 px snapping the worst is 2.19 µrad in 64-bit arithmetic and in 32-bit, inside the snapping’s bound of 3.73 µrad; a pixel is about 1,350 µrad. The slider changes the snapping.

Every pixel on both sides reports its ray’s elevation to within 2.19 millionths of a radian. A pixel is about 1,350 millionths. The two sides of the edge agree, pixel by pixel, to about a six-hundredth of a pixel, and there is nothing at the horizon that a quantity computed from elevation could step across.

The error is not the arithmetic’s. Single precision gives the same 2.19 as double — unlike the depth that the precision a depth buffer has left measured running out near the far plane: the weight is small near the horizon, but it multiplies the eye’s height and divides nothing, so its relative rounding stays at single precision’s one part in sixteen million and the elevation’s absolute error stays near a ten-millionth of a radian. The error comes from the snapping. Every vertex position was moved by up to half a grid step before the triangle was drawn, and the attributes were computed for the unmoved vertices, so every pixel’s attribute belongs to a ray a little off its own; the dashed band is half the grid’s diagonal over the focal length, and every point lies inside it. The slider shows the proportion: at 1/16 of a pixel the band is sixteen times wider and the worst error 32 millionths of a radian; at 1/65,536 of a pixel the band is 256 times narrower and the worst error nine thousand-millionths.

So the earlier essay’s question has a clean answer. A haze whose strength depends only on the elevation of the ray — how much air a line of sight passes through, for an atmosphere of fixed depth, depends on nothing else — can be computed from the ground’s rate and weight below the horizon and from the sky’s direction above it, and the two agree at the edge to the snapping. The rasteriser does not make a seam, and the attributes do not either.

Why the weight belongs on top

How the ground’s elevation is written matters at exactly one kind of pixel, and the earlier essay found that kind. A pixel whose centre falls precisely on the snapped horizon edge goes, by the top-left rule, to one side or the other, and for some rolls it goes to the ground — with an interpolated weight of exactly zero, since the weight is zero along the whole of that edge. Written as the depression of a ground point at distance rate over weight, that pixel’s elevation is a division by zero: its distance is infinite, or, in floating point, an infinity whose arc-tangent happens to come out right, or a zero over zero that does not.

Written with the weight on top, the eye’s height times the weight over the rate’s length, the same pixel’s elevation is the arc-tangent of zero over a finite number: exactly zero, the elevation of the horizon, which is where its centre is. Nothing downstream needs to know that the pixel was a special case. A haze that reads the ground’s elevation this way can be evaluated on every ground pixel the rasteriser produces, including the ones that sit on the edge, and the same form gives the sky’s side nothing to reconcile.

Fog by distance is a different quantity

The fog most renderers draw is not written in elevation. It is written in distance: a point D metres away is hidden by a share one minus e to the minus D over L of the view, with L the visibility. The ground knows its distance, from its rate and weight. The sky is at infinity and fully fogged. At the horizon the ground’s distance runs off to infinity too, so the fog ought to approach one from the ground’s side as it does from the sky’s.

It does approach one. It does it in less than a pixel.

Fog by distance falls from the sky's 1 to 0.06 within one row below the horizon at 20 km of visibility: the ground a row down is only 1.2 km awayThe fog a ground and sky share if it is fog by distance, 1 − e^(−D/L), against rows below the horizon, for visibilities L of 300 m, 1.2 km, 5 km, 20 km; the eye 1.62 m up with 740 px of focal length, so a ray d rows below the horizon meets the ground about 1199/d metres out. The sky is at infinity and fully fogged. A row below the horizon the ground is 1.20 km away and fogged 0.98, 0.63, 0.21, 0.06; the whole rise from there to the sky's 1 happens inside the last row, over the 0.060 px nearest the horizon at 20 km. The curve is continuous; a pixel is too coarse to follow it once L is longer than f·h, 1199 m.00.2500.5000.7501-20246rows below the horizon (negative: sky)fog by distance, 1 − e^(−D/L)visibility 300 mvisibility 1.2 kmvisibility 5 kmvisibility 20 kmeye 1.62 m up, f = 740 pxthe last row holds the rest
Fig. 2 Fog by distance against rows below the horizon, for visibilities of 300 m, 1.2 km, 5 km and 20 km. A row below the horizon the ground is 1.2 km away and fogged 0.98, 0.63, 0.21 and 0.06; at 20 km the whole rise to the sky’s 1 happens in the 0.06 px nearest the horizon.

A ray a row below the horizon meets the ground about fh/dfh/d metres out — the focal length times the eye’s height over the rows below the horizon — which is 1,199 metres for this eye and this lens. So the ground a single row below the horizon is only a kilometre and a fifth away. At three hundred metres of visibility that is well fogged, 0.98, and the step to the sky’s 1 is invisible. At twenty kilometres it is fogged 0.06, nearly clear, and all the rest of the rise to 1 happens between that row’s centre and the horizon itself: the ground’s distance passes twenty kilometres only in the last 0.06 of a pixel.

The fog is a continuous function of the elevation, as every quantity the ground carries is. What it is not is a function a pixel can follow. Its rise from the ground’s value a row down to the sky’s is spread over the rows in which the ground’s distance runs from about L to infinity, and there are fh/Lfh/L of them. When the visibility is shorter than fhfh, about 1,200 metres here, the rise takes more than a row and the rasteriser draws it. When the visibility is longer, the rise lives inside the last row, and the row samples it once.

The step, measured along the horizon

The figure above follows one column. Along the whole horizon the rolled camera puts every column’s last ground pixel at a different depth below the edge, and the step from the last ground pixel to the first sky pixel varies with it.

The step in a distance fog at the horizon is 0.10 at 1 km of visibility and 0.85 at 30 km; averaging the pixel's area lowers it to 0.69, not to nothingAlong the whole horizon, the camera rolled 3° and snapped to 1/256 px, the mean difference per column between the fog of the sky pixel nearest the horizon and of the ground pixel nearest it, fog by distance with visibilities 100 m, 300 m, 1 km, 3 km, 10 km, 30 km, 100 km. Sampled at the pixel's centre, as a fragment is: 0.000, 0.002, 0.101, 0.374, 0.678, 0.848, 0.940. Averaged over 64 points of the pixel's square, as a camera's pixel averages: 0.000, 0.005, 0.114, 0.361, 0.587, 0.689, 0.731. The step is not a fault of the shared edge. It is the distance to the ground in the last row, which is finite, against the sky's, which is not — and the averaged pixel, the one a camera would record, keeps most of it.100 m300 m1 km3 km10 km30 km100 km00.2500.5000.7501visibility (log scale)the step in fog from the last ground row to the first sky rowsampled at the centreaveraged over the pixeleye 1.62 m up, roll 3°, the whole horizona finite ground against an infinite sky
Fig. 3 The mean step per column from the last ground row’s fog to the first sky row’s, over the whole horizon. Sampled at pixel centres: 0.10 at 1 km of visibility, 0.37 at 3 km, 0.68 at 10 km, 0.85 at 30 km, 0.94 at 100 km. Averaged over 64 points of each pixel: 0.11, 0.36, 0.59, 0.69 and 0.73.

Sampled at pixel centres, as a fragment shader samples, the step is a tenth at a kilometre of visibility, a third at three, two-thirds at ten and 0.85 at thirty. A horizon drawn under a thin haze has a sharp line along it: ground a kilometre away and mostly clear, sky fully fogged, nothing between.

The obvious repair is the one a pixel is not a point always prescribes: a pixel is an area, so average the fog over the area rather than sampling it at the centre. The dashed curve does that, with sixty-four points spread over each pixel’s square and each point’s fog read from whichever side owns it. It helps less than one might expect. At thirty kilometres the step falls from 0.85 to 0.69, at a hundred from 0.94 to 0.73. The last ground row’s pixels are mostly ground a kilometre or two away, and averaging over them does not change that; it only lets the sliver of sky and the sliver of very distant ground at the top of the pixel contribute their share.

And the averaged value is the right one. A camera photographing a flat plain under twenty kilometres of haze records exactly this: the ground a pixel below its horizon really is a kilometre away and really is nearly clear, and the sky above it really is the full brightness of the haze. That is why a real horizon is visible through haze at all. The step in the rendered picture is not a fault of the shared edge or of the attributes; it is the picture being right about distance, which jumps from a kilometre to infinity in one row.

Where the step goes away

The step depends on how much distance the last row holds, and that is set by the eye’s height as much as by the lens.

At 10 km of visibility the step at the horizon is 0.59 from a standing eye and 0.00 from 150 m up, where the fog's rise spreads over 11 rowsThe pixel-averaged step in a distance fog of 10 km visibility between the last ground row and the first sky row, the camera rolled 3°, for eyes 1.62, 5, 15, 50, 150 m above the ground: 0.587, 0.378, 0.128, 0.007, 0.000. The fog's rise from the ground's value to the sky's happens over the rows in which the ground's distance runs from about L to infinity, and there are f·h/L of them: 0.12, 0.37, 1.11, 3.70, 11.10 rows. From about 15 m up the rise is at least a row wide and a pixel can follow it; a standing eye puts it all inside a tenth of a row. A flight or a drone renders its horizon's haze continuously; a walker's horizon steps.1.625155015000.2500.5000.7501the eye's height above the ground, metres (log scale)the step in fog at the horizon, pixel-averaged0.12 rows0.37 rows1 rows4 rows11 rowsvisibility 10 km, pixel-averaged, roll 3°the rise spreads over f·h/L rows
Fig. 4 The pixel-averaged step at 10 km of visibility for eyes 1.62, 5, 15, 50 and 150 m above the ground: 0.59, 0.38, 0.13, 0.007 and 0. The rise spreads over 0.12, 0.37, 1.1, 3.7 and 11 rows. From about 15 m up a pixel can follow it.

From a standing eye at ten kilometres of visibility the step is 0.59, and the fog’s rise to the sky fits inside an eighth of a row. From five metres up — an upper window — it is 0.38. From fifteen metres the rise spans a whole row and the step falls to 0.13; from fifty it is under a hundredth; from a hundred and fifty metres, an aircraft on approach, the rise spreads over eleven rows and there is no step at all. A flight simulator’s horizon hazes continuously and a walker’s does not, and the reason is one ratio: the eye’s height times the focal length, over the visibility.

The saw-tooth along a rolled horizon

There is one artefact in the rendered step that a camera would not record, and it is the one a viewer notices first in motion.

Along a rolled horizon the last ground row's fog, sampled at its centres, jumps by 0.082 from one column to the next on average; averaged over each pixel, by 0.061Columns 100 to 400 of the horizon, the camera rolled 3° so that the horizon crosses a row every 19 columns, fog by distance at 5 km of visibility: the fog of each column's ground pixel nearest the horizon. Sampled at the pixel's centre (solid), it follows where that centre happens to fall below the horizon — a ground 1.2 km away when the centre is a row down, arbitrarily far when it grazes the edge — and jumps by 0.082 a column on average. Averaged over 64 points of each pixel (dashed), the sky's share of the pixel enters too and the jump is 0.061. Each time the horizon crosses a row the last ground pixel changes, and both readings saw-tooth with it; the centre-sampled one also carries the fall of every centre that nearly touches the edge.00.2500.5000.7501100200300400column along the horizonfog of the ground pixel nearest the horizonsampled at the centreaveraged over the pixelvisibility 5 km, roll 3°, 1/256 pxthe last row saw-tooths
Fig. 5 Columns 100 to 400 of the horizon at 5 km of visibility, the camera rolled 3° so the horizon crosses a row every 19 columns: the fog of each column’s last ground pixel, sampled at its centre (solid) and averaged over it (dashed). The centre-sampled value jumps by 0.082 a column on average, the averaged one by 0.061.

With the camera rolled three degrees the horizon crosses a row of pixels every nineteen columns, and each crossing changes which pixel is the last ground pixel in its column. Just after a crossing, the last ground pixel’s centre is almost a whole row below the horizon, and it sees ground 1.2 kilometres away; just before the next crossing, its centre grazes the edge and sees ground arbitrarily far away. So the last row’s fog runs in a saw-tooth along the horizon, rising towards one and dropping back once every nineteen columns. Sampled at centres it jumps by 0.082 a column on average; averaged over each pixel, where the sky’s share of the pixel rises as the pixel’s own fog falls, by 0.061.

Both saw-tooth, because both are honest about each pixel’s own ground. What the averaged one removes is the spike when a centre nearly touches the edge, where a centre-sampled pixel reports a ground a hundred kilometres away that fills a thousandth of it. In a moving picture those spikes are the shimmer along a hazy horizon as the camera rolls. They are the horizon’s version of the texture sparkle a texture reaches the horizon as a rate described, and they have the same remedy: sample the area, not the point.

What the edge shares

Put together, the two sides of a rasterised horizon share their edge exactly, as the earlier essay found, and they share the elevation of their pixels’ rays to the snapping, as this one finds. Any quantity that depends only on the direction of the ray — elevation, azimuth, the angle to the sun, the length of air a line of sight crosses in an atmosphere of fixed depth — can be computed from the ground’s rate and weight below the horizon and from the sky’s direction above it, and painted across the edge without a seam. A sky is carried as a direction was the half of this that made the sky’s side possible; a point at infinity is an ordinary vertex was the half that let the ground reach the same edge.

What they cannot share is anything that depends on distance, because the ground’s distance a row below the horizon is finite and small, and the sky’s is not. A renderer that wants a fog to meet its sky has two honest choices. It can write the fog in elevation, which is physically right for atmospheric haze seen from low down and meets the sky exactly; or it can write it in distance, which is right for a ground fog, keep the step, and average each pixel’s area so that the step is the one a camera would record rather than a shimmering one.

What was assumed

The ground is flat and level. The elevation of a ground point is read from its distance by the eye’s height, which is true only for a level plane. A ground that rises towards the horizon — a hillside, the curve of the earth over tens of kilometres — has a horizon of its own below the level one, and the sky and the ground then do not share an edge at all; what fills the gap is the subject of the ground stopped short figure of the earlier essay.

Fog is a simple exponential of distance. Real atmospheric scattering depends on the density of the air along the line of sight, which thins with height, so a line of sight’s optical depth is a function of its elevation and its length together. The pure-elevation haze and the pure-distance fog are the two ends of that, and a physically based sky model lies between them.

The pixel’s area is its square. A camera’s pixel integrates over a footprint set by its lens and its sensor, not a square, and a renderer’s anti-aliasing filter is wider than a square. Either spreads the horizon’s step over more than one row, which would soften it further than the averages here.

The attributes are exact at the vertices. They are computed in double precision for the unsnapped vertices and then interpolated over the snapped triangles. A renderer that computes them from the snapped positions would have attributes consistent with its own triangles, and its elevation errors would be the arithmetic’s alone — about a ten-millionth of a radian in single precision.

Still open: whether an elevation haze and a distance fog can be blended across the edge

The two honest choices above are ends of a range, and a real scene wants both: a ground fog that thickens with distance near the eye, and an atmospheric haze that depends on the line of sight’s elevation far away.

The measurement that settles whether they can be combined without a step writes the fog as a blend — distance-based below some angle below the horizon, elevation-based above it, the two joined smoothly over a band of rows — and asks how wide the band has to be before the combined fog’s step at the horizon, averaged over each pixel, is smaller than the least step a viewer can see, and how far the blend then departs from the true distance fog on ground a few hundred metres away. If a band of two or three rows is enough, a renderer can have a physically honest fog near the eye and a seamless horizon at once; if it needs tens of rows, the choice between them is real and has to be made per scene.

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.

Clip spaceHomogeneous coordinatesHorizonpoint at infinityProjective mapSampling grid