Skip to content
Copied!
published on 2026-06-08

8. インスタンス

Ray Tracing: The Next Week (v3.2.3): 8 Instances / 2.8 インスタンス

前章の Cornell box には壁と天井のライトしか置きませんでした。この章では中に 2 つの白い箱を置き,さらに壁に対して少しだけ回転させて Cornell box の定番構図に近づけます。そのために必要な部品は次の 3 つです。

  1. 6 枚の矩形をまとめた (box)プリミティブ
  2. オブジェクトを世界座標で平行移動する Translate
  3. オブジェクトを Y 軸まわりに回転させる RotateY

「インスタンス」とは,ジオメトリ自体を動かさず,入射レイの方を逆向きに変換することで「見かけ上動いた」ように見せる仕組みのことです。レイトレーシングの強力なテクニックで,1 つの形状をシーン中の任意の位置・向きに何度でも使い回せます。

軸並行な箱

まずは軸並行な直方体を作ります。前章で導入した 3 種類の矩形(XyRect/XzRect/YzRect)を 6 枚並べれば直方体になるので,これらを HittableList にまとめて Hittable トレイトを実装するだけです。

Rust では型名に Box を使うと std::boxed::Box と紛らわしいので,RectBox という名前にしています。

common/src/rect_box.rs
rust
pub struct RectBox {
    pub box_min: Point3,
    pub box_max: Point3,
    sides: HittableList,
}

impl RectBox {
    pub fn new(p0: Point3, p1: Point3, material: Arc<dyn Material>) -> Self {
        let mut sides = HittableList::new();

        sides.add(Box::new(XyRect::new(p0.x(), p1.x(), p0.y(), p1.y(), p1.z(), material.clone())));
        sides.add(Box::new(XyRect::new(p0.x(), p1.x(), p0.y(), p1.y(), p0.z(), material.clone())));
        sides.add(Box::new(XzRect::new(p0.x(), p1.x(), p0.z(), p1.z(), p1.y(), material.clone())));
        sides.add(Box::new(XzRect::new(p0.x(), p1.x(), p0.z(), p1.z(), p0.y(), material.clone())));
        sides.add(Box::new(YzRect::new(p0.y(), p1.y(), p0.z(), p1.z(), p1.x(), material.clone())));
        sides.add(Box::new(YzRect::new(p0.y(), p1.y(), p0.z(), p1.z(), p0.x(), material)));

        RectBox { box_min: p0, box_max: p1, sides }
    }
}

impl Hittable for RectBox {
    fn hit(&self, r: &Ray, t_min: f64, t_max: f64) -> Option<HitRecord> {
        self.sides.hit(r, t_min, t_max)
    }
    fn bounding_box(&self, _t0: f64, _t1: f64) -> Option<Aabb> {
        Some(Aabb::new(self.box_min, self.box_max))
    }
}

hit は内部の HittableList に丸投げ,bounding_box は 2 隅から作るだけ。原典の C++ クラスと完全に同じ構造で,違いは型名 RectBox(vs. box)と Rust の Arc<dyn Material>(vs. shared_ptr<material>)だけです。

C++ と Rust の違い

C++ では同名の box クラスを問題なく定義できますが,Rust の Box<T> は所有権の根幹を成すスマートポインタなので,もしユーザーが use common::Box; してしまうと Box::new(...) が大混乱します。RectBox のように接頭辞を変えてしまうのが Rust らしい安全策です。

回転していない 2 つの箱を Cornell box に追加すると,原典 7.34 の Image 19 に相当する絵が得られます。

r208-cornell-blocks/src/lib.rs
rust
objects.add(Box::new(RectBox::new(
    Point3::new(130.0, 0.0, 65.0),
    Point3::new(295.0, 165.0, 230.0),
    white.clone(),
)));
objects.add(Box::new(RectBox::new(
    Point3::new(265.0, 0.0, 295.0),
    Point3::new(430.0, 330.0, 460.0),
    white,
)));

ここまではただの直方体の集合です。次に,これらを壁に対して斜めに置きます。

インスタンスの基本アイデア

Cornell box の定番構図では,箱は壁に対して 15° 程度傾いています。原典の指摘通り,レイトレーシングではジオメトリを動かさずに レイのほうを逆方向に変換するのが定石です:

「ピンクの箱の x 座標すべてに 2 を足す代わりに,箱はそこに置いたまま hit ルーチンの中で入射レイの x 起点から 2 を引く」

これは座標変換の標準的な扱いで,変換後のレイで「ローカル座標系」のオブジェクトに当ててヒットを取り,得られた交点や法線を逆変換して世界座標に戻します。利点は二つあります:

  • 同じジオメトリを複数の位置・向きでインスタンス化できる(メモリは 1 個分)
  • AABB は変換前のローカル AABB を変換するだけで作れる(BVH と組み合わせて速い)

Translate

平行移動が一番単純です。「世界座標で offset だけ動かす」ということは,逆に「ローカル座標系では入射レイの起点を -offset する」ということに相当します。

common/src/instance.rs
rust
pub struct Translate {
    pub object: Box<dyn Hittable>,
    pub offset: Vec3,
}

impl Hittable for Translate {
    fn hit(&self, r: &Ray, t_min: f64, t_max: f64) -> Option<HitRecord> {
        let moved = Ray::with_time(r.origin() - self.offset, r.direction(), r.time());
        let mut rec = self.object.hit(&moved, t_min, t_max)?;
        rec.p = rec.p + self.offset;
        let n = rec.normal;
        rec.set_face_normal(&moved, n);
        Some(rec)
    }

    fn bounding_box(&self, time0: f64, time1: f64) -> Option<Aabb> {
        let bbox = self.object.bounding_box(time0, time1)?;
        Some(Aabb::new(bbox.min() + self.offset, bbox.max() + self.offset))
    }
}

注目点:

  • レイの方向は変えず,起点だけを -offset する。方向ベクトルの平行移動は意味を持たないので変換不要です。
  • 当たった点 rec.p には逆に +offset を足し戻して世界座標に持ち上げる。法線は平行移動で変わらないのでそのまま。
  • AABB は両端をシフトするだけ。これにより Translate を BVH に乗せても問題なく動きます(2.3 章で Hittable::bounding_boxOption<Aabb> で導入しておいた効果がここで効いてきます)。

RotateY

Y 軸まわりに角度 θ 回転すると,xz 成分が混ざります:

x=cosθx+sinθz,z=sinθx+cosθz

逆回転(θ)は sin の符号が反転するだけなので,

x=cosθxsinθz,z=sinθx+cosθz

となります。RotateY::hit では「世界座標のレイ」を「ローカル座標のレイ」に逆回転で変換し,ヒット後の点・法線は順回転で世界座標に戻します。法線も方向ベクトルなので回転で変換が必要です(平行移動とはここが違います)。

common/src/instance.rs
rust
impl Hittable for RotateY {
    fn hit(&self, r: &Ray, t_min: f64, t_max: f64) -> Option<HitRecord> {
        let mut origin = r.origin();
        let mut direction = r.direction();

        // World -> local: rotate by -theta.
        origin[0]    =  self.cos_theta * r.origin()[0]    - self.sin_theta * r.origin()[2];
        origin[2]    =  self.sin_theta * r.origin()[0]    + self.cos_theta * r.origin()[2];
        direction[0] =  self.cos_theta * r.direction()[0] - self.sin_theta * r.direction()[2];
        direction[2] =  self.sin_theta * r.direction()[0] + self.cos_theta * r.direction()[2];

        let rotated = Ray::with_time(origin, direction, r.time());
        let mut rec = self.object.hit(&rotated, t_min, t_max)?;

        // Local -> world: rotate by +theta.
        let mut p = rec.p;
        let mut normal = rec.normal;
        p[0]      =  self.cos_theta * rec.p[0]      + self.sin_theta * rec.p[2];
        p[2]      = -self.sin_theta * rec.p[0]      + self.cos_theta * rec.p[2];
        normal[0] =  self.cos_theta * rec.normal[0] + self.sin_theta * rec.normal[2];
        normal[2] = -self.sin_theta * rec.normal[0] + self.cos_theta * rec.normal[2];

        rec.p = p;
        rec.set_face_normal(&rotated, normal);
        Some(rec)
    }

    fn bounding_box(&self, _t0: f64, _t1: f64) -> Option<Aabb> {
        self.bbox
    }
}

hit のローカル変換に cos/sin の同じ係数が 2 回ずつ現れているのに違和感を持つかもしれませんが,順方向と逆方向で sin の符号が逆になっているところが要点です。

回転後の AABB

回転前の AABB をそのまま使うとボックスが回転後の形状を覆いきれないので,8 つの隅すべてを回転して新しい min/max を取り直します。コンストラクタで一度だけ計算してキャッシュします。

common/src/instance.rs(RotateY コンストラクタ)
rust
pub fn new(object: Box<dyn Hittable>, angle_deg: f64) -> Self {
    let radians = degrees_to_radians(angle_deg);
    let sin_theta = radians.sin();
    let cos_theta = radians.cos();
    let inner_bbox = object.bounding_box(0.0, 1.0);

    let bbox = inner_bbox.map(|bbox| {
        let mut min = Point3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY);
        let mut max = Point3::new(-f64::INFINITY, -f64::INFINITY, -f64::INFINITY);
        for i in 0..2 {
            for j in 0..2 {
                for k in 0..2 {
                    let x = i as f64 * bbox.max().x() + (1 - i) as f64 * bbox.min().x();
                    let y = j as f64 * bbox.max().y() + (1 - j) as f64 * bbox.min().y();
                    let z = k as f64 * bbox.max().z() + (1 - k) as f64 * bbox.min().z();

                    let newx =  cos_theta * x + sin_theta * z;
                    let newz = -sin_theta * x + cos_theta * z;

                    let tester = Vec3::new(newx, y, newz);
                    for c in 0..3 {
                        min[c] = min[c].min(tester[c]);
                        max[c] = max[c].max(tester[c]);
                    }
                }
            }
        }
        Aabb::new(min, max)
    });

    RotateY { object, sin_theta, cos_theta, bbox }
}

C++ と Rust の違い

原典の C++ コードは bool hasbox フラグを別に持って「内部オブジェクトに有限な境界がない場合」を表現しています。Rust 版では Hittable::bounding_box がもともと Option<Aabb> を返すので,inner_bbox.map(...)Option を持ち上げるだけで等価な動作が得られます。フラグ管理が消えて素直になるのが嬉しいところです。

定番 Cornell box

以上を組み合わせると,原典の標準 Cornell box(Image 20)が組めます。Box<dyn Hittable> でラップしながら Translate(RotateY(RectBox)) を作る部分が,C++ の make_shared チェーンに対応します。

r208-cornell-standard/src/lib.rs
rust
// Tall block: 165 x 330 x 165, +15 deg, then translate to (265, 0, 295).
let box1: Box<dyn Hittable> = Box::new(RectBox::new(
    Point3::new(0.0, 0.0, 0.0),
    Point3::new(165.0, 330.0, 165.0),
    white.clone(),
));
let box1: Box<dyn Hittable> = Box::new(RotateY::new(box1, 15.0));
let box1: Box<dyn Hittable> = Box::new(Translate::new(box1, Vec3::new(265.0, 0.0, 295.0)));
objects.add(box1);

// Short block: 165 x 165 x 165, -18 deg, then translate to (130, 0, 65).
let box2: Box<dyn Hittable> = Box::new(RectBox::new(
    Point3::new(0.0, 0.0, 0.0),
    Point3::new(165.0, 165.0, 165.0),
    white,
));
let box2: Box<dyn Hittable> = Box::new(RotateY::new(box2, -18.0));
let box2: Box<dyn Hittable> = Box::new(Translate::new(box2, Vec3::new(130.0, 0.0, 65.0)));
objects.add(box2);

順序が大事で,「原点で回転 → 平行移動」 とするのが正しい流儀です。逆順(先に平行移動してから回転)にすると箱が原点まわりにぐるっと回ってしまい,意図した位置から外れます。同じことを 2.7 で見た Arc::new(...) の連鎖と比べると,こちらは型注釈 Box<dyn Hittable> を毎回書く必要があってやや冗長ですが,型がはっきり追えるという読者向けの利点もあります。

C++ と Rust の違い

C++ 原典は shared_ptr<hittable> box1 = ...; のあと 同じ変数 box1 に再代入してインスタンスを積み上げています:

cpp
shared_ptr<hittable> box1 = make_shared<box>(...);
box1 = make_shared<rotate_y>(box1, 15);
box1 = make_shared<translate>(box1, vec3(265,0,295));

Rust では let box1 = ...; let box1 = ...; という シャドーイングで同じ書き味を再現しました。mut を付けずに済み,各段階の box1 がそれぞれ独立した束縛である点が読み手の助けになります。

まとめ

  • 6 枚の軸並行矩形を束ねた RectBoxcommon/src/rect_box.rs に追加。Rust では Box の名前衝突を避けるため RectBox とした。
  • レイ側を逆変換する形で「インスタンス」を実装。common/src/instance.rsTranslate(起点を -offset)と RotateYx/z を逆回転)を追加。AABB はそれぞれ「両端シフト」「8 隅を回転して min/max 取り直し」で更新。
  • これらを使って Cornell box の定番構図 r208-cornell-standard と,比較用の回転なし版 r208-cornell-blocks をデモ化した。
  • 既存のパイプライン(ray_colorbackground 引数化,Material::emitted のデフォルト実装,BVH の Option<Aabb>)はすべてそのまま動く。

次章 2.9 では,箱の中に煙や霧を入れるための Volumes(一様密度の媒質) を扱います。ConstantMedium という新種の Hittable と,等方散乱(isotropic)マテリアルを導入することで,おなじみの「煙の入った箱」が作れるようになります。