8. 拡散マテリアル
Ray Tracing in One Weekend (v3.2.3): 8 Diffuse Materials / 1.8 拡散マテリアル
この章では拡散マテリアル(非光沢マテリアル)を実装します。拡散マテリアルは光を吸収しながらランダムな方向に散乱させます。今章では3通りの実装を順に紹介しています。それぞれ散乱方向の確率分布が異なり,仕上がりの明るさや影の雰囲気に違いが生じます。
拡散マテリアルの仕組み
拡散マテリアルに当たった光は次の2つを同時に行います。
- 吸収:光の一部が表面に吸収されて失われる
- 散乱:残りがランダムな方向に跳ね返る
これをシミュレートするには,交点 ray_color を呼んで色を求め,係数 0.5 を乗じます(50% 吸収)。
再帰の打ち切り
散乱レイが生成されると再びどこかに当たり,さらに散乱レイを生成……と再帰が続きます。永遠に続かないよう depth パラメータで最大反射回数を制限し,depth <= 0 のときは黒 (0, 0, 0) を返します。
C++ の実装です。
color ray_color(const ray& r, const hittable& world, int depth) {
if (depth <= 0)
return color(0,0,0);
// ...
return 0.5 * ray_color(scattered, world, depth-1);
}Rust の実装です。
fn ray_color(r: &Ray, world: &dyn Hittable, depth: i32) -> Color {
if depth <= 0 {
return Color::new(0.0, 0.0, 0.0);
}
// ...
return 0.5 * ray_color(&scattered, world, depth - 1);
}シャドウアクネの対策
浮動小数点演算の丸め誤差により,交点から生成した散乱レイが
対策は単純です。hit の t_min を 0 から 0.001 に変えることで,交点の直近での自己交差を除外します。
// causes shadow acne
if let Some(rec) = world.hit(r, 0.0, f64::INFINITY) { ... }
// fixed
if let Some(rec) = world.hit(r, 0.001, f64::INFINITY) { ... }1. 単純な拡散マテリアル(棄却法)
最初の実装では,交点
ランダム点は棄却法(rejection method)で求めます。
棄却法は,確率分布からの乱数生成において,目的とする複雑な確率分布を直接サンプリングする代わりに,より単純な提案分布からサンプルを生成し,一定の基準に基づいて受理または棄却を繰り返すことで目的の分布に従う乱数を得る手法です。具体的には,目的とする確率密度関数を別の扱いやすい確率密度関数と定数の積で上から覆うように設定し,まず提案分布から候補点を生成した後,一様乱数を用いた確率的な判定によってその候補を採用するか破棄するかを決定します。この判定により,最終的に受理されたサンプルは目的の分布に従うことが保証されます。
C++ の実装です。
vec3 random_in_unit_sphere() {
while (true) {
auto p = vec3::random(-1, 1);
if (p.length_squared() >= 1) continue;
return p;
}
}Rust の実装です。
/// Returns a random point **inside** the unit sphere (rejection method).
pub fn random_in_unit_sphere() -> Vec3 {
use crate::utils::random_double_range;
loop {
let p = Vec3::new(
random_double_range(-1.0, 1.0),
random_double_range(-1.0, 1.0),
random_double_range(-1.0, 1.0),
);
if p.length_squared() < 1.0 {
return p;
}
}
}loop はRustのループ構文で,while true に相当します。ランダム点が単位球内に収まるまで繰り返します。平均では約1.91回の試行で成功するため,実用上の問題はありません。
散乱レイの生成コードです。
if let Some(rec) = world.hit(r, 0.001, f64::INFINITY) {
let target = rec.p + rec.normal + random_in_unit_sphere();
let scattered = Ray::new(rec.p, target - rec.p);
return 0.5 * ray_color(&scattered, world, depth - 1);
}ガンマ補正
レンダリング結果がかなり暗く見えます。これは画像ビューアがガンマ補正済みの値を前提としているためです。ここでは
C++ では write_color() 内で行います。
auto scale = 1.0 / samples_per_pixel;
r = sqrt(scale * r); // Gamma=2 correction
g = sqrt(scale * g);
b = sqrt(scale * b);Rust でも同様に write_color_gamma() 関数を追加します。write_color(線形)との違いはスケール後に .sqrt() を挟む点だけです。
/// Averages `pixel_color` over `samples_per_pixel` samples, applies gamma=2 correction (square root), and converts to an "R G B" string.
pub fn write_color_gamma(pixel_color: Color, samples_per_pixel: i32) -> String {
let scale = 1.0 / samples_per_pixel as f64;
let r = (pixel_color.x() * scale).sqrt().clamp(0.0, 0.999);
let g = (pixel_color.y() * scale).sqrt().clamp(0.0, 0.999);
let b = (pixel_color.z() * scale).sqrt().clamp(0.0, 0.999);
let ir = (256.0 * r) as i32;
let ig = (256.0 * g) as i32;
let ib = (256.0 * b) as i32;
format!("{} {} {}", ir, ig, ib)
}1.8 章以降は write_color_gamma を使います。write_color(非ガンマ)は 1.2〜1.7 章の既存クレートとの互換用に残します。
2. 真のランバーティアン反射
棄却法による random_in_unit_sphere() は,確率密度が
単位球面上のランダム点を random_unit_vector() で求めることで,正確な
C++ の実装です。
vec3 random_unit_vector() {
return unit_vector(random_in_unit_sphere());
}Rust の実装です。
/// Returns a random point **on** the unit sphere surface (true Lambertian reflection).
pub fn random_unit_vector() -> Vec3 {
unit_vector(random_in_unit_sphere())
}散乱レイを rec.normal + random_unit_vector() に変えるだけです。
let target = rec.p + rec.normal + random_unit_vector();見た目の違いは,影がより薄く,球がより明るくなることです。これは
3. 半球一様サンプリング
初期の研究論文が採用した方法では,法線方向の半球を一様にサンプリングします。
C++ の実装です。
vec3 random_in_hemisphere(const vec3& normal) {
vec3 in_unit_sphere = random_in_unit_sphere();
if (dot(in_unit_sphere, normal) > 0.0)
return in_unit_sphere;
else
return -in_unit_sphere;
}Rust の実装です。
/// Returns a random point in the hemisphere around the normal (uniform hemisphere sampling).
pub fn random_in_hemisphere(normal: Vec3) -> Vec3 {
let in_unit_sphere = random_in_unit_sphere();
if dot(in_unit_sphere, normal) > 0.0 {
in_unit_sphere
} else {
-in_unit_sphere
}
}散乱レイには rec.p + random_in_hemisphere(rec.normal) を使います(rec.normal を足す必要がない点に注意)。
let target = rec.p + random_in_hemisphere(rec.normal);3手法のビジュアル比較です(いずれもガンマ補正あり・シャドウアクネ対策済み)。
| 手法 | 散乱関数 | 確率密度 | 見た目の特徴 |
|---|---|---|---|
| 単純な拡散 | random_in_unit_sphere() | やや暗め・重い影 | |
| 真のランバーティアン | random_unit_vector() | 明るめ・薄い影 | |
| 半球一様 | random_in_hemisphere() | 一様 | 中間的な見た目 |
C++ と Rust の違い
loop vs while(true)
C++ の無限ループは while(true) が一般的ですが,Rust では loop キーワードが推奨されます。
// C++
while (true) { ... }
// Rust
loop { ... }loop はコンパイラに「必ずどこかで break または return がある」と知らせるため,戻り値の型推論が正確になります。
sqrt の呼び出し方
// C++
double r = sqrt(scale * pixel_color.x());// Rust
let r = (pixel_color.x() * scale).sqrt();Rust では f64::sqrt() はメソッドとして呼び出します。C の sqrt() 関数のような自由関数は使いません。
common クレートへの追加
common/src/vec3.rs(追加分)
impl Vec3 {
// ... add to existing methods ...
/// Returns `true` if all components are very close to zero (zero-vector check).
pub fn near_zero(self) -> bool {
const S: f64 = 1e-8;
self.e[0].abs() < S && self.e[1].abs() < S && self.e[2].abs() < S
}
}
/// Returns a random point **inside** the unit sphere (rejection method).
pub fn random_in_unit_sphere() -> Vec3 {
use crate::utils::random_double_range;
loop {
let p = Vec3::new(
random_double_range(-1.0, 1.0),
random_double_range(-1.0, 1.0),
random_double_range(-1.0, 1.0),
);
if p.length_squared() < 1.0 {
return p;
}
}
}
/// Returns a random point **on** the unit sphere surface (true Lambertian reflection).
pub fn random_unit_vector() -> Vec3 {
unit_vector(random_in_unit_sphere())
}
/// Returns a random point in the hemisphere around the normal (uniform hemisphere sampling).
pub fn random_in_hemisphere(normal: Vec3) -> Vec3 {
let in_unit_sphere = random_in_unit_sphere();
if dot(in_unit_sphere, normal) > 0.0 {
in_unit_sphere
} else {
-in_unit_sphere
}
}
/// Averages `pixel_color` over `samples_per_pixel` samples, applies gamma=2 correction (square root), and converts to an "R G B" string.
pub fn write_color_gamma(pixel_color: Color, samples_per_pixel: i32) -> String {
let scale = 1.0 / samples_per_pixel as f64;
let r = (pixel_color.x() * scale).sqrt().clamp(0.0, 0.999);
let g = (pixel_color.y() * scale).sqrt().clamp(0.0, 0.999);
let b = (pixel_color.z() * scale).sqrt().clamp(0.0, 0.999);
let ir = (256.0 * r) as i32;
let ig = (256.0 * g) as i32;
let ib = (256.0 * b) as i32;
format!("{} {} {}", ir, ig, ib)
}r108-diffuse クレートの実装(単純な拡散)
r108-diffuse/Cargo.toml を用意します。
[package]
name = "r108-diffuse"
version = "0.1.0"
edition = "2024"
[dependencies]
common = { workspace = true }r108-diffuse/src/lib.rs の完全な実装です。
use common::{
Camera, Color, Hittable, HittableList, Point3, Sphere,
unit_vector, write_color_gamma, Ray, random_double, random_in_unit_sphere,
};
fn ray_color(r: &Ray, world: &dyn Hittable, depth: i32) -> Color {
if depth <= 0 {
return Color::new(0.0, 0.0, 0.0);
}
// t_min = 0.001 prevents shadow acne.
if let Some(rec) = world.hit(r, 0.001, f64::INFINITY) {
let target = rec.p + rec.normal + random_in_unit_sphere();
let scattered = Ray::new(rec.p, target - rec.p);
return 0.5 * ray_color(&scattered, world, depth - 1);
}
let unit_direction = unit_vector(r.direction());
let t = 0.5 * (unit_direction.y() + 1.0);
(1.0 - t) * Color::new(1.0, 1.0, 1.0) + t * Color::new(0.5, 0.7, 1.0)
}
pub fn render_image() -> String {
let aspect_ratio = 16.0_f64 / 9.0;
let image_width = 384_i32;
let image_height = (image_width as f64 / aspect_ratio) as i32;
let samples_per_pixel = 50_i32;
let max_depth = 20_i32;
let mut world = HittableList::new();
world.add(Box::new(Sphere::new(Point3::new(0.0, 0.0, -1.0), 0.5)));
world.add(Box::new(Sphere::new(Point3::new(0.0, -100.5, -1.0), 100.0)));
let camera = Camera::new();
let mut output = String::new();
output.push_str("P3\n");
output.push_str(&format!("{} {}\n", image_width, image_height));
output.push_str("255\n");
for j in (0..image_height).rev() {
for i in 0..image_width {
let mut pixel_color = Color::new(0.0, 0.0, 0.0);
for _ in 0..samples_per_pixel {
let u = (i as f64 + random_double()) / (image_width - 1) as f64;
let v = (j as f64 + random_double()) / (image_height - 1) as f64;
let r = camera.get_ray(u, v);
pixel_color += ray_color(&r, &world, max_depth);
}
output.push_str(&format!("{}\n", write_color_gamma(pixel_color, samples_per_pixel)));
}
}
output
}r108-lambertian クレートの実装(真のランバーティアン)
r108-lambertian/src/lib.rs の完全な実装です。random_in_unit_sphere() の代わりに random_unit_vector() を使います。
use common::{
Camera, Color, Hittable, HittableList, Point3, Sphere,
unit_vector, write_color_gamma, Ray, random_double, random_unit_vector,
};
fn ray_color(r: &Ray, world: &dyn Hittable, depth: i32) -> Color {
if depth <= 0{
return Color::new(0.0, 0.0, 0.0);
}
if let Some(rec) = world.hit(r, 0.001, f64::INFINITY) {
let target = rec.p + rec.normal + random_unit_vector();
let scattered = Ray::new(rec.p, target - rec.p);
return 0.5 * ray_color(&scattered, world, depth - 1);
}
let unit_direction = unit_vector(r.direction());
let t = 0.5 * (unit_direction.y() + 1.0);
(1.0 - t) * Color::new(1.0, 1.0, 1.0) + t * Color::new(0.5, 0.7, 1.0)
}
pub fn render_image() -> String {
// ... same settings as r108-diffuse ...
}r108-hemisphere クレートの実装(半球一様サンプリング)
r108-hemisphere/src/lib.rs の完全な実装です。random_in_hemisphere(rec.normal) を使い,接線球への法線オフセットは不要です。
use common::{
Camera, Color, Hittable, HittableList, Point3, Sphere,
unit_vector, write_color_gamma, Ray, random_double, random_in_hemisphere,
};
fn ray_color(r: &Ray, world: &dyn Hittable, depth: i32) -> Color {
if depth <= 0 {
return Color::new(0.0, 0.0, 0.0);
}
if let Some(rec) = world.hit(r, 0.001, f64::INFINITY) {
// Uniform sampling from the hemisphere around the normal (no need to add rec.normal)
let target = rec.p + random_in_hemisphere(rec.normal);
let scattered = Ray::new(rec.p, target - rec.p);
return 0.5 * ray_color(&scattered, world, depth - 1);
}
let unit_direction = unit_vector(r.direction());
let t = 0.5 * (unit_direction.y() + 1.0);
(1.0 - t) * Color::new(1.0, 1.0, 1.0) + t * Color::new(0.5, 0.7, 1.0)
}
pub fn render_image() -> String {
// ... same settings as r108-diffuse ...
}WASM エクスポート
raytracing-demos/src/lib.rs に追記します。
// Chapter 1.8: Diffuse Materials
#[wasm_bindgen]
pub fn render_diffuse() -> String {
r108_diffuse::render_image()
}
#[wasm_bindgen]
pub fn render_lambertian() -> String {
r108_lambertian::render_image()
}
#[wasm_bindgen]
pub fn render_hemisphere() -> String {
r108_hemisphere::render_image()
}