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

7. ランダムな方向の生成 ​

Ray Tracing: The Rest of Your Life (v3.2.3): 7 Generating Random Directions / 3.7 ランダムな方向の生成

3.2〜3.6 のモンテカルロ推定は「z 軸周りの散乱角 θ だけ考える 1 次元の議論」でした。ここでは3 次元空間の方向ベクトルを確率的に生成する手法を体系化します。次章で学ぶ ONB(正規直交基底)と組み合わせると,任意の法線方向に対して散乱させることができます。

扱うのは 3 つのトピックです。

  • 球面座標の PDF から逆変換法で方向を生成する一般的な手順
  • 全球面一様・半球一様・cosine 重み付きの 3 種類の具体的な公式
  • 同じ積分 ∫hemicos3⁡θdω を 2 通りの PDF で推定して一致を確認

球面座標と立体角の PDF ​

単位球面上の面積素は

dA=sin⁡θdθdϕ

です。PDF p(direction)=f(θ)(ϕ について対称)が与えられたとき,θ と ϕ の周辺 PDF は

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

となります。逆変換法で乱数 r1,r2∈[0,1) から方向をサンプリングします。

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

r2 の式を cos⁡θ について解くと,各 PDF に対する具体的な式が得られます。

全球面一様サンプリング ​

p(direction)=1/(4π)(球面積 4π で正規化)のとき:

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

sin⁡θ=1−cos2⁡θ=2r2(1−r2) を使って Cartesian 座標に変換すると:

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

半球一様サンプリング ​

p(direction)=1/(2π)(半球面積 2π で正規化)のとき:

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

z=1−r2 は 0(水平)から 1(真上)まで変化します。

このサンプリングで ∫hemicos3⁡θdω を推定します。解析解は

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

です。モンテカルロ推定量は f(d)/p(d)=cos3⁡θ/(1/(2π))=2πcos3⁡θ です。

Cosine 重み付きサンプリング ​

p(direction)=cos⁡θ/π(Lambertian 散乱 PDF)のとき:

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

Cartesian 座標に変換すると(sin⁡θ=r2):

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

同じ ∫hemicos3⁡θdω に対する推定量は

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

となります。半球一様(分散大)と cosine 重み付き(Lambertian に最適化)の両方が同じ π/2 に収束することを確認します。

Rust 実装 ​

r307-random-directions クレートは XorShift64 をローカルに実装し,3 種類の方向生成関数を提供します。

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 は 3 つのセクションを出力します。まず球面一様分布のベクトル例を 10 点表示し,続いて半球一様サンプリングと cosine サンプリングそれぞれで ∫hemicos3⁡θdω を N=106 サンプルで推定します。

C++ と Rust の違い ​

C++ 版では random_cosine_direction() は vec3 を返し,グローバルな乱数関数(random_double())を使います。Rust 版はこの章では 3 タプル (f64, f64, f64) を返すシンプルな実装にしています。次章(3.8)で 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("=== 球面一様分布:単位ベクトルの例(先頭 10 点 / 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("... (残り 190 点省略)\n\n");

    let n = 1_000_000usize;

    // --- Section 2: MC estimate of ∫ cos³θ dA via uniform hemisphere --------
    out.push_str("=== 一様半球サンプリングで ∫_hemi cos³θ dA を推定 ===\n");
    out.push_str(&format!("  π/2 (解析解) = {:.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!(
        "  推定値 (N={}) = {:.12}\n\n",
        n,
        sum2 / n as f64
    ));

    // --- Section 3: MC estimate of ∫ cos³θ dA via cosine direction ----------
    out.push_str("=== cosine 方向サンプリングで ∫_hemi cos³θ dA を推定 ===\n");
    out.push_str(&format!("  π/2 (解析解) = {:.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!(
        "  推定値 (N={}) = {:.12}\n",
        n,
        sum3 / n as f64
    ));

    out
}

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

ブラウザ上での実行結果 ​

2 つの推定値がどちらも解析解 π/2≈1.5708 に近い値を示すことを確認できます。cosine 方向サンプリングは f/p=πcos2⁡θ と cos の因子が 1 つ消えるため,一様半球より分散が小さくなる傾向があります。

まとめ ​

  • 球面座標の PDF f(θ) から逆変換法で ϕ=2πr1,cos⁡θ を求める一般手順を確立した。
  • 全球面一様(cos⁡θ=1−2r2),半球一様(cos⁡θ=1−r2),cosine 重み付き(cos⁡θ=1−r2)の 3 公式を導出した。
  • 半球一様と cosine 重み付きの両方で ∫hemicos3⁡θdω=π/2 が得られることを数値確認した。

次章では ONB(正規直交基底)を導入し,これらの方向を任意の法線座標系に変換する仕組みを作ります。