RadianceLabAnalytic
volumetric
caustics

Strategies 20 & 21 · the photon-primitive family

The Photon
Hourglass

A caustic in fog is a transport path you rarely see drawn interactively: light leaves a lamp, refracts twice through a glass ball, and scatters off a fog particle on its way to the eye. The obstacle is not the budget — it is that every vertex on that path is either a delta or a point you cannot connect to, so there is nothing to sample toward. So we stopped sampling it and solved it: by giving up nearly all the freedom a medium path has, and taking a closed form in exchange.

A glass sphere and a gilt sphere lit by a small quad lamp in a dark, foggy room. A pale cone of light falls from the underside of the glass sphere toward the floor: the volumetric caustic. The room is refracted inside both spheres.
Plate 1 Preset 55, “The Reliquary”. 607×447, 400 passes, specular surfaces only. The cone below the glass ball is not a billboard, a blur or a photon map — it is one analytic surface, intersected per camera ray. docs/collage/harness/films/p55_collageSpec_1950x1258_400.png
01The
problem

Caustics are the hard case. Caustics in a medium are the hard case of the hard case.

A caustic is what happens when a smooth refracting surface folds a bundle of light rays onto itself. All the energy that entered a wide patch of the lens leaves through a narrow one, so the image is bright exactly where the map from light source to receiver becomes singular. That singularity is the whole difficulty: the paths that matter are the ones a random sampler is least likely to find, and their contribution is concentrated on a set of near-zero measure.

Put the receiver on a surface and there are answers — photon mapping, SDS handling, manifold walks. Put it in a medium and the receiver is every point in a volume. Now the caustic is not a curve on a floor; it is a two-dimensional sheet hanging in space, seen edge-on and face-on at once along a single camera ray, and the eye can be anywhere relative to it. This is the shaft of light under a wine glass, the bright knot in a smoky room beneath a crystal.

You do not often see it drawn interactively. That is an observation about what tends to get shipped, and it is worth flagging as exactly that: this repository holds no survey of what other renderers do, and nothing on this page should be read as one. What the repository does hold is the structural reason, which is sharper than any cost argument — and section 02 is that reason.

This renderer's own audit is blunt about the starting point: it had no working volumetric photon beam at all. VCM and the photon mapper were surface-only, the UPBP beams were stubs, and its volumetric photon points sit behind a flag that defaults to off. There was no caustic-in-fog reference to compare against, so the reference had to be built — which is also why every number further down is a ratio against something built here, rather than against a published baseline.

02Why
sampling
fails

You cannot connect through glass

A smooth dielectric has a delta BSDF. Fix the incoming direction and Snell's law determines the outgoing one — there is no lobe to sample, no density to evaluate, no freedom to spin. That single fact removes every tool a path tracer normally reaches for.

Next-event estimation asks: from this fog particle, what is the direct connection to the light? Through a glass ball, that connection exists for a measure-zero set of positions and has zero probability density everywhere else. Bidirectional path tracing asks the mirror question and gets the mirror answer. The only way through is to already be on the path — which is to say, by luck. This is the classic specular-connection problem, and in a medium it applies at every scattering point along the camera ray, not just at the ones that happen to land on a surface.

The usual escape is to trace photons forward from the light, let them refract, and then blur them: a photon map with a kernel, or a photon beam with a radius. That works, and it is what strategy 18 in this renderer does. But the kernel is the price. A caustic is a structure whose whole content is its sharpness, and a radius large enough to gather reliably is a radius large enough to erase what you came for.

The photon is not the problem. The blur is the problem. So: keep the photon, and delete the kernel.

03The
trade

Spend degrees of freedom, buy a closed form

Here is the organising idea behind the whole family this estimator belongs to. A photon leaves a vertex with a direction and a distance: three coordinates, (θ, φ, t), in a frame at that vertex. Ordinarily you sample all three and get a point, which a camera ray will never hit, which is why you need a kernel to catch it.

Instead: fix some coordinates and sweep the rest. Sweep k of the three and the photon's endpoint is no longer a point but a k-manifold in space. The camera ray is a 1-manifold. A generic crossing of the two has dimension k + 1 − 3 — and the arithmetic decides everything:

The lattice — docs/handoff/RESEARCH.md §1
sweptthe photon becomescrossingwhat it needs
k = 0a point−3a 3-D ball kernel, 3/4πr³
k = 1a curve — beam, ring, arc−1a 1-D tube kernel, 1/2r
k = 2a surface0nothing. Isolated roots.
k = 3a solid+1a segment, collapsed by co-area

At k = 2 the crossing has dimension zero. A line meets a surface in points, generically, with no coincidence required and no radius involved. The estimator's weight is the integrand divided by a Jacobian, and there is no kernel anywhere in it — nothing to widen, nothing to tune, nothing to blur. In this renderer's roster of 31 strategies, exactly six carry a kernel; the hourglass is not one of them.

That is the trade, stated plainly. Sweeping two coordinates means giving up two thirds of the freedom a general photon path has. The estimator can no longer express arbitrary transport — it can only express the family of paths that lies on the swept manifold. What it gets back is that a camera ray lands on it exactly.

The bet is that for this one transport — light, glass, fog, eye — the freedom you gave up was freedom you did not need.

And be precise about what makes the bet pay, because it is easy to get wrong and this repository measured it the hard way: the operative condition is not “fewer dimensions”. It is “no connection to make”. Where a next-event connection does exist — ordinary single scatter from an area light — the same swept machinery loses to a light sample and a shadow ray by 2.2–3.1× per pass, at every emitter size tested, and the repository's own instruction is not to use it there. Collapsing degrees of freedom is the method. Having no density to sample is the reason. Section 08 has that ledger in full.

three coordinates at a photon vertex: (θ, φ, t) in a frame (a, U, V) FIX ONE, SWEEP TWO a sheet. the camera ray crosses it at a point. no kernel. θ 08 disk · sweeps θ,t 09 cone · sweeps φ,t 07 sphere · sweeps θ,φ holds φ holds θ holds t each column is a complementary pair — the sheet holds what the curve sweeps FIX TWO, SWEEP ONE a curve. the camera ray misses it. one 1-D kernel. t r r r 28 ring · sweeps φ 29 arc · sweeps θ 18 beam · sweeps t holds θ,t holds φ,t holds θ,φ one expression: integrand × (1/2r) ÷ |(∂x/∂u) × d| — the beam's 1/sin θ is that, at unit speed
Fig. 1 The lattice, as the repository draws it: which coordinates are swept, what the photon becomes, and which of the 31 strategy slots land in each row. docs/portfolio/fig/f28-lattice.svg
04The
construction

Why an hourglass

The insight is small and it is the whole thing. At a refraction, rotating the plane of incidence about the surface normal rotates the incoming direction and the Snell-refracted outgoing direction by the same azimuth. Hold the incidence angle fixed and sweep φ: ωin traces one cone, ωout traces a different cone on the far side, and the two are locked together at a shared azimuth. Two cones meeting at a waist. An hourglass.

The form that ships is stronger than that. For a light point y and a glass sphere of centre C, take the axis a = normalize(C − y). The configuration is now exactly rotationally symmetric about a, so rotating the entire refraction chain — entry point, internal chord, exit point, exit ray — about that axis is a rigid motion. The azimuth lock is not an approximation to be maintained; it is automatic, exact, and it survives arbitrarily many specular vertices.

So the per-sample recipe is: pick a ball; sample cos ψ on the cap of directions that actually hit it; trace one deterministic chain at φ = 0, with two exact refractions and their Fresnel transmittances carried as deterministic weights; then sweep {φ, r}. The swept manifold is the surface of revolution of the exit ray. It is a quadric.

J = |( ( a × (x − y) ) × ωout ) · d|     [length]
because dx/dφ = a × (x − y)  (rigid rotation)  and  dx/dr = ωout

J is the co-area Jacobian: the rate at which the swept surface sweeps area, measured against the camera direction d. It appears in the denominator of the estimator's weight, and that is not a defect — it is the caustic. Where the surface folds, J shrinks, and the same photon energy is credited over a smaller area. Brightness where the map is singular is precisely what a caustic is.

Two anchors confirm the formula is the right one. As ior → 1 the glass ball dissolves and J expands algebraically onto the plain direction-swept cone's factor — the same measure, conversion factor 1.000000000, zero outside the cap. And a finite-difference check of the analytic co-area agrees to 1.7×10−7, with a step-size sweep that shows the residual is roundoff in the probe and not error in the formula.

medium a ψmax C glass ball φ waist ψ x1 x2 r ωout eye d a b x J = |((a×(x−y))×ωout)·d| a = ∂x/∂φ = a×(x−y) b = ∂x/∂r = ωout 20 · photon hourglass · sample ψ on the cap, sweep φ, r
Fig. 2 The shipped form: axis revolution. The light point y, the cap of directions ψmax that reach the ball, the two refractions, and the surface of revolution the exit ray sweeps about a. The camera ray pierces it in at most two points. docs/portfolio/fig/f20-hourglass.svg
05Why it
is fast

The whole caustic, in the roots of one quadratic

Everything above is geometry. This is the part that buys the speed, and it comes down to three facts that stack. Scope first: everything in this section is true of the one-ball sheet and of a chain that keeps the revolution symmetry. Break the symmetry — put the second ball off the axis — and the closed form is the exact thing you lose. Section 06 is about that, and about what it costs.

One. The intersection is a quadratic, not a search.

The swept sheet is a surface of revolution of a straight ray about an axis — a quadric. Substituting the camera ray into it gives an honest quadratic A s² + B s + C in the ray parameter. Two roots, closed form, no iteration, no Newton, no interval subdivision, no bisection. Per camera ray, finding where the entire caustic sheet lies costs one discriminant and one square root.

Two. The Jacobian falls out of the same discriminant.

This is the part that surprises people. J is not a second computation. In its analytically cancelled, factored form it is the discriminant:

N = wr (qp × d) + mp (a × d)    τ = (qp × d) · a    W = wz² + wr²
J² = |N|² − W τ² = ( B² − 4AC ) / ( 4 wz² )

One difference of two squares replaces a seven-term cancellation, no wz sits in a denominator, and — the point — the quantity that decides whether a root exists and the quantity the estimator divides by are the same number. You do not test for an intersection and then evaluate a Jacobian. You solve once and read both off.

Three. The MIS weights need no root-find either.

The hourglass is multiple-importance-sampled against the photon beam and the photon point, which is what makes it robust at the grazing angles where any one technique degenerates. Normally that means every technique must evaluate every other technique's density at a shared path — and for an analytic swept primitive that would mean inverting the sweep, i.e. a root-find inside the weight. It does not, here. At a shared path the fold dilation det3 is identical across techniques, so it cancels out of the balance heuristic entirely and each numerator is a product of quantities already in scope:

A18 = pdfDir(ω0) · T₁T₂ · 2r · sin θ
A19 = pdfDir(ω0) · T₁T₂ · e−σtt₁ · σte−σtr · πr²
A20 = pBall · pOrigin · pc · J
wi = Ai / ∑A  —  O(1) in both directions

The density and the caustic are the same object seen from two sides: 1/|det3| is the estimator's pdf conversion and it is the fold brightening.

Add it up. Per camera ray, per photon sample, the cost of the entire volumetric caustic is: one deterministic two-refraction chain, one quadratic, one square root, a Jacobian that was already computed, and a closed-form weight. No kernel to size. No photon map to build, store or query. No neural anything. No temporal accumulation to hide the noise. That is why the picture is recognisable in the first few frames instead of the first few thousand.

One label, because this repository is strict about it and a new reader should inherit the strictness: strategy 21 — the hourglass MIS-ed with the beam and the point — is CONSISTENT, NOT UNBIASED, and the code says so at the site. At a finite beam radius the members deposit on manifolds a radius apart, so the balance weights do not sum to one pointwise and the pair's energy moves with r. Strategy 20 alone is kernel-free, but its grazing guard is a discard rather than a clamp, so its support is narrower than its declared class. Neither is a defect being hidden; both are printed next to the estimator that carries them.

What was actually verified about that solver

The closed-form roots were checked against an independently derived arbiter and read exact 0.0 residual. On the shipped binary, replayed over 10.5 million crossing records across seven dump jobs, a float64 replay reproduces the binary's roots and its J bit for bit on 100 % of records; there were 0 invented and 0 missed crossings in every job, and 0 of 226 063 accepted crossings carry |J/Jexact − 1| > 10−6 (worst 5.9×10−8, which is the float32 cast of J itself). Rotation-equivariance of the two-refraction chain: 3.5×10−15. docs/hourglass/README.md

06Chains

More than one ball, and where the symmetry runs out

The rigid-motion argument does not care how many glass balls the photon passes through — only that the whole configuration is a surface of revolution about one axis. So a collinear chain, every centre on the line from the light through the first ball, is free: the same machinery, the same quadric, the same solver, with more refractions folded into the deterministic chain. Two balls in a row focus twice, and the waist gets tighter and stranger. A planar mirror behind the ball is free too, by reflecting the light point across it to a virtual origin: the configuration is again rotationally symmetric, and no new mathematics is needed.

Take the balls off the axis and the symmetry breaks. The exit ray no longer sweeps a quadric — it sweeps a ruled surface with no closed-form intersection, so the azimuth has to be found by search: scan for sign changes, bracket, then bisect. That is tier T3, it works, its energy at the shipped default reads 1.0006 ± 0.0063 on the caustic mask — 0.1 σ from one — and it costs 9.32 ± 0.15× a collinear two-ball chain and 11.79 ± 0.18× a single ball, both measured within one round rather than across sessions, for exactly the reason you would expect — the thing section 05 is about, the closed form, is the thing it gives up. The accounting is unambiguous about where the time goes: at the shipped 64 rulings, 83.8 % of the pass is the sign scan alone, and everything that is not the root-find is 8.7 %. An attempt to bound the search by screen extent was built, measured at 1.00–1.73× and filed as a no-go: interval arithmetic overestimates the relevant derivative by 3.3–48.5×, so the bound is too loose to pay. The live candidate is a closed-form rewrite of the azimuth function, verified to 6.2×10−15 over 4096 azimuths and not yet built.

This is a good place to notice what the estimator is really made of. Collinear chains are free because they preserve a symmetry; off-axis chains are expensive because they destroy it. The speed was never about the code. It was about the constraint.

Four glass spheres in a dark room, arranged as two chains of two. A gold caustic cone falls from the upper-right sphere, is interrupted by the sphere below it, and resumes underneath. A blue-white cone falls from the upper-left pair.
Plate 2 Preset 78 — two off-axis chains, two lamps of different size and colour. The gold cone is interrupted by the ball in front of it and resumes below: the occlusion is decided per sample, not per pixel. 400 passes, 26.8 s measured.
A dark room containing three chains of glass spheres, each with a coloured caustic shaft falling from it, and a large dispersive sphere at the bottom centre with the room refracted inside it.
Plate 3 Preset 79, “The Atrium” — three chains, three lamps, and a dispersive hero ball. 607×447, 400 passes — 20.7 s measured, on the CPU, with no GPU involved at any point.
07What it
costs

Honest numbers, one claim that does not survive them, and the claim that replaces it

The tempting headline is that the analytic caustic is cheaper than rendering the surfaces. On the scene where both are timed in one campaign, that is not true per pass, and it should be said before anything else is. Here is the whole column, one binary, preset 79:

ms per pass — preset 79, difference of two run lengths, 2026-09-10
what is being rendered217×140607×447
Surface render, no medium at all1.86214.982
Surface render, full medium camera walk4.62137.656
The hourglass alone — the whole volumetric caustic5.71941.703
Both together, composited7.20867.576

So the caustic costs about 3× a medium-free surface pass, and 1.24× a full medium camera walk at 217×140, 1.11× at 607×447. Dearer, on every row. No scene anywhere in this repository publishes an hourglass-alone cell that is cheaper per pass than a surface render of the same scene in the same campaign.

But that was the wrong comparison, and noticing why is the point of this section. A surface render does not draw this. Turn the fog on in a path tracer and you get airlight and multiple scatter and no caustic at all, because the specular connection has no density. The comparison that means something is against the estimator that draws the same transport — the kernel-based photon beam — on the same scene, at equal time, in one campaign. That comparison is not close.

equal time — relative variance × ms/pass, against the tracer photon beam
the same transport, drawn two wayshourglassphoton beamfactor
Two-ball collinear chain (preset 68)6.798241.1 / 329.435–48×
The post-ball mirror leg (preset 71)7.019122.1 (r 0.12)17–21×
The two-ball caustic, as shipped (preset 68)——30–54×
and where the beam wins — at the waist, kernel r = 0.0111.39.270.82×

The repository states its own verdict on that rig without a ratio to soften it: “the collinear chain is 35–48× better than the reference on equal time and agrees with it on energy inside the bars; the post-ball mirror is 17–21× on equal time and agrees to 0.2 % against its isometry.” A second, independent probe puts the cushion the beam leaves at 17–32×. The last row is there on purpose: shrink the beam’s kernel far enough and aim the camera at the waist itself and it wins by 0.82×, because a narrow kernel sitting exactly on the fold is a good estimator of the fold. Everywhere else on the sheet, it is not.

Not faster than drawing the surfaces. Between 17× and 48× faster than the only other thing that draws the caustic.

The absolute numbers, which are the surprising part

A ratio can hide a slow baseline, so here is the clock — and each figure with the film it was measured on, because film size moves these by a factor of twenty. On a 217×140 film of a single glass ball, the hourglass costs 1.17–1.49 ms per pass, of which roughly 0.76 ms is the photon-primitive estimation itself and the rest is per-pass overhead that every strategy pays. On a two-ball collinear chain, same film, 2.013 ± 0.042 ms.

At a viewport somebody would actually sit in front of — 605×397 — exactly one published cell supports the word interactive, and it is the double-chain showcase: 20.042 ± 0.675 ms per pass, 49.9 passes a second, CPU only. Read that cell for what it is: the hourglass alone, every surface flag forced off, so it is a caustic on black rather than a finished frame — and this project reserves the word real-time for its GPU path, which runs none of these strategies at all. The heavier scenes do not: the off-axis pair is 19.7 passes/s and the Atrium 15.1 at 607×447. What that buys is a caustic you can fly around, not one that is finished on arrival — the showcase sits at 15 % relative error on its caustic after 1600 passes and 7 % after 6400, which at 50 passes/s is half a minute and two minutes of holding still. The converged beauty shot, 4.5 % on the caustic itself rather than on the easy parts of the frame, is 5 minutes 25 s.

And on the composited showcase frame, measured against each cell’s own converged mean, the typical lit pixel arrives 57× and 227× sooner than under the stochastic partition it replaces:

passes to 10 % relative error on the typical lit pixel — 217×140, six batch means of 150 passes
estimatorpreset 79preset 78
Analytic caustic + surfaces, composited14584
The stochastic partition it replaces8 28219 042
ratio57×227×

At 217×140 that composited frame runs at about 139 passes per second on the CPU alone and reaches a clean typical pixel in 145 passes — roughly one second. The stochastic route needs 8 282 passes for the same frame at about the same cost per pass, so it needs a minute of the same machine. Read the two tables together and that is the shape of the result: the same picture, sooner, by between one and two orders of magnitude, for a per-pass cost in the same bracket as an ordinary render.

One more axis, because a caustic is judged on sharpness and not only on noise. At equal passes the hourglass pair’s caustic waist is 7.7 px against the beam’s 70 px at the beam’s own radius — and the correction travels with that number, because a 1-D kernel’s width scales with its radius, so at matched radius the pair is sharper by 2.0×, not by nine. On error rather than width: 2.1× lower at equal passes on the fold scene, median relative error 0.1324 against 0.2542 on the isolated caustic, and below the focus — where the beam’s geometry degenerates — 6× lower variance.

Read the timings with this attached

ms/pass on this machine is a machine reading. A cell that cannot have changed — a surface pass with no hourglass in it — read 13.860 in one campaign and 14.982 in the next, and a whole-run ladder moved 23 % across the same gap. Every ratio quoted above is taken inside a single measurement campaign, and none of them crosses two. The producers are docs/collage/harness/collage.py and docs/hero-scenes/harness/atrium.py, and the two use different protocols whose difference is one headless startup.

08Where it
loses

The constraint cuts both ways

A page that only sells is worth nothing to whoever inherits the code, so here is the other column. This repository's own thesis, written after the measurements rather than before, is that a swept primitive is more machinery than a light sample plus a shadow ray — and applied to transport that was easy to sample, the machinery does not pay.

  • On easy single scatter, the plain camera walk's NEE beats the direction-swept trio by 1.4–3.6× on equal time at every emitter size tested.
  • The 1-D ring/arc/beam MIS trio loses to the plain beam by 2.51 ± 0.19× on equal time.
  • The k = 3 solid primitives lose to the walk by 12–344× on median variance per pass.
  • The production render deliberately gives the ambient airlight back to the stochastic walk, because that is where it belongs.

And the hourglass itself carries real restrictions, each of them a direct consequence of the symmetry it is built on. It needs a sphere dielectric — a box or a planar slab breaks the revolution and would need a different form. It needs a point-sampled source. It has no member on the k = 0 class at all, so it draws the caustic and not the lamp's ambient glow in the fog; that is why it is paired with the beam in strategy 21, and why the showcase mode uses strategy 20 alone when it wants a dark room. Its characteristic artefact is the tangency locus, where the camera ray goes parallel to the sheet, J → 0, and the weight blows up — which is the entire reason MIS is not optional here. There is a guard for the residual tail; measured, it discards 0.18 % of the film, touching 42 of 30 380 pixels, none of them in the caustic core.

And the win covers the caustic, not the frame. The probe puts it plainly: the gain is on the 9–13 % of the frame the caustic footprint actually covers, and every other pixel pays the same pass and gains nothing from it. On the off-axis chain that is starker still — 73.8 % of the pass is chain walks that find no root at all, because the azimuth scan runs at every pixel while the caustic is a tenth of the picture. An estimator that is 30× better on a tenth of the frame is a very good thing to own and it is not a 30× faster renderer.

One more, and it matters for the word real-time: every one of the 31 photon-primitive strategies runs on the CPU. Nothing here is a GPU megakernel. The frame rates quoted above are one CPU's, and the analysis in the handover docs is that the closed-form group is portable to the GPU while the hourglass specifically would be inefficient there. Read “fast” as “fast for what it draws, on a CPU, against alternatives that draw the same thing” — which is still the surprising claim.

09The
map

Where everything already lives

This page is an orientation, not a replacement. Everything it summarises is written down somewhere in this repository in far more detail, with its producers and its gates attached. Start here:

  • start heredocs/handoff/
    The six-file handover. README.md is a map of every other doc, ARCHITECTURE.md is the code shape, RESEARCH.md is the lattice and the thesis with the evidence both ways, VERIFICATION.md is how anything here is checked, HISTORY.md is how it got built, and LOOSE-ENDS.md is 25 ranked open items, each labelled as a defect, a missing measurement, a scope limit or a decision for the owner.
  • theory — the estimatordocs/hourglass/
    The recovered derivation, the pre-C++ validation gate, the MIS pdf derived four independent ways, the killed NDF-partner hypothesis (recorded so it is not retried), and the two-phase story of the crossing solver — including the day the “catastrophic” grazing behaviour turned out to be float32 in the solver rather than tangency in the geometry.
  • theory — the familydocs/pp-primitives/README.md
    The whole photon-primitive family in one document: the idea, the roster of 31, when to reach for a primitive and when not to, why MIS is not optional, and the hourglass in depth (§4).
  • theory — chainsdocs/chain-hourglass/
    SPEC.md for tiers T1(a) collinear, T1(b) mirror and T3 general off-axis. T3-PROBE.md §5 is the accounting of where the off-axis time actually goes, and §5.8 is the screen-bound no-go with the numbers that killed it.
  • figuresdocs/portfolio/fig/
    The authored schematics — the ones on this page are f20-hourglass.svg and f28-lattice.svg. Also f18 (the tracer beam), f21 (the hourglass/beam MIS pair), f22 (the vertex variants), f30 (the subsystem map). All 640×360, all hand-authored for a dark ground.
  • records — modesdocs/comp-mode/
    How the analytic layer and the stochastic layer are kept apart. HOURGLASS-SURFACE-MODE.md is the transport partition; COLLAGE-MODE.md is the two-render composite the plates on this page come from, and its §1 is a table of the difference between the two, which is easy to get wrong. ADVERSARY.md is the standing red-team record.
  • records — scenesdocs/hero-scenes/ATRIUM.md
    Preset 79 in full: how the room is built, what its density knob does, the ms/pass ladder, the convergence figures and how long a converged frame takes.
  • harnessesdocs/tools/pins.py
    The 31-film byte-identity gate. Any change that should move no picture is checked by rendering the pinned set and comparing md5s against a named tag. It is the reason a claim like “off is byte-identical” is a fact here rather than an intention.
  • triagedocs/EXPECTED-BEHAVIOUR.md
    Read this before investigating anything that looks broken: symptom → expected / bug / unverified. Which strategies structurally cannot show caustics, what a scene must contain for a given strategy to draw at all, and what is built-but-unseen.
  • the codesrc/render/PPIntegrator.cpp
    All 31 strategies. For the hourglass: ppBuildHgPrim builds the primitive, ppHgSolveSheet is the quadratic of §05, ppHgGatherLeg gathers one camera segment, ppHgT3Gather is the off-axis search, and kPPClass[] is the authoritative table of which slot carries a kernel — trust that column over any prose, including this page's.