Path Tracing
Monte Carlo path tracing is a global illumination algorithm that solves the rendering equation to produce realistic lighting effects.
The Rendering Equation
The core of path tracing is solving the Kajiya rendering equation:
$$L_o(x, \omega_o) = L_e(x, \omega_o) + \int_{\Omega} f_r(x, \omega_i, \omega_o) L_i(x, \omega_i) (\omega_i \cdot n) d\omega_i$$
Where:
- $L_o$ is outgoing radiance
- $L_e$ is emitted radiance
- $f_r$ is the BRDF
- $L_i$ is incoming radiance
- $\omega_i \cdot n$ is the cosine term
Monte Carlo Integration
Using Monte Carlo methods to estimate the integral:
$$\int f(x) dx \approx \frac{1}{N} \sum_{i=1}^{N} \frac{f(X_i)}{p(X_i)}$$
Applied to the rendering equation:
$$L_o \approx L_e + \frac{1}{N} \sum_{i=1}^{N} \frac{f_r L_i (\omega_i \cdot n)}{p(\omega_i)}$$
Cosine-Weighted Sampling
For Lambertian materials, use cosine-weighted hemisphere sampling:
$$p(\omega) = \frac{\cos\theta}{\pi}$$
Direction generation:
__device__ vec3 cosine_weighted_hemisphere(curandState* state) {
float r1 = curand_uniform(state);
float r2 = curand_uniform(state);
float phi = 2.0f * M_PI * r1;
float sqrt_r2 = sqrtf(r2);
float x = cosf(phi) * sqrt_r2;
float y = sinf(phi) * sqrt_r2;
float z = sqrtf(1.0f - r2);
return vec3(x, y, z);
}Russian Roulette
To avoid infinite recursion, use Russian roulette termination:
$$L = \frac{L_{direct} + p_{rr} \cdot L_{indirect}}{p_{rr}}$$
When path length > 3, continue with probability $p_{rr} = 0.8$:
__device__ bool russian_roulette(curandState* state, int depth) {
if (depth <= 3) return true;
return curand_uniform(state) < 0.8f;
}Complete Path Tracing Kernel
__device__ vec3 trace_path(
const Ray& ray,
const Scene& scene,
curandState* state,
int max_depth
) {
vec3 radiance = vec3(0.0f);
vec3 throughput = vec3(1.0f);
Ray current_ray = ray;
for (int depth = 0; depth < max_depth; ++depth) {
HitRecord hit;
if (!scene.intersect(current_ray, hit)) {
radiance += throughput * scene.background(current_ray);
break;
}
// Emissive materials
radiance += throughput * hit.material->emitted();
// Russian roulette
if (depth > 3 && !russian_roulette(state, depth)) break;
// Sample BSDF
vec3 wi;
float pdf;
vec3 f = hit.material->sample(state, hit, wi, pdf);
// Update throughput
throughput *= f * fabsf(dot(wi, hit.normal)) / pdf;
// Update ray
current_ray = Ray(hit.p, wi);
}
return radiance;
}Denoising
High sample counts converge slowly. Denoising techniques include:
- Temporal accumulation
- Spatial filtering
- AI denoising (OptiX Denoiser)
Sampling Strategies
Uniform Hemisphere Sampling
$$p(\omega) = \frac{1}{2\pi}$$
Simple but inefficient for diffuse surfaces.
Cosine-Weighted Sampling
$$p(\omega) = \frac{\cos\theta}{\pi}$$
Importance sampling for Lambertian BRDF - reduces variance significantly.
Multiple Importance Sampling (MIS)
Combine BSDF sampling and light sampling:
$$L = w_1 \cdot \frac{f(X_1)}{p_1(X_1)} + w_2 \cdot \frac{f(X_2)}{p_2(X_2)}$$
References
- [Kajiya 1986] "The Rendering Equation"
- [Pharr 2016] "Physically Based Rendering"