Theme Catppuccin
Make yourself at homeCatppuccin

A softer light for long learning sessions.

Appearance

Loading your preference…

Dark palette

Choose a dark palette to try it.

Light uses Latte. Your choices stay in this browser.

Back to your source shelf

Inside the companion

Light Transport

Build a diffuse CPU path renderer and inspect its sampling, geometry, and tests.

Project files 11

Name the sampling contracts

Random state, direction samples, running statistics, bounds, and an owning scene index.

64 lines lantern.hpp
#pragma once
#include <cstddef>
#include <cstdint>
#include <expected>
#include <optional>
#include <span>
#include <string>
#include <vector>

namespace lantern {
struct Error { std::string message; };
struct Vec3 { double x{}, y{}, z{}; };
struct Ray { Vec3 origin; Vec3 direction; }; // Finite, unit direction required.
struct Aabb { Vec3 low; Vec3 high; };
struct Sphere { std::uint32_t id{}; Vec3 center; double radius{}; };
struct Hit { std::uint32_t id{}; double distance{}; };
inline constexpr std::size_t max_spheres = 128;
inline constexpr std::size_t max_samples = 1'048'576;

// Explicit algorithm and owned state: useful reproducibility, not cryptography.
class Rng {
public:
    explicit Rng(std::uint64_t seed) noexcept : state_{seed} {}
    [[nodiscard]] std::uint64_t next() noexcept;
    [[nodiscard]] double uniform() noexcept; // [0, 1), using the high 53 bits.
private:
    std::uint64_t state_;
};
struct DirectionSample { Vec3 direction; double pdf{}; }; // Density per steradian.
[[nodiscard]] std::expected<DirectionSample, Error> cosine_hemisphere(double u, double v);
[[nodiscard]] std::expected<double, Error> cosine_integral(std::size_t samples, std::uint64_t seed);
[[nodiscard]] std::expected<double, Error> power_weight(double pdf_a, double pdf_b);

// Linear-sample statistics. Display transforms belong AFTER averaging.
class Moments {
public:
    [[nodiscard]] std::expected<void, Error> add(double value);
    [[nodiscard]] std::size_t count() const noexcept { return count_; }
    [[nodiscard]] double mean() const noexcept { return mean_; }
    [[nodiscard]] double sample_variance() const noexcept { return count_ > 1 ? m2_ / static_cast<double>(count_ - 1) : 0; }
private:
    std::size_t count_{};
    double mean_{}, m2_{};
};

[[nodiscard]] std::expected<bool, Error> box_hit(const Ray& ray, const Aabb& box);

// Owns copied primitives and its index. Construction is transactional; no query
// borrows the caller's span after build() returns. Binary median splits are an
// educational baseline, not SAH, spatial splits, or a measured speed promise.
class SceneIndex {
public:
    [[nodiscard]] static std::expected<SceneIndex, Error> build(std::span<const Sphere> spheres);
    [[nodiscard]] std::expected<std::optional<Hit>, Error> closest(const Ray& ray) const;
    [[nodiscard]] std::expected<std::optional<Hit>, Error> linear_closest(const Ray& ray) const;
    [[nodiscard]] std::size_t node_count() const noexcept { return nodes_.size(); }
private:
    struct Node { Aabb bounds; std::size_t first{}, count{}, left{}, right{}; };
    std::size_t build_node(std::size_t first, std::size_t count);
    std::vector<Sphere> spheres_;
    std::vector<Node> nodes_;
};
} // namespace lantern

This is the file included with the course. Copy and download give you its complete source.

Taking your work offline? Download all course companions