blockの巣

Rustでコンパイル時レイトレーシング6

2022/03/31 00:22 公開
Compile Time Ray Tracing in Rust Ray Tracing Rust

前回記事
リポジトリ
記事の内容はコミットIDbcc80697fd56821fc0f2ef09a2f092284c9bf3aeのものです。

今回は複数オブジェクトでワールドを作りレイが交差するかを判定するところまでです。

レンダリング結果
レンダリング結果

今回の変更点

です。

また参考元のRay Tracing in One Weekendと大きく変わっている箇所もいくつかあります。

Sphere型を追加

今までは関数に中心座標と半径を渡して計算していましたが今回から球が複数登場するためSphere型を定義しています。
中心座標と半径を持っているだけのstructです。

#[derive(Clone, Copy)]
pub struct Sphere {
    pub center: Point3,
    pub radius: f64,
}

impl Sphere {
    pub const fn new(center: Point3, radius: f64) -> Self { /* 省略 */ }
    pub const fn hit(&self, r: &Ray, t_min: f64, t_max: f64) -> Option<HitRecord> { /* 省略 */ }
}

レイとオブジェクトの交差判定を保持するHitRecord型を追加

レイと球の交差した情報を保持するための型であるHitRecord型を追加しました。 交差した座標(p)、交差した箇所の法線(normal)、レイの始点との距離(t)、交差した箇所が球の表(外側)かどうか(front_face)を持っています。
ここはRay Tracing in One Weekendと大きな違いはありません。

pub struct HitRecord {
    pub p: Point3,
    pub normal: Vec3,
    pub t: f64,
    pub front_face: bool,
}

impl HitRecord {
    pub const fn set_face_normal(&mut self, r: &Ray, outward_normal: Vec3) {
        self.front_face = dot(&r.direction(), &outward_normal) < 0.0;
        self.normal = if self.front_face {
            outward_normal
        } else {
            -outward_normal
        };
    }
}

レイと交差判定を行える型の列挙Hittableを追加

Hittableの定義は以下のようになっており、現状ではSphereのみが要素として存在します。

#[derive(Clone, Copy)]
pub enum Hittable {
    Sphere(Sphere),
}

Ray Tracing in One Weekendを参考にするのであればHittableTraitとして実装するべきだと思うのですが、それでは実装できない理由があるため最後に書きます。

Hittableを複数保持するHittableListを追加

HittableListHittableを複数保持できるstructです。
Ray Tracing in One Weekendではvecを使用していますがconst fnの中では動的なメモリ確保ができないため配列を使用しています。
HittableListusize型のコンパイル時定数CAPACITYを受け取ります。これは配列のサイズで、この数までであればHittableを保持できます。add関数呼び出し時にCAPACITYを超える場合にはコンパイルエラーが発生します。
配列の要素として持っているのがHittableそのものではなくOption<Hittable>なのは空の要素を表現する必要があるからです。
hit関数では保持しているSome(Hittable)全てに対して交差判定を行っています。

pub struct HittableList<const CAPACITY: usize> {
    size: usize,
    objects: [Option<Hittable>; CAPACITY],
}

impl<const CAPACITY: usize> HittableList<CAPACITY> {
    pub const fn new() -> Self {
        Self {
            size: 0,
            objects: [None; CAPACITY],
        }
    }
}

impl<const CAPACITY: usize> HittableList<CAPACITY> {
    pub const fn clear(&mut self) {
        self.objects = [None; CAPACITY];
        self.size = 0;
    }

    pub const fn add(&mut self, hittable: Hittable) {
        assert!(self.size < CAPACITY);
        self.objects[self.size] = Some(hittable);
        self.size += 1;
    }

    pub const fn hit(&self, r: &Ray, t_min: f64, t_max: f64) -> Option<HitRecord> {
        let mut result: Option<HitRecord> = None;
        let mut closest_so_far = t_max;

        let mut index = 0;
        while index < self.size {
            let hit_record = match &self.objects[index] {
                Some(Hittable::Sphere(ref s)) => s.hit(&r, t_min, closest_so_far),
                _ => None,
            };

            if let Some(hit_record) = hit_record {
                closest_so_far = hit_record.t;
                result = Some(hit_record);
            }

            index += 1;
        }

        result
    }
}

レイとの交差判定を単一の球とではなくワールドと行うようにする

main.rsの変更点だけ抜き出して説明します。 まずray_color関数です。
球ではなくworld: &HittableList<N>を受け取りworldとの交差判定を行います。交差した場合は交差した箇所の法線を返し、交差しなかった場合は背景色を返すという点は変わりません。

const fn ray_color<const N: usize>(ray: &Ray, world: &HittableList<N>) -> Color {
    if let Some(rec) = world.hit(&ray, 0.0, std::f64::INFINITY) {
        return 0.5 * (rec.normal + Color::new(1.0, 1.0, 1.0));
    }
    let unit_direction = unit_vector(&ray.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)
}

次にworldの作成です。 HittableList<2>型のworldを作成しそこに球を2つ追加しています。
このコードだけであれば定数ジェネリクスの引数を使用している部分以外はHittableListの中身がvecであった場合と変わらないように見えると思います。

    let mut world: HittableList<2> = HittableList::<2>::new();
    world.add(Hittable::Sphere(Sphere::new(
        Vec3::new(0.0, 0.0, -1.0),
        0.5,
    )));
    world.add(Hittable::Sphere(Sphere::new(
        Vec3::new(0.0, -100.5, -1.0),
        100.0,
    )));

最後にray_colorの呼び出しの変更です。
第2引数に&worldを渡すようになりました。

pixel_colors[pixel_index] = ray_color(&r, &world);

Ray Tracing in One Weekendとの変更点

参考元のRay Tracing in One Weekend大きく違う部分がいくつかあります。

HittableListが保持している変数が(スマート)ポインタの動的配列ではない

参考元ではhittable_listが保持するobjectsvec<shared_ptr<hittable>>として実装されています。
これをRustで実装するのであればVec<Box<Hittable>>とかVec<Rc<Cell<Hittable>>>になるかと思います。しかしそのどちらもconst fnの中で使用することができないので[Option<Hittable>; CAPACITY]として実装しています。

HittableTraitではなくEnum

参考元ではhittableの定義は以下のようになっており純粋仮想関数のhitを持つクラス(interface)として実装されています。

class hittable {
    public:
        virtual bool hit(const ray& r, double t_min, double t_max, hit_record& rec) const = 0;
};

これをRustで実装するのであればHittableTraitとして以下のように実装するのが良いように思えます。

trait Hittable {
    fn hit(&self, r: &Ray, t_min: f64, t_max: f64) -> Option<HitRecord>;
}

しかしそうするとHittableListHittableの配列を持つには[Box<Hittable>; N]のように必要があります。
前述したようにconst fnの中で動的メモリ確保が行えずBoxを使用することはできないのでこのような実装はできませんでした。そのためHittableEnumとして実装しています。 また参考元ではhittable_listhittableを継承していますが、HittableHittableListを追加するとHittable, HittableListのサイズが未定となってしまいBoxを使用する必要が出てくるためHittableListHittableに含んでいません。

あとがき

今回はconst fnの縛りによって動的メモリ確保が行えない影響で普通に書くコードや参考元との差が大きくなったと思います。

次回はレンダリング後の画像にアンチエイリアシングをかけるところまでやります。
ついに乱数が必要になります。