Skip to content
Copied!
published on 2026-09-17

7. Generating Random Directions ​

Ray Tracing: The Rest of Your Life (v3.2.3): 7 Generating Random Directions / 3.7 Generating Random Directions

The Monte Carlo estimates in Sections 3.2 through 3.6 were one-dimensional discussions that considered only the scattering angle θ around the z axis. I now systematize methods for generating random direction vectors in three-dimensional space. Combined with the ONB, or orthonormal basis, introduced in the next chapter, these methods allow scattering around any surface normal.

This chapter covers three topics:

  • A general procedure for generating directions from a PDF in spherical coordinates using inverse transform sampling
  • Specific formulas for uniform full-sphere, uniform hemisphere, and cosine-weighted sampling
  • Estimating the same integral ∫hemicos3⁡θdω with two PDFs and confirming agreement

Spherical Coordinates and Solid-Angle PDFs ​

The area element on the unit sphere is

dA=sin⁡θdθdϕ

Given a PDF p(direction)=f(θ) that is symmetric in ϕ, the marginal PDFs of θ and ϕ are

a(ϕ)=12π,b(θ)=2πf(θ)sin⁡θ

A direction is sampled from random values r1,r2∈[0,1) using inverse transform sampling.

r1=∫0ϕ12πdt=ϕ2π⇒ϕ=2πr1r2=∫0θ2πf(t)sin⁡tdt

Solving the equation for r2 in terms of cos⁡θ produces a specific formula for each PDF.

Uniform Full-Sphere Sampling ​

For p(direction)=1/(4π), normalized by the sphere's area 4π:

r2=∫0θ12sin⁡tdt=1−cos⁡θ2⇒cos⁡θ=1−2r2

Converting to Cartesian coordinates with sin⁡θ=1−cos2⁡θ=2r2(1−r2) gives

x=cos⁡(2πr1)⋅2r2(1−r2),y=sin⁡(2πr1)⋅2r2(1−r2),z=1−2r2

Uniform Hemisphere Sampling ​

For p(direction)=1/(2π), normalized by the hemisphere's area 2π:

r2=∫0θsin⁡tdt=1−cos⁡θ⇒cos⁡θ=1−r2

The value z=1−r2 ranges from 0 at the horizon to 1 directly overhead.

This distribution is used to estimate ∫hemicos3⁡θdω. The analytic solution is

∫02π∫0π/2cos3⁡θsin⁡θdθdϕ=2π∫01u3du=π2≈1.5708

The Monte Carlo estimator is f(d)/p(d)=cos3⁡θ/(1/(2π))=2πcos3⁡θ.

Cosine-Weighted Sampling ​

For the Lambertian scattering PDF p(direction)=cos⁡θ/π:

r2=∫0θ2cos⁡tsin⁡tdt=1−cos2⁡θ⇒cos⁡θ=1−r2

Converting to Cartesian coordinates, with sin⁡θ=r2, gives

z=1−r2,x=cos⁡(2πr1)r2,y=sin⁡(2πr1)r2

For the same integral ∫hemicos3⁡θdω, the estimator is

f(d)p(d)=cos3⁡θcos⁡θ/π=πcos2⁡θ

Both uniform-hemisphere sampling, with its higher variance, and cosine-weighted sampling, optimized for Lambertian scattering, converge to the same value π/2.

Rust Implementation ​

The r307-random-directions crate implements XorShift64 locally and provides three direction-generation functions.

r307-random-directions/src/lib.rs
rust
/// Uniform distribution on the full unit sphere.
///
/// Inversion method: `φ = 2π·r1`, `cos(θ) = 1 - 2·r2`.
fn random_unit_sphere(rng: &mut XorShift64) -> (f64, f64, f64) {
    let r1 = rng.next_f64();
    let r2 = rng.next_f64();
    let z = 1.0 - 2.0 * r2;
    let r = (1.0 - z * z).sqrt(); // = 2·sqrt(r2·(1-r2))
    let phi = 2.0 * PI * r1;
    (phi.cos() * r, phi.sin() * r, z)
}

/// Uniform distribution on the hemisphere around +Z.
///
/// Inversion method: `φ = 2π·r1`, `cos(θ) = 1 - r2`.
fn random_uniform_hemi(rng: &mut XorShift64) -> (f64, f64, f64) {
    let r1 = rng.next_f64();
    let r2 = rng.next_f64();
    let z = 1.0 - r2;
    let r = (1.0 - z * z).sqrt();
    let phi = 2.0 * PI * r1;
    (phi.cos() * r, phi.sin() * r, z)
}

/// Cosine-weighted distribution on the hemisphere around +Z.
///
/// Inversion method: `φ = 2π·r1`, `cos(θ) = sqrt(1 - r2)`.
fn random_cosine_direction(rng: &mut XorShift64) -> (f64, f64, f64) {
    let r1 = rng.next_f64();
    let r2 = rng.next_f64();
    let z = (1.0 - r2).sqrt();
    let phi = 2.0 * PI * r1;
    let r = r2.sqrt(); // = sin(θ)
    (phi.cos() * r, phi.sin() * r, z)
}

generate_report produces three sections. It first displays ten example vectors from the uniform spherical distribution, then estimates ∫hemicos3⁡θdω with N=106 samples using both uniform-hemisphere and cosine-weighted sampling.

Differences Between C++ and Rust ​

In the C++ version, random_cosine_direction() returns a vec3 and uses the global random_double() function. For simplicity, the Rust version in this chapter returns a three-tuple, (f64, f64, f64). The next chapter uses the vector type when combining these directions with an ONB.

r307-random-directions/src/lib.rs
rust
use std::f64::consts::PI;

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)
    }
}

/// Uniform distribution on the full unit sphere.
///
/// Inversion method: `φ = 2π·r1`, `cos(θ) = 1 - 2·r2`.
fn random_unit_sphere(rng: &mut XorShift64) -> (f64, f64, f64) {
    let r1 = rng.next_f64();
    let r2 = rng.next_f64();
    let z = 1.0 - 2.0 * r2;
    let r = (1.0 - z * z).sqrt(); // = 2·sqrt(r2·(1-r2))
    let phi = 2.0 * PI * r1;
    (phi.cos() * r, phi.sin() * r, z)
}

/// Uniform distribution on the hemisphere around +Z.
///
/// Inversion method: `φ = 2π·r1`, `cos(θ) = 1 - r2`.
fn random_uniform_hemi(rng: &mut XorShift64) -> (f64, f64, f64) {
    let r1 = rng.next_f64();
    let r2 = rng.next_f64();
    let z = 1.0 - r2;
    let r = (1.0 - z * z).sqrt();
    let phi = 2.0 * PI * r1;
    (phi.cos() * r, phi.sin() * r, z)
}

/// Cosine-weighted distribution on the hemisphere around +Z.
///
/// Inversion method: `φ = 2π·r1`, `cos(θ) = sqrt(1 - r2)`.
fn random_cosine_direction(rng: &mut XorShift64) -> (f64, f64, f64) {
    let r1 = rng.next_f64();
    let r2 = rng.next_f64();
    let z = (1.0 - r2).sqrt();
    let phi = 2.0 * PI * r1;
    let r = r2.sqrt(); // = sin(θ)
    (phi.cos() * r, phi.sin() * r, z)
}

pub fn generate_report() -> String {
    let mut out = String::new();

    // --- Section 1: first 10 of 200 random unit vectors on the sphere -------
    out.push_str("=== Uniform Sphere: Example Unit Vectors (First 10 of 200) ===\n");
    out.push_str("         x           y           z\n");
    let mut rng1 = XorShift64::new(0x1a2b_3c4d_0001);
    for _ in 0..10 {
        let (x, y, z) = random_unit_sphere(&mut rng1);
        out.push_str(&format!("{:11.6} {:11.6} {:11.6}\n", x, y, z));
    }
    out.push_str("... (remaining 190 points omitted)\n\n");

    let n = 1_000_000usize;

    // --- Section 2: MC estimate of ∫ cos³θ dA via uniform hemisphere --------
    out.push_str("=== Estimate ∫_hemi cos³θ dA with Uniform Hemisphere Sampling ===\n");
    out.push_str(&format!("  π/2 (analytic solution) = {:.12}\n", PI / 2.0));
    let mut rng2 = XorShift64::new(0x1a2b_3c4d_0002);
    let sum2: f64 = (0..n)
        .map(|_| {
            let (_, _, z) = random_uniform_hemi(&mut rng2);
            // p(direction) = 1/(2π), f = cos³θ → f/p = 2π·cos³θ
            z * z * z / (1.0 / (2.0 * PI))
        })
        .sum();
    out.push_str(&format!(
        "  estimate (N={}) = {:.12}\n\n",
        n,
        sum2 / n as f64
    ));

    // --- Section 3: MC estimate of ∫ cos³θ dA via cosine direction ----------
    out.push_str("=== Estimate ∫_hemi cos³θ dA with Cosine-Weighted Sampling ===\n");
    out.push_str(&format!("  π/2 (analytic solution) = {:.12}\n", PI / 2.0));
    let mut rng3 = XorShift64::new(0x1a2b_3c4d_0003);
    let sum3: f64 = (0..n)
        .map(|_| {
            let (_, _, z) = random_cosine_direction(&mut rng3);
            // p(direction) = cos(θ)/π, f = cos³θ → f/p = π·cos²θ
            z * z * z / (z / PI)
        })
        .sum();
    out.push_str(&format!(
        "  estimate (N={}) = {:.12}\n",
        n,
        sum3 / n as f64
    ));

    out
}

pub fn report_random_directions() -> String {
    generate_report()
}

Results in the Browser ​

Both estimates are close to the analytic solution π/2≈1.5708. With cosine-weighted sampling, one cosine factor cancels to give f/p=πcos2⁡θ, so its variance tends to be lower than with uniform-hemisphere sampling.

Summary ​

  • I established a general inverse-transform procedure for deriving ϕ=2πr1 and cos⁡θ from a spherical-coordinate PDF f(θ).
  • I derived formulas for uniform full-sphere sampling (cos⁡θ=1−2r2), uniform-hemisphere sampling (cos⁡θ=1−r2), and cosine-weighted sampling (cos⁡θ=1−r2).
  • I numerically confirmed that both uniform-hemisphere and cosine-weighted sampling produce ∫hemicos3⁡θdω=π/2.

The next chapter introduces an orthonormal basis (ONB) that transforms these directions into a coordinate system around any surface normal.