Skip to main content
2023 C++20 CMake

Monte Carlo Path Tracer + Spline Engine

Physically-based renderer with importance sampling and a functorial spline animation system

Monte Carlo Path Tracer + Spline Engine screenshot

Overview

Three assignments from an advanced computer graphics course spanning physically-based rendering and geometric animation. The core is a Monte Carlo path tracer with a PBRT-inspired material system (BxDFs, BSDFs, Fresnel equations), explicit direct light sampling, Russian roulette termination, and stratified subpixel anti-aliasing. A companion spline animation engine uses Catmull-Rom and Bezier interpolation with arc-length reparametrization for constant-speed playback.

Architecture

Monte Carlo Path Tracer + Spline Engine architecture

The renderer follows a clean PBRT-inspired pipeline: camera ray generation with tent filter anti-aliasing, scene intersection, surface interaction, BxDF sampling, direct light sampling via solid-angle cones, and recursive radiance estimation with Russian roulette. The material system uses a BxDF hierarchy (Lambertian, Specular with Fresnel, Oren-Nayar) aggregated into BSDFs. The spline engine uses a hierarchical Interpolator<T> abstraction composed via fmap from scalars to vertices to meshes.

Key Concepts

BxDF Material Models

BxDF Material Models

Three physically-based BRDFs: Lambertian diffuse with cosine-weighted hemisphere sampling (Malley's method), specular reflection with full Fresnel equations handling total internal reflection, and Oren-Nayar rough diffuse with precomputed A/B coefficients from surface roughness.

Direct Light Sampling

Rather than waiting for paths to randomly hit a light, explicitly samples a cone around each spherical light source using the solid-angle formula omega = 2*pi*(1 - cos_a_max). This dramatically reduces variance for small light sources compared to naive path tracing.

Spline Interpolation with Arc-Length Reparametrization

Catmull-Rom and Bezier splines composed hierarchically via Interpolator<T> = std::function<T(float)>. Arc-length reparametrization solves the constant-speed problem by numerically integrating derivative magnitudes and building per-vertex cumulative arc-length tables with binary-search inverse lookup.

Code Highlights

Interpolator<T> + fmap pattern
template <typename T>
using Interpolator = std::function<T(float)>;

// Functor map over vectors
template<class T, class U>
vector<U> fmap(const vector<T> &vec, function<U(const T&)> map_to) {
    auto mapped = vector<U>();
    transform(vec.begin(), vec.end(), back_inserter(mapped), map_to);
    return mapped;
}
Fresnel dielectric with total internal reflection
Vector3d fresnelDialectricEvaluate(double eta1, double eta2, double cosThetaI) {
    if (etaI == 0 && etaT == 0) return Vector3d(1,1,1);  // perfect mirror
    auto sinThetaT = etaI / etaT * sinThetaI;
    if (sinThetaT >= 1) return Vector3d(1,1,1);  // total internal reflection
    auto Rparl = ((etaT * cosThetaI) - (etaI * cosThetaT)) /
                 ((etaT * cosThetaI) + (etaI * cosThetaT));
    auto Rperp = ((etaI * cosThetaI) - (etaT * cosThetaT)) /
                 ((etaI * cosThetaI) + (etaT * cosThetaT));
    auto val = (Rparl * Rparl + Rperp * Rperp) / 2;
    return Vector3d(val, val, val);
}
Direct light sampling with solid-angle cone
auto cos_a_max = sqrt(1 - light->radius * light->radius /
    dot(interaction.point - light->position, interaction.point - light->position));
double cos_a = 1 - eps1 + eps1 * cos_a_max;
double sin_a = sqrt(1 - cos_a * cos_a);
auto l = su * cos(phi) * sin_a + sv * sin(phi) * sin_a + sw * cos_a;

auto omega = 2 * M_PI * (1 - cos_a_max);
e = e + interaction.color *
    (light->emission * dot(l, interaction.oriented_normal) * omega) * M_1_PI;

Performance

Monte Carlo Path Tracer + Spline Engine performance chart

Russian roulette path termination (depth > 5) with survival probability p = max(color.r, color.g, color.b) maintains an unbiased estimator. Direct light sampling via solid-angle cones dramatically reduces variance for small lights. 2x2 subpixel stratification and tent filter anti-aliasing improve convergence. Octree spatial acceleration with early culling for range queries and grid hash tables for uniform distributions.

Highlights

  • Physically-based Monte Carlo path tracer implementing the rendering equation with importance sampling, Russian roulette termination, explicit direct light sampling via solid-angle cone sampling, and three BxDF models (Lambertian, specular with Fresnel, Oren-Nayar rough diffuse)
  • Interpolator<T> = std::function<T(float)> with fmap: a functorial abstraction for spline interpolation that composes hierarchically from scalars to vertices to meshes
  • Arc-length reparametrization for constant-speed animation via numerical integration of spline derivatives with per-vertex cumulative arc-length tables and inverse lookup
  • Dual spatial acceleration structures: octree (recursive subdivision with range queries) and grid hash tables (uniform spatial hashing) for nearest-neighbor queries
  • Systematic refactoring of smallpt into PBRT architecture with abstract interfaces, pluggable materials, and separated concerns