Monte Carlo Path Tracer + Spline Engine
Physically-based renderer with importance sampling and a functorial spline animation system
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
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
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
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;
}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);
}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
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
Related Projects
3D Rendering Engine with Shadow Mapping
Custom OpenGL engine comparing four shadow mapping algorithms with runtime switching
Implemented and compared four shadow mapping algorithms (default, PCF, VSM with Gaussian blur, CSM with per-cascade resolution) in a single engine with runtime switching
Tossing Balls
High school graduation project — 2D physics puzzle game with Box2D integration and data-driven level loading
High school graduation project (age 17) — built a complete 2D physics game engine from scratch in C++ with a custom entity system, polymorphic animation framework, Box2D physics integration, and data-driven level loading from XML
Pac-Man
Arcade-faithful clone with per-ghost personality AI and A* pathfinding
Implemented authentic Pac-Man ghost AI with per-ghost personality targeting algorithms (direct pursuit, 4-tile ambush, vector-doubling flanking, distance-threshold retreat) faithfully matching the original arcade's documented behavior