# Light & Render Lab

## Learning goal
Tilt an incoming ray. Discover where reflection and refraction go, then see why collecting more samples stabilises an estimate.

## Model and limits
Snell refraction and ideal reflection directions are calculated exactly for this interface; energy uses the Schlick approximation. The roughness fan illustrates normal variation, not a full microfacet BRDF or ray-traced image. The chart is a separate Monte Carlo experiment drawing uniform samples from 0.2 to 1 (mean 0.6). Standard error is undefined with fewer than two samples.

## Primary references
- [PBRT: reflection and transmission](https://www.pbr-book.org/4ed/Reflection_Models/Specular_Reflection_and_Transmission)
- [PBRT: microfacet roughness](https://www.pbr-book.org/4ed/Reflection_Models/Roughness_Using_Microfacet_Theory)

## Experiment design
- **Air meets glass**: Compare the incident, reflected and transmitted directions. Parameters: `{"angle":45,"ior":1.5}`.
- **Light trapped inside**: At this angle, the material-to-air boundary has no transmitted ray. Parameters: `{"angle":60,"ior":1.5,"inside":1}`.
- **Rough reflections**: The fan illustrates varying surface normals. Parameters: `{"roughness":0.65,"samples":256}`.

These presets are design predictions to investigate, not claims from a usability study. All readouts, animation positions and exported observations come from the same local model. The 2D view uses the same state. Saved setups restart from their parameters, rather than claiming to resume exact progress. Captured baselines are stored separately and survive resets and preset changes.

## Verification
Run `node --test apps/shared/discovery/tests/models.test.cjs` for numerical invariants, limiting cases, determinism and failure-state checks. Run `node apps/shared/tests/catalog.test.cjs` for routes and references. Browser and keyboard validation must be reported separately from numerical tests; automated tests are not user research.

## Discovery studio interaction layer
The studio adds selectable model objects, ray-based 3D picking, keyboard-accessible object selection, an inspector, scene editing where appropriate, model-specific stepping, and 24 measured challenges across the twelve labs. This is an interaction redesign, not a claim of measured learning improvement.

Most parameter changes now preserve current state. Structural changes explicitly start a new run; the exact per-app list is in `apps/shared/discovery/studio-model.js`. Current settings, independently captured baselines and challenge progress retain separate storage. Undo/redo keeps up to 30 edit checkpoints; the timeline keeps the latest 80 model snapshots. Resuming from a historical state creates a new branch. Saved setups still restart from parameters; they do not claim to retain the timeline or local particle pin edits.

Additional instruments derive from the existing model and cited references. Camera and light sweeps hold the remaining inputs fixed; neural curves separate training and validation; compression uses actual coefficients and recomputed quality samples; traffic uses recorded car positions/speeds; factory queues and utilisation use actual events and busy time. Visible instruments can continue collecting data while the hidden 3D view stops drawing.

Run `node --test apps/shared/discovery/tests/studio.test.cjs` for live edits, preserved state, all challenge solutions, direct manipulation, rewind, picking, instrument exports and controller integration. These DOM/renderer-double checks do not replace rendered browser and device review.


## Detailed 3D apparatus
Shared procedural geometry in `apps/shared/discovery/apparatus.js` adds mechanical housings, lens and iris parts, cables, wheels, connectors, moving machine heads and subject-specific markings. The scene uses bevelled housings, smoother curved geometry, instanced material families, a key/fill/rim light arrangement and bounded soft shadows. Cloth has a procedural weave bump map attached to its original model coordinates.

Full detail can be switched to Essential detail without resetting the model. All movements use model state or model time: wheel markers use road position, machine heads use scheduled job progress, conveyors use accumulated worker time, iris blades use the illustrated aperture, and network packets use their scheduled flights. Signal and energy markers illustrate direction, not measured transit speed. The fan is an indicator of the wind setting; it does not add a fluid simulation. Shells, grippers, cables and surface textures do not change the underlying collision model. The 2D fallback retains the same state and geometry.

Additional checks: `node --test apps/shared/discovery/tests/apparatus.test.cjs`. Geometry, state preservation, bounded batching, semantic selection, pause/rewind consistency and fallback behavior are checked with renderer doubles; browser pixels and device frame rates remain unverified.


## Authored model assets
The physical-object labs now load original Blender-authored GLB assets through `model-assets.js` and `model-worlds.js`: articulated industrial arm, mirrorless camera, car (overview and inspection versions), network router, gantry station, optical laser and companion character. The geometry, authored material surfaces and packed texture maps replace corresponding primitive bodies only after loading succeeds. Source `.blend` files, reproducible Python generators, manifests and inspected Blender renders are retained in `assets/models/` and `scripts/`. Mathematical data surfaces stay tied to their original calculations.

The robot uses actual solved joints; camera iris hinges respond to the aperture and a Cutaway toggle exposes the sensor/rays; car wheels and brake lights follow model position/acceleration; factory heads use scheduled job progress. GLB equipment covers remain illustrative, not new collision or optical physics. Camera cutaway distances are display distances; numerical thin-lens readings remain authoritative. The companion uses the motion model's translation and squash/stretch.

Assets load on demand, repeated equipment shares instanced geometry/materials, and only the selected close-up car upgrades from the lighter road mesh. Failure preserves the diagram and exposes Retry model. Inspect model fits the selected geometry rather than applying a fixed camera distance. Full/Essential detail, pause, rewind and existing models/data remain available. A procedural studio reflection environment lights PBR surfaces without external network requests.

Run `node --test apps/shared/discovery/tests/model-assets.test.cjs` for real GLB parsing, file loading, texture/UV presence, hierarchy transforms, exact solver alignment, cutaways, pause/rewind, fitting and car detail selection. Asset product renders were visually inspected in Blender. Browser pixels and device frame rates are still unverified; product renders are not screenshots of the web app.

## Bespoke rebuild (2026-10)
The page no longer uses the shared discovery template; `model.js` and `config.js` are unchanged.
- `light-derive.js` wraps the model. Everything shown comes from `optics()`, `create()`, `advance()` and `observe()`-equivalent statistics, plus documented derivations: critical angle `asin(n_low/n_high)`; the Fresnel curve, which evaluates `optics()` from 0–90° (the sliders stop at 80°); the exact unpolarised Fresnel equations, drawn only as a dashed comparison line; and a 64-pixel "noise patch" made of independent runs of the model's estimator with seeds `seed … seed+63`.
- `section-view.js` draws the 2D views as SVG. The **cross-section** shows the boundary edge-on: the normal, incoming, reflected and refracted rays at their exact model angles, angle arcs in degrees, ray thickness and opacity set by the reflected and transmitted shares, the critical-angle line with a shaded "trapped" zone (leaving the material) or "no rays here" zone (entering it), a surface whose bumpiness follows roughness, and the model's 32-direction roughness fan. The **Fresnel curve** plots the model's reflected share against angle, with the current angle, the head-on value and the critical angle marked.
- `render3d.js` is a procedural three.js scene: a tiled floor with a pool of the chosen material, a striped straw dipped into it, a ball of the same material and a matte reference block. The pool is a custom shader that, per pixel, uses the model's Schlick formula for the reflected share and GLSL `refract()` (Snell's law, η = 1/n) for the transmitted ray. The ray is traced analytically to the pool floor, the walls and the straw (a capsule), so the bent straw and the shifted floor lines come from real refraction. Reflections are a planar mirror render. Roughness tilts the normal by up to ±0.45·roughness rad per axis (a reflected spread of about ±0.9·roughness rad, matching the model's fan) and averages six samples per pixel. The ball uses three.js's `MeshPhysicalMaterial` with the same F0 (reflectivity = √(F0/0.16), capped at 1) and roughness. "Set the angle to my view" and "Look from the slider angle" connect the camera's angle to the pool with the angle slider. The viewer has orbit, zoom, pan, keyboard control and a reset; it renders only on demand, pauses when off-screen or hidden, caps the pixel ratio on phones, and falls back to the 2D views when WebGL is unavailable or the context is lost.
- `sampling-view.js` draws the sampling experiment: the running mean with a ±2 standard-error band against the true mean of 0.6, and the 64-pixel patch. Sampling runs only while the chart is on screen. With reduced motion it completes at once.
- `app.js` connects presets (six questions), controls, readouts, the "What's happening" text and three checked challenges. `styles.css` holds the page styles. On desktop, the main controls sit in a column beside the stage. On phones, the active view stays sticky above the controls.

Limits: Schlick underestimates reflection just below the critical angle when leaving a denser material, then jumps to 100% (the exact comparison curve shows this). No dispersion, absorption or polarisation. Diamond is capped at the model's n = 2.4. The 3D view always looks in from the air, its water tint is decorative, its roughness blur is a visual approximation and slightly grainy, and the ball's head-on shine is capped at 16% by three.js. The sampling chart uses the model's synthetic uniform samples, not this scene's light.
