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

10. 全機能を試すシーン

第 2 編の締めくくりに,これまで追加したすべての機能を一度に投入したシーンを組み立てます。原典は 800 × 800 で 10 000 サンプル/px,CPU で何時間もかけてレンダリングする規模ですが,本サイトでは WASM で実用的に動かすため 300 × 300 / 50 サンプル / 最大深度 20 に控えめに調整しています。それでも以下の全機能が一枚に同居します:

  • BVH — 地面の 400 個のブロック,及び 1000 個の小球クラスタ
  • MovingSphere(モーションブラー)— 横方向に動く Lambertian 球
  • Dielectric / Metal — ガラス球と擦りガラス的な金属球
  • ConstantMedium — 色付きの「ガラスの中の媒質」と,全体を覆う薄い霧
  • ImageTexture — 地球テクスチャの球
  • NoiseTexture(Marble モード)— Perlin マーブル球
  • DiffuseLight — 天井の大型エリアライト
  • RotateY + Translate — 小球クラスタの回転・平行移動

つまり共通ライブラリ common/ に第 2 編で増やした機能のほぼ全てが直接呼ばれます。

地面:BVH に詰めた凸凹のブロック

地面を平らな矩形 1 枚で済ませず,20 × 20 = 400 個のブロックを並べて高さをランダムにします。シーンを賑やかにするだけでなく,400 個のヒッタブルを線形リストではなく BVH で扱うことで第 2.3 章の効果を実演する役割もあります。

r210-final-scene/src/lib.rs
rust
let ground = Arc::new(Lambertian::new(Color::new(0.48, 0.83, 0.53)));
let boxes_per_side = 20;
let mut boxes1: Vec<Arc<dyn Hittable>> = Vec::with_capacity(boxes_per_side * boxes_per_side);
for i in 0..boxes_per_side {
    for j in 0..boxes_per_side {
        let w = 100.0;
        let x0 = -1000.0 + i as f64 * w;
        let z0 = -1000.0 + j as f64 * w;
        let y1 = random_double_range(1.0, 101.0);
        boxes1.push(Arc::new(RectBox::new(
            Point3::new(x0, 0.0, z0),
            Point3::new(x0 + w, y1, z0 + w),
            ground.clone(),
        )));
    }
}
objects.add(Box::new(BvhNode::new(boxes1, 0.0, 1.0)));

BvhNode::newVec<Arc<dyn Hittable>> を取る点は HittableList::addBox<dyn Hittable>)とインターフェイスが違うので,ここだけ Arc を使います。BVH 内部で左右の子ノードに共有所有権が必要になるための差です(2.3 章で書いた通り)。

単純な球たち:モーションブラー・ガラス・金属

r210-final-scene/src/lib.rs
rust
// Moving Lambertian.
let center1 = Point3::new(400.0, 400.0, 200.0);
let center2 = center1 + Vec3::new(30.0, 0.0, 0.0);
objects.add(Box::new(MovingSphere::new(
    center1, center2, 0.0, 1.0, 50.0,
    Arc::new(Lambertian::new(Color::new(0.7, 0.3, 0.1))),
)));

// Glass.
objects.add(Box::new(Sphere::with_material(
    Point3::new(260.0, 150.0, 45.0), 50.0,
    Arc::new(Dielectric::new(1.5)),
)));

// Fuzzy metal.
objects.add(Box::new(Sphere::with_material(
    Point3::new(0.0, 150.0, 145.0), 50.0,
    Arc::new(Metal::new(Color::new(0.8, 0.8, 0.9), 1.0)),
)));

カメラのシャッターは [0, 1] 区間で開いており,モーションブラー球はこの間に x 方向へ 30 動きます。完成画像でこの球が横にぶれて見えるのが 2.2 章の効果です。

色付きガラスボリューム

ガラス球の 内部に色付きの煙を入れる小技です。Dielectric の球を 2 つ用意し,1 つは可視のガラス面として,もう 1 つは ConstantMedium の境界として使います。

r210-final-scene/src/lib.rs
rust
// Visible glass surface.
objects.add(Box::new(Sphere::with_material(
    Point3::new(360.0, 150.0, 145.0), 70.0,
    Arc::new(Dielectric::new(1.5)),
)));
// Boundary of the colored medium (same sphere, used inside ConstantMedium).
let boundary = Sphere::with_material(
    Point3::new(360.0, 150.0, 145.0), 70.0,
    Arc::new(Dielectric::new(1.5)),
);
objects.add(Box::new(ConstantMedium::new(
    Box::new(boundary),
    0.2,
    Color::new(0.2, 0.4, 0.9),
)));

C++ と Rust の違い

C++ 原典は shared_ptr<hittable> boundary = make_shared<sphere>(...); を作り,それを objects.add(boundary)make_shared<constant_medium>(boundary, ...) の 両方に同じポインタで渡しています。Rust 版は objects.addBox<dyn Hittable> を要求するため,同一の Sphere を 2 つ作ることで等価な動作を再現しました。1 つを Arc に切り替えるとさらに原典に近づきますが,HittableList::add のシグネチャを変更する必要があり波及が大きいため,2 個作るほうを選択しました。コストは「Sphere は 24 バイト程度の値型なのでほぼ無視できる」レベルです。

密度 0.2 で色 (0.2,0.4,0.9) の媒質が球内部を満たし,外側はガラス,内側は青く濁っているという見え方になります。

全体を覆う薄い霧

シーン全体を半径 5000 の巨大なガラス球で囲み,その内側に超低密度(d=0.0001)の白い ConstantMedium を入れることで,遠くの物体が霞む効果を出します。

r210-final-scene/src/lib.rs
rust
let global_boundary = Sphere::with_material(
    Point3::new(0.0, 0.0, 0.0), 5000.0,
    Arc::new(Dielectric::new(1.5)),
);
objects.add(Box::new(ConstantMedium::new(
    Box::new(global_boundary),
    0.0001,
    Color::new(1.0, 1.0, 1.0),
)));

ガラス球の屈折を経由しない直接見えの物体には影響しないように見えて,実は媒質を通った経路長に応じて散乱が起きます。d = 0.0001 は「単位距離あたり 0.01% で散乱」程度の薄さで,シーン全体(数百単位)を渡るレイにじわっと効きます。

テクスチャ球:地球とマーブル

r210-final-scene/src/lib.rs
rust
let earth_texture: Arc<dyn Texture> = Arc::new(ImageTexture::from_rgb_bytes(
    EARTHMAP_RGB.to_vec(), EARTHMAP_WIDTH, EARTHMAP_HEIGHT,
));
objects.add(Box::new(Sphere::with_material(
    Point3::new(400.0, 200.0, 400.0), 100.0,
    Arc::new(Lambertian::with_texture(earth_texture)),
)));

let pertext: Arc<dyn Texture> = Arc::new(NoiseTexture::new(NoiseMode::Marble, 0.1));
objects.add(Box::new(Sphere::with_material(
    Point3::new(220.0, 280.0, 300.0), 80.0,
    Arc::new(Lambertian::with_texture(pertext)),
)));

地球テクスチャは 2.6 章の r206-earth と同じ assets/earthmap.jpgbuild.rs でデコードしてバイト列に埋め込みます。アセットは r206 と共有させ,build.rs 内で ../r206-earth/assets/earthmap.jpg を相対参照しています。同じ JPEG を 2 度コミットしないための小細工です。

r210-final-scene/build.rs
rust
let asset = "../r206-earth/assets/earthmap.jpg";

1000 個の小球クラスタ+インスタンス変換

最後の見せ場が,1000 個の白い小球を立方体領域 [0,165]3 にランダム配置し,BVH に詰めてから回転・平行移動するパターンです。

r210-final-scene/src/lib.rs
rust
let white = Arc::new(Lambertian::new(Color::new(0.73, 0.73, 0.73)));
let ns = 1000;
let mut boxes2: Vec<Arc<dyn Hittable>> = Vec::with_capacity(ns);
for _ in 0..ns {
    let p = Point3::new(
        random_double_range(0.0, 165.0),
        random_double_range(0.0, 165.0),
        random_double_range(0.0, 165.0),
    );
    boxes2.push(Arc::new(Sphere::with_material(p, 10.0, white.clone())));
}
let cluster: Box<dyn Hittable> = Box::new(BvhNode::new(boxes2, 0.0, 1.0));
let cluster: Box<dyn Hittable> = Box::new(RotateY::new(cluster, 15.0));
let cluster: Box<dyn Hittable> = Box::new(Translate::new(cluster, Vec3::new(-100.0, 270.0, 395.0)));
objects.add(cluster);

ここでも 2.8 章で見たシャドーイングのパターンが効いてきます。BvhNode 1 つに RotateY をかぶせ,さらに Translate をかぶせる三段重ねで,1001 個の Hittable(実体は 1000 個の球+ BVH 内部ノード)がたった一つの Box<dyn Hittable> としてシーンに追加されます。BVH をインスタンス変換と組み合わせることの威力がここに現れています。

レンダリング設定

カメラと最終的なレンダリング設定です。

r210-final-scene/src/lib.rs
rust
let aspect_ratio = 1.0_f64;
let image_width = 300_i32;
let samples_per_pixel = 50_i32;
let max_depth = 20_i32;

let lookfrom = Point3::new(478.0, 278.0, -600.0);
let lookat = Point3::new(278.0, 278.0, 0.0);
let camera = Camera::new_with_shutter(
    lookfrom, lookat, Vec3::new(0.0, 1.0, 0.0),
    40.0, aspect_ratio, 0.0, 10.0, 0.0, 1.0,
);
let background = Color::new(0.0, 0.0, 0.0);

原典 samples_per_pixel = 10000 に対して 50 とかなり控えめなので,ファイヤフライ(特に明るすぎる飛び値ピクセル)や全体的なノイズが目立ちます。これらは第 3 編で導入する 重要度サンプリングで大きく改善されます。最大深度を 20 にしているのも,色付きガラス内部の媒質で何度も散乱が起きるシーンへの妥協で,原典の 50 にすると WASM では描画が辛くなります。

まとめ

  • 第 2 編で追加したすべての機能(BVH,モーションブラー,ガラス・金属・Lambertian,定密度媒質,イメージ・ノイズテクスチャ,矩形ライト,回転・平行移動インスタンス)を一画面に詰め込んだ最終シーンを r210-final-scene クレートとして実装した。
  • 地球テクスチャ用 JPEG は r206-earth のものを build.rs で相対参照し,アセット重複を避けた。
  • WASM 実行のために解像度・サンプル数・最大深度を縮小しているため,原典のような滑らかな絵にはならないが,全機能が一枚で動いていることを確認できる。

これで第 2 編「Ray Tracing: The Next Week」の全章を Rust + WebAssembly に移植し終えました。第 3 編「Ray Tracing: The Rest of Your Life」では,本章で残ったノイズ問題に 重要度サンプリングで立ち向かいます。具体的には:

  1. PDF(確率密度関数)を陽に扱うパイプラインへの書き換え
  2. 光源を狙うサンプリング戦略
  3. Cosine 重みつきの Lambertian サンプリング
  4. 複数 PDF の混合(ミックスチャ PDF)

など,これまで暗黙にしていた「サンプリング」の中身を全面的に作り直す,本シリーズで一番理論寄りの編に入ります。