Visualisation · RadianceLab
The renderer can draw its own sampling geometry and colour it by the place its outliers are born.
Above: sixteen photon cones sharing one apex at the light, frozen for a whole frame instead of redrawn per sample. Ordinarily they average away before anyone sees one; this is the estimator's own geometry, photographed.
01 — What a firefly actually is
Every estimator in this renderer's analytic family sweeps two dimensions of a light path into a surface, intersects the camera ray with it, and divides by the co-area Jacobian of the sweep. The Jacobian sits in the denominator. A camera ray tangent to the swept sheet drives it toward zero and the sample explodes.
So a bright isolated pixel is not a mystery to be denoised. It is a geometric event with a location, and the location is computable. The useful question is not how big the outlier was but how the Jacobian vanished, because that decides whether the estimator has a heavy tail at all.
| How J vanishes | Members | What the tail does | Consequence |
|---|---|---|---|
| constant | 3 | max / median 1.7–3.0 | No in-pixel tail at all. The tangency shows up across pixels instead, as the crossing probability collapsing |
| linear | 10, 11, 14, 16 | max / median 200–7000, top-1% share 20–45% | Integrable. The mean converged on all 16,758 (ray, estimator) pairs tested |
| square-root fold | 7, 9, 20, 22, 23, 24 | W ~ ε−1/2, Hill index 2 | E[W²] is log-divergent. The sample variance grows like ln N and never converges |
| Cauchy fan | 8 | relvar × δaxis = 0.42–0.45 | Bounded weights, no fold. Two exactly silent rays instead — through the axis and parallel to it |
The fold row is the one that changes how you read every other number on this site. Two quadric roots merge at the silhouette of the swept surface, the weight diverges like the inverse square root of the distance to that merge, and the resulting Pareto tail has index exactly 2 — measured at 1.96 for the photon sphere and 2.00 for the photon cone, over three decades of the complementary CDF. A tail index of 2 puts the second moment on the boundary of existence, so a variance estimate from a fold member is a lower bound that keeps rising as you sample.
There are two firefly instruments in the megakernel and they answer two different questions. pathTrace.comp carries eight non-finite tests. One discards a splat outright: splatToImage returns before writing anything, under the comment “discard invalid contributions to prevent pixel poisoning”. Two replace a sample with black rather than dropping it — the returned radiance and the eye-subpath result. The other five gate a connection’s add: the BDPT connection, the light tracer’s emitter and camera contributions, the internal-connection loop and next-event estimation. Separately, a large but finite sample meets a hard clamp at 1000.0 with no UI at all, applied after the MIS weight; the source records that it replaced a pre-weight clamp of 100 which had been truncating bright glossy splats the path tracer counts in full.
Neither instrument can see the fireflies this page is about. A fold weight is W ~ ε−1/2: enormous, and perfectly finite. isnan and isinf are false for every one of them, so the guard never fires on a grazing event — it is there for a division that produced a NaN, which is a different failure. Only the clamp touches a fold sample, and the clamp lives on the GPU megakernel.
The film that actually carries this family has neither. In PPIntegrator.cpp the only finiteness test on a contribution is inside the camera-side splat of strategy 17; the main film write is an unguarded, unclamped running mean. That is a decision, not an oversight — a clamp is a bias, and this family’s whole acceptance argument is a ratio against a reference that has no clamp either. It is also the reason the statistics used on the evidence page are a per-pixel median or a top-0.1 % clipped total and never a raw frame energy. The 8.44 in section 03 is one pixel arriving in a film with nothing at all in its way.
The amplitude of that tail is not only observed; it is predicted in closed form, and the prediction lands within half a percent on the top 0.1 % of the tail. Working the fold out from the sampled density gives a scale of 1.537 for the sphere and 1.011 for the cone; the measured values are 1.536 and 1.016.
The same analysis says which member is worth using where. A 40×40 pinhole map, point light, 6000 samples per pixel per strategy, colouring each pixel by the strategy with the lowest relative variance × cost:
Where these numbers come from. Every number in this section is from docs/variance/, a numpy port of the C++ estimator validated against finite-difference co-area to a median agreement of 1e-10, with Monte-Carlo means matching closed-form quadrature on all 16,758 (ray, estimator) pairs of the plane study. It is a result about the mathematics, and the renderer was not run for these figures. The renderer takes over below this line — and the fold row arrives in it, measured, in section 03.
02 — The Primitive Explorer
Tick one checkbox and the renderer stops estimating light and starts drawing the object it would have sampled. One representative primitive, ray-traced per pixel, deterministic, coloured blue where the camera ray meets the sheet face-on and red where it grazes. The red band is the firefly locus, drawn in the picture.
This is the difference between a theorem and an instrument. The variance study above locates the singularity in parameter space, as a condition on tangent vectors. The explorer locates it on screen, in the same scene and from the same camera you are about to render from — the difference between knowing an estimator has a grazing band and knowing where that band is going to land across your caustic.
Nothing is hardcoded per primitive. The tool evaluates x(a,b) = y(u,v) + t·ω(θ,φ) on a grid, with a and b picked from {u, v, θ, φ, t}, and tessellates the result — so the sphere, the cone, the meridian half-plane and the ruled plane all emerge from which two dimensions you chose to sweep, rather than from twelve special cases.
The explorer covers twelve of the thirty-one strategy slots — 3, 4 and 7 through 16: the quad, sphere, disk, cone and plane primitives and three of the four mixtures. The rest fall through to a flat grey fill: the NEE baselines, the volume-light chords, the camera-side splat, the rasterised gather, the cross-family mixture, the glossy ring, the one-dimensional group, and the entire hourglass family, which is the research this renderer exists for. The tool that best explains the estimators does not yet draw the best one; the frozen batch in the next section does, stochastically, and that is where the hourglass is photographed.
Two smaller limits, both worth knowing before a capture session. It needs an emissive quad in the scene — findEmissiveQuad failing gives the same flat grey as an unsupported strategy, so grey is ambiguous. And the manifold trace runs at a maximum internal width of 420 pixels and is nearest-neighbour upsampled to the viewport, so a full-screen capture is soft. It is cached against a camera fingerprint and re-traced only when the camera or the parameters move, which is what keeps the orbit smooth.
03 — The frozen batch
Normally each camera sample draws a fresh photon primitive, so the sheets average away before anything is visible. Freeze a batch of sixteen instead, share it across the whole frame, and skip the divide by N — and the estimator's own geometry appears, one hue per member, at its own brightness.
The hero image at the top of this page is that picture, and it is the clearest explanation of the family that exists anywhere in this project. Sixteen photon cones, apexes coincident at the light, each a distinct hue, each with a thin incandescent outline where the camera ray runs tangent to it. It is the same object the explorer traces analytically, except that here it was sampled: these are real primitives that a real render drew, carrying real weights, and the bright outline is a real firefly locus rather than a colour ramp.
| pp.correlated | Estimator | Cost / pass | What it looks like |
|---|---|---|---|
| 0 · off | a fresh primitive per camera sample | 1× | Byte-identical to before the feature existed |
| 1 · stippled | one uniformly chosen member of the batch per sample; no 1/N, one uniform pick is already the mean | 1× | Neighbours show different sheets — the primitives read as dithered together |
| 2 · all-N | every pixel sums the whole batch and divides by N drawn | N× | Clean hard-edged overlapping sheets, roughly N× less noise |
It cost almost nothing to build, for a structural reason worth stating. sampleOnePixel already draws every photon-side random number after the camera ray is built, in every one of its strategy blocks. So correlating is a split of the RNG stream rather than a refactor: re-seed once, immediately after cam.generateRay returns, and every pixel handed batch index k reconstructs primitive k bit for bit. Not one strategy block was touched, and the mode covers strategies 0–16 and 18–30 — the mixtures, the hourglass family, the glossy ring and the one-dimensional group included; the swarm of rings and hooks in section 04b of Caustics without a kernel is that reach arriving on three strategies that did not exist when the mode was written, and it needed no code. Only strategy 17 opts out, because it is a whole-image splat pass that returns before the branch.
The count that used to be in that sentence. It read “all nineteen strategy blocks”, which was true when it was written and is a figure the source's own comment still carries at the correlated branch. Strategies 26 and 27 have blocks of their own now, and rather than print a new number that will go stale the same way, this page states the property that is actually load-bearing: every block draws its photon-side randomness after the camera ray, and the byte-identity guarantee is what proves none was missed.
The palette is deliberately shared with the explorer: primitive k gets the same hue circle the explorer uses for strategy 15's orientation set, so a sheet in the deterministic picture and a sheet in the stochastic one are the same colour.
The divisor is N drawn, never N that crossed the camera ray. Dividing by the crossing count inflates the image by the reciprocal of the crossing probability. That was built behind a debug flag, fired on purpose, measured, and deleted:
Read the third row again. The wrong normalisation is flat to 1.5% over a sixteen-fold range in pass count. It does not flicker, it does not drift, it does not look noisy. It converges cleanly to four and a half times the right answer, and nothing in the image says so. This is the failure mode a picture cannot catch and only a reference can.
The design record's own prediction was 2.5×–3.7×, from a Python model of the cone. The renderer's answer is 2.1×–6.8× on frame energy, because the crossing probability in these presets is much smaller than in the model's geometry. Same mechanism, bigger multiplier here — so the page quotes the mechanism, and the correction stands in the record next to the prediction it replaced.
Twelve films were pinned from the pre-change binary and re-rendered from the final one at 300 passes each, across three presets and ten strategies. All twelve byte-identical, checked twice — once with the debug trap compiled in and once after removing it. Then the correlated films were compared against the uncorrelated reference at 2400 passes:
| Film / reference | N = 64 | N = 1024 | N = 65536 |
|---|---|---|---|
| p47 s8 disk · stippled | 1.0087 | 1.0024 | 1.0009 |
| p47 s18 beam · stippled | — | 0.9993 | 1.0003 |
| p48 s21 MIS · stippled | — | 1.0012 | 1.0034 |
| p47 s9 cone · stippled, raw | 1.1239 | 0.9221 | 8.4436 |
| p47 s9 cone · stippled, top-0.1% clipped | 1.0022 | 0.9989 | 0.9985 |
The last two rows are the fold row of section 01 arriving in the shipped renderer. Strategy 9's raw frame energy wanders in both directions with no trend and the 8.44 is one pixel; clip the top 0.1% from both films and every fold member agrees to 0.2% at every N. That is a finding about the reference, not about correlation — the uncorrelated film is itself unconverged in its own tail at 2400 passes. Strategy 8, the one member with bounded weights, needs no clipping at all and agrees to four digits raw, which is why it is the clean unbiasedness gate.
And one thing the frozen mode is not, by design. Freezing the batch across passes converges the film to that batch's realisation, not to L. More camera samples only clean up the camera side; the light-side error is permanent. The UI says so in orange while the toggle is on. Relatedly, the per-pixel variance of a correlated pass is not an error estimate — the correlation coefficient is independent of N, so a bigger batch shrinks the amplitude of the structure without changing its shape, and any adaptive sampler or denoiser reading that variance is being lied to.
04 — One primitive, one picture
These five frames are the same feature pointed at different strategies. Read them against the table in section 01: the shape you see is how that member's Jacobian vanishes, made visible, because the Jacobian is in the denominator and the bright places are where it is small.
05 — Two predictions that did not survive
Section 3 of the correlated-mode design record predicted every frame in the gallery above before any of them existed, reading each prediction off the variance classification. Six landed as predicted. Two did not, and neither failure was in the mathematics — both were geometry that no preset in this repository can produce.
Predicted: a pinwheel of blades with a dark hole through the light. What appeared: a fan of vertical blades hanging off the light, with a bright seam where the hole was supposed to be.
The blades are half-planes that all contain the light's axis, so a pinwheel is what you see looking down that axis. The axis is the emitter's normal for an area light and world-up for a point light, so in every scene here it is vertical — and every PP preset points the camera level. The pinwheel view would require looking straight down at the ceiling light, and no preset does. From the side it projects to a fan; on preset 41, with the camera almost at the light's own height, it degenerates further into two half-frames split by the axis. The predicted dark column is there, but neighbouring blades close over it, so it reads bright.
Predicted: a few blinding thin slivers among ordinary patches, wherever a quad happened to be seen edge-on. What appeared: a single incandescent line straight across the frame at the camera's own eye height, carrying essentially all of the estimator's variance.
Strategy 3 translates the emissive quad along ω; it does not rotate it. A ceiling quad therefore stays horizontal at every distance, and a level camera sees every member of the batch near edge-on at exactly one height — its own. The slivers are not scattered because nothing scatters them. The same geometry explains why this family needs a batch in the thousands before anything crosses the frame: its screen footprint is small because of the edge-on geometry, not incidentally.
Why this stays on the page. Both misses came from reasoning about a sheet in isolation and forgetting that a camera has to look at it — a specific, repeatable failure mode, and the same one the triage document records after three confident wrong conclusions in a single session, each settled by one counter. The rule it left behind is the reason the two tools above exist: instrument first.
06 — The mixture, on one frozen batch
This is the strongest argument for multiple importance sampling anywhere in this project, and it is an argument that only a frozen batch can make. The balance heuristic changes a weight formula and draws no random numbers, so freezing the batch pins every shape in place and lets the weighting act alone: one batch, film maximum 4.9 × 10⁴ for the cone by itself, 368.7 for the trio under the balance heuristic, and every rim between them going out along its whole length.
Both panes are unbiased. Flat weights are a legitimate MIS combination and converge to the same image; they are simply a bad one. What the balance heuristic does is replace the effective Jacobian with the sum of the members' densities, so a set whose tangency loci are disjoint has no zero that any camera ray can reach. In the numpy study the same change turns the folds of the sphere and the cone into Hill indices between 5 and 60, and the worst pixel of the image from 2.7×10³ to 3.9.
Correlation also changes how this reads in motion. Uncorrelated, "MIS removes fireflies" looks like isolated pixels going out. Correlated, it looks like a hot rim extinguishing along its whole length — which is what is actually happening, and is much harder to mistake for a denoiser.
07 — The rest of the bench
The explorer and the frozen batch are the two that changed how the research ran. Three others sit alongside them, and one of them has never been looked at.
Two independent estimator passes over the same scene and the same camera, composited as a wipe in the main viewport with a draggable divider, plus a radio for which side advances so you can freeze one and let the other climb. Side B's configuration is a copy of side A's with only the compared fields overridden, so a field added later stays in lockstep automatically instead of silently diverging.
Two deliberate details. Both panes show raw estimator output with the post-process and stylisation chain bypassed — that chain is single-input and carries temporal state, so running it on one side only would put a filter on half of an A/B, which is a lie. And identical settings on both sides give bit-identical panes, because the two sides share an RNG stream. That is correct and it reads as broken, so it is the first thing to test in a capture session.
never viewed Built, headless-reachable through pp.split, and not yet seen by a person on screen — which is why it is first on the shooting list.
A separate tool with a different job: it visualises sampling geometry, it does not estimate. The panel names itself — Photon Primitive Validator, at Render Settings ▸ Photon Primitives ▸ Enable — and it is as unphotographed as anything here: eleven rows in the inventory, and every single one of them graded never viewed. Take one traced sub-path, choose vertices, sweep {θ, φ, distance} at them, and watch the endpoint become a curve, a surface or a volume. Nine render modes, of which four are worth naming — the seamless surface, whose quad corners are the neighbouring real sample points rather than a bilinear guess, so curved sweeps show no diagonal seams; the ray-marched volume, which splats photons into an integer voxel grid through atomics, converts to a filterable float texture and marches it fullscreen, so a two-dimensional sweep reads as a glowing shell; the MIS double primitive, shading each cell by the harmonic mean of its own and the other primitive's Jacobian; and the analytical tracer, which ray-traces the exact closed-form surface per pixel with a per-hit exact Jacobian.
You pick both ends of the path by hand, and the panel prices the picks. One pick-mode radio puts the cursor in Camera or Photon mode; a camera pick is a viewport pixel, committed and then locked in world space along with the camera basis, field of view and aspect at pick time, so the gizmo stays where you put it when you orbit away; a photon pick is a world-space hit on a light, and the panel reports which light and whether it came from scene.lights[] or from an emissive entity. Above them sits one shared bounce budget, and this is the part worth copying: the camera and photon sliders are each clamped by the other, and the remainder is printed as its own line — green Slack: n (available for sweep), grey Slack: 0 (no sweep capacity), orange Over budget by n. The dimension count this family’s bugs live in is on the panel face, exactly as it is in the explorer’s Σ free readout in section 02.
| Row | What it is | Mark |
|---|---|---|
| PPE — Per-leg bounce budgets and seeds | Total budget 2–16, split between a camera leg and a photon leg with the slack readout above. Each leg carries its own RNG seed, shown as an integer beside a Resample button that increments it — so a sub-path is addressable as a side and a number, and can be returned to | never viewed |
| PPE — Regex path descriptors | A per-vertex type string for each leg, printed live. Not a pattern and not a filter — see below | never viewed |
| PPE — Occlusion and reachability gates | Two different mechanisms sharing a heading. Occlusion depth-tests the drawn segments against the raster pre-pass and continues a blocked one at low alpha. Reachability is a real cull, run per photon inside the march shader — see below | never viewed |
| PPE — Derived-path diagnostic | Answers “why is this pairing zero”: occluded photons are tinted dark red instead of being culled, and hovering one draws its whole reconstructed sub-path in magenta with red overdraw on the segments that are blocked. Disabled in the volume-march mode, which has no per-photon path to draw | never viewed |
| PPE — Batch and pinning | The hover gathers every photon whose screen projection lands inside a pick radius — 5 to 200 px on a logarithmic slider, default 30 — and depth-stratifies the result to 32; the GPU picker keeps up to 1024 hits before the CPU thins them. Left-click pins that whole set, right-click clears it. A Show structure sub-toggle draws a regular 64-path grid across the swept parameter space at low alpha, independent of the cursor | never viewed |
| PPE — 9 render modes | Disc, oriented splat, surface patch, seamless surface, transport estimate, ray-marched volume, analytical surface, MIS double primitive, analytical tracer. Modes 1–4 need exactly two swept dimensions and the panel says so in place, falling back to Disc rather than drawing something wrong | never viewed |
| PPE — Volume grid march | Core GL has no atomic add on a float image and none on a four-component one, so the accumulation grid is four separate r32ui images and each photon’s rgb is added as fixed point scaled by 1024, with a fourth image counting hits. A convert pass turns it into a filterable float texture and a fullscreen pass marches it. Grid 32–256 (default 128), steps 16–256 (default 256) or auto-tracked to about twice the grid resolution; deposit either a flat 1.0 for clean geometry or the transport weight | never viewed |
| PPE — Analytical primitive rendering, 4 materials | Matte, Glossy, Metallic and Anisotropic (silk), the last elongating its highlight along the parametric u direction so a swept sheet reads as brushed metal. Shaded fill and wireframe are independent toggles — with both off, nothing renders — and a smoothing option takes the fragment normal from screen-space derivatives of world position instead of the per-cell cross product, to remove faceting at cell boundaries | never viewed |
| PPE — A/B harmonic-mean MIS editing | Mode 7 marches two fully independent swept primitives and draws both clouds at once; the MIS toggle replaces each cell’s own Jacobian with the harmonic mean of its own and the other primitive’s. The result is cached on a hash of all four march hashes plus the toggle, so editing B does not re-march A | never viewed |
| PPE — The floating sweep editor | A separate auto-resizing window joined to the selected vertex by a dashed arrow, opened by clicking a vertex in either the schematic or the viewport. Per-dimension enable, 2–1024 samples and a range: θ is a full sphere at π, φ a full revolution at 2π, and dist scales the leg by u·R, so R = 0 collapses the vertex and the sweep closes into a solid ball. In mode 7 the same window switches between primitive A and primitive B | never viewed |
The “regex” is a label, not a pattern. buildRegex walks a sub-path’s vertices and emits one character each from a seven-letter alphabet — L light, E camera, D diffuse, S specular, V volume, G glass, e emitter — separated by spaces, and the panel prints it. Nothing is matched against it and nothing is filtered by it. It is the path’s type written down so that the eye can check the pairing it is looking at, which is the right thing for a validator to do and is not what the name promises. It is also a different object from the path-type filter on the tracer’s own Filters tab, which does filter.
The reachability cull carries this project’s byte-identity convention into a visualiser. With the gate off, per-photon visibility is left at 1.0 and the march shader’s own comment records that the output is byte-identical to the behaviour before the feature existed. With it on, every segment of the reconstructed sub-path gets an any-hit test, and the photon is culled on the first blocked one. The tests are written inside the march shader to mirror the CPU intersector exactly — the comment says same epsilons, same clip and slab arithmetic, and the same minT semantics — because a cull that disagreed with the renderer’s own intersector would delete photons the estimator can genuinely reach, and a picture of a sampling geometry that quietly drops reachable samples is worse than no picture.
Underneath it is a taxonomy: a header-only classifier that maps a set of swept dimensions to an analytical shape, unit-tested by a target that links glm and nothing else. The test binary carries 61 assertions across two files — the shape classifier and the evaluator — and returns non-zero on any failure, so it doubles as a gate.
Self-described status is ongoing, and the document says where. The analytic tracer handles sphere, cone, disk, parallelogram, parallelepiped, planar sector and ball sector; the higher-dimensional shapes have no closed form and are not traced. The rasterised MIS mode has a cache-timing bug where the density sometimes only appears after a resample, and its own notes propose deprecating it in favour of doing MIS in the analytic tracer instead.
Fifteen overlay draw functions cover the rest of the renderer — seven of them per-integrator path and photon visualisations for the photon mapper, VCM, MLT, PSSMLT, UPBP, path guiding and manifold exploration, plus ray differentials, normal arrows, the light tree, the acceleration structures and the BDPT strategy pyramid.
The acceleration overlay is worth calling out because of what it reveals rather than what it draws. The BVH and the uniform grid are built, and the overlay is their only consumer — nothing traverses them on either the CPU or the GPU, where the tracer is a flat linear scan over all primitives. The visualiser is drawing a data structure the renderer does not use.
All of the estimator-side drawing shares one glyph vocabulary: cyan for the camera side, warm yellow for the photon side, a red centre dot for a swept or specular vertex, a dashed ring for a camera or light endpoint and a solid ring for an emitter or a surface. The same vocabulary is used by the in-app 2D schematic, the 3D overlay, and the schematics drawn for these pages — which is why a diagram here and a screenshot from the app read as the same object.
08 — What is still invisible
A page about visualisation should be able to say which of its own things have never been on a screen. These compile, pass their headless or numeric checks, and have never been looked at by a person. Anything odd in them is a real candidate bug, not an expected gap.
| Thing | Status | Note |
|---|---|---|
| The Photon Primitive Validator, entire | never viewed | Eleven rows in the inventory and not one of them has been on a screen; I have not found another cluster on this site that size. Section 07 is its roster, and the two clips there are the shooting list |
| Split A/B compare | never viewed | Headless-reachable via pp.split. Shoot it early, in case it is broken |
| Clean view (G) | never viewed | And it is incomplete by inspection — it does not hide photon dots, path overlays, the KD-tree, physics particles, the hover BSDF badge or the dolly-zoom HUD |
| Hourglass controls — mirror relays, J-floor slider, include-photon-point | never viewed | All three are measured; none has been driven by hand |
| Explorer coverage for strategies 0–2, 5, 6, 17–30 | design only | Nineteen of thirty-one render flat grey — including the whole hourglass family, the cross-family mixture, the glossy ring and the one-dimensional group |
| Correlated rasterising gather | measured | Built. Bounds each primitive in screen space and gathers per scanline instead of tracing per pixel, and it passed the strongest acceptance test here: md5-identical to the all-N tracer on 97 of 97 comparisons, the same sum in a different order. 16.7× on the quad and 5.8× on the beam at N = 256, 1.1× on the hourglass. Invisible in the truest sense — its film is bit for bit the traced one |
| Photon Mapper panel · "Show Jacobian" | dead | The flag has no reader anywhere in the source. Since 2026-09-04 the toggle is greyed and labelled [inert], and the two placeholder switches beside it are greyed with it |
The last row is the one to keep, and it is why the control was greyed rather than left alone. A control that looks like a visualiser and does nothing is worse than a missing feature, because a screenshot of it would be a lie, and this whole page is built on screenshots meaning what they appear to mean.
What none of this replaces. Every picture on this page is a diagnostic, not evidence. The frozen batch is explicitly not converging to the right answer; the display mode skips the divide by N and switches the grazing guard off; the explorer's colour is a ramp, not a radiance. The wrong-divisor experiment in section 03 is the whole argument in one number — an image that looked entirely correct at every sample count was four and a half times too bright, and only a reference film and a median ratio ever said so. These tools decide where to look. They never decide whether the answer is right.