Part V · Light the camera can't find · Chapter 20
Dust in the air
So far light has travelled in straight lines from one surface to the next, and the air between has done nothing. Dust changes that: a sunbeam in a dusty room is visible because specks in the air turn a little of its light towards the eye. ShadyTracer has one box of dust, a haze box, inside which the air scatters. It is no material: a material scatters inside a solid, whose surface would stop the shadow rays that find a beam in air, and the box has no surface. This chapter is how a ray meets a particle, which way the light goes then, how a beam becomes visible, and how to check that dust neither makes nor loses light.
1Meeting a particle
This chapter's figures are in the flat world (this tutorial's 2D world, where light spreads round a circle instead of over a sphere, chapter 0), the 3D formulas beside them. A box of dust, 4 long. Its mean free path is 4: on average a ray goes 4 before it meets a particle. The sheet of rays in the sun's colour comes in from the left; the dots are where rays met one, and the rays with arrows passed through. The chance of getting a distance t into the dust untouched falls as e(−t/mfp), the transmittance: each bit of distance stops the same share of the rays that reach it, so what is left shrinks by the same factor every unit of distance, which is what an exponential does. The chance of crossing all 4 is e−1, 37%. A ray meets a particle at t when it got to t untouched and then meets one, so the meetings fall off with distance as that curve does. The chart is 4000 crossings: the distances at which they met a particle (bars) against that curve's prediction (line), and below it, 1453 passed where e−1 says 1472.
The distance is drawn directly, by free flight. Take a random number u from 0 to 1, set 1 − u = e(−t/mfp), and solve for t (ln, the natural logarithm, undoes e: ln(ex) = x): t = −mfp × ln(1 − u). A share e(−t/mfp) of the values of u give a distance past t, exactly the share of rays that get that far untouched, so the drawn distances are spread as the curve says. Then the weight. A ray that meets a particle goes on carrying the dust's albedo, the share the particle scatters (the rest it absorbs). One that meets none carries all of its light, unchanged: 37% of the rays get through carrying everything, which is 37% of the light, the transmittance, already, so nothing is multiplied in. The figure draws its distances with ShadyTracer's own function for it.
Mean free path 0.7: e(−4/0.7) is a third of a percent, and 9 of the 4000 crossings pass (13 expected). Thick dust is a wall.
Now one ray in ShadyTracer's way of moving, in dust of mean free path 2, a stone (the circle) in it and a floor (the slab) under it. A path is made of legs, the stretches between one event and the next (a surface, a particle). The march steps along a leg by the distance to the nearest surface (the circles: chapter 2), steps that know nothing of dust. The user who asked for dust described what to do in one line: "If a ray has moved XX, random chance it met a particle; if so move back some random YY distance and scatter." That is free flight. ShadyTracer draws the leg's distance to its particle as the leg starts and stores it in the ray; the march steps on, and on the first step that passes it, the ray is moved back along the line to that distance (the dot) and handed on as a point in the air. A surface met first ends the leg as it always did.
2Which way it goes
At a particle the light goes on in a new direction, drawn from the dust's phase function: how likely each direction is, against the one it came from. The shape round the point is that likelihood as a distance. With an asymmetry g of 0, every direction is as likely as any other, and the shape is a circle. The small blue dots are 240 directions drawn from it, on a circle round the point; the chart counts 3000 by their angle from straight on. Most are near 90°, not because the dust prefers it but because a sphere has more directions there than near its poles (the same reason as chapter 8's cosine).
Dust and fog scatter mostly forwards: g = 0.7, the haze box's own default. ShadyTracer uses the Henyey–Greenstein phase function (1941, written for dust between the stars), one number g setting its shape, which is also the average cosine of the angle it turns light through: the drawn directions' average cosine is in the readout. It is drawn exactly, from the inverse of its distribution (the fold-out), so a drawn direction carries weight 1.
A negative g sends light back towards where it came from. ShadyTracer holds g between −0.95 and 0.95: at 1 the shape is a single direction, and nothing could aim light at it (a delta, chapter 8). Switch on The flat world's too for the dashed shape the flat world's dust uses: Henyey and Greenstein's idea on a circle instead of a sphere, the same average cosine, a gentler shape. (The red shape is the 3D function's, drawn in the plane through the light's line; the dots are 3D directions drawn from it and laid on that plane by their angle from straight on.) The peak away from straight on at g = 0.7, about 14°, is the same effect as at 0: near straight on there are few directions on a sphere.
The mathsHenyey–Greenstein, and drawing from it
Per steradian, for the angle θ between the direction travelled and the direction scattered into:
p(θ) = 1 − g²4π (1 + g² − 2g cos θ)(3/2)
(the bracket to the power 3/2: cubed, then the square root). It adds up to 1 over the sphere, whatever g (the readout checks). Its cosine is drawn from one random number u: with s = (1 − g²) / (1 − g + 2gu),
cos θ = (1 + g² − s²) / (2g)
and at g = 0, where that divides by 0, simply cos θ = 1 − 2u, every direction alike (the code switches below |g| = 0.001). The angle round the old direction comes from a second random number, evenly. The flat world's version, per radian of a circle, is (1 − g²) / (2π (1 + g² − 2g cos θ)), drawn by tan(θ/2) = (1 − g)/(1 + g) × tan(π(u − ½)).
3A beam you can see
A dark room: a pale floor, walls and ceiling of albedo 0.2, a window in the right wall and the sun 35° up outside it. No dust. The sun lights a patch of floor (the dashed lines are the beam's edges), and in the picture the patch's two pixels, 27 and 28, read 0.120. The air across the beam is invisible: those pixels, 16 to 26, read 0.002, the patch's light bounced off the dark walls.
Dust fills the room: mean free path 6 in a room 10 wide, albedo 0.9, g 0.7. A path from the eye now meets particles (the dots), and at each one it draws its next direction from the phase function. The sun counts only if a drawn direction happens to leave through the window within the sun's half a degree: chapter 18's problem again. A sample is one path through every pixel, the strip their average, and ± the error bar (chapter 12). After 300 samples the pixels across the beam read 0.0038 ± 0.0003, two fifths of what they will read below, and pixel 46, on the floor near the eye, reads 0.19 ± 0.19: a single find (a look at fewer samples may not have it yet). The bounces are not wrong, only slow: given samples enough they read what aiming reads, but the error bar cannot show the light no sample has found yet. (The surfaces aim their shadow rays at the sun in both modes, chapter 14; only the points in the dust differ.)
So aim. At every point in the dust the path also sends a shadow ray towards the sun (in the shadow rays' colour), as a surface does (chapter 14), with the phase function where a surface has its f cos θ (its material's share of light turned towards the eye: chapter 8), and dimmed by the transmittance of the dust it crosses on the way. The beam appears: across it the pixels read 0.0096 ± 0.0006, two and a half times the 0.0038 the bounces alone had found, where the pixels outside the beam read about 0.003. The patch reads 0.035, dimmer than without dust (the sunlight lost some on its way in and more on its way to the eye), but brighter than the 0.025 of the bounces only: the eye's line to the floor runs through lit dust, and that glow is found by aiming too. ShadyTracer does both at every point in the dust, the aimed ray and the drawn direction. Adding the two would count the sun twice, so it weighs them by MIS (chapter 15: each way's find gets a weight, the two weights adding to 1, more to whichever way finds that light more easily).
With g = −0.5 the dust sends light back the way it came, out of the window, and little towards the eye: the beam reads 0.0047.
With a mean free path of 1, the dust stops the light within a few units of the window and dims what the eye sees of it: the beam reads 0.0013, the patch 0.0014. A beam shows best in dust thin enough to let it through and looked at towards the light, which is why this room is dark and the eye looks towards the window.
4Checking it adds up
Chapter 8's white furnace, with dust: a white disc in a box of white dust (albedo 1, mean free path 1.5), all of it in a sky of brightness 1 in every direction, and nothing else. Nothing absorbs and nothing glows but the sky, so whatever the dust does, every pixel must read 1. It does, and with no noise at all: every path carries exactly 1 out to the sky, however many particles it meets. (Here a path finds the sky only by leaving the box. ShadyTracer also aims shadow rays at the sky from the air, each a random guess, so its furnace is a little noisy and its test reads the picture's mean.)
A test that cannot fail tests nothing, so the test has two controls that must fail. A black disc absorbs what reaches it: the picture reads 0.558.
Dust of albedo 0.5 absorbs half at every particle: 0.447. Both well under 1, so the furnace's 1 meant something.
Now turn the eye to the sky, the black disc still there. The picture reads 1.0000 with no noise, exactly as the passing furnace did. This is what ShadyTracer's first run of its own dust test did on the user's card. Its sky is 1000 bright, so a passing row reads about 1000; its camera, aimed at the view's own target instead of one it named, looked at nothing but sky, and every row read 1000.000244 with noise 0, the controls with them. The controls are how it was caught; the noise of 0 was a clue too, since ShadyTracer's own furnace is a little noisy (above), and only a picture of sky alone is not.
5How ShadyTracer does it
The user's sketch, and what it became:
The distance, drawn as the leg starts, and why it moved there:
...and where the march has passed it:
The phase function's draw:
Light sampling from the air (the function's comment: ) ends as a surface's does, the phase function in the place of f cos θ; its last three lines:
And every shadow ray is dimmed by the dust it crosses:
Where the dust is off, and why:
6Tricks and trade-offs
Each is stamped with how it is known; point at a stamp to see what it means.
It adds up. The bench's furnace (a 3D scene of its own, so its numbers are not the flat
figure's) (Tests > haze: a white sphere in a box of albedo-1 dust 3.2 m
wide, mean free path 1.5 m, a sky 1000 bright) on the user's Pascal card, 819 × 1031, 32 samples, version 0.62.2.
Each row is read against the same scene with no box, which read 999.768: the box hidden +0.0002%, the dust with
light sampling off +0.002%, with it on +0.024%, +0.015% and −0.010% at g 0.6, 0 and −0.5. The
controls, as shares of that reading, 0.152 (a black sphere) and 0.4315 (dust of albedo 0.5), the 3D test's own
controls: a sphere filling its frame takes out more of the sky than the flat figure's disc does, so they are
not expected to match the flat world's 0.558 and 0.447, only to be well under 1 (docs/TESTING.md,
the haze row).
What it costs. In the same run, as wall-clock time for the row: the haze's code with no dust
1.2 s, as without a box; with the dust 1.5 to 1.6 s; with the dust and light sampling off 0.7 s
(docs/TESTING.md). Each particle met is a bounce, so the bench allows 40.
A particle is a diffuse bounce. Max Diffuse Bounces (chapter 13) counts a point in the dust as it counts a matte wall, so at 0 the dust scatters light towards the eye once and no more, single scattering: a camera path that meets a particle with no diffuse bounce left ends there ().
Only air. A ray in glass or water inside the box never meets a particle (its medium's index is
not 1): the user asked for dust that "does not add scattering in transparent materials" (haze.js's header). In
Sketch and the Optics view the dust is off; in Draft a path ends at its first point in the dust, so only light
scattered once is seen (mediumVertex). A material can scatter inside itself, milky glass (version
0.82.0), and that is the same free flight and the same phase function, but no vertex: inside a solid every shadow
ray is stopped by its own wall, so a collision there only turns the ray, and the march does it and marches on
().
What the glass scatters reaches the eye through the photon map instead (chapter 19): photons are stored where
they collide, and a camera ray's first collision, handed out of the march, gathers them
().
A scene without a box pays nothing. The haze's code goes into the kernels only for a scene that
has one, so without it they are the text they were (raytrace_main.js; the build test holds them
byte for byte).
Real dust is faint. A beam 1 cm across in dust of mean free path 10 m and g 0.7,
seen side on, is about 7 × 10−5 as bright as the white floor it lights: σs
p(90°) w against 1/π (σs the share of light the dust scatters per metre, the
albedo over the mean free path; p(90°) the phase function side on; w the beam's width), worked out on paper, not measured (roadmap/MEDIA.md,
the scale). Beams show at room scale, looking into the light, against a dark surround.
7What went wrong before
- A particle inside a sphere. At first the collision was decided after the march, from where the leg ended, worked back to where it began. A leg that escaped ended at the scene's radius, 400 m away, where a 32-bit number's steps are 30 µm (chapter 4), so the start came out a few steps off, and a particle just past a start on a surface landed inside the solid the leg had left. The furnace lost 4 × 10−4 of its light on the user's card; drawing the distance as the leg starts fixed it (the excerpt above; 0.62.2).
- A test that looked at the sky. Section 4's last step, as it happened (). Every test camera now names what it looks at.
- A heavy tail (rare, very large values, which an average takes long to settle over). With light sampling off, the phase function's draw alone finds the sun a few times a pixel, and a test of the two halves of MIS read 0.91 ± 0.03 one time and 0.98 ± 0.04 another (): section 3's second step, in 3D. The test now uses a larger sun and more samples.
8Try it
- In the first figure, find the mean free path at which half the crossings pass. Is it 4 / ln 2?
- In the second, slide g from −0.95 to 0.95 and watch where the drawn directions bunch.
- In the third, with the sun aimed at, slide g from 0.95 down to −0.9. Where is the beam brightest, and why does the floor's patch change too?
- In the fourth, set the dust's albedo to 0.9 with the disc white. How far below 1 does it read?
- In ShadyTracer, open the example Beam in dusty air, or add a Haze box (dust in the air) to a scene in the builder's tree, and move its mean free path while the picture refines.