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

Check light before trusting the image

Analytic radiance, known pixels, bounded work, and diffuse-renderer failure cases.

69 lines diffuse_tests.cpp
#include "diffuse.hpp"
#include <cmath>
#include <iostream>
#include <limits>
#include <stdexcept>

namespace {
std::size_t checks{};
void require(bool okay,const char* message) { ++checks; if (!okay) throw std::runtime_error(message); }
bool near(double a,double b) { return std::abs(a-b)<1e-10; }
bool near(lantern::Vec3 a,lantern::Vec3 b) { return near(a.x,b.x)&&near(a.y,b.y)&&near(a.z,b.z); }
double dot(lantern::Vec3 a,lantern::Vec3 b) { return a.x*b.x+a.y*b.y+a.z*b.z; }
}
int main() {
    try {
        using namespace lantern;
        for (const auto n:std::array<Vec3,4>{{{0,0,1},{0,0,-1},{1,0,0},{0,0.6,0.8}}}) {
            const auto f=frame_from_normal(n);
            require(f&&near(dot(f->tangent,f->normal),0)&&near(dot(f->bitangent,f->normal),0)&&
                    near(dot(f->tangent,f->bitangent),0)&&near(dot(f->tangent,f->tangent),1)&&
                    near(dot(f->bitangent,f->bitangent),1),"frame is orthonormal including both poles");
            require(near(to_world(*f,{0,0,1}),n),"local north maps to the stated normal");
        }
        require(!frame_from_normal({0,0,0})&&!frame_from_normal({0,0,2}),"nonunit frames rejected");
        std::array<Surface,1> surfaces{{{{7,{0,0,0},1},{0.6,0.3,0.15},{}}}};
        const auto constant=DiffuseScene::build(surfaces,{2,1,0.5});
        require(constant.has_value(),"constant-environment scene builds");
        for (std::uint64_t seed=0;seed<64;++seed) {
            Rng rng{seed}; const auto t=constant->trace({{0,0,3},{0,0,-1}},rng,1);
            require(t&&near(t->radiance,{1.2,0.3,0.075})&&t->count==2&&t->end==PathEnd::environment,
                    "every one-bounce sample equals reflectance times constant environment");
        }
        Rng rng{47};
        const auto zero=constant->trace({{0,0,3},{0,0,-1}},rng,0);
        require(zero&&near(zero->radiance,{})&&zero->count==1&&zero->end==PathEnd::depth_limit,"zero scatters at nonemissive surface is black");
        const auto miss=constant->trace({{0,0,3},{0,0,1}},rng,0);
        require(miss&&near(miss->radiance,{2,1,0.5}),"zero scatter miss still sees environment");
        require(!constant->trace({{0,0,3},{0,0,-2}},rng,1),"invalid initial ray rejected");
        require(!constant->trace({{0,0,3},{0,0,-1}},rng,9),"path work cap rejected");
        surfaces[0].sphere.radius=8; surfaces[0].reflectance={0.5,0.5,0.5}; surfaces[0].emission={0.1,0.2,0.3};
        const auto enclosure=DiffuseScene::build(surfaces,{16,16,16});
        const auto bounded=enclosure->trace({{0,0,0},{0,0,1}},rng,4);
        require(bounded&&bounded->count==5&&bounded->end==PathEnd::depth_limit&&
                near(bounded->radiance,{0.19375,0.3875,0.58125}),"enclosure adds five known geometric-series emission terms and no environment");
        const auto maximum=enclosure->trace({{0,0,0},{0,0,1}},rng,8);
        require(maximum&&maximum->count==9&&maximum->end==PathEnd::depth_limit&&
                near(maximum->radiance,{0.199609375,0.39921875,0.598828125}),"maximum eight scatters use exactly nine owned visits and known emission terms");
        surfaces[0].reflectance={0,0,0};
        const auto emitter=DiffuseScene::build(surfaces,{});
        const auto absorbed=emitter->trace({{0,0,0},{0,0,1}},rng,4);
        require(absorbed&&near(absorbed->radiance,{0.1,0.2,0.3})&&absorbed->count==1&&absorbed->end==PathEnd::absorbed,"black emitter adds emission before termination");
        surfaces[0].reflectance.x=1.1;
        require(!DiffuseScene::build(surfaces,{}),"energy-gaining reflectance rejected");
        require(!DiffuseScene::build({}, {std::numeric_limits<double>::infinity(),0,0}),"nonfinite environment rejected");
        const auto empty=DiffuseScene::build({}, {0.1,0.2,0.3});
        const auto image=render_diffuse(*empty,{4,3,2,1,47});
        require(image&&image->linear.size()==12,"owned image has validated extent");
        for (const auto color:image->linear) require(near(color,{0.1,0.2,0.3}),"every empty-scene pixel has analytically known radiance");
        require(!render_diffuse(*empty,{0,3,2,1,47})&&!render_diffuse(*empty,{512,512,128,1,47}),"invalid dimensions and camera-ray budget rejected");
        const auto scene=study_scene(); require(scene.has_value(),"study scene is valid");
        const auto first=render_diffuse(*scene,{8,6,2,4,47}), second=render_diffuse(*scene,{8,6,2,4,47});
        bool equal=first&&second;
        if (equal) for (std::size_t i=0;i<first->linear.size();++i) equal &= near(first->linear[i],second->linear[i]);
        require(equal,"fixed image/settings replay on this implementation");
        std::cout<<checks<<" diffuse bridge checks passed.\n";
        return 0;
    } catch (const std::exception& e) { std::cerr<<e.what()<<'\n'; return 1; }
}

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

Taking your work offline? Download all course companions