Rustでコンパイル時レイトレーシング6
| 2022/03/31 00:22 | 公開 |
前回記事
リポジトリ
記事の内容はコミットIDbcc80697fd56821fc0f2ef09a2f092284c9bf3aeのものです。
今回は複数オブジェクトでワールドを作りレイが交差するかを判定するところまでです。
レンダリング結果

今回の変更点
- 球を表す
Sphere型を追加 - レイとオブジェクトが交差した情報を保持する
HitRecord型を追加 - レイと交差判定を行える型の列挙
Hittableを追加 Hittableを複数保持するHittableListを追加- レイとの交差判定を単一の球とではなくワールドと行うようにする
です。
また参考元の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を参考にするのであればHittableはTraitとして実装するべきだと思うのですが、それでは実装できない理由があるため最後に書きます。
Hittableを複数保持するHittableListを追加
HittableListはHittableを複数保持できるstructです。
Ray Tracing in One Weekendではvecを使用していますがconst fnの中では動的なメモリ確保ができないため配列を使用しています。
HittableListはusize型のコンパイル時定数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が保持するobjectsはvec<shared_ptr<hittable>>として実装されています。
これをRustで実装するのであればVec<Box<Hittable>>とかVec<Rc<Cell<Hittable>>>になるかと思います。しかしそのどちらもconst fnの中で使用することができないので[Option<Hittable>; CAPACITY]として実装しています。
HittableがTraitではなく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で実装するのであればHittableはTraitとして以下のように実装するのが良いように思えます。
trait Hittable {
fn hit(&self, r: &Ray, t_min: f64, t_max: f64) -> Option<HitRecord>;
}
しかしそうするとHittableListがHittableの配列を持つには[Box<Hittable>; N]のように必要があります。
前述したようにconst fnの中で動的メモリ確保が行えずBoxを使用することはできないのでこのような実装はできませんでした。そのためHittableはEnumとして実装しています。
また参考元ではhittable_listもhittableを継承していますが、HittableにHittableListを追加するとHittable, HittableListのサイズが未定となってしまいBoxを使用する必要が出てくるためHittableListはHittableに含んでいません。
あとがき
今回はconst fnの縛りによって動的メモリ確保が行えない影響で普通に書くコードや参考元との差が大きくなったと思います。
次回はレンダリング後の画像にアンチエイリアシングをかけるところまでやります。
ついに乱数が必要になります。