Planes

Chapter 9

Shape trait

Plane の導入にあたって、今まで Sphere を使用していた場所で Plane や他の形状も使えるようにする。そのため trait Shape を用意して各形状で実装する。

pub trait Shape: Debug {
}

以降、Sphere を扱っていた場所を Shape trait object を使うように変更する。なお、trait object なので、コンパイル時に object size が決まらない。というわけで World のように Shape を所有する個所では直接所有する代わりに Box<dyn Shape> を所有する。

pub struct World {
    /// ライト
    lights: Vec<Light>,
    /// オブジェクト
    shapes: Vec<Box<dyn Shape>>,
}

また、Shape やその参照を所有する struct が Debug print できるように Shape は Debug trait を継承するようにした。そのため、Shape trait を実装する struct は必ず Debug trait も実装しなくてはならなくなったが、他に良いやり方がわからなかったのでとりあえずはこのようにしておく。

Shape trait の interface

基本的には本で書かれていた interface をそのまま定義するだけで問題ない。

pub trait Shape: Debug {
    fn transform(&self) -> &Transform;
    fn transform_mut(&mut self) -> &mut Transform;

    fn material(&self) -> &Material;
    fn material_mut(&mut self) -> &mut Material;

    fn intersect(&self, r: &Ray) -> Vec<Intersection> {
        let local_ray = self.transform().inv() * r;
        self.local_intersect(&local_ray)
    }
    fn local_intersect(&self, r: &Ray) -> Vec<Intersection>;

    fn normal_at(&self, p: &Point3D) -> Vector3D {
        let p_in_local = self.transform().inv() * p;
        let n = self.local_normal_at(&p_in_local);

        self.transform().apply_to_normal(&n)
    }
    fn local_normal_at(&self, p: &Point3D) -> Vector3D;
}

ここで intersect() と normal_at() はそれぞれ World-Local 変換をしてから local_intersect() と local_normal_at() を呼ぶという処理で、全ての Shape に共通していることから Shape で default の実装をする。各 Shape 固有の実装は local_*() の側で行う。

なので直接呼ばない local_*() は本来公開する必要はない。というか可能なら公開したくないのだけれど、trait の 一部メソッドのみを非公開にすることが可能なのか良くわからず現状は全て公開になってしまっている。

test の変更

Sphere から Shape への変更に伴い、一部テストを変更する必要がでてきた。

Intersection は hit した Shape の参照をメンバに持つが、それを確認するテストでは Shape 間の同一性を調べる必要が出でくる。以前はPartialEq を実装した Sphere のみであったため、単純な == による比較が使えたが、trait object となると同じ手法は使えない。

代わりにアドレスによる比較を行う。要は

        assert!(std::ptr::eq(&s as &dyn Shape, i.object));

のようにすれば万事 ok ... のはずだったのだけれど、うまくいかないケースがあった。アドレスは一致しているにもかかわらず、std::ptr::eq() が false を返すことがある。

どうも std::ptr::eq() は単純なアドレス比較以上のことを行っているみたいで、false を返す原因は使用する codegen Unit が異なる場合には同一アドレスの trait object でも vtable が異なる可能性があるためらしい。
std::ptr::eq() は trait object の fat pointer に対しては vtable のアドレス比較まで行うらしく、たとえ同一オブジェクトであったとしても、異なる module 内で trait object にした場合には std::ptr::eq() で比較すると false になり得る。

今回は vtable のアドレスの違いを気にする必要はないため fat pointer を regular pointer にキャストしてから比較することで、この問題を回避した。

        assert!(std::ptr::eq(
            xs[0].object as *const _ as *const (),
            &s as &dyn Shape as *const _ as *const (),
        ));

ところで、object のアドレスは一致するけれど vtable のアドレスが違うことを識別したいケースというのはどのくらいあるものなのだろう。ちょっと想像がつかない。

Plane

こちらが本題になるはずなのだけれど、特に書くことがない。

Plane は y 軸正方向を向いた xz 平面なので、local_intersect() では Ray の y 成分のみを考慮すれば良い。direction が平面と平行(direction の y 成分がほぼ 0)な場合は hit しない。

f:id:mtXTJocj:20201227125024p:plain

結果

f:id:mtXTJocj:20201227130930p:plain

前の見た目はほとんど変わっていないけれど、背景の壁が Sphere から Plane になっている。