Inside the companion
Light Transport
Build a diffuse CPU path renderer and inspect its sampling, geometry, and tests.
Project files 11
Build the Lantern Room study
Run the sampling experiments and diffuse renderer, then check images and failure paths.
# The Lantern Room — CPU companion experiments
This independent C++23 project connects replayable sampling, a sphere BVH, and
an inspectable bounded diffuse path loop to a small actual image. The reference
supports two-sided Lambertian spheres, two-sided emission, a constant environment,
and a jittered pinhole camera. Scalar integral/statistics/MIS-weight experiments
remain available separately. Richer materials, light sampling/MIS, progressive
updates, and your final room composition remain original learner extensions.
There are no external dependencies or GPU APIs. A suitable C++23 compiler and
standard library (including `std::expected`) and CMake 3.25+ are required. CPU
execution is intended for macOS and AMD/Linux systems; only the documented local
macOS arm64 host was tested. No GPU support or cross-platform performance is
inferred from that verification.
## Run
From the project root:
```sh
cmake -S native/advanced/pathtracing -B build-lantern -G Ninja \
-DCMAKE_CXX_COMPILER=clang++ -DCMAKE_BUILD_TYPE=Debug
cmake --build build-lantern --parallel
ctest --test-dir build-lantern --output-on-failure
./build-lantern/lantern-experiment
```
Sanitizer configuration with a supported Clang/GCC toolchain:
```sh
cmake -S native/advanced/pathtracing -B build-lantern-sanitize \
-DCMAKE_BUILD_TYPE=Debug -DLANTERN_SANITIZERS=ON
cmake --build build-lantern-sanitize --parallel
ctest --test-dir build-lantern-sanitize --output-on-failure
```
The executable prints an estimate of the hemisphere cosine integral (analytic
answer pi), the node count of a tiny index, and an independently known central
sphere hit. This scalar experiment produces no image or performance score; lantern-render below writes an image. Exit 0 means the
experiment completed; exit 1 reports a failed operation.
## Ordered renderer checkpoints
Start with the original files. Each step has one concrete result before the next
extension; the course's “The diffuse walk” chapter explains each transition and
provides three revealable hints per exercise.
1. **Build and calculate.** Inspect `CMakeLists.txt` and `src/render.cpp`, run the
commands above, then `./build-lantern/lantern-render --constant`. Expected and
observed linear RGB are **1.2 0.3 0.075**: reflectance `(0.6,0.3,0.15)` times
environment `(2,1,0.5)`. No display clamp belongs in this calculation. The
isolated sphere and cosine-weighted diffuse model make this a zero-variance
special case. Tests repeat it across 64 seeds.
2. **Build the frame.** Read `frame_from_normal` and `to_world` in `src/diffuse.cpp`.
Local +Z becomes the facing unit normal through an orthonormal basis. Read
the four basis cases in `tests/diffuse_tests.cpp`, then run
`./build-lantern/lantern-diffuse-tests` (99 checks). Add normal `(0.6,0.8,0)` as
your first independent case. If a pole fails, inspect the helper-axis branch
before changing the PDF.
3. **Follow one path.** Run `./build-lantern/lantern-render --trace`. The fixed ray
hits object 1, with cosine about `0.718880`, PDF `0.228827`, and updated beta
`(0.8,0.2,0.1)`, then sees the environment: final L is `(0.28,0.09,0.065)`.
Scatter records show updated beta; terminal records retain incoming beta and
have no sampled direction/PDF. A miss adds environment, a hit adds emission,
and the scatter cap is tested after that contribution.
4. **Make and change one image.** Run:
```sh
./build-lantern/lantern-render lantern.ppm 4
./build-lantern/lantern-render lantern-zero.ppm 0
./build-lantern/lantern-render lantern.ppm 9
```
The first two write 96×64 P6 PPM files at 32 samples/pixel, seed 47. The last
deliberately fails before writing. Four scatters reveal red/blue spheres,
a curved floor, environment and emitter; zero scatters leave nonemissive
surfaces black while camera-visible emission/environment remain. Files are
replaced deliberately. Use a viewer with PPM support or an existing conversion
tool. Change one reflectance in `study_scene`, rebuild, and keep the original
analytic tests unchanged.
`PathTrace` owns a fixed nine-record array; `max_bounces` permits 0–8 scattered
rays, followed by a terminal visit. The loop evaluates `beta *= f*cosine/pdf`
with the matched cosine sampler, records the terms, and keeps RGB radiance
separate. The scene owns copies of material and geometry input. Its tighter
transport bounds are centers ±32, radii 0.1–32, reflectance [0,1], and
emission/environment [0,16]. The fixed `1e-4` normal offset is a stated small-scene
approximation, not a floating-point error bound or a solution for arbitrary
scales/touching geometry. The hard scatter cap truncates transport and is biased.
A render validates dimensions 1–512, samples 1–128, and at most 2,097,152 camera
samples before image allocation. Each pixel owns a deterministic stream derived
from seed+pixel index; this does not prove statistical independence. Changing the
bounce limit may consume a different number of random values and change later
jitter within that pixel. Replay applies to unchanged settings on this
implementation, not to bit-identical floating-point output on every compiler.
Image storage contains the **linear mean**. The PPM writer validates storage and
finite nonnegative channels, then clamps and applies a gamma-2 approximation;
this is neither exact sRGB nor calibrated color management. Failed writes return
an error and may leave an incomplete destination. The executable catches ordinary
exceptions; an allocation-owning error string means `expected` is not a no-throw
promise. No worker pool, direct-light sampler, specular model or RR is hidden in
this bridge.
## Contracts
- `Rng` owns explicit SplitMix64 state. `uniform()` uses the high 53 bits for a
half-open [0,1) double. The algorithm is credited in `NOTICE`; it is not
cryptographic, and distinct seeds are not a proof of independent streams.
- `cosine_hemisphere(u,v)` validates finite half-open inputs and returns a local
positive-Z unit direction with a PDF per steradian. It does not build a
world-space frame or implement a material system.
- `cosine_integral(count,seed)` estimates the integral of cosine using a uniform
hemisphere density. Counts must be 1–1,048,576.
- `Moments` accepts at most 1,048,576 finite scalar values with absolute value
at most 1e12. It keeps a Welford mean and sample variance; this is not a
universal convergence or error-bound API. Invalid input preserves prior state.
- `power_weight(a,b)` takes finite nonnegative directional PDFs no larger than
1e6, with a nonzero total. It assumes one sample from each of two strategies
using the same measure. Scaling before squaring avoids an all-zero
denominator for tiny PDFs. This scalar helper is not an MIS integrator.
- `SceneIndex::build(span)` accepts at most 128 finite spheres, unique ids,
centers within ±1,000, and radii 0.01–100. It validates before copying and owns
both reordered spheres and index nodes. Equal-count splits terminate even
when centroids coincide. A binary node reservation and fixed traversal stack
are bounded by the accepted primitive count.
- `closest(ray)` and `linear_closest(ray)` require a finite origin within
±1,000 and a direction with components within ±1 and squared length within
1e-10 of one. Hits use positive t > 1e-5 and equal-distance ties choose the
smaller stable id. Geometry is queried without borrowed caller storage.
- AABB slab tests handle parallel signed-zero components without division and
conservatively accept tangency. The primitive test uses a closest-point
projection with `std::fma` to avoid large quadratic-term cancellation near
a far-origin tangent. The tests cover adjacent representable grazing rays.
The BVH is a bounded median-split sphere reference, not SAH, a wide BVH, moving
geometry, transformed instances, or a complete floating-point robustness proof.
Keep the linear oracle and independently calculated cases when extending it.
There is no measured speedup claim. Beyond the bounded diffuse/emission bridge,
delta-event accounting, area-light sampling, NEE/MIS integration, Russian roulette,
progressive accumulation, color management and parallel scheduling remain guided
learner extensions.
## Verification evidence
Fresh independent review, 24 October 2026: Apple Clang 21.0.0 and CMake 4.4.4
on local macOS arm64: Debug, Release, and Debug
with AddressSanitizer + UndefinedBehaviorSanitizer each build and pass 5/5 CTest
targets, with warnings treated as errors. The original contract executable reports
37 checks and the new diffuse bridge reports 99 checks. The sanitizer run emits no
diagnostics. No Linux, ThreadSanitizer, GPU, or performance result is claimed.
Checks cover a known RNG word, replay/range, sampler domains and geometric
invariants, a known distribution moment, the analytic integral, invalid count,
MIS identities, zero-support strategies, and tiny/subnormal PDFs, known online statistics, parallel/tangent/behind
box cases, empty/owned indexes after caller allocation release, exact primitive bounds,
BVH-versus-linear deterministic rays, centroid
and hit ties, known oblique/inside-sphere roots, grazing neighbors, invalid ids/geometry, and
bounded construction/query input.
The test near x = nextafter(1,2), z = 1000 caught a false linear hit from
quadratic cancellation. The closest-point implementation rejects that miss;
exact and inward-adjacent tangent cases remain hits. This regression is an
independent geometric expectation, not merely agreement between two queries. An
independent reviewer rechecked all three adjacent-ray outcomes against the final
source and confirmed the reported cancellation case is fixed.
## Primary reading and credit
The course explanations and assignments are original. Further reading:
- [PBRT fourth edition · Monte Carlo Integration](https://pbr-book.org/4ed/Monte_Carlo_Integration)
- [PBRT · Sampling Multidimensional Functions](https://pbr-book.org/4ed/Sampling_Algorithms/Sampling_Multidimensional_Functions)
- [PBRT · A Simple Path Tracer](https://pbr-book.org/4ed/Light_Transport_I_Surface_Reflection/A_Simple_Path_Tracer)
- [PBRT · Bounding Volume Hierarchies](https://pbr-book.org/4ed/Primitives_and_Intersection_Acceleration/Bounding_Volume_Hierarchies)
- [SplitMix implementation and dedication, Sebastiano Vigna](https://prng.di.unimi.it/splitmix.c)
Keep supplied-code and algorithm credit when adapting the reference. A finished
learner renderer should identify its own integration, scene, tests, evidence,
and limitations separately from these experiments.
The 13 runnable C++ snippets across Light Transport and Renderer Engineering
also compile independently as C++23 with strict warnings treated as errors.
Their combined review harness passes 21 runtime checks under ASan/UBSan,
including the restricted entry-only Schlick approximation. The exact scope and
remaining limits are recorded in `audits/transport-concurrency.md` at the project
root. The diagnostic-string error representation can itself allocate; `expected`
is not a universal no-throw guarantee, and the executable catches exceptions.
The new bridge verification and original-source fact checks are recorded in
`audits/renderer-cpu-opengl-update.md`. A completed 96×64 image was visually inspected;
its Debug and Release captures replayed identically on the local host.
This is the file included with the course. Copy and download give you its complete source.
Taking your work offline? Download all course companions
