4. Monte Carlo Integration on the Sphere of Directions
Ray Tracing: The Rest of Your Life (v3.2.3): 4 MC Integration on the Sphere of Directions / 3.4 Monte Carlo Integration on the Sphere of Directions
Section 3.3 estimated an integral over a one-dimensional interval. I now integrate over the space of directions—the unit sphere
This chapter covers two topics:
- Uniform sampling on a sphere and its corresponding PDF
- Empirical verification using the spherical integral of
The Exact Value of the Integral
The integral of interest is
where
Substituting
Uniform Sampling on the Sphere
Generating a uniform distribution over the unit sphere uses the fact that
The correct approach is to choose
to construct the direction
Rust Implementation
I add the r304-sphere-directions-mc crate for this chapter, with another local implementation of XorShift64.
The spherical sampling function and PDF constant are as follows.
fn sample_unit_vector(rng: &mut XorShift64) -> (f64, f64, f64) {
let z = 1.0 - 2.0 * rng.next_f64();
let phi = 2.0 * PI * rng.next_f64();
let r = (1.0 - z * z).max(0.0).sqrt();
(r * phi.cos(), r * phi.sin(), z)
}
fn sphere_pdf() -> f64 {
1.0 / (4.0 * PI)
}The .max(0.0) guard prevents floating-point error from making
The estimator divides
fn estimate_integral(samples: usize, rng: &mut XorShift64) -> f64 {
let pdf = sphere_pdf();
let mut sum = 0.0;
for _ in 0..samples {
let (_, _, z) = sample_unit_vector(rng);
let cosine_squared = z * z;
sum += cosine_squared / pdf;
}
sum / samples as f64
}generate_report constructs its output in two stages. It first reports a single estimate with
let checkpoints = [100usize, 1_000, 10_000, 100_000, 500_000];
for (index, n) in checkpoints.iter().copied().enumerate() {
let mut rng = XorShift64::new(0x3040_1000u64 + index as u64);
let est = estimate_integral(n, &mut rng);
output.push_str(&format!(
"{},{:.12},{:.12}\n",
n,
est,
(est - EXACT_INTEGRAL).abs()
));
}Because each checkpoint has an independent seed, estimates at small
Differences Between C++ and Rust
The original C++ random_unit_vector has the same structure.
inline vec3 random_unit_vector() {
auto z = random_double(-1, 1);
auto a = random_double(0, 2*pi);
auto r = sqrt(1 - z*z);
return vec3(r*cos(a), r*sin(a), z);
}C++ calculates sqrt(1 - z*z) without the .max(0.0) guard. The Rust implementation adds .max(0.0) as protection against floating-point error.
r304-sphere-directions-mc/src/lib.rs
const PI: f64 = std::f64::consts::PI;
const EXACT_INTEGRAL: f64 = 4.0 * PI / 3.0;
struct XorShift64 {
state: u64,
}
impl XorShift64 {
fn new(seed: u64) -> Self {
let state = if seed == 0 { 0x9e3779b97f4a7c15 } else { seed };
Self { state }
}
fn next_u64(&mut self) -> u64 {
let mut x = self.state;
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
self.state = x;
x
}
fn next_f64(&mut self) -> f64 {
(self.next_u64() >> 11) as f64 * (1.0 / (1u64 << 53) as f64)
}
}
fn sample_unit_vector(rng: &mut XorShift64) -> (f64, f64, f64) {
let z = 1.0 - 2.0 * rng.next_f64();
let phi = 2.0 * PI * rng.next_f64();
let r = (1.0 - z * z).max(0.0).sqrt();
(r * phi.cos(), r * phi.sin(), z)
}
fn sphere_pdf() -> f64 {
1.0 / (4.0 * PI)
}
fn estimate_integral(samples: usize, rng: &mut XorShift64) -> f64 {
let pdf = sphere_pdf();
let mut sum = 0.0;
for _ in 0..samples {
let (_, _, z) = sample_unit_vector(rng);
let cosine_squared = z * z;
sum += cosine_squared / pdf;
}
sum / samples as f64
}
pub fn generate_report() -> String {
let mut output = String::new();
output.push_str("MC Integration on the Sphere of Directions\n");
output.push_str("========================================\n\n");
output.push_str("Target integral: integral_{S^2} cos^2(theta) d_omega\n");
output.push_str("Sampling PDF: p(direction) = 1/(4*pi)\n");
output.push_str(&format!("Exact value: {:.12}\n\n", EXACT_INTEGRAL));
let mut single_rng = XorShift64::new(0x3040_0001u64);
let single_estimate = estimate_integral(1_000_000, &mut single_rng);
output.push_str("Single run (N=1,000,000):\n");
output.push_str("samples,estimate,abs_error\n");
output.push_str(&format!(
"1000000,{:.12},{:.12}\n\n",
single_estimate,
(single_estimate - EXACT_INTEGRAL).abs()
));
output.push_str("Convergence checkpoints:\n");
output.push_str("samples,estimate,abs_error\n");
let checkpoints = [100usize, 1_000, 10_000, 100_000, 500_000];
for (index, n) in checkpoints.iter().copied().enumerate() {
let mut rng = XorShift64::new(0x3040_1000u64 + index as u64);
let est = estimate_integral(n, &mut rng);
output.push_str(&format!(
"{},{:.12},{:.12}\n",
n,
est,
(est - EXACT_INTEGRAL).abs()
));
}
output
}Results in the Browser
abs_error is the absolute difference from the exact value
Summary
- I derived
by integration in spherical coordinates. - I implemented uniform spherical sampling using
and . Choosing uniformly accounts for the area element. - I empirically confirmed that the Monte Carlo estimator
converges to .
The next chapter connects the idea of a spherical PDF to the ray tracer's scattering model.