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

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

全球面一様サンプリング

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

r2=0θ12sintdt=1cosθ2cosθ=12r2

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

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

半球一様サンプリング

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

r2=0θsintdt=1cosθcosθ=1r2

z=1r20(水平)から 1(真上)まで変化します。

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

02π0π/2cos3θsinθdθdϕ=2π01u3du=π21.5708

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

Cosine 重み付きサンプリング

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

r2=0θ2costsintdt=1cos2θcosθ=1r2

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

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

まとめ

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

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