6. 重点サンプリング付きマテリアル
Ray Tracing: The Rest of Your Life (v3.2.3): 6 Importance Sampling Materials / 3.6 重点サンプリング付きマテリアル
3.5 で Lambertian 表面の散乱 PDF
扱うのは 2 つのトピックです。
(一致), (不一致), 自体を変える(別マテリアル)の 3 通りの比較 - テスト関数
を使った数値実験
scatter_pdf と pdf_value の枠組み
レンダリング方程式のモンテカルロ推定は
と書けます。ここで
:マテリアルの散乱 PDF( scattering_pdf):方向をサンプリングするためのサンプリング PDF( pdf_value)
3 通りの組み合わせ
原典 6.1〜6.3 では以下の 3 通りを比較しています。
| 方法 | 散乱 PDF | サンプリング PDF | 積分値 | 収束速度 |
|---|---|---|---|---|
| A | 高(分散 0 に近い) | |||
| B | 低 | |||
| C | 高(ただし別マテリアル) |
方法 A と B は
原典ではこの 3 通りの違いをコーネルボックスのレンダリング画像で示しています。本章では数値実験で同じ結論(方法 A・B は
数値実験: の重み付き積分(方法 A・B・C の比較)
議論を具体化するため,テスト関数
を評価します。
Lambertian の場合:
一様半球の場合:
Rust 実装
r306-importance-sampling-play クレートは 3 通りの推定器をそれぞれ独立した RNG で実行します。
sample_cosine は Malley's method の公式
// Sample a direction from the cosine-weighted hemisphere (z = cosθ).
fn sample_cosine(rng: &mut XorShift64) -> f64 {
let r2 = rng.next_f64();
rng.next_f64(); // consume r1 (we only need cosθ = sqrt(1-r2))
(1.0 - r2).sqrt()
}
// Sample a direction from the uniform hemisphere (z = cosθ = 1 - r2).
fn sample_uniform(rng: &mut XorShift64) -> f64 {
let r2 = rng.next_f64();
rng.next_f64(); // consume r1
1.0 - r2
}両関数が 2 回 RNG を消費するのは,実際は 2D 方向(r1)は不要なので消費のみ行います。
3 通りの推定器はそれぞれ
// Method A: Lambertian scatter + cosine sampling (perfect match).
// contribution = f(d) · scatter_pdf(d) / sampling_pdf(d) = cos²θ
fn estimate_method_a(n: usize, rng: &mut XorShift64) -> f64 {
let sum: f64 = (0..n)
.map(|_| {
let cos_theta = sample_cosine(rng);
let s = scatter_pdf_lambertian(cos_theta);
let p = pdf_cosine(cos_theta);
f(cos_theta) * s / p
})
.sum();
sum / n as f64
}
// Method B: Lambertian scatter + uniform sampling (imperfect match).
// contribution = f(d) · scatter_pdf_lambertian(d) / sampling_pdf_uniform(d)
// = cos²θ · (cosθ/π) / (1/(2π)) = 2cos³θ
fn estimate_method_b(n: usize, rng: &mut XorShift64) -> f64 {
let sum: f64 = (0..n)
.map(|_| {
let cos_theta = sample_uniform(rng);
let s = scatter_pdf_lambertian(cos_theta);
let p = pdf_uniform(cos_theta);
f(cos_theta) * s / p
})
.sum();
sum / n as f64
}
// Method C: Uniform hemisphere scatter + uniform sampling (different material).
// contribution = f(d) · scatter_pdf_uniform(d) / sampling_pdf_uniform(d) = cos²θ
// Expected value = 1/3 (different from Lambertian → different render result)
fn estimate_method_c(n: usize, rng: &mut XorShift64) -> f64 {
let sum: f64 = (0..n)
.map(|_| {
let cos_theta = sample_uniform(rng);
let s = scatter_pdf_uniform_hemi(cos_theta);
let p = pdf_uniform(cos_theta);
f(cos_theta) * s / p
})
.sum();
sum / n as f64
}generate_report は
C++ と Rust の違い
C++ 版では ray_color() 関数内で
double scattering_pdf = rec.mat->scattering_pdf(r, rec, scattered);
double pdf_value = scattering_pdf; // or a different PDF
color color_from_scatter =
(attenuation * scattering_pdf * ray_color(scattered, depth-1, world))
/ pdf_value;として比較しています。Rust 版の estimate_method_a/b/c はこれと同じ
r306-importance-sampling-play/src/lib.rs
use std::fmt::Write;
const PI: f64 = std::f64::consts::PI;
// Exact values of the integrals:
// ∫_hemi f(d) · scatter_pdf(d) dω where f(d) = cos²θ
// Lambertian scatter_pdf = cosθ/π → integral = 1/2
// Uniform scatter_pdf = 1/(2π) → integral = 1/3
const LAMBERTIAN_INTEGRAL: f64 = 0.5;
const UNIFORM_INTEGRAL: f64 = 1.0 / 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)
}
}
// Sample a direction from the cosine-weighted hemisphere (z = cosθ).
fn sample_cosine(rng: &mut XorShift64) -> f64 {
let r2 = rng.next_f64();
rng.next_f64(); // consume r1 (we only need cosθ = sqrt(1-r2))
(1.0 - r2).sqrt()
}
// Sample a direction from the uniform hemisphere (z = cosθ = 1 - r2).
fn sample_uniform(rng: &mut XorShift64) -> f64 {
let r2 = rng.next_f64();
rng.next_f64(); // consume r1
1.0 - r2
}
fn pdf_cosine(cos_theta: f64) -> f64 {
cos_theta / PI
}
fn pdf_uniform(_cos_theta: f64) -> f64 {
1.0 / (2.0 * PI)
}
fn scatter_pdf_lambertian(cos_theta: f64) -> f64 {
cos_theta / PI
}
fn scatter_pdf_uniform_hemi(_cos_theta: f64) -> f64 {
1.0 / (2.0 * PI)
}
// f(d) = cos²θ (arbitrary smooth function for demonstration)
fn f(cos_theta: f64) -> f64 {
cos_theta * cos_theta
}
// Method A: Lambertian scatter + cosine sampling (perfect match).
// contribution = f(d) · scatter_pdf(d) / sampling_pdf(d) = cos²θ
fn estimate_method_a(n: usize, rng: &mut XorShift64) -> f64 {
let sum: f64 = (0..n)
.map(|_| {
let cos_theta = sample_cosine(rng);
let s = scatter_pdf_lambertian(cos_theta);
let p = pdf_cosine(cos_theta);
f(cos_theta) * s / p
})
.sum();
sum / n as f64
}
// Method B: Lambertian scatter + uniform sampling (imperfect match).
// contribution = f(d) · scatter_pdf_lambertian(d) / sampling_pdf_uniform(d)
// = cos²θ · (cosθ/π) / (1/(2π)) = 2cos³θ
fn estimate_method_b(n: usize, rng: &mut XorShift64) -> f64 {
let sum: f64 = (0..n)
.map(|_| {
let cos_theta = sample_uniform(rng);
let s = scatter_pdf_lambertian(cos_theta);
let p = pdf_uniform(cos_theta);
f(cos_theta) * s / p
})
.sum();
sum / n as f64
}
// Method C: Uniform hemisphere scatter + uniform sampling (different material).
// contribution = f(d) · scatter_pdf_uniform(d) / sampling_pdf_uniform(d) = cos²θ
// Expected value = 1/3 (different from Lambertian → different render result)
fn estimate_method_c(n: usize, rng: &mut XorShift64) -> f64 {
let sum: f64 = (0..n)
.map(|_| {
let cos_theta = sample_uniform(rng);
let s = scatter_pdf_uniform_hemi(cos_theta);
let p = pdf_uniform(cos_theta);
f(cos_theta) * s / p
})
.sum();
sum / n as f64
}
pub fn generate_report() -> String {
let mut out = String::new();
// Header
writeln!(out, "Integral: ∫_hemi cos²θ · scatter_pdf(d) dω").unwrap();
writeln!(out).unwrap();
// Single-run comparison with N = 1_000_000
let n = 1_000_000;
let mut rng_a = XorShift64::new(0x1a2b_3c4d_0001);
let mut rng_b = XorShift64::new(0x1a2b_3c4d_0002);
let mut rng_c = XorShift64::new(0x1a2b_3c4d_0003);
let result_a = estimate_method_a(n, &mut rng_a);
let result_b = estimate_method_b(n, &mut rng_b);
let result_c = estimate_method_c(n, &mut rng_c);
writeln!(out, "=== Single Run (N = {n}) ===").unwrap();
writeln!(out, "{:<10} {:<12} {:<12} {:<12}", "Method", "Estimate", "Exact", "Error").unwrap();
writeln!(
out,
"{:<10} {:<12.8} {:<12.8} {:<+12.2e}",
"A (cos/cos)",
result_a,
LAMBERTIAN_INTEGRAL,
result_a - LAMBERTIAN_INTEGRAL
)
.unwrap();
writeln!(
out,
"{:<10} {:<12.8} {:<12.8} {:<+12.2e}",
"B (cos/uni)",
result_b,
LAMBERTIAN_INTEGRAL,
result_b - LAMBERTIAN_INTEGRAL
)
.unwrap();
writeln!(
out,
"{:<10} {:<12.8} {:<12.8} {:<+12.2e}",
"C (uni/uni)",
result_c,
UNIFORM_INTEGRAL,
result_c - UNIFORM_INTEGRAL
)
.unwrap();
writeln!(out).unwrap();
// Convergence table for A and B (same target, different PDFs)
writeln!(out, "=== Convergence: Method A (cos/cos) vs B (cos/uni) ===").unwrap();
writeln!(
out,
"{:<10} {:<14} {:<12} {:<14} {:<12}",
"N", "A Estimate", "A Error", "B Estimate", "B Error"
)
.unwrap();
let checkpoints = [100usize, 1_000, 10_000, 100_000, 500_000];
let mut rng_a = XorShift64::new(0x5e6f_7a8b_0001);
let mut rng_b = XorShift64::new(0x5e6f_7a8b_0002);
for &n in &checkpoints {
let a = estimate_method_a(n, &mut rng_a);
let b = estimate_method_b(n, &mut rng_b);
writeln!(
out,
"{:<10} {:<14.8} {:<12.2e} {:<14.8} {:<12.2e}",
n,
a,
a - LAMBERTIAN_INTEGRAL,
b,
b - LAMBERTIAN_INTEGRAL
)
.unwrap();
}
writeln!(out).unwrap();
// Method C comparison
writeln!(out, "=== Note: Method C gives a different answer ===").unwrap();
writeln!(
out,
"Lambertian scatter_pdf (A & B): exact = {}",
LAMBERTIAN_INTEGRAL
)
.unwrap();
writeln!(
out,
"Uniform scatter_pdf (C): exact = {:.8}",
UNIFORM_INTEGRAL
)
.unwrap();
out
}ブラウザ上での実行結果
収束チェックポイントの表では,方法 A の A Error が方法 B の B Error より一貫して小さくなります。単発比較の Error 欄では方法 A と B の厳密値が同じ(1/2)であり,方法 C だけが 1/3 と異なることを確認できます。
まとめ
- レンダリング方程式の推定
において, が積分値を決め, が収束速度を決める。 (方法 A)では分散が最小になり, (方法 B)でも同じ積分値に収束する。 自体を変えると(方法 C)別マテリアルになり,積分値が変わる( )。
次章では,この枠組みをレイトレーサ本体に接続します。