Image operators · RadianceLab
Sitting on top of a physically-based path tracer is a bank of stackable image operators that is much stranger than a post-process stack. Ricci flow, Hodge decomposition, persistent homology and walk-on-spheres share a combo box with halftone, ASCII art and Kuwahara. The strangeness is the point.
This page expands a single row on the landing-page ledger into 157 operators, three families, and an audit of every theorem-named shader against its own source. It is also the least photographed territory in the project — almost every figure below is a capture instruction, not a capture — and the shot list is a deliverable in its own right: eighteen captures nobody has taken — fifteen stills and three clips — of operators that all exist and are all one click away.
01 — What is actually there
The landing page carries one row for all of this: "157 stackable image operators — over 21 non-photorealistic modes — Ricci flow and persistent homology share a combo with halftone." That row is the only place on this site the bank exists. Everything below is what is behind it.
The claim is not "we have effects". Every renderer has effects. What is unusual here is the company these operators keep: a lattice U(1) gauge field with Wilson-loop holonomy is nineteen rows below Risograph misregistration, in the same scrolling panel, and both are one click. And what is unusual in the other direction is that fifty-five of them read the path tracer's G-buffer, so they are not image filters at all — run them on a photograph and they do nothing.
The bank is genuinely wired. That was the first thing to check, because a list of 157 named toggles is exactly the sort of thing that turns out to be a menu with no engine behind it. It is not.
Every operator toggle in the bank reaches a dispatch site and a shader, and that is the claim worth making. An earlier draft of this paragraph tried to settle it with a stronger one — that of the 178 *Enabled flags the UI declares, exactly one has no reader outside src/ui/. That is wrong: three do. diffRenderEnabled is the one that draft had in mind; envEnabled is defensible, because UIManager.h:214 says it zeroes envStrength at the use sites; and the third is a good joke at this page’s expense. ltEnabled has exactly one reference in the entire tree, its own declaration, and it is the light tracer’s enable flag — sitting a few lines from the comment that this project’s whole reachability audit exists because of. The bank is genuinely wired; the sentence advertised as proving it was not.
On the number 157. The catalogue's figure is 157 and the landing page repeats it. The chain itself holds exactly 157 dispatch sites — 146 in EngineRender.cpp:2710–3036 and 11 in the tail at :3825–3849 — and applyCVFilter, one line after the block closes, is a 158th. An earlier version of this read-out stopped the chain at :3010 and reached 157 by adding the CV filter to a short count of 156; it landed on the right total by cancelling two errors, and the group table in §08 (which sums to 157 without the CV filter) was right all along. Separately there are 164 _toggle knobs in UIManagerRender.cpp, and the gap to 157 is not the single subtraction an earlier version of this note claimed. Eight of the 164 drive something other than a bank operator — spectralMode, volLightSampling, bdptPyramid, enableMedium, ppDOFEnabled, ppGlowEnabled, cvFilterEnabled and dataMoshEnabled — leaving 156; the 157th operator, tofRecoveryEnabled, has no _toggle at all but an ImGui::Checkbox in a different file (UIManagerPanels.cpp:1034). 164 − 8 + 1 = 157. Four of the 157 are mutually exclusive (see the conformal note in §07), so the largest number that can be simultaneously on is 154.
Presented as a flat list of 157 shaders this reads as sprawl. It is range. Three quite different projects share one panel — second-pass shading that reads the renderer's own geometry, a bank of operators named after theorems, and a set of output-device emulators — and each carries a claim the other two cannot make. The rest of the page is three contact sheets, one per family.
Contact sheet I
Fifty-five operators consume depth, normals or the position/material buffer. They are not image filters — they are second-pass shading, and the panel already grades which ones need the tracer.
Contact sheet II
Ricci flow, the heat method, Hodge decomposition, walk-on-spheres, persistent homology, lattice gauge theory. Every name checked against what the shader computes. Some are earned. Some are not.
Contact sheet III
Halftone, dither, Risograph, linocut, woodblock, sumi-e, screen tone, ASCII, pixel sort, data mosh. Emulating an output medium is the same operation as emulating a lens — and this one destroys the radiometry on purpose.
02 — Contact sheet I
Buried in the Advanced VFX panel is a grading system nobody documented. Every operator row draws two glyphs beside its name: a star (gold, silver, amber or none) and a renderer badge. The badge is a tiny drawing of a raster frustum, a ray fan, or both — and the code that draws it takes three values.
| Badge | Means | VFX band | CV band | Drawn as |
|---|---|---|---|---|
| 0 | tracer only | 15 | 4 | ray fan alone |
| 1 | works in both renderers | 75 | 2 | frustum + ray fan, side by side |
| 2 | both, but better with the tracer | 29 | 7 | dimmed frustum + bright ray fan |
| — | no badge drawn | 23 | 1 | — |
What the two columns add to. The VFX band draws 119 badges over 142 toggles (drawBadge, UIManagerRender.cpp:1800); the CV band draws 13 over 14 (drawBadgeCV), the unbadged one being holoV2_toggle at :4503. 142 + 14 = 156, which is every operator with a _toggle; the 157th is the checkbox named in §01. An earlier version of this table put 0 in the CV column's last cell and so summed to 155.
So the author already sorted the bank by how much renderer it needs, in the UI, and then never wrote it down. Nineteen operators are marked tracer-only. Those nineteen are the contact sheet:
The G-buffer is only bound when the tracer is active. The dispatch block opens with two lines that decide the whole family's behaviour:
Twenty of the fifty-eight branch on uHasGBuffer and fall back to a luminance-derived surrogate; linocut.frag:89 is a clean example. (Twenty-two shaders mention uHasGBuffer; the two extra — parallaxLayers.frag and volLightCone.frag — guard the depth and position buffers and never declare uNormalTex at all. Counting all 22 as guarded normals is the error this read-out used to make.) The other thirty-eight sample the normal buffer unconditionally. With texture zero bound that read returns black, and a normalize(vec3(0)) is not defined — so the predicted behaviour under the rasteriser is somewhere between "does nothing" and "NaN". I have not run it. That is the first shot on the list and it is a bug hunt as much as a photograph.
Marked unverified. The fallback claim is read from source, not observed. Nothing in this repository records what any of these thirty-eight operators looks like under the rasteriser.
Two operators in that list deserve a note rather than a shot, because they are funny. SS Caustics and Screen-Space Reflections are screen-space approximations of exactly the two phenomena this renderer computes correctly and at length elsewhere on this site. They are not redundancy by accident — they are the cheap version, kept, in the same program as the expensive version, and there is currently no picture comparing the two. That comparison is a shot on the list below and it is arguably the most useful thing on this page.
03 — Contact sheet II
The most striking thing in the panel is the row of names: Ricci Flow, Heat Method Geodesics, Hodge Decomposition, Walk on Spheres PDE, Persistent Homology, Gauge Theory Viz, Parallel Transport, Spinor Field, Optimal Transport Color, Morse-Smale Topology, Reeb Graph Contours, Laplacian Eigenfunctions, Willmore Energy, Cotangent Laplacian Flow. Several carry a citation in their tooltip. It would be easy to write a page that just listed those names.
So I read the shaders instead — every one below, end to end, against its own tooltip. The table gives each name, what the shader actually computes, and whether the name is earned. Three are earned outright. One is a plain mislabel. The rest are in between, and the table says exactly where, because a precise "in between" is a stronger result than a rounded verdict in either direction.
What I checked. The shader source for each row, end to end, plus the operator's UI tooltip. Not the rendered output — none of these has ever been rendered on purpose. Verdicts are about what the code computes, which is a question source can answer; nothing here is a claim about how the image looks.
| Operator | What the shader computes | Name | Anchor |
|---|---|---|---|
| Gauge Theory Viz | Assigns a U(1) link phase to each edge between neighbouring pixels from the difference of structure-tensor angles, wraps it to (−π,π] with atan(sinΔ,cosΔ), and sums link phases around a closed rectangular plaquette of side 1–5 texels. That sum is the lattice field strength F = ∮A·dℓ. | earned | gaugeTheory.frag |
| Optimal Transport Color | Sliced Wasserstein: projects colours onto 4–16 random 1-D directions, matches quantiles against a loaded reference image, accumulates the displacement. Structurally the Bonneel construction its tooltip cites. | earned | optimalTransportColor.frag |
| Gauss Map Visualisation | Normal → point on S² → colour. That is the Gauss map. | earned | gaussMap.frag |
| Hodge Decomposition | Builds a structure-tensor tangent field, takes its divergence and curl, and runs a genuine 8-iteration Jacobi Poisson solve for both the potential φ and the stream function ψ, then forms ∇φ + curlψ + remainder. The decomposition is real. But the solve is on a 5-point stencil with the outer ring pinned to zero — a Dirichlet-zero boundary one texel away — so φ and ψ are near-local and the "harmonic" remainder is nearly the whole input field. | structure right, support one pixel | hodgeDecomposition.frag:89–115 |
| Walk on Spheres PDE | 4–32 independent random walks per pixel, 4–16 steps each, averaging the scene colour at the walk endpoint — a real Monte-Carlo Laplace solver in shape. Two deviations from Sawhney & Crane: the step is radius · bDist · (0.3 + 0.7·ξ), a random fraction of the largest empty ball rather than a jump to its boundary, which is the step the mean-value property makes unbiased; and the walk terminates on a fixed step count rather than an ε-shell. | right estimator, biased | walkOnSpheres.frag:84–96 |
| Persistent Homology | Mode 0 is genuine discrete Morse theory: steepest 8-connected descent from each pixel to its basin minimum, hashed to a colour — that recovers connected components of a sublevel set. But there is no filtration sweep and no persistence pairing; "birth/death" (mode 1) is per-pixel critical-point classification at one threshold. The shader's own header says "approximating persistence diagrams". | homology yes, persistent no | persistentHomology.frag:60–90 |
| Ricci Flow | See below — this one needs a paragraph. | half earned | ricciFlow.frag |
| Kuwahara Multi-Scale | Its own first source line reads "Single-scale anisotropic Kuwahara filter". The slider labelled Scales (1–5) sets nSamples = 8 + (uScales−1)×4 — samples per sector, not scales. The tooltip cites the multi-scale paper. | mislabelled control | kuwaharaMultiScale.frag:12,55–56 |
| Heat Method Geodesics | Tooltip: "Brief heat diffusion, then Poisson solve gives geodesic distances." The shader diffuses heat (as a closed-form Gaussian, correct on a flat unbounded domain), computes the normalised gradient X = −∇u/|∇u| — and then never uses X again. The distance is a 16-tap luminance line integral along the straight segment from source to pixel, scaled by |uv − src|. There is no Poisson solve. The output is a luminance-weighted Euclidean radius. | not earned | heatMethod.frag:72–91 |
The tooltip reads: "Flow the metric by −2·Ric(g). (Perelman's tool for Poincaré conjecture.) Curvature-weighted smoothing." Here is what is right and what is not, in order.
Right: the shader's own header states the correct reduction — in two dimensions a conformal metric g = e2uδ turns the flow into an equation on the single scalar u. And the curvature overlay really is the conformal-metric Gauss curvature rather than an edge detector wearing its name. It really does run explicit Euler on a 5-point Laplacian for 4–20 iterations, not a blur pretending to be one.
A correction to an earlier version of this section, which printed the shader's curvature line and called it exact. It is not: it is off by a factor of two, and the reason is more interesting than the size. The same identity has two standard forms. Writing the metric as g = e2uδ gives K = −e−2uΔu; writing it as g = ρδ gives K = −(1/2ρ)Δlogρ. They agree, because logρ = 2u and the 2 cancels. ricciFlow.frag takes the 1/(2ρ) prefactor from the second form and the Δu from the first, so the 2 never cancels and the overlay shows K/2. Nothing visible depends on it — the value is immediately normalised by clamp(K·8.0, −1, 1), so the factor is absorbed into a display gain — but this was the one claim the page made in the operator's favour, on a page whose whole thesis is that a name has to be checked against the code. It did not survive its own standard.
Not right, in three parts. First, the missing e−2u: real 2-D Ricci flow is a nonlinear fast-diffusion equation, and the shader integrates its linearisation. Second, u is image luminance, not a metric on any surface — there is no manifold here, and no depth or normal is read, so this operator is running on the picture, not on the geometry the renderer knows. Third, the iteration does not close: the comment concedes that the ring neighbours are "updated with their own simpler one-step estimates", so past the first step the stencil is inconsistent and the scheme is not the explicit Euler iterate it claims to be.
And one line in the shader is worth quoting because it is the exact species of argument this page exists to catch: "Cap iterations to keep worst-case cost reasonable; mathematically Ricci flow converges in 2D regardless of specific step count." That is a true statement about the continuous flow being used to license a truncation of a discrete scheme, which it does not license. The truncation is probably fine — with dt ≤ 0.05 in texel units the explicit scheme is comfortably inside its stability bound — but it is fine for a different reason than the one given.
The fair summary. "Ricci Flow" here is curvature-coloured heat-equation smoothing of luminance with a gradient warp. That is a real and slightly unusual image operator, and the overlay genuinely is the conformal-metric Gauss curvature, up to the stray factor of two above. It is not Perelman's tool, and nothing in this program flows a metric. The name would be earned by a rename — "Conformal Curvature Flow" — and by keeping the e−2u, which is one multiply.
These I read enough of to classify but did not audit line by line. Where I could not tell from the source what an operator computes, the row says so rather than guessing.
| Operator | Reading | Grade |
|---|---|---|
| Morse-Smale Topology | Critical points and separatrices from the Hessian determinant of the luminance height field. Correct construction for a height function; "complex" in the full sense (a cell decomposition) is not built. | never viewed |
| Reeb Graph Contours | Level sets of luminance plus merge/split detection at critical points. Its own header calls the nodes "critical points, edges = persistence", which is the Reeb graph's structure honestly described. | never viewed |
| Cotangent Laplacian Flow | Cotangent weights derived from triangles formed by depth samples — so this one does use the geometry, unlike Ricci flow. Strength −2 to +2 gives sharpen or smooth. | never viewed |
| Willmore Energy | ∫H² over the depth surface, three display modes. Geometry-derived. Tracer-only badge. | never viewed |
| Parallel Transport | Transports a tangent vector from image centre to each pixel, accumulating Christoffel contributions from the luminance metric gradient. Its own uniform block admits uPathLength is unused — the path is always centre→pixel. | one dead uniform |
| Spinor Field | Hues cycling with a 4π period to expose the SU(2) double cover. A visualisation of a fact, not a computation on the scene — and honest about it. | never viewed |
| Navier-Stokes | Self-contained: luminance as density, structure tensor as initial velocity, then advection + viscous diffusion + vorticity confinement for a few internal iterations, then the colour is advected. Its own header says "Navier-Stokes inspired". | honest in its own header |
| Schrödinger Evolution | Luminance as potential V, an analytic Gaussian wave packet evolved by a split-operator-style update, |ψ|² as intensity and argψ as hue. | never viewed |
| Laplacian Eigenfunctions | Named for eigenmodes; the header describes multi-scale disc sampling with bilateral-style spectral smoothing and a "modes" count of 1–8. I could not establish from the source that any eigenproblem is solved. Marked unresolved rather than guessed at. | unresolved — needs a closer read |
| Geodesic Distance | Distinct operator from Heat Method Geodesics, and more honest: its header says "single-pass approximation" outright, with three seed modes and a height-scale slider. | never viewed |
| Hyperbolic Tiling, Mandelbulb Overlay, Domain Colouring, Julia Overlay, Quaternion Colour, Clifford Attractor, Fractal Dimension | Overlays of known mathematical objects rather than analyses of the frame. Nothing to overclaim; the names describe the pictures. | never viewed |
| Percolation (pc=0.593 button), Ising (Tc=2.269 button), Markov Chain Walk | Statistical-physics automata with one-click critical-parameter buttons. All three carry the "better with tracer" badge, which is hard to justify — they are image-domain and read no G-buffer. | badge looks wrong |
04 — Contact sheet III
The renderer spends enormous effort producing a linear HDR film: unbiased estimators, byte-identity contracts, energy ratios to four digits. The third family exists to throw that away in a specific, historically-motivated manner. A linocut takes a float32 linear radiance field and returns one bit per pixel.
The reason this belongs on a rendering site rather than in a footnote is that it is the same class of modelling as everything else here. A thin-lens camera is a model of an input device. A Risograph is a model of an output device: two ink drums, halftone screens at different angles, misregistration in pixels, grain, overprint. Both are physical-process emulation; only one of them is usually treated as serious.
| Operator | The physical process being modelled | Controls | Reads G-buffer |
|---|---|---|---|
| Risograph | Two-drum stencil duplication: two ink colours, each halftoned, printed in separate passes that do not register perfectly | 2 ink colours · Dot Size 3–12 · Misregister 0–3 px · Grain · Overprint | no |
| Woodblock Print | Ukiyo-e multi-plate relief printing: a limited palette carved as separate plates, each with its own registration error, on grained wood | Palette 3–6 plates · registration offset · wood grain · carved outlines | no |
| Linocut / Scratchboard | Relief carving: white lines cut into black, thickness carrying tone, cross-hatching in the mid-tones. Binary output. | Spacing 3–15 · Width 0.3–2.0 · Contrast 1–3 · Flow 0–1 | yes — lines follow the 3-D surface flow, with a uHasGBuffer fallback |
| Sumi-e Ink Wash | Ink on absorbent paper: wet-edge bleeding, anisotropic brush strokes, paper absorption, atmospheric fade with distance | depth-aware fade | yes |
| Screen Tone | Ben-Day dots: adhesive manga tone sheets, denser dots for darker regions. Three pattern modes. | dots / lines / crosshatch | no |
| Advanced Halftone | Process halftone screening, multi-shape and multi-colour-mode, with the screen angle steered by surface normal | shape · colour mode | yes |
| Advanced Dithering | Bayer, blue noise, normal-aware and spatially-consistent modes — the ordered and error-diffused ends of the same problem | 4 modes | yes |
| Pointillist (Seurat) | Neo-impressionist optical mixing: a jittered grid of complementary-paired dots that mix in the eye rather than on the canvas | colour-theory pairing | no |
| ASCII Art | Character-cell reproduction: luminance per cell selects a glyph from a procedural 4×5 bitmask table. The oldest output medium in the list. | Cell Size 4–16 px | no |
| Pixel Sorting | Not a print process — a codec artefact, deliberately induced. Sorts runs of pixels by brightness within luminance-bounded segments, capped at 64 taps. | direction · 3 sort keys · two thresholds · Max Segment 16–512 | no |
| WFC Mosaic | Procedural tiles chosen by luminance and gradient with adjacency constraints. Header says "WFC-inspired" — no constraint propagation happens, which a fragment shader could not do anyway. | cell size · tile set | no |
Four of these read the G-buffer, which is the unusual part. A linocut whose carving direction follows the surface flow of a 3-D normal field rather than an image gradient is not something a photo-editing filter can do; it is a print emulator that knows the geometry it is printing. That is the intersection of family I and family III, and it is where I would start if the goal were one image that justifies the entire bank.
What this family costs. Every operator here discards the property the rest of the site is about. There is no tonemapping choice to make — the megakernel's Reinhard is hardwired at pathTrace.comp:4064 and the Tonemapping combo above it is inert — so the image the press family receives has already been through a fixed curve and a gamma. Nothing downstream of the bank is linear, nothing is measured, and no claim on any other page of this site survives contact with these operators. That is the correct trade for what they are for, and it is worth saying out loud on the page that shows them.
05 — State
The default assumption about a post-process is that it is a pure function: same input, same output. Seventeen shaders in this territory break that. They sample a previous-frame texture, which makes them simulations running on the display buffer — and it means a still photograph of them is not a description of them.
Sampled, not declared — and this page got that wrong. An earlier version of this table counted the shaders that declare a previous-frame sampler and put three innocent operators in the stateful column. Two of them tell you so in the declaration: reactionFlow.frag:10 reads uniform sampler2D uPrevTex; // unused but kept for C++ binding compat, and morphogenesis.frag:9 is commented // unused. Neither ever samples it. The third, temporalNoise.frag, declares no previous-frame sampler at all — it varies frame to frame because it is seeded from uTime and uFrame, which is a different thing from memory. The simulation page's §07 states this correctly and always did; this page is now the one that changed.
| Stateful shader | What accumulates |
|---|---|
| waveEquation.frag | A second-order wave field; needs two previous states to step, so it genuinely propagates |
| rdStylize.frag | Reaction-diffusion concentrations, ping-ponged. Turing patterns emerge over frames, not within one. The only genuinely stateful RD operator in the bank — it reads uPrevTex at five sites (:64, :94, :100, :138, :139) |
| smoothlife.frag | A continuous-domain cellular automaton with its own reset button; this is the only real SmoothLife in the program — the two scene presets named SmoothLife are legacy FBM noise |
| ibfv.frag | Image-based flow visualisation: noise advected along the field and blended with the previous frame, which is the whole IBFV method |
| nbodyAttractor.frag, curlNoise.frag | Particle-like positions carried forward through an advection field |
| anisotropicDiffusion.frag, heatEquation.frag, totalVariation.frag, mumfordShah.frag, poissonGradient.frag | Iterative solvers that use the frame loop as their iteration loop — the honest way to do what §03 shows Ricci flow and the heat method trying to do inside one invocation |
| progressiveDraw.frag | Strokes deposited over time; the image is a drawing being made |
| fractalFeedback.frag, phaseAmplify.frag | Feedback, and Eulerian phase amplification — the Gabor response of the previous frame is what the current one is measured against |
| dataMosh.frag | A frozen frame and its depth buffer, held until you unfreeze. See below |
| ballpointB.frag, temporalBlur.frag | The two outside the bank: pass B of NPR mode 7 ping-pongs its own stroke buffer, and the tail's temporal blur is a straight mix(current, prev, blend). Both are in this territory and neither is an Advanced VFX toggle |
| Looks stateful, is not | What the source says |
|---|---|
| reactionFlow.frag | :10 — uniform sampler2D uPrevTex; // unused but kept for C++ binding compat. One occurrence in the file, the declaration. A per-frame filter with a reaction-diffusion look |
| morphogenesis.frag | :9 — uPrevTex, commented // unused. Same: declared for the C++ binding, never sampled |
| temporalNoise.frag | No previous-frame sampler of any kind. It changes every frame because it hashes uTime and uFrame — unphotographable in a still, but stateless |
Note the row of five solvers. Anisotropic diffusion, the heat equation, total variation, Mumford-Shah and Poisson gradient editing solve their PDE the correct way, by iterating across frames. The mathematics family in §03 does not, and compresses the iteration into a single fragment invocation on a local stencil instead. Both approaches are in the same panel. The difference between them is exactly the difference between the earned names and the unearned ones, and it is a difference of engineering, not of ambition.
The menu bar carries Tools ▸ Data Moshing (WIP), unwired. The feature is fully implemented one panel over, at Effects ▸ Post Process, and it is the most interesting stateful operator in the program because one of its three modes is not an image effect at all.
| Mode | What it does | Grade |
|---|---|---|
| Reproject | Freeze a frame and its depth buffer. As the camera moves, re-project the frozen pixels through the frozen depth into the new view. The moshed image sticks to 3-D geometry. This is a depth-correct temporal artefact, not a 2-D smear. | never viewed |
| Screen-Lock | The 2-D version: the frozen frame stays pinned to the screen. The conventional data-mosh look. | never viewed |
| Difference | Current minus frozen. Incidentally a usable debugging tool: freeze, change one setting, see exactly which pixels moved. | never viewed |
| Menu route | Tools ▸ Data Moshing (WIP) — unwired menu item advertising a finished feature as unfinished | mislabel |
06 — The layer above
Above the bank sits a single combo box with 22 entries — "Off" plus 21 modes — and it behaves differently from everything below it in two ways. It is exclusive, so exactly one can be active. And five of its modes are not applied to the finished frame; they are compiled into both renderers and applied to radiance before the frame exists.
| # | Mode | Where it runs | Grade |
|---|---|---|---|
| 1–5 | Toon · Outline · Toon+Outline · Pencil Sketch · Oil (simple) | Inside both renderers. basic.frag:896–910 per fragment; pathTrace.comp:2293–2350 per sample. The panel's "Affects both renderers" label is literal. | seen |
| 6 | Oil Painting [Advanced] | 2-pass post with relief lighting and a canvas freeze | seen |
| 7 | Ballpoint Pen | 3-pass post | seen |
| 8 | Ink Spiral | 2-pass post, with a Variation switch to a Hilbert curve family whose density is modulated by scene luminance and optionally by estimated depth | never viewed |
| 9–12 | Halftone · Cross-Hatching · Kuwahara · Watercolor | Post. Mode 11 is a second, separate Kuwahara from the bank's "Kuwahara Multi-Scale"; both can be on at once | seen |
| 13 | SmoothLife | A continuous cellular automaton run as a ping-pong post-process with a reset button. Evolves over frames. The only real SmoothLife in the program | never viewed |
| 14–17 | Pencil Crystal · Noise Contour · Pixel Art · Stained Glass | Post | never viewed (Noise Contour) |
| 18–21 | Depth Fog · Depth Edges · Tilt-Shift Miniature · Depth Warp/Sponge | Depth-driven post, marked [D] in the combo | never viewed |
Modes 1–5 in the path tracer are applied to radiance at line 2293, then firefly-clamped at 2372, and only then accumulated into the running mean at 2443. So the toon quantisation happens per sample, before averaging. Averaging N independently quantised samples of a noisy estimate reconstructs a continuous ramp. The prediction from source is therefore:
Marked unverified, deliberately. Every line of that read-out is derived from the source and none of it has been observed. It is on this page as a prediction with an address, so that the shot below either confirms it or falsifies it. If the bands do survive at 500 passes on a noisy scene, the reading above is wrong and I would want to know why.
Three things about that Variation switch, because it is the only control in the combo that swaps a shader rather than a branch. The two variations run two different pass-A shaders, inkA.frag and inkA_hilbert.frag, and share pass B; the switch is guarded by m_inkAHilbertShader.isValid() (EngineNPR.cpp:428), so if that shader ever failed to compile the combo would still read "Hilbert Curve" and the spiral would run. The two variations also share one backing variable under two labels: inkSpiralSlope, 5–30, is presented as Spiral Slope in one mode and Curve Spacing in the other (UIManagerRender.cpp:1497, :1501), so a value set under one name follows you into the other.
And the Depth Density toggle reads no depth buffer. Its tooltip says "vary ink density by estimated depth"; the shader's own uniform comment says "vary density by estimated depth (luminance proxy)", and what it computes is local luminance variance, smoothstepped to a 0.4–1.5 density multiplier on the reasoning that detailed regions are near ones (inkA_hilbert.frag:16, 145–155). The shader is honest and the tooltip is the part that rounds up. The curve itself is real — a proper xy→d Hilbert mapping, layered at orders 6, 4 and 3 — and it is not the program's only one: the bank carries a separate Hilbert Line Stylization operator in dispatch group 13, different code, same curve. never viewed, both of them.
07 — The ledger for this territory
A page that lists 157 capabilities and no defects is a brochure. Here is everything in this territory that does not do what its label says, separated by kind of failure, because the three kinds have very different consequences for a user.
Orphaned files first. I re-derived this list independently of the catalogue by checking every one of the 227 .frag and .comp files in shaders/ for any mention anywhere under src/. Eight have none, and they fill the first five rows of the table below. The sixth row, fullscreen.frag, is a different animal and is here because its consequence is the only one that matters: it is mentioned — loaded, even — and still never runs.
Not a corroboration, and this page said it was. An earlier version of this paragraph claimed the eight matched docs/FEATURE-CATALOGUE.md exactly and called that "two people counting the same thing separately and agreeing." They were not counting the same thing. The catalogue's §5.3 is headed "Seven files" and lists nine filenames in six rows; my scan finds eight files with no mention plus fullscreen.frag, which the catalogue also lists. Eight, seven and nine are three numbers, and agreement between two of them was asserted rather than checked. The catalogue's heading has been corrected to nine; the count that is actually derivable from a rule is the eight-with-no-mention, and that rule is now stated above so the next person can re-run it.
| File | Why it has no caller | Is a capability missing? | Grade |
|---|---|---|---|
| conformalWarp.frag | m_conformalWarpShader loads conformalGlitched.frag instead — the shader named after the function is not the one the function uses | No. Conformal Glitched renders. | superseded |
| deepDreamCNN.comp deepDreamGrad.comp | Never loaded. generateCNNWeights(int seed) sets a bool and returns. | DeepDream [Advanced] still renders — as iterated gradient-ascent ping-pong. The image changes; the CNN does not exist. | superseded |
| styleGram.comp | Never loaded; m_stAdvGramComputed is written once and never read | No — and this row is the opposite of what it looks like. Style Transfer [Advanced] is a real 2-pass structure-tensor plus statistics transfer against a loaded reference, despite its own header calling itself a "stub — Phase 3". | superseded |
| houghLines.frag houghCircles.frag houghCirclesDraw.frag | v1 single-pass fragment implementations, replaced by compute accumulate + NMS + draw-peaks | No. Both Hough operators work, better. | superseded |
| eulerFluid.comp | The Euler solver is CPU, in PhysicsWorld.cpp:565–732. (An earlier anchor here said :521–554, inherited from the catalogue; that range is inside the two initialisers, not the stepper.) | No. | superseded |
| fullscreen.frag | m_fullscreenShader is loaded at Engine.cpp:236 and has exactly two references in the repository — that load and its declaration. It is never .use()d. | Yes, one. This file implements Reinhard and ACESFilm. Because it never runs, the Tonemapping combo's three options all render the megakernel's hardwired Reinhard. ACES is compiled into the binary and unreachable. | orphan with a consequence |
Rule 1 applies to every row above. Not one of them is dead. The question "is the feature reachable by any route" is yes in all six cases; the question "is this file live" is no. That pair is superseded, and each row says where the feature actually lives. The only row with real consequence is the last, and it is a consequence for the tonemapper, not for an effect.
These came out of reading the shaders for §03 and §06 and are not in docs/FEATURE-CATALOGUE.md's mislabel table.
| Control | What the label promises | What it does | Anchor |
|---|---|---|---|
| Kuwahara Multi-Scale ▸ Scales | A multi-scale filter, cited to Kyprianidis & Kang 2011, with a 1–5 scale count | The shader's own first line says "Single-scale anisotropic Kuwahara filter". uScales sets samples per sector: 8 + (uScales−1)×4. There is one scale. | kuwaharaMultiScale.frag:2, 12, 55–56 |
| Heat Method Geodesics | Tooltip: "Brief heat diffusion, then Poisson solve gives geodesic distances." | The normalised heat gradient X is computed and then never referenced again. No Poisson solve exists. The displayed distance is |uv−src| weighted by a 16-tap luminance line integral. | heatMethod.frag:72–91 |
| Parallel Transport ▸ Path Length | A slider, 0.05–0.5 | Its own uniform declaration carries the comment "(unused: path always uses centre→pixel)". An inert control that documents its own inertness in the shader but not in the UI. | parallelTransport.frag:14 |
Inert knobs already on the record, both in this territory: stAdvIterations (Style Transfer [Advanced] ▸ Iterations, 1–8) and ddAdvChannels (DeepDream [Advanced] ▸ Channels, 8 or 16). Each has exactly two references in the repository — its declaration and its own widget. Dragging either changes nothing.
One structural surprise, not a defect. The four conformal-map operators — Conformal Fixed, Glitched, Glass, Correct — are dispatched as an if / else if / else if / else if chain. They are the only mutually exclusive group in an otherwise freely stackable bank, and the UI gives no sign of it: four independent-looking toggles, of which only the highest-priority enabled one runs. Turning on Conformal Glass while Conformal Fixed is already on does nothing and looks broken.
08 — The two ends nobody described
Two clusters sit at the edges of this territory and no section above has touched either. One is the computer-vision pair, which is the only place in the bank where an operator had to be redesigned because the input was a renderer's output rather than a photograph. The other is the fixed tail that the pipeline diagram in §01 names in eight words and nothing on the page unpacks. Both are tables; three rows in them carry a mechanism or a defect and those are written out.
| Computer-vision row | What it is | Grade |
|---|---|---|
| Shared edge pre-filter | One Sobel-family pass feeding both Hough transforms: four denoise modes (off, Gaussian, bilateral, median 3×3) × four edge methods (Sobel, difference-of-Gaussians, Canny-lite with non-maximum suppression, Scharr), a binary threshold, and a contrast boost applied before detection (EngineVFX2.cpp:4205-4231). Bypassable, in which case the raw gradient votes | never viewed |
| Edge pre-filter debug view | Shows the pre-filter's output instead of the Hough output. Not a rendered visualisation — it hands you the intermediate texture itself, whose channels are R = the binary edge, G and B = the gradient direction, A = the soft magnitude (:4237-4240) | never viewed |
| Hough line detection [GPU] | Edge pixels vote atomically into a (ρ, θ) accumulator in a compute shader, spread over 3 θ bins × 3 ρ bins with Gaussian-like weights so that a diagonal line is not penalised against an axis-aligned one. 60–360 θ bins, 64–512 ρ bins, a peak cutoff, a bounded mode that draws segments only where real edges are, and a Hough-space view | never viewed |
| Hough circle / ellipse [GPU + NMS] | The same pre-pass into a three-dimensional (r, cy, cx) accumulator, then a compute-shader non-maximum-suppression pass extracting up to 256 peaks (EngineVFX2.cpp:4393). Radius is in min-dimension fractions, so a circle stays round at any viewport aspect; an aspect parameter with a one-click = 1 reset turns it into an ellipse detector; the Hough-space view is a max-over-radius projection with hue encoding the winning radius bin | never viewed |
The pre-filter is not boilerplate; it is the concession to running on a path tracer. Its own tooltip says why it exists: both transforms "vote ONLY from clean binary edges… this prevents a few bright HDR regions from dominating the accumulator". A Hough transform written for an 8-bit photograph can vote by gradient magnitude and be fine. Vote by gradient magnitude on a linear HDR film and a specular highlight three stops above everything else wins every bin it touches. So there is a tone-map multiplier ahead of the edge detector and a hard binary threshold after it, and the denoise combo exists because the tracer's own sampling noise is impulse noise — which is why median 3×3 is in a list that would otherwise be Gaussian and bilateral. That is a computer-vision operator adapted to a renderer, and it is the one thing in this pair worth a paragraph.
One defect, in the circle pass. The NMS shader's header says it finds peaks that are a "strict local max in 3x3x3", and the test it runs is if (n > v) isMax = false — which passes when a neighbour equals the centre. Ties are not suppressed. Every cell of a flat plateau in the accumulator is emitted as its own peak, and because the accumulator holds integer vote counts on a downscaled grid (the Accum Downscale slider goes to 16), plateaus are the normal case at low vote counts rather than a pathological one. The visible consequence would be several concentric or near-coincident rings drawn for one circle, and the 256-peak cap being reached by duplicates. I have not run it — this is read from houghCirclesNMS.comp:42-59, where the test itself is line 53, and the shot below is the test.
After the 157 comes a short, unreorderable sequence that every frame passes through: applyCVFilter, then depth-of-field, then the in-engine photo tap, then data moshing, then the node projector's reproject mode, then glow, then the final temporal blur, then the selection overlay. Four of its rows have never been photographed and one of them is not a filter at all.
| Tail row | What it does | Grade |
|---|---|---|
| Blur (temporal + spatial) | An exponential moving average across frames plus a Gaussian across pixels, on the accumulated image. The panel header was renamed on 2026-09-04 and now carries the sentence "Blurs, not a denoiser. There is no denoiser here." in the UI itself (UIManagerRender.cpp:1419-1428) — a rare case of a program correcting its own overclaim in the widget rather than in a commit | seen |
| Post ▸ Depth-of-Field blur | Distinct from the tracer's real thin-lens depth of field, with per-renderer enables and a Decouple from Camera switch giving it its own aperture and focus distance | seen |
| Post ▸ Glow / Bloom | Threshold, intensity, radius. The knee is not a control: it is fixed at 0.1 in glow.frag:26, and the shader's comment gives the threshold's intended range as 0.5–2.0 where the slider offers 0–1 — so the bottom of the slider blooms everything above 0.1 luminance at full weight | seen |
| Post ▸ CV / Image Filter | An always-available slot outside the bank: Sobel edge with an overlay-on-image option, unsharp-mask sharpen, or a bilateral filter with spatial sigma, range sigma and a 1–8 kernel radius | never viewed |
| — live bilateral-grid preview | A diagram, not a preview — see below | never viewed |
| Post ▸ Final temporal blur | A second, independent EMA blending with the previously displayed frame, 0–0.95, with an early-out below 0.001 that also drops the history so the effect does not resume mid-fade | never viewed |
| Selection overlay / hover mask | A screen-space masked overlay for the selected and hovered entity, suppressed entirely by Clean View. It runs after the final temporal blur — see below | seen |
| Background depth prepass | A lightweight raster linear-depth pass kept alive whichever integrator is active, so the depth-driven operators and the photon-primitive overlay always have a fresh buffer even under a CPU integrator | seen |
Two sliders in this tab are both called Temporal Blur, and they are different things at different points in the pipeline. One is in Blur (temporal + spatial), ranges 0–1, and smooths the accumulator. The other is in Post Process, ranges 0–0.95, and blends the finished display texture with the last one shown (UIManagerRender.cpp:1424 and :1791). Both are on screen at once if both headers are open. The second is the one that ghosts a camera move; the first is the one that makes an animated volume converge faster and quietly biases it.
And the second one is not last. The catalogue calls it "a second, independent EMA at the very end of the chain", and the source disagrees: applySelectionOverlay runs after it. The consequence is small and specific — the selection outline is composited on top of the blur and is therefore the one element in the frame that does not ghost when you move the camera — and it is invisible under Clean View, which is where every shot on this page is taken. The correction is here rather than in the catalogue because the source is what settles it.
Turn on the bilateral filter and a Show Grid Preview toggle appears. What it draws is not your image. It builds a hard-coded one-dimensional test signal — flat at 0.3, a linear ramp between x = 0.45 and 0.55, flat at 0.8 — and splats it into a grid whose two axes are spatial x and range, with the cell counts derived live from your two sigma sliders: the x cell count from the spatial sigma, the range cell count as 1/(σr + 0.01), both clamped. Occupied cells are tinted by their mean value, the signal curve is drawn over the top, and hovering the canvas draws the filter's support ellipse at the cursor with semi-axes proportional to σs and σr — so you can see, directly, that raising the range sigma is what lets the filter reach across the step (UIManagerRender.cpp:1694-1785).
That is a teaching figure for the bilateral grid, live-parametrised by the controls it explains, sitting inside a filter panel. It is the most unusual widget in this territory and it has never been photographed. Two things about it should be said plainly. Its own footer label reads "Splat → Blur 3D → Slice (bilateral grid)" and the widget performs the first of those three: the code splats and draws, and there is no blur pass over the accumulated cells and no slice back to the signal. The catalogue's row says it animates all three; the source says otherwise, and the source wins. The label is best read as naming the method the drawing belongs to rather than the stages the drawing shows. And the toggle is a function-local static bool, so unlike every other control in the panel it lives outside UIManager and is not saved with the rest of the UI state.
09 — The deliverable
Contact sheets need a shot list, and this page's real output is the twenty capture instructions above — seventeen stills and three clips — plus the protocol that makes them comparable. Every shot on this page obeys the same five rules, and the two added in §08 obey a sixth: where a shot can be checked against arithmetic rather than against taste, the expected numbers are written into the recipe.
And the accounting, so nothing in the bank is hidden by the fact that three curated sheets cannot hold 157 things. These are the source's own dispatch groups — twenty-two of them, each one a comment line in EngineRender.cpp — in the order the chain applies them. The n column sums to 157, which is the check the read-out in §01 failed and this table passed.
| # | Dispatch group | n | Sheet | Grade |
|---|---|---|---|---|
| 01 | Basic — style transfer, deep dream, neural edge glow | 3 | — | never viewed |
| 02 | Spatial / structural (applied first) — SSAO, depth contours, bilateral grid, feature volume | 4 | I | never viewed |
| 03 | NPR / artistic — normal hatching, voxel grid, texel splatting, advanced dither, advanced halftone | 5 | I + III | never viewed |
| 04 | G-buffer 3-D-aware | 11 | I | never viewed |
| 05 | Differential geometry + print media | 8 | II + III | never viewed |
| 06 | Histogram / painterly / progressive | 3 | — | never viewed |
| 07 | Anime / Live2D / manga | 8 | III (screen tone) | never viewed |
| 08 | Volumetric ray-marching — god rays, fog, aura, atmosphere, SSR, light cone | 6 | I | never viewed |
| 09 | Topology / flow — Morse-Smale, level sets, gradient flow | 3 | II | never viewed |
| 10 | Advanced techniques — walk-on-spheres, anisotropic diffusion, TV, Fourier spectral, heat equation, morphological, geodesic distance, power Voronoi | 8 | II | never viewed |
| 11 | Mathematical / artistic — Laplacian eigenfunctions, OT colour, Poisson gradient, Mumford-Shah, XDoG, stroke-based, phase amplification, discrete curvature | 8 | II | never viewed |
| 12 | Research-level — multi-scale Kuwahara, coherent line drawing, four conformal maps, contour families, transport and OT warps, and eighteen more | 29 | mixed | never viewed |
| 13 | Siggraph-inspired — guided filter, palette recolour, Voronoi stipple, ASCII, 2-D Gaussian splatting, RD stylize, layered abstraction, Hilbert stylization | 8 | III (ASCII) | never viewed |
| 14 | Glitch / generative — pixel sort, WFC mosaic, temporal noise | 3 | III | never viewed |
| 15 | Neural, applied last — style transfer [advanced], deep dream [advanced] | 2 | — | 2 inert knobs |
| 16 | Pure mathematics — domain colouring, Julia, fractal dimension, quaternion colour, Clifford attractor | 5 | II | never viewed |
| 17 | Field-based / animated — IBFV, curl noise, reaction flow, n-body, wave equation, morphogenesis, magnetic field, plasma, Schrödinger, Navier-Stokes, EM scattering, gradient-descent flow, Hodge, Helmholtz, heat method, Laplace eigenfunctions, Ricci flow, parallel transport, spinor, gauge theory | 20 | II | never viewed |
| 18 | Computer vision — Hough lines, Hough circles, epipolar, stereo, geometry reconstruction | 5 | I + §08 | never viewed |
| 19 | Depth extraction / holography | 7 | see the imaging page | measured |
| 20 | Statistical physics — percolation, Ising, Markov chain | 3 | II | never viewed |
| 21 | Research-grade math — hyperbolic tiling, persistent homology, mandelbulb, Reeb contours, kaleidoscope | 5 | II | never viewed |
| 22 | Tail — chromatic hallucination, fractal feedback, depth abstraction | 3 | I | never viewed |
One group in that table carries a grade the others do not. Group 19, depth extraction and holography, is measured — three time-of-flight systems and three holographic reconstructions with error figures scored against ground-truth depth. It sits in the same panel as ASCII art, and it is the only part of this territory with a measurement behind it — scored, though still never photographed. It has its own page, and it is the proof that this bank carries real work: the same dispatch chain that runs a halftone runs a self-scoring phase unwrapper.