Simulation · RadianceLab

Things that move
on their own

Five physics solvers, a Gray-Scott reaction and a bank of shaders that carry state between frames run inside a real-time path tracer, and three of the solvers put their state into the transport itself — as media that shadow rays intersect, not as an overlay. Which of these systems a photon can actually see is the subject of this page.

Every claim on this page was read out of the source and anchored to a line, not measured off a film, so the marks are seen and never viewed rather than measured. Almost none of it has been photographed yet: simulation is the one subject on this site a still cannot carry, so the placeholders below are shooting lists for clips.

Solvers that reach the traced image
5→3
SPH and MPM as injected volume spheres, Euler by density grid — and §03 on what happens to that grid
Integrators that read the noise config
2 / 9
of the nine integrator .cpp files in src/render/ — megakernel 17 references, UPBP 7, the rest 0
Accumulated passes while physics runs
1
the stepper sets needsAccumReset every frame
Live Gray-Scott sim, warmed and bound
64³→0
sampling sites reached by the presets that start it

01 — Where simulation meets transport

Three routes carry simulated state into the light transport; the best of them turns a fluid into participating media, and each has a limit worth naming

The test of a renderer with a physics engine inside it is not whether the physics runs. It is whether a photon can see the result. Here it can, by three genuinely different mechanisms — and the strongest of the three is not the one the UI advertises.

The advertised route is the Physics density source: a button in the volume properties panel whose tooltip reads “Euler/SPH/MPM fluid density from physics simulation.” That tooltip is a mislabel and the catalogue records it as one. uploadPhysDensityTexture reads m_physics.eulerDensity() and nothing else, and m_eulerDensity[…] is written only inside the three Euler routines. Choose SPH, MPM, Simple Particles or Soft Body and the grid stays at the zeros init() filled it with, so the volume renders empty.

The unadvertised route is the real coupling. When Show Particles is on and the solver is SPH or MPM, every particle is pushed into the tracer’s primitive buffer as a real volume sphere — radius 0.03, σs 20, σa 2, isotropic phase, material 0. These are not an overlay. They are scene primitives that shadow rays intersect, that appear in mirrors, and that scatter along every bounce. It is the only place in the renderer where a particle simulation becomes participating media.

RouteWhat travelsWho can see itMark
Euler grid → density texture
EnginePhysics.cpp:278-300
An R16F 3-D texture, 64³, re-uploaded every physics frameGPU megakernel only, as volType == 5. The transport side of this route is complete; the initialisation side has an ordering problem — §03scene-dependent
SPH / MPM particles → volume spheres
EngineRender.cpp:2010-2026
One GPUPrimitive per particle, appended after the scene’s own primitivesGPU megakernel only. Every CPU integrator builds its scene from scene.entities, which never contains themnever viewed
Node-graph live procedural → surface channels
pathTrace.comp:1017-1136
A time-linked 3-D pattern overriding albedo, roughness, metallic, emission colour or opacity per hitGPU megakernel only. NEE reads the material buffer and never sees the overridenever viewed
Live Gray-Scott 3-D texture
EngineRender.cpp:1835-1841
An RGBA16F 64³ ping-pong sim, bound as uRDTextureTwo shader branches, both behind a gate the presets that start the sim happen to close — §04superseded
SPH / MPM / Soft Body particles → GL pointsScreen-space points coloured by speed or material typeDrawn over the finished frame. No shadow, no reflection, no lightseen

The coupling, and its edge in the same breath. Three of the five solvers reach the traced image — two by particle injection, one by the density grid — and they reach it on the GPU megakernel, the renderer that runs in real time, not on any of the other eight integrators. The remaining two, Simple Particles and Soft Body, reach it by no route at all; their output is the GL point overlay and nothing else. So “five physics solvers coupled to a path-traced medium,” the phrase on the landing page, is exact for three of them on one renderer, and this is the page that names the other two.

The injection route also costs more than it looks. invalidateScene() is called on the frame the particle list changes, which rebuilds and re-uploads the entire primitive SSBO; while the simulation is running that is every frame. And the GPU intersector is a linear scan, with a second full scan per bounce for the volume interaction, so the SPH Dam Break preset’s 2,000 particles are 2,000 extra entries in a list that is walked twice on every bounce of every path. This is a physically correct visualisation mode rather than a production path — and physically correct is the part that matters in a renderer whose subject is where light goes.

video · Particles that are actually media
Preset 0 Cornell Box, GPU tracer, integrator Path Tracing. Advanced ▸ Volume ▸ Physics Simulation on, Sim Type SPH Fluid, preset Dam Break (2,000 particles), Show Particles ON. Press Play and record 20 seconds from the collapse to the first rebound. Keep the camera still.
Watch for the particle cloud casting a real soft shadow on the Cornell floor and picking up the red and green wall bounce. That colour is the proof it is inside the transport rather than composited on top; then note the single-sample grain that never resolves, which is section 02
The one route worth photographingSPH and MPM particles enter the tracer as volume spheres with σs = 20. Compare against the same clip with Show Particles off and the density source set to Physics, where the same solver produces an empty grid.
Screenshot — THE DECIDING CAPTURE for section 03. Preset 0 Cornell Box, GPU tracer. Select the box, Properties ▸ + Add Media ▸ VolumeBox, density source Physics. Advanced ▸ Volume ▸ Physics Simulation on, Sim Type Euler Fluid, press Play, wait five seconds, then pause and let it converge to 300 passes. Beside it, the identical setup with Sim Type switched to SPH Fluid and nothing else touched. Caption whichever way it comes out
The tooltip names three solvers; at most one writes the gridThe right-hand frame is what “Euler/SPH/MPM fluid density from physics simulation” delivers for two of the three solvers it names: an empty box. Whether the left-hand frame shows smoke is the open question of §03, and this is the capture that answers it.

02 — What motion costs a progressive renderer

The engine has two answers to “the scene changed”, and the good one is a per-pixel adaptive accumulator

A progressive path tracer accumulates. Anything that moves invalidates what has been accumulated. This renderer solves it twice, with two different mechanisms; one of them is genuinely good, and the subsystem that gets it is not the one that needs it most.

Animated volume noise gets the good one. The Animate Volumes toggle does not reset accumulation. It changes the accumulator’s weight from the uniform running mean 1/(n+1) to a per-pixel floor derived from how much that pixel actually changed:

diff = |c − prev| / max(|prev|, 0.01)
w = max( 1/(n+1),  mix( 1/64, 1/4, clamp(2·diff, 0, 1) ) )
A pixel whose radiance is not changing keeps converging at 1/(n+1). A pixel that is changing falls back to a four-frame exponential memory. Verified at pathTrace.comp:3750-3754.

That is a genuinely good design, and the code says so out loud — the reaction-diffusion stepper carries the comment “Do NOT resetAccumulation here — the adaptive temporal EMA in the shader handles the time-varying RD texture, keeping static parts smooth.” The walls of an RD room converge to a clean image while the volume in the middle churns.

Physics gets the blunt one. The physics step sets m_ui.needsAccumReset = true unconditionally at the end of every stepped frame, and the render loop turns that into a full resetAccumulation() — the accumulation image is cleared and the frame counter returns to zero. So while a simulation is playing, every displayed frame is a fresh one-sample-per-pixel path trace, everywhere in the image, including the parts of the room that are not moving at all. The adaptive EMA that would have fixed exactly this is sitting in the same shader.

Animated volumes — accumulationnever reset; per-pixel adaptive EMA
Animated volumes — fastest weight1/4 (four-frame memory)
Animated volumes — slowest weight1/64, or 1/(n+1) if that is larger
Reaction-diffusion — accumulationnever reset; relies on the same EMA
Physics running — accumulationcleared every frame; effective 1 spp
Where the volume clock comes fromuTime = ImGui::GetTime() — wall clock, not frame count

The wall clock in that last row has a consequence worth stating. Because scene.nodeTime is assigned ImGui::GetTime() unconditionally every frame, and the uniform prefers it over the frame-counter fallback, a slow render skips animation rather than slowing it. A heterogeneous volume that runs at 8 fps advances the same distance per second as one running at 60, so the EMA is averaging over larger jumps and the fog reads blurrier. The frame-counter fallback at ComputePathTracer.cpp:1803-1805 would have made animation deterministic per frame; it is unreachable, because nodeTime is positive from the first frame onward.

A second self-adjusting knob, in the other direction. The physics stepper measures its own wall time and edits the UI. SPH and MPM get a minimum substep count computed from a target sub-timestep — 0.004 s for SPH, 0.002 s for MPM, hard-capped at 16 — written back into m_ui.physSubsteps so the slider visibly jumps up when you select a solver. Then, if a step takes longer than 16 ms, the substep count is decremented and written back again. Under load the simulation silently trades accuracy for frame rate and moves the slider while you watch. Honest, undocumented, and worth one clip.

video · Two answers to the same problem
Split screen, same room, GPU tracer, Path Tracing. Left half: preset 15 Cornell + Volume (animated), Render ▸ animate volumes ON, physics OFF. Right half: preset 0 with a VolumeBox on the Physics source, Sim Type Euler Fluid, physics Playing. Record 30 seconds of each side from a cold start.
Watch for the left frame's walls and floor going clean within a few seconds while only the fog stays grainy, against the right frame staying uniformly noisy forever including the static walls. Same accumulation buffer, two different policies
Adaptive EMA versus a reset every frameThe mechanism that keeps the left frame’s static geometry smooth is available to the right frame and is not used by it.

03 — The physics bank

Five solvers, sixteen seeding presets, and a sixth solver that steps every frame over an empty list

All five live in PhysicsWorld behind one simType integer, stepped from Engine::update with a substep loop. Four of the five are guarded by an emptiness check, so selecting a solver does nothing until something seeds it; Euler is the exception and steps unconditionally, which is part of why it is the one that ends up with a grid worth uploading.

Sixteen is what the buttons come to. Exactly sixteen named preset buttons exist — SPH 4, MPM 6, Simple Particles 6, all built from one presetBtn lambda at UIManagerRender.cpp:5009–5123. The Seeding column below also mentions SPH's default uniform grid and Soft Body's two shapes, which are not buttons; an earlier heading rounded the whole thing up to twenty, and twenty is not derivable from anything.

SolverMethodSeeding presetsFeeds the tracerMark
SPH FluidSmoothed-particle hydrodynamics; kernel radius and rest density auto-solved from particle spacing on init and logged4 — Dam Break, Droplet, Double Dam, Column — plus a default uniform gridYes, as injected volume spheresseen
MPMMLS-MPM with an APIC affine velocity field; three material types — elastic, snow, fluid6 — Snowball, Elastic Cube, Two Material, Fluid Pour, Wall Smash, Snow StackYes, as injected volume spheresseen
Euler FluidMAC grid, semi-Lagrangian advection with Jacobi pressure projection, buoyancy and vorticity confinement. Labelled [WIP] in its own panelNone. initEulerFluidTopEmpty fills the lower half and is called once; initEulerFluidBlob has no callerIts grid is the only one uploaded — but see the ordering problem belowscene-dependent
Simple ParticlesAABB-bounded balls, two collision modes — soft repulsive spring or hard positional penalty6 — Rain, Fountain, Explosion, Dam Break, Snow Globe, Zero-GNoseen
Soft BodyMass-spring with structural, shear and bend springs; box or ball. Labelled [WIP]2 shapes × grid resolution 2–10Nonever viewed
Rigid bodiesstepRigidBodies runs every substep regardless of simType, with restitution, friction and a sphere inertia modelNone. addRigidBody has zero call sites anywhere in src/No — the list is always emptyabsent

Reachability, not call sites. The rigid-body row is the one case here that survives the audit rule that caught the light tracer. The function has a caller — it runs on every substep of every solver — so a naive grep grades it live. But the feature has no route: nothing anywhere constructs a RigidBody, no preset, no panel, no config key. Q1 is no, so this is genuinely absent, and it is absent while still costing a loop iteration sixteen times a frame.

The Euler grid is seeded, then erased, on the same frame

This is a source read, not a measurement, and it comes with the one capture that settles it, because it bears directly on the claim the landing page makes.

One press of Play, or any change that sets physNeedsInit, runs a single block in Engine::update in this order:

m_physics.reset()
m_physics.setEulerRes(ui.physEulerRes)
… seed the chosen solver — for Euler, initEulerFluidTopEmpty() fills the lower half
m_physics.init(cfg)
Engine.cpp:576-1030. The last line is the problem.

PhysicsWorld::init is four statements long. It stores the config, sets m_eulerRes = 64 unconditionally, and reassigns both the density and the velocity vector to 64³ zeros. So it runs after the seeding and undoes it: the resolution the slider asked for is replaced by 64, and the “bottom half dense” field initEulerFluidTopEmpty just built is replaced by zeros. uploadPhysDensityTexture(), three lines later, uploads that.

Nothing puts density back. stepEulerFluid has no source term — it advects and dissipates what is already there, at 0.995 per step — and the wind injects velocity, not density. The only two routines that write density are the two initialisers, one of which is erased and the other of which, initEulerFluidBlob, has no caller anywhere in src/.

What this predicts, and how to falsify it in one screenshot. If the reading is right, a VolumeBox on the Physics density source renders empty for every solver, Euler included, and the Resolution slider is inert while still visibly restarting the sim — the same “fakes a response” class as the three photon-map controls on the catalogue’s inert list. The test is the shot specified at the top of §01: preset 0, a VolumeBox with density source Physics, Sim Type Euler Fluid, press Play, wait five seconds. Smoke means the reading is wrong and this paragraph should be deleted. An empty box means the advertised coupling has been broken by a four-line function called one line too late. Either way it is one capture, and it is the highest-value capture on this page.

Three more Euler parameters sit just behind that. eulerBuoyancy, eulerVorticity and eulerDissipation each occur exactly twice in the entire source tree — the struct default and the read inside the solver. There is no slider, no config key and no writer, so buoyancy is pinned at 1.0, vorticity confinement at 0.5 and dissipation at 0.995 forever. The solver implements them properly; nothing can reach them.

Wind is camera-driven, and its shape control is a drawing

Pressing R in FPS capture toggles a wind force whose origin is the camera position and whose direction is the camera forward vector. You aim it by looking. On the CPU path it adds a quadratic-falloff velocity impulse to the Euler grid cells within the radius, and separately pushes SPH, MPM and Simple particles; on the GPU particle path the same parameters go to particleSim.comp as uniforms.

The Wind Blower panel offers a Shape combo — Cone or Cylinder — and a Width slider that only appears for Cylinder. Neither reaches the solver. windShape occurs in five places in EngineRenderViz.cpp, three in the panel, one in the header; windWidth in two, one and one. applyWindForce takes origin, direction, strength and radius, and the field it applies is a sphere. So the shape control changes the overlay you are shown and not the force you are applying, which is the same defect class as the physics-density tooltip: a widget that draws its own promise.

One more asymmetry: the wind pushes SPH, MPM and Simple particles, and does not touch soft-body nodes. A soft body is immune to the wind.

Wind strength0.1 – 200, logarithmic
Wind radius0.1 – 50, logarithmic
Wind width (Cylinder)0.1 – 20 — overlay only
Solvers the force reachesEuler grid, SPH, MPM, Simple
Solvers it does notSoft Body
Force shape actually appliedsphere, quadratic falloff, always
video · Aiming the wind with the camera
Preset 0, a VolumeBox on the Physics source, Sim Type Euler Fluid, Fluid Viz set to Both so the velocity arrows are visible whether or not any density survives. Note that the Fluid Viz overlay is silent unless some entity's density source is Physics — it looks for one to map grid cells to world space. Enter FPS capture, press R, then walk around the box and look at it from three sides over 25 seconds.
Watch for the velocity arrows in the Y slice swinging to follow the crosshair and the quadratic falloff at the edge of the radius. Then switch Shape from Cone to Cylinder mid-clip: the drawn overlay changes and the arrows do not, because the force is a sphere either way
The one control in the renderer you aim by lookingOrigin is camera.position, direction is the normalised look vector, and the cone drawn on screen is a separate piece of code from the force. Watch the arrows rather than the smoke — velocity is the field the wind actually writes.
Screenshot — Advanced ▸ Volume ▸ Physics Simulation with Sim Type MPM and preset Two Material selected, Show Grid on, Show Particles on, captured mid-fall so both material colours and the background grid are visible in one frame
MPM, two materialsParticle colour is assigned by materialType — blue elastic, white snow, cyan fluid — in the GL overlay path. In the tracer-injection path all particles share material 0 and the distinction disappears.

04 — The simulation nobody has seen

A real Gray-Scott simulation runs 64³ cells a frame, drives density and colour, and is pre-empted three lines before it is read

This is the sharpest example on the page of a system that evolves state, feeds the transport, and is invisible — and unlike most such stories the mechanism is short enough to state in full.

Loading any of the eight RD scene presets does four things. It applies a Gray-Scott parameter pair to ReactionDiffusion, initialises a 64³ RGBA16F ping-pong pair seeded with five overlapping blobs plus noise, runs 500 warm-up steps so the pattern is already formed when the first frame draws, and builds a scene whose volume spheres carry volNoiseScale = 51.0f with the source comment // Style 51 = RD 3D texture. From then on the sim steps rdSubsteps times per frame — a 1 to 32 slider — and the result is bound as uRDTexture.

Two shader branches read that texture. One turns the V concentration into density through a smoothstep(0.05, 0.35, v) with a boundary fade. The other — the better one — turns it into coloured emission: a hue cycled by V concentration and world position, masked by pow(smoothstep(0.25, 0.5, v), 2.0) and multiplied by 8.0, so the activated regions of the reaction glow and light the room.

Both branches sit inside if (!typeHandled). And the same scene builders that set volNoiseScale = 51 also set volumeType = ReactionDiffusion, which is volType == 2, which is dispatched about eighty lines earlier and sets typeHandled = true. What you see in presets 5–8 and 10–13 is therefore reactionDiffusionPattern3DNode — a hand-tuned procedural emulation built from Worley cells, a sine fold and a domain warp — while the actual simulation runs, steps, and is thrown away.

WhatWhereStateMark
Gray-Scott 64³ GPU compute simReactionDiffusion.h:29-145, reactionDiffusion.compRuns, steps, is bound as a samplersuperseded
— density branchpathTrace.comp:384-395Behind !typeHandled and vNoise > 50never viewed
— coloured emission branchpathTrace.comp:671-686Also needs a non-zero volume emission strength, which no RD preset sets — the field defaults to 0never viewed
Procedural RD emulation (what you actually see)pt_noise.glsl:267-318Three pattern families in the function; the volume path hardcodes rdType = 0seen
RD panel — 8 presets, feed / kill / diffusion U and V, substeps 1–32, Reset GridUIManagerRender.cpp:5148-5182Drives the sim that is not sampledseen

The route back, and why it exists by accident

There is a way to see the real simulation, and it is one click. The volume properties panel’s Remove Source button sets volumeType = None, clears useNoiseLab and clears proceduralStyle. It does not touch volNoiseScale. So the entity drops out of the typed dispatch, falls through to the legacy fallback, arrives at vNoise > 50.0 with its 51 still set, and samples uRDTexture — the live sim, stepping, warmed. Add a volume emission strength above 0.01 and the emission branch lights up too.

Correction to a page already published. ten-integrators-one-scene.html §06 grades this row “branch runs, result discarded” and describes the investigation honestly: the branch was forced to density = 0.0 and the probe re-run byte-identical, centre pixel 0.04631 either way, concluding “something downstream throws its result away. Unfixed.” The measurement was right and the conclusion was one step short. Nothing downstream discards anything — the branch that was zeroed is never reached, because a typed dispatch upstream claims the entity first. The same page’s practical advice, shoot one preset per RD group, holds for a different reason than the one given: the emulation does vary with preset index, but only through two coefficients, so the four RD presets differ far less than the four Gray-Scott parameter pairs would.

video · The simulation, once you get out of its way
Scene ▸ RD Box: Coral Growth. Let it run 10 seconds as shipped. Then select the volume, Properties ▸ Remove Source, and let it run 30 more without touching anything else. Then add Volume Emission at strength 2 and run 20 more.
Watch for the pattern changing character at the first cut — the shipped view drifts and folds like advected noise, the post-Remove-Source view grows and splits like an actual reaction front, because it is one. At the second cut the activated regions start emitting and lighting the box from inside
Emulation, then simulation, then simulation as a light sourceThree states of the same volume, reached by one button and one slider. The third has never once been rendered on purpose; this clip would be the first.
Screenshot — Contact sheet, four frames, GPU tracer, identical camera, animate volumes on, 200 frames of EMA settling each: Scene ▸ RD: Mitosis, RD: Coral, RD: Worms, RD: Spirals, all as shipped. Label each with its rdPreset index
Four presets of the emulationPreset index enters the emulation twice, as worleyNoise(q · (1.4 + 0.1·preset)) and as the cell-to-fold mix weight 0.22 + 0.06·preset. That is the entire difference between these four frames.

05 — Every generator, and who reads it

The noise is good, it lives in one 853-line module, and seven of the nine integrators cannot see any of it

All procedural density in this renderer comes from shaders/modules/pt_noise.glsl, a single 853-line module built on one value-noise base. It generates eight density sources and six Noise Lab types, animated; the finding is which renderer is allowed to look.

The generators

FunctionWhat it isCalls in the volume pathMark
hash3 / noise3The value-noise base everything else is built on14 / 39seen
fbmNoise(p, octaves)Fixed-parameter FBM; the workhorse of the legacy style bands5seen
worleyNoiseCellular F1 distance, inverted so cells read dense7seen
voronoiNoiseF2 minus F1 — cell-edge detection3seen
fbmConfigurableFBM with explicit lacunarity and persistence1seen
ridgedNoise, marbleNoise, turbulenceNoise, worleyFBMNamed, compiled, never called0 eachsuperseded

The last row is a Rule-1 case in the good direction. Those four functions have no call site, but the noise types they name are all reachable, because noiseLabDensity inlines its own octave loop with the same formulae so it can apply the eight per-octave amplitude mutes. Ridged is rn² where rn = 1 − |2n − 1|, turbulence is |2n − 1|, Voronoi is 1 − voronoiNoise, and Marble skips the loop entirely for a multi-axis sinusoidal warp with 1 − |vein|0.3 giving thin bright veins. The functions are dead; the features are not. Grade superseded, and say where they live.

Animation enters the Noise Lab as an anisotropic drift of the sample position, vt · timeSpeed · (1, 0.7, −0.5), gated by uAnimateVolumes. Every legacy style band advects differently — the Fire band falls at −0.4t in y so the flames rise, the Lightning band shifts its filaments on a sine.

Who reads it

This is the finding, counted. Whole-word occurrences of the six entity fields that carry heterogeneous density — noiseConfig, volNoiseScale, proceduralStyle, volumeType, sdfShape, rdPreset — across every file in src/render/:

IntegratorRefsWhat a structured medium looks like thereMark
GPU megakernel (ComputePathTracer.cpp)17All eight density sources, all six Noise Lab types, all three phase functions, animatedmeasured
UPBP (UPBPIntegrator.cpp)7A partial CPU re-implementation — see belowscene-dependent
Photon primitives (PPIntegrator.cpp, mode 9)0Homogeneous. Structured fog is inert; the Noise Lab scale slider does nothingsuperseded
VCM, MLT / PSSMLT, Path Guiding, Manifold, Photon Mapper, Light Tracer0 eachHomogeneous, and static — none of them has a time input eithersuperseded

Two counting notes, because both numbers have been wrong on this page before and the corrections stay visible. The nine is nine integrator .cpp files — src/render/ holds twelve, of which RasterRenderer.cpp, PPDiagram.cpp and PhotonPrimitive.cpp are not integrators. MLT and PSSMLT share MLTIntegrator.cpp, and the ninth file is LightTracer.cpp, which an earlier version of this table left out. It belongs in the row: the file is superseded, but the feature is reachable as two Light Strategy combo entries running inside the megakernel — the exemplar case in docs/FEATURE-REACHABILITY.md, and not one to leave off a list of integrators on this of all sites. And the reference counts are whole-word occurrences of the six fields. A substring grep returns 23 for the megakernel, because it also matches noiseConfigs — the local vector of packed GPU structs, a different identifier; counting matching lines instead returns 21, because two of those lines carry two matches each. Seventeen and seven are the occurrences of the six named fields, and that is the number this row means.

UPBP’s seven references are a partial re-implementation with its own Perlin FBM, and its failure modes are specific enough to be worth naming. Its Noise Lab branch honours octaves, lacunarity, persistence and frequency and ignores the noise type, so Worley, Ridged, Marble, Turbulence and Voronoi all render as Perlin FBM. Its legacy-procedural branch covers styles 1–10 and falls back to FBM for 11–15. SDF, Physics and NanoVDB are not handled at all. And the fall-through case, volNoiseScale > 0.01, is a plain FBM at that frequency — which means loading an RD scene in UPBP renders the volume as fine-grained FBM at frequency 51, because 51 was a style selector, not a frequency. Nothing in UPBP takes a time argument, so all of it is frozen.

The finding, stated plainly. A noise field that drives density in one renderer and is silently ignored by another is not a bug report; it is a property of the system, and a reader needs it before comparing two images. Loading preset 15 and switching from the GPU tracer to mode 9 does not give you the same scene at a different noise level. It gives you a different scene: uniform fog at the volume’s base σ, holding still.

Density only, on the volume path — but not on the other one

The volume noise config drives density and nothing else. It never displaces geometry and never touches surface colour, and evalVolDensityAt returns a single float.

The node graph is the other mechanism, and it does more. A live 3-D procedural wired into a BSDF or Emission output is evaluated per hit in the shader and can override albedo, roughness, metallic, emission colour or opacity, either replacing the material value or multiplying it, chosen per channel by a blend bitmask. On the volume side the same machinery reaches σa, σs, medium tint, volume emission colour and volume emission strength, from up to two procedural slots per primitive.

Two limits sit on that, and both are one line of source each.

video · An animated emitter that the shadows do not know about
Preset 0 Cornell Box. Open the node editor on the ceiling quad, wire Time into a Noise3D node into EmissionOutput colour, set Speed to 1. Render 20 seconds under Render ▸ Light Strategy MIS, then switch to Pure NEE and render 20 more without touching anything else.
Watch for under MIS the emitter surface visibly churns while the light it casts on the floor stays perfectly steady — the shadows are being lit by the material value, not the animation. Under Pure NEE the churn vanishes and the quad goes flat
Two consumers of the same emission, one of them animatedThe gap is not subtle once you know to look for it: an emitter can be visibly flickering while the room it lights holds still.
Screenshot — Same scene, same camera, same seed: preset 15 Cornell + Volume (animated) rendered by the GPU path tracer at 400 passes, beside the identical scene rendered by integrator mode 9 (photon primitives) at equivalent cost. The GPU frame shows five structurally different volume spheres; the mode 9 frame shows five uniform fog balls
The same scene file, two integratorsZero references to the noise configuration in PPIntegrator.cpp. The scene did not change; the reader did.

Two more noises in this program, and neither is the thing its name says

The eight density sources above are the noise a reader comes to this page for. Two others sit outside them: the random stream that drives every sample in the megakernel, and the only sensor-side noise model in the renderer. Both are worth a line because in both cases the source is narrower than the name.

WhatWhereWhat it actually isMark
Sampler RNGpt_random.glsl:5-66, seeded pathTrace.comp:3685One PCG hash and a five-line rng() advancing a single uint in place. Three samplers are built on it — cosine-hemisphere, isotropic GGX, anisotropic GGX — and so is the sub-pixel filter offsetseen
Sensor noise modelstofCW.frag:164-175, tofTracer.frag:263-267, 347-349An ambient offset and a “shot noise” term added to a synthetic four-shot phase measurement, in the two time-of-flight benches. Uniform, signal-independent, and frozen — see belownever viewed

The RNG is seeded once per pixel per frame as pcgHash(pixelIndex ^ pcgHash(uGlobalFrame)), and everything a path needs is drawn from that one stream in order. There is no low-discrepancy sequence and no stratification: sobol, halton, low-discrepancy and owen have zero occurrences anywhere in shaders/; blue noise appears only as a 64×64 R8 texture generated for the dithering post-effect, and never in the tracer; stratify appears only in the photon-primitive picker, where it thins a hover selection for display. The tracer is plain independent Monte Carlo, and the site should not be read as claiming otherwise.

A catalogue row this page has to correct. The inventory’s sampler row names four samplers built on rng(), the fourth being VNDF — visible-normal-distribution sampling. vndf and visible normal have zero occurrences in src/ and in shaders/, case-insensitively. The GGX samplers here draw a half-vector from the NDF itself, and the renderer states that in the one place it had to be precise: the photon-primitive integrator’s white-furnace self-test records that it integrates the GGX arms “by sampling the NDF half-vector … pdf_wi = D NdotH / (4 VdotH)”, because a uniform hemisphere cannot integrate a roughness-0.05 lobe. That is the NDF form and not the visible-normal one. Three of the four names are right; the fourth is not, and where a row and the source disagree the source wins.

The ambient slider cancels itself, by construction

The two time-of-flight benches each synthesise a four-shot continuous-wave measurement per pixel and recover the phase from it. Both use the same two lines:

Ik = clamp( ½ + ½·cos(φ + kπ/2)  +  ambient  +  noise,  0,  1.5 )
φ̂ = atan2( I3 − I1,  I0 − I2 )
The ambient term is added identically to all four shots, so it cancels exactly in both differences.

Its only remaining route to the recovered phase is the clamp, and over the sliders’ full range the upper bound is unreachable: the largest a shot can be is 1.0 + 0.3 + 0.1 = 1.4, against a ceiling of 1.5. The lower bound at zero does bite, and only near a trough, and only when the ambient offset is smaller than the noise amplitude. So the one effect the Ambient slider has on a recovered depth is to reduce the error, by lifting the shots off the zero clamp — the opposite of what a control labelled Ambient reads as promising.

The noise term is (hash(uv, k) − 0.5) · 2 · uNoise: uniform on a symmetric interval and independent of the signal. Shot noise in a real sensor is Poisson and scales with the square root of the collected charge; this does not, so the slider is a fixed-amplitude dither wearing the name of a physical process. And neither shader takes a time or frame uniform, so the pattern is identical every frame — it is fixed-pattern noise, pinned to sensor uv, and accumulating passes will never average it away. That is the property that decides whether the multi-frequency unwrapping cascade beside it is being tested or merely decorated, and it is the reason this row is worth a paragraph rather than a line.

Screenshot — Effects ▸ CV ▸ CW ToF on, any converged render with real depth range — preset 47 Vol Caustic Lab works. Three panels, same camera, Display set to "Phase error vs GT" with Error Scale 5: (a) Ambient 0.00, Shot Noise 0.05; (b) Ambient 0.20, Shot Noise 0.05; (c) Ambient 0.20, Shot Noise 0.00. Label each with its two slider values
The falsifiable version of the paragraph aboveIf the reading is right, panel (b) is cleaner than panel (a) — the ambient offset lifts the four shots off the zero clamp and removes the only nonlinearity the recovery has — and panel (c) is exact. If (b) is noisier than (a), this subsection is wrong and should be deleted. One capture settles it.

06 — Simulations that are patterns

Three node types name a solver and implement a pattern, and two of them say so

Sitting in the node editor’s texture palette are three nodes whose names promise simulation. All three are procedural fields. They are not fake — a shaped noise field that reads as fluid is a legitimate texture, and one of the three is a rather good one — but the naming invites a reader to expect a coupling that is not there, and one of the three exposes a solver selector that changes almost nothing.

NodeName promisesImplementationMark
Eulerian Fluid 3D
pt_noise.glsl:360-396
An Eulerian fluidA curl-like flow field built from six noise3 differences advecting the sample point, then a billow FBM mixed with inverted Worley filaments and a sine eddy term. Snapped to a voxel lattice by a gridRes parameter, which is what gives it the grid look. Genuinely time-varying and rather goodnever viewed
Physics 2D / Physics 3D
NodeGraph.cpp:1096-1112
A Source combo reading Euler / SPH / MPMOrbiting Gaussian blobs on sine and cosine paths. The solver selector sets the blob count — 3 for Euler, 6 for SPH, 4 for MPM — and changes nothing else. No connection to PhysicsWorld of any kind. The panel says so: “Physics nodes are still preview approximations while the real source path is being restructured.”honest stub
Reaction Diffusion 3D
pt_noise.glsl:267-318
Gray-Scott, SmoothLife or Stylized, by a Type comboAll three families are implemented and reachable from the node editor. The volume Properties panel’s RD density source hardcodes rdType = 0, so two of the three are unreachable by that routenever viewed
Volume Incremental 3D
pt_noise.glsl:412-432
—A translating soft shape with a velocity vector, shape selector and softness — the closest thing in the file to a genuine advected emitternever viewed

Where the Physics node ends up. detectProcType, which decides what a live 3-D procedural becomes on the GPU, lists ten node types and does not list Physics3D. But isSpatial3DNodeType does. So a Physics node is treated as a live 3-D procedural for one purpose and as an ordinary bakeable node for the other. Whatever reaches the render from it reaches through the CPU bake, not through the per-hit path the other nine take. The panel’s own note is the correct summary and this is a footnote to it, not a contradiction.

The Eulerian Fluid node deserves better than its neighbours. It is called from two sites in the megakernel — the volume density path and the per-hit surface override path — so the same field can drive fog density and a surface’s albedo in one scene, at the same wall-clock time, from one node. Nobody has rendered that yet.

video · One field, fog and surface at once
Node editor, one EulerianFluid3D node with Time wired in, Speed 0.6. Wire its output into a MediumContainer sigma-s on a VolumeBox and into the BSDF albedo of the floor plane under it. Render 30 seconds with the GPU tracer.
Watch for the floor pattern and the fog structure moving in lock-step because they are literally the same evaluation at the same uTime, and the fog's cast shadow on the floor tracking a pattern the floor is already wearing
The node the palette buriedTwo call sites, one field, one clock. The grid snap in gridRes is what makes it read as a solver rather than as noise.

07 — Which of these actually evolve

The useful test is whether the thing reads its own previous frame

The image-space bank contains twenty operators filed under “field-based and animated” plus three under “statistical physics and stochastic,” and their names — Navier-Stokes, Schrödinger evolution, Ising model, percolation, morphogenesis — all suggest state evolving over time. Most of them have none. The distinction is mechanical and cheap to check: does the shader sample a previous-frame texture? Note sample, not declare — two shaders here declare one and never read it, and both say so in the declaration. The image-operator page applies the same test to the whole display chain, NPR combo and tail included, and finds seventeen; this table is the twenty-three field-based and statistical-physics operators only, which is why the two numbers differ.

Five do — four from those twenty-three, plus SmoothLife, which is NPR mode 13 rather than a bank toggle and is here because it is the cleanest example of the mechanism. The rest are pure functions of the current frame, re-derived from scratch every time it is drawn, which is why they hold still when the camera does and why they change discontinuously when it moves.

OperatorReads prior stateWhat that means on screenMark
Wave equationYes — two levels, uPrevTex and uPrev2Tex, ping-ponging over three buffersA genuine explicit leapfrog. Ripples propagate, reflect and interfere across framesnever viewed
SmoothLife (NPR mode 13)Yes — ping-pong RGBA16F pair, with a scene-influence parameter for continuous coupling to the renderThe only true cellular automaton in the repository. Evolves over frames rather than filtering onenever viewed
IBFVYes — advects the previous IBFV frame along the flowImage-based flow visualisation as designed; streaks build up over timenever viewed
Curl noise flowYesDivergence-free advection of the accumulated framenever viewed
n-body attractorYes — warps the previous warped frameCompounding deformationnever viewed
Reaction flowNo — uPrevTex is declared and commented “unused but kept for C++ binding compat”A per-frame filter with a reaction-diffusion lookstateless
MorphogenesisNo — uPrevTex declared, marked unusedSamestateless
Navier-Stokes, Schrödinger evolution, magnetic field, plasma spectrum, EM scattering, gradient-descent flow, Hodge and Helmholtz decomposition, heat method, Laplace eigenfunctions, Ricci flow, parallel transport, spinor field, gauge theoryNo — no previous-frame sampler declared at allEach is a closed-form or fixed-iteration function of the current framestateless
Ising modelNo — N Metropolis sweeps run inside one shader invocation, on a lattice re-seeded from scene luminance every frameDomains form, but from the image, not from the last frame. The shader header states this in fullnever viewed
PercolationNo — occupancy hashed from luminance, then N label-propagation passes in one invocationConnected components resolved out to a neighbourhood of 2N pixels; the spanning cluster is highlighted separatelynever viewed
Markov chain walkNo — N steps back-evolved per pixel in one invocationA stationary-like density over the image’s luminance basinsnever viewed

Being stateless is not a defect here and the source does not pretend otherwise — the Ising shader’s header explains the choice and its reason: “no ping-pong state needed … a re-seeded run per frame … visualises how correlations build up from the image content.” That is a design decision with an argument attached. But it changes what you are looking at. A per-frame Ising is a function of the image, so once the path tracer has converged and the input stops changing, the Ising output stops changing too. It looks like a simulation that has reached equilibrium. It is a still image of a fixed computation.

The three statistical-physics operators are also the only ones in the bank shipped with their critical constant on a button: p_c = 0.593 for percolation, kT_c = 2.269 for Ising. One press puts you on the critical point, which is the only place the fractal structure exists, and the sliders around it are wide enough to walk off it in either direction.

Percolation — p, iterations0.05 – 0.95, one-click 0.593 · 2 – 48
Ising — kT/J, sweeps, external H0.2 – 5.0, one-click 2.269 · 1 – 48 · −2 to +2
Ising — displaysspin map, energy, magnetisation bands
Markov — steps, temperature4 – 80 · 0.02 – 2.0
Markov — displaysdensity heatmap, trajectory field, entropy
Ising lattice cell size4 px — a coarsening chosen so domains are visible
video · Walking through the critical point
Effects ▸ CV ▸ Ising Model on, Overlay 1.0, Display Spin Map, on any converged render — preset 47 Vol Caustic Lab is a good input because it has large smooth luminance gradients. Sweep Temperature slowly from 0.5 to 4.0 over 40 seconds without moving the camera, pausing 5 seconds on the T_c button value of 2.269.
Watch for the ordered ferromagnetic blocks at low T breaking up into scale-free domains right at 2.269 and dissolving into salt-and-pepper above it. Then move the camera one pixel and watch the entire lattice re-seed, which is the tell that there is no state
The critical point, and the tellThe camera nudge at the end is the whole point of the clip: a simulation with memory would perturb; this one restarts.
Screenshot — Three panels of the same converged frame with Effects ▸ CV ▸ Percolation on, Colour Clusters on, Overlay 0.85, at p = 0.45, p = 0.593 via the p_c button, and p = 0.75. Iterations 48 in all three so the labelling is as complete as the shader allows
Below, at, and above thresholdCluster colouring is label propagation, so the labels resolve components only out to about 2×iterations pixels — at 48 iterations a large cluster can still show as several colours. That is a property of the algorithm, not of the percolation.

08 — The absences

Nothing here can be recorded yet: the machinery for recording it is designed to the header and unbuilt

Everything above happens in real time in a viewport and then stops existing. The renderer has no timeline, no keyframes, no motion blur and no frame export. That is the single largest gap on this page, and unlike most gaps on this site it is not a case of a feature living somewhere else.

What is not thereWhat exists insteadMark
Animation timelinesrc/ui/Timeline.h is a complete 108-line design — tracks with string target paths like entity.0.transform.position.x, four interpolation modes including cubic Bezier with tangent handles, track groups with mute and lock, playback state. There is no Timeline.cpp, no file includes the header, and it is not in CMakeLists.txt. Nothing is reachable, so this is absent rather than supersededdesign only
Motion blurshutterOpen and shutterClose exist in CameraParams and Camera::generateRay writes ray.time from them. Nothing downstream reads ray.time. The field is not merely unused: it consumes an RNG draw, which the photon-primitive integrator has to account for explicitly in its correlated-batch stream split — a dead feature that still perturbs the sample sequenceabsent
Frame or sequence exportFile ▸ Export Image and Render ▸ Take Snapshot (F12) are stubs; F12 logs “Snapshot requested” and returns. The only working image writer in the program is the comparison-report generatorabsent
NanoVDB volumesAn enum value, a name in the type-label array, and a disabled button reading “NanoVDB (soon)”. No loader, no shader branch; volType == 4 falls through to the legacy pathabsent
A GPU Euler solvershaders/eulerFluid.comp exists, complete with its own wind term, and is loaded by nothing. The live solver is CPU, in PhysicsWorld.cpp:565-732superseded
Surfacing a simulated fieldmarchingCubes and splatNodesToDensity compile and have no callers, so a fluid can be a medium but never a meshabsent
A SmoothLife volumePresets 4 and 14 are labelled SmoothLife and are legacy noise band 41–50: an FBM bell with a tentacle falloff and a sine pulse. The only real SmoothLife in the repository is the NPR post-process in §07mislabelled

The timeline row is the one to sit with. Somebody designed an animation system carefully — the target-path scheme is the right one, the Bezier handles are stored normalised, groups carry mute and lock — and then stopped before the implementation file. Every simulation on this page is consequently a live performance: you can drive it with the wind key, watch it, and lose it. That is why this page is mostly video placeholders, and why every one of them is a screen recording of a session rather than a render.

Two of the rows above — the Euler solver and the soft body, both of which print [WIP] on their own panel — are also entries in the program-wide register of thirteen partly-built modules on the page for everything that did not get one. That register counts them; this page is the census behind them, and neither repeats the other.

What would change the picture. Four of these gaps are one route each rather than one subsystem. Moving m_physics.init(cfg) above the seeding block instead of below it would give the Euler grid its initial density and give the resolution slider an effect — §03, and one screenshot away from being confirmed or dropped. Injecting Simple Particles alongside SPH and MPM in the tracer-visualisation path would take the coupled-solver count from three to four. Clearing volNoiseScale in Remove Source and adding the missing dispatch case would make the live Gray-Scott sim the thing the RD presets show. And moving the physics stepper from resetAccumulation() onto the adaptive EMA that reaction-diffusion already uses would let a simulated fluid converge everywhere it is not moving. None of the four is a research problem; each is a short edit to a route that already exists, and they are listed here so the next reader knows exactly where the coupling widens.

Screenshot — The Advanced ▸ Volume tab with Physics Simulation expanded on Sim Type Euler Fluid, showing the [WIP] label, the resolution slider, the Fluid Viz combo, the inert Show Debug Viz toggle beside it, and the three grey hint lines ending in "R=wind blow, E=projector (in FPS mode)". This one panel contains a working solver, an inert toggle, an honest WIP label and the only documentation of the wind hotkey
One panel, four kinds of honestyThe hotkey line is the only place in the interface where the wind key is written down. The toggle above the combo is one of the thirty-two catalogued inert controls: physDebugViz has two references and the combo beside it is the real gate.