← Depth & Light II

Depth & Light II · Part 6 of 8

Rain by the Cubic Metre

An app that adds rain to a photo usually lays a sheet of streaks over it: every drop in front of everything, large and small mixed at random. Real rain is not in front of the scene. It fills the air between the camera and the scene, so a few drops are near and large and a great many are far and fine, and any drop behind a person is hidden by that person. Weather, the second Rake Plus Pro tool, puts rain and snow into that air using the depth map, and this article is about how many drops that is, and why most of them first vanished on the GPU.

The kill test

The plan gave Weather one way to die: if rain placed by depth read the same as the same drops laid flat over the photo, there was no tool. So the test renders each photo twice from one list of drops. In the scene, each drop is drawn at the width, streak length and blur its distance gives it, and hidden wherever the scene is nearer than it. As the control, the very same drops, sizes and streaks are drawn over everything with no veil, as an overlay app does. No other app was compared.

The photos were the nine the earlier tests used: four Portraits (a vase, a man in a café, a laptop, an ice cream) and five with estimated depth (a forest path, a town street, a woman at a brick wall, a man outdoors, sun through trees). Each row showed rain and snow, overlaid and in the scene, side by side, judged by eye.

The verdict, on 2026-09-23, was continue. On the town street the overlay is a grey sheet over the bus, the sweeper and the road; in the scene, the rain falls behind the bus and the sweeper and stops at their outlines, thickens towards the buildings, and leaves the near road almost clear, because there is little air between the camera and a road a few metres off. With the man outdoors, the rain falls behind him and his face stays clean, where the overlay streaks across it; on the forest path the rain collects over the far pines and thins over the near grass.

Indoors and close up there is little rain to show, and that is the right answer. The woman stands a metre from a brick wall and the café man a few metres from the back of the room, so few drops are drawn in front of them, while the overlay puts streaks on their faces. Weather needs a scene with depth behind the subject.

One number the eye can’t argue with

The synthetic scene the engine’s tests use, a sphere in front of a wall, turns the judgement into a measurement: paint it black, switch the veil off, set the amount to full, and read the brightness the drops add.

0.10.200.0420.1970.1980.198spherewallspherewallin the sceneoverlay
Brightness the drops add in the synthetic scene, black, veil off, full amount. In the scene, only the drops in front of the sphere show over it; as an overlay, the same drops land on both alike.

Over the wall the two agree, as they should. Over the sphere, the scene adds about a fifth of what the overlay does.

Counting by volume

The first run gave every photo 30,000 drops, which crammed a shallow indoor scene into a white-out. The count now comes from the volume of the view. Between a near plane and a far one, the camera sees a frustum whose cross-section grows with the square of the distance, so its volume is the frame’s width and height over the focal length, times a third of far cubed minus near cubed. Multiply by drops per cubic metre and that is the count, capped at 400,000.

Swift
let near = 0.12 * S, far = min(scene.maxDepth, 12.5 * S)
let perCubicMetre = s.amount * (rain ? 900 : 250)
let n3 = near * near * near, f3 = far * far * far
let volume = (w / fx) * (h / fy) * (f3 - n3) / 3 / (unit * unit * unit)
let count = min(Int(perCubicMetre * volume * 1.32), 400_000)

Portrait depth maps are metric only up to a scale that changes from photo to photo, which the last article dealt with for the grade. Weather takes the same way out: every size and distance is relative to the subject, as if the subject stood two metres away. In those units the drops start a quarter of a metre from the lens and stop at 25 metres, or at the back of the scene if that comes first.

subject S = 2 m (by definition) near 0.12 × S = 0.24 m far 12.5 × S = 25 m, or the scene’s far end rain at full Amount 900 drops a cubic metre snow at full Amount 250 flakes a cubic metre margin round the frame 1.1 × 1.2 = 1.32

The margin lets streaks enter from above the frame. The default Amount is a half.

Before you read on

If drops are even per cubic metre, where in depth should most of them be?

Far away. The frame’s cross-section at distance z grows as z², so a slab of air at 20 metres holds a hundred times the drops of an equally thick slab at 2 metres. The code draws each drop’s depth with a density proportional to z², by taking the cube root of a uniform draw between near³ and far³.

Swift
let r1 = rng.unit(), r2 = rng.unit(), r3 = rng.unit()
let z = cbrt(n3 + r1 * (f3 - n3))

Every drop takes three draws whether it is kept or not, so the sequence is the same at any size, and a full-resolution save holds the same drops as the preview, drawn finer. “New drops” in the app changes the seed: another moment of the same weather.

The veil

A drop at depth z is drawn with its core width, a raindrop 1.5 mm across or a flake 12 mm, projected to pixels, and its blur, a circle of confusion that grows as z moves away from the subject. A drop whose core comes out under 0.08 of a pixel and whose blur is under 0.6 of one is too small and sharp to see alone, and it is not drawn at all.

Swift
// Too small and sharp to see alone: part of the veil.
if core < 0.08 && coc < 0.6 { continue }

What they do instead, together with everything beyond 25 metres, is fade the distance; that is a fog with a transmittance that falls exponentially with the depth behind the subject, and nothing in front of the subject at all. Its strength is Amount times the Veil control, and it fades towards the drops’ own colour, a little darker.

Swift
let sigma = s.amount * s.veil * (s.kind == .rain ? 0.35 : 0.5) / scene.subject
return (scene.depth.map { exp(-sigma * max($0 - scene.subject, 0)) }, colour(scene, s) * 0.85)

One correction came out of the kill test here. Rain drawn by its width alone was too faint, because a raindrop is a lens and carries a bright image of the sky along its whole streak. A sub-pixel raindrop now shows at twice the share its width alone would give it. On the Mac’s CPU at 768 pixels a style took 2–30 ms, and 219 ms for the man outdoors, a deep scene with 168,000 drops.

Faint drops in eight bits

On the GPU the two halves go to different places. The veil is one more channel of a map the composite already carries: Clear’s transmittance is in r, Grade’s far weight in g, and Weather’s veil now in b of the same rgba32Float texture, applied after Air and before Grade. The drops are drawn last, after Lens, because each drop carries its own blur and should not be blurred a second time.

Each drop is one instanced quad whose fragment is the CPU’s inner loop, depth test included. The first version blended each drop straight into the 8-bit output, and the result sat a mean of 4.2 steps off the CPU. Thousands of the far drops are each fainter than 1/255, and every one of them rounded away to nothing.

The fix rests on the fact that every drop is the same colour. The drops only need to record what they let through, so they are drawn into an r16Float texture cleared to 1, each multiplying it by one minus its alpha, and the colour is mixed in once at the end.

Swift
// Into a float texture cleared to 1: each drop multiplies it by (1 − a).
c.pixelFormat = .r16Float
c.isBlendingEnabled = true
c.sourceRGBBlendFactor = .zero
c.destinationRGBBlendFactor = .oneMinusSourceAlpha
Metal
float t = through.read(gid).r;
float3 lin = float3(srgbToLinearW(c.r), srgbToLinearW(c.g), srgbToLinearW(c.b));
lin = lin * t + u.colour.rgb * (1.0f - t);

With the product held in floats, the GPU came to a mean of 0.14 steps off the CPU, and never more than one. The drop list is cached for the settings and size it was made for.

What it doesn’t do

The kill test wrote its limits down rather than fixing them. Near drops out of focus draw as wide soft columns, which read as smears in the grass of the sun-through-trees photo. Snow is flat discs, weaker sharp and near. Every drop is one neutral grey, the bright end of the scene, so a drop in front of a bright sky comes out lighter than the sky when it should be darker. And wet ground, which the plan lists for Weather, is not done: it needs reflections along the ground, a separate piece of work.

In the app, Weather is a Pro tab after Air, with Rain or Snow, Amount, Size, Wind, Streaks or Drift, Veil and New drops. It can be tried on any photo; saving or sharing with it needs the unlock. It was checked in the Simulator on the town street; how it runs on a device is not yet measured.

What the tests hold

  • In the synthetic scene, painted black with the veil off and the amount at full, the drops in the scene add more than twice as much brightness over the wall as over the sphere, and less than 0.6 of the overlay’s over the sphere.
  • With no lights, air, haze or grade, the GPU’s veil and drops at the working size come within a mean of one step of the CPU’s.
  • The same seed gives the same number of drops and the same first drop; another seed gives a different first drop.
  • The kill test against the same drops as an overlay: continue, with depth winning outdoors and little rain shown, correctly, indoors and close up.

Weather was the second Pro tool to pass its test. The third, a sun that could be set to any hour, did not, and the next article is about how it failed and what took its place.