From 78ddaff5855bf8446adef9e18eb0d7b7ddcee52a Mon Sep 17 00:00:00 2001
From: lamp
Date: Sun, 5 Mar 2023 21:45:56 +0000
Subject: init

---
 src/hittable/aabb.rs               |  46 +++++++++
 src/hittable/bvh_node.rs           |  90 +++++++++++++++++
 src/hittable/constant_medium.rs    |  65 ++++++++++++
 src/hittable/hittable_box.rs       |  40 ++++++++
 src/hittable/hittable_list.rs      |  52 ++++++++++
 src/hittable/instance/mod.rs       |  10 ++
 src/hittable/instance/moving.rs    |  40 ++++++++
 src/hittable/instance/rotate_x.rs  |  83 +++++++++++++++
 src/hittable/instance/rotate_y.rs  |  83 +++++++++++++++
 src/hittable/instance/rotate_z.rs  |  83 +++++++++++++++
 src/hittable/instance/translate.rs |  27 +++++
 src/hittable/mod.rs                |  76 ++++++++++++++
 src/hittable/model.rs              | 200 +++++++++++++++++++++++++++++++++++++
 src/hittable/sphere.rs             |  61 +++++++++++
 src/hittable/triangle.rs           |  79 +++++++++++++++
 src/hittable/xy_rect.rs            |  48 +++++++++
 src/hittable/xz_rect.rs            |  48 +++++++++
 src/hittable/yz_rect.rs            |  48 +++++++++
 18 files changed, 1179 insertions(+)
 create mode 100644 src/hittable/aabb.rs
 create mode 100644 src/hittable/bvh_node.rs
 create mode 100644 src/hittable/constant_medium.rs
 create mode 100644 src/hittable/hittable_box.rs
 create mode 100644 src/hittable/hittable_list.rs
 create mode 100644 src/hittable/instance/mod.rs
 create mode 100644 src/hittable/instance/moving.rs
 create mode 100644 src/hittable/instance/rotate_x.rs
 create mode 100644 src/hittable/instance/rotate_y.rs
 create mode 100644 src/hittable/instance/rotate_z.rs
 create mode 100644 src/hittable/instance/translate.rs
 create mode 100644 src/hittable/mod.rs
 create mode 100644 src/hittable/model.rs
 create mode 100644 src/hittable/sphere.rs
 create mode 100644 src/hittable/triangle.rs
 create mode 100644 src/hittable/xy_rect.rs
 create mode 100644 src/hittable/xz_rect.rs
 create mode 100644 src/hittable/yz_rect.rs

(limited to 'src/hittable')

diff --git a/src/hittable/aabb.rs b/src/hittable/aabb.rs
new file mode 100644
index 0000000..5c5c9fa
--- /dev/null
+++ b/src/hittable/aabb.rs
@@ -0,0 +1,46 @@
+use crate::ray::Ray;
+use crate::vec3::Point3;
+
+#[derive(Clone)]
+pub struct AABB {
+    pub minimum: Point3,
+    pub maximum: Point3,
+}
+
+impl AABB {
+    pub fn hit(&self, ray: &Ray, t_min: f64, t_max: f64) -> bool {
+        let mut t_min = t_min;
+        let mut t_max = t_max;
+        for a in 0..3 {
+            let inv_d = 1.0 / ray.direction.get(a).unwrap();
+            let mut t0 = (self.minimum.get(a).unwrap() - ray.origin.get(a).unwrap()) * inv_d;
+            let mut t1 = (self.maximum.get(a).unwrap() - ray.origin.get(a).unwrap()) * inv_d;
+            if inv_d < 0.0 {
+                // TODO: destructuring assignments are unstable :(
+                //(t0, t1) = (t1, t0);
+                std::mem::swap(&mut t0, &mut t1);
+            }
+            t_min = if t0 > t_min { t0 } else { t_min };
+            t_max = if t1 < t_max { t1 } else { t_max };
+            if t_max <= t_min {
+                return false;
+            }
+        }
+        true
+    }
+
+    pub fn surrounding_box(&self, other: &AABB) -> AABB {
+        AABB {
+            minimum: Point3 {
+                x: self.minimum.x.min(other.minimum.x),
+                y: self.minimum.y.min(other.minimum.y),
+                z: self.minimum.z.min(other.minimum.z),
+            },
+            maximum: Point3 {
+                x: self.maximum.x.max(other.maximum.x),
+                y: self.maximum.y.max(other.maximum.y),
+                z: self.maximum.z.max(other.maximum.z),
+            },
+        }
+    }
+}
diff --git a/src/hittable/bvh_node.rs b/src/hittable/bvh_node.rs
new file mode 100644
index 0000000..d215bc3
--- /dev/null
+++ b/src/hittable/bvh_node.rs
@@ -0,0 +1,90 @@
+use std::{cmp, sync::Arc};
+
+use rand::seq::SliceRandom;
+
+use crate::hittable::{HitRecord, Hittable, AABB, hittable_list::HittableList};
+use crate::ray::Ray;
+
+pub struct BVHNode {
+    left: Arc<dyn Hittable>,
+    right: Arc<dyn Hittable>,
+    aabb: AABB,
+}
+
+#[derive(Clone, Copy)]
+enum Axis {
+    X,
+    Y,
+    Z,
+}
+
+impl BVHNode {
+    pub fn new(hittable_list: &HittableList, time_start: f64, time_end: f64) -> BVHNode {
+        Self::from_objects(&hittable_list.objects, 0, hittable_list.objects.len(), time_start, time_end)
+    }
+
+    fn from_objects(src_objects: &Vec<Arc<dyn Hittable>>, start: usize, end: usize, time_start: f64, time_end: f64) -> BVHNode {
+        let mut objects = src_objects.clone();
+        let comparator = [
+            |a: &Arc<dyn Hittable>, b: &Arc<dyn Hittable>| Self::box_compare(a.clone(), b.clone(), Axis::X),
+            |a: &Arc<dyn Hittable>, b: &Arc<dyn Hittable>| Self::box_compare(a.clone(), b.clone(), Axis::Y),
+            |a: &Arc<dyn Hittable>, b: &Arc<dyn Hittable>| Self::box_compare(a.clone(), b.clone(), Axis::Z),
+        ].choose(&mut rand::thread_rng()).unwrap();
+        let object_span = end - start;
+
+        let (left, right) = match object_span {
+            1 => (objects.get(start).unwrap().clone(), objects.get(start).unwrap().clone()),
+            2 => match comparator(objects.get(start).unwrap(), objects.get(start + 1).unwrap()) {
+                    cmp::Ordering::Less => (objects.get(start).unwrap().clone(), objects.get(start + 1).unwrap().clone()),
+                    _ => (objects.get(start + 1).unwrap().clone(), objects.get(start).unwrap().clone()),
+                }
+            _ => {
+                objects[start..end].sort_by(comparator);
+                let mid = start + object_span / 2;
+                (Arc::new(BVHNode::from_objects(&objects, start, mid, time_start, time_end)) as Arc<dyn Hittable>,
+                    Arc::new(BVHNode::from_objects(&objects, mid, end, time_start, time_end)) as Arc<dyn Hittable>)
+
+            }
+        };
+        let box_left = left.bounding_box(time_start, time_end).expect("No bounding box in bvh_node constructor!");
+        let box_right = right.bounding_box(time_start, time_end).expect("No bounding box in bvh_node constructor!");
+
+        BVHNode {
+            left,
+            right,
+            aabb: box_left.surrounding_box(&box_right),
+        }
+    }
+
+    fn box_compare (a: Arc<dyn Hittable>, b: Arc<dyn Hittable>, axis: Axis) -> cmp::Ordering {
+        let box_a = a.bounding_box(0.0, 0.0).expect("No bounding box in bvh_node constructor!");
+        let box_b = b.bounding_box(0.0, 0.0).expect("No bounding box in bvh_node constructor!");
+
+        // TODO: total_cmp is unstable :(
+        box_a.minimum.get(axis as usize).unwrap().partial_cmp(box_b.minimum.get(axis as usize).unwrap()).unwrap()
+    }
+}
+
+impl Hittable for BVHNode {
+    fn hit(&self, ray: &Ray, t_min: f64, t_max: f64) -> Option<HitRecord> {
+        if !self.aabb.hit(ray, t_min, t_max) {
+            return None
+        }
+        let hit_left = self.left.hit(ray, t_min, t_max);
+        let hit_right_threshold = if let Some(hit_record_left) = &hit_left {
+            hit_record_left.t
+        } else {
+            t_max
+        };
+        let hit_right = self.right.hit(ray, t_min, hit_right_threshold);
+        if let Some(_) = &hit_right {
+            hit_right
+        } else {
+            hit_left
+        }
+    }
+
+    fn bounding_box(&self, _: f64, _: f64) -> Option<AABB> {
+        Some(self.aabb.clone())
+    }
+}
diff --git a/src/hittable/constant_medium.rs b/src/hittable/constant_medium.rs
new file mode 100644
index 0000000..b047c23
--- /dev/null
+++ b/src/hittable/constant_medium.rs
@@ -0,0 +1,65 @@
+use std::sync::Arc;
+
+use crate::{hittable::{HitRecord, Hittable, AABB}, material::{Isotropic, Material}, ray::Ray, texture::Texture, vec3::Vec3};
+
+pub struct ConstantMedium {
+    boundary: Arc<dyn Hittable>,
+    phase_function: Arc<dyn Material>,
+    neg_inv_density: f64,
+}
+
+impl ConstantMedium {
+    pub fn new(boundary: Arc<dyn Hittable>, density: f64, texture: Arc<dyn Texture>) -> Self {
+        Self {
+            boundary,
+            phase_function: Arc::new(Isotropic::from_texture(texture)),
+            neg_inv_density: -1.0/density,
+        }
+    }
+}
+
+impl Hittable for ConstantMedium {
+    // TODO: this only support convex shapes.
+    fn hit(&self, ray: &Ray, t_min: f64, t_max: f64) -> Option<HitRecord> {
+        let mut record_1 = self.boundary.hit(ray, -f64::INFINITY, f64::INFINITY)?;
+        let mut record_2 = self.boundary.hit(ray, record_1.t + 0.0001, f64::INFINITY)?;
+
+        if record_1.t < t_min {
+            record_1.t = t_min;
+        }
+        if record_2.t > t_max {
+            record_2.t = t_max;
+        }
+
+        if record_1.t >= record_2.t {
+            return None;
+        }
+
+        if record_1.t < 0.0 {
+            record_1.t = 0.0;
+        }
+
+        let ray_length = ray.direction.length();
+        let distance_inside_boundary = (record_2.t - record_1.t) * ray_length;
+        let hit_distance = self.neg_inv_density * rand::random::<f64>().ln();
+
+        if hit_distance > distance_inside_boundary {
+            return None;
+        }
+
+        let t = record_1.t + hit_distance / ray_length;
+        Some(HitRecord {
+            p: ray.at(t),
+            t,
+            material: Some(self.phase_function.clone()),
+            normal: Vec3 { x: 1.0, y: 0.0, z: 0.0 }, // arbitrary
+            front_face: true, // arbitrary
+            u: 0.0, // arbitrary
+            v: 0.0, // arbitrary
+        })
+    }
+
+    fn bounding_box(&self, time_start: f64, time_end: f64) -> Option<AABB> {
+        self.boundary.bounding_box(time_start, time_end)
+    }
+}
diff --git a/src/hittable/hittable_box.rs b/src/hittable/hittable_box.rs
new file mode 100644
index 0000000..7b95cc7
--- /dev/null
+++ b/src/hittable/hittable_box.rs
@@ -0,0 +1,40 @@
+use std::sync::Arc;
+
+use crate::{hittable::{HitRecord, Hittable, AABB, hittable_list::HittableList, xy_rect::XYRect, xz_rect::XZRect, yz_rect::YZRect}, material::Material, ray::Ray, vec3::Point3};
+
+pub struct HittableBox {
+    min: Point3,
+    max: Point3,
+    sides: HittableList,
+}
+
+impl HittableBox {
+    pub fn new(min: Point3, max: Point3, material: Arc<dyn Material>) -> Self {
+        let mut sides = HittableList::new();
+
+        sides.add(Arc::new(XYRect { material: material.clone(), x0: min.x, x1: max.x, y0: min.y, y1: max.y, k: max.z }));
+        sides.add(Arc::new(XYRect { material: material.clone(), x0: min.x, x1: max.x, y0: min.y, y1: max.y, k: min.z }));
+
+        sides.add(Arc::new(XZRect { material: material.clone(), x0: min.x, x1: max.x, z0: min.z, z1: max.z, k: max.y }));
+        sides.add(Arc::new(XZRect { material: material.clone(), x0: min.x, x1: max.x, z0: min.z, z1: max.z, k: min.y }));
+
+        sides.add(Arc::new(YZRect { material: material.clone(), y0: min.y, y1: max.y, z0: min.z, z1: max.z, k: max.x }));
+        sides.add(Arc::new(YZRect { material: material.clone(), y0: min.y, y1: max.y, z0: min.z, z1: max.z, k: min.x }));
+
+        Self {
+            min,
+            max,
+            sides,
+        }
+    }
+}
+
+impl Hittable for HittableBox {
+    fn hit(&self, ray: &Ray, t_min: f64, t_max: f64) -> Option<HitRecord> {
+        self.sides.hit(ray, t_min, t_max)
+    }
+
+    fn bounding_box(&self, _: f64, _: f64) -> Option<AABB> {
+        Some(AABB { minimum: self.min.clone(), maximum: self.max.clone() })
+    }
+}
diff --git a/src/hittable/hittable_list.rs b/src/hittable/hittable_list.rs
new file mode 100644
index 0000000..735509c
--- /dev/null
+++ b/src/hittable/hittable_list.rs
@@ -0,0 +1,52 @@
+use std::sync::Arc;
+
+use crate::hittable::{HitRecord, Hittable, AABB};
+use crate::ray::Ray;
+
+pub struct HittableList {
+    pub objects: Vec<Arc<dyn Hittable>>,
+}
+
+impl HittableList {
+    pub fn new() -> HittableList {
+        HittableList {
+            objects: Vec::new(),
+        }
+    }
+
+    pub fn clear(&mut self) {
+        self.objects.clear();
+    }
+
+    pub fn add(&mut self, object: Arc<dyn Hittable>) {
+        self.objects.push(object);
+    }
+}
+
+impl Hittable for HittableList {
+    fn hit(&self, ray: &Ray, t_min: f64, t_max: f64) -> Option<HitRecord> {
+        let mut record = None;
+        let mut closest_so_far = t_max;
+
+        for object in &self.objects {
+            let temp_rec = object.hit(ray, t_min, closest_so_far);
+            if let Some(hit_record) = &temp_rec {
+                closest_so_far = hit_record.t;
+                record = temp_rec;
+            }
+        }
+        record
+    }
+
+    fn bounding_box(&self, time_start: f64, time_end: f64) -> Option<AABB> {
+        let mut output_box: Option<AABB> = None;
+        for object in &self.objects {
+            let temp_box = object.bounding_box(time_start, time_end)?;
+            output_box = match output_box {
+                Some(aabb) => Some(aabb.surrounding_box(&temp_box)),
+                None => Some(temp_box.clone()),
+            };
+        }
+        output_box
+    }
+}
diff --git a/src/hittable/instance/mod.rs b/src/hittable/instance/mod.rs
new file mode 100644
index 0000000..d40dcc5
--- /dev/null
+++ b/src/hittable/instance/mod.rs
@@ -0,0 +1,10 @@
+mod moving;
+pub use moving::Moving;
+mod rotate_y;
+pub use rotate_y::RotateY;
+mod rotate_x;
+pub use rotate_x::RotateX;
+mod rotate_z;
+pub use rotate_z::RotateZ;
+mod translate;
+pub use translate::Translate;
diff --git a/src/hittable/instance/moving.rs b/src/hittable/instance/moving.rs
new file mode 100644
index 0000000..418fdbb
--- /dev/null
+++ b/src/hittable/instance/moving.rs
@@ -0,0 +1,40 @@
+use std::sync::Arc;
+
+use crate::{ray::Ray, vec3::Vec3};
+use crate::hittable::{HitRecord, Hittable, AABB};
+
+pub struct Moving {
+    pub hittable: Arc<dyn Hittable>,
+    pub offset_start: Vec3,
+    pub offset_end: Vec3,
+    pub time_start: f64,
+    pub time_end: f64,
+}
+
+impl Moving {
+    fn offset_at(&self, time: f64) -> Vec3 {
+        &self.offset_start + ((time - self.time_start) / (self.time_end - self.time_start)) * (&self.offset_end - &self.offset_start)
+    }
+}
+
+impl Hittable for Moving {
+    fn hit(&self, ray: &Ray, t_min: f64, t_max: f64) -> Option<HitRecord> {
+        let moved_ray = Ray { origin: &ray.origin - &self.offset_at(ray.time), direction: ray.direction.clone(), time: ray.time };
+        let mut hit_record = self.hittable.hit(&moved_ray, t_min, t_max)?;
+        hit_record.p += self.offset_at(ray.time).clone();
+        let normal = hit_record.normal.clone();
+        hit_record.set_face_normal(&moved_ray, &normal);
+        Some(hit_record)
+    }
+
+    fn bounding_box(&self, time_start: f64, time_end: f64) -> Option<AABB> {
+        let output_box = self.hittable.bounding_box(time_start, time_end)?;
+        Some(AABB {
+            minimum: &output_box.minimum + &self.offset_at(time_start),
+            maximum: &output_box.maximum + &self.offset_at(time_start),
+        }.surrounding_box(&AABB {
+            minimum: &output_box.minimum + &self.offset_at(time_end),
+            maximum: &output_box.maximum + &self.offset_at(time_end),
+        }))
+    }
+}
diff --git a/src/hittable/instance/rotate_x.rs b/src/hittable/instance/rotate_x.rs
new file mode 100644
index 0000000..4ebc04d
--- /dev/null
+++ b/src/hittable/instance/rotate_x.rs
@@ -0,0 +1,83 @@
+use std::sync::Arc;
+
+use crate::{hittable::{HitRecord, Hittable, AABB}, ray::Ray, util::degrees_to_radians, vec3::{Point3, Vec3}};
+
+pub struct RotateX {
+    hittable: Arc<dyn Hittable>,
+    sin_theta: f64,
+    cos_theta: f64,
+    aabb: Option<AABB>,
+}
+
+impl RotateX {
+    pub fn new(hittable: Arc<dyn Hittable>, angle: f64) -> Self {
+        let radians = degrees_to_radians(angle);
+        let sin_theta = radians.sin();
+        let cos_theta = radians.cos();
+        match hittable.bounding_box(0.0, 1.0) { // TODO: passing in 0.0 and 1.0 for time seems suspicious.
+            None => Self { hittable, sin_theta, cos_theta, aabb: None },
+            Some(aabb) => {
+                let mut min = Point3 { x: f64::INFINITY, y: f64::INFINITY, z: f64::INFINITY };
+                let mut max = Point3 { x: -f64::INFINITY, y: -f64::INFINITY, z: -f64::INFINITY };
+                for i in 0..2 {
+                    for j in 0..2 {
+                        for k in 0..2 {
+                            let x = i as f64 * aabb.maximum.x + (1.0 - i as f64) * aabb.minimum.x;
+                            let y = j as f64 * aabb.maximum.y + (1.0 - j as f64) * aabb.minimum.y;
+                            let z = k as f64 * aabb.maximum.z + (1.0 - k as f64) * aabb.minimum.z;
+                            let new_y = cos_theta * y + sin_theta * y;
+                            let new_z = -sin_theta * y + cos_theta * z;
+
+                            let tester = Vec3 { y: new_y, x, z: new_z };
+                            for c in 0..3 {
+                                *min.get_mut(c).unwrap() = min.get(c).unwrap().min(*tester.get(c).unwrap());
+                                *max.get_mut(c).unwrap() = max.get(c).unwrap().max(*tester.get(c).unwrap());
+                            }
+                        }
+                    }
+                }
+                let aabb = AABB { minimum: min, maximum: max };
+
+                Self {
+                    hittable,
+                    sin_theta,
+                    cos_theta,
+                    aabb: Some(aabb),
+                }
+            }
+        }
+    }
+}
+
+impl Hittable for RotateX {
+    fn hit(&self, ray: &Ray, t_min: f64, t_max: f64) -> Option<HitRecord> {
+        let mut origin = ray.origin.clone();
+        let mut direction = ray.direction.clone();
+
+        origin.y = self.cos_theta * ray.origin.y - self.sin_theta * ray.origin.z;
+        origin.z = self.sin_theta * ray.origin.y + self.cos_theta * ray.origin.z;
+        
+        direction.y = self.cos_theta * ray.direction.y - self.sin_theta * ray.direction.z;
+        direction.z = self.sin_theta * ray.direction.y + self.cos_theta * ray.direction.z;
+
+        let rotated_ray = Ray { origin, direction, time: ray.time };
+        let mut hit_record = self.hittable.hit(&rotated_ray, t_min, t_max)?;
+
+        let mut p = hit_record.p.clone();
+        let mut normal = hit_record.normal.clone();
+
+        p.y = self.cos_theta * hit_record.p.y+ self.sin_theta * hit_record.p.z;
+        p.z = -self.sin_theta * hit_record.p.y + self.cos_theta * hit_record.p.z;
+
+        normal.y = self.cos_theta * hit_record.normal.y + self.sin_theta * hit_record.normal.z;
+        normal.z = -self.sin_theta * hit_record.normal.y + self.cos_theta * hit_record.normal.z;
+
+        hit_record.p = p;
+        hit_record.set_face_normal(&ray, &normal);
+        Some(hit_record)
+    }
+
+    fn bounding_box(&self, _: f64, _: f64) -> Option<AABB> {
+        self.aabb.clone()
+    }
+}
diff --git a/src/hittable/instance/rotate_y.rs b/src/hittable/instance/rotate_y.rs
new file mode 100644
index 0000000..8611616
--- /dev/null
+++ b/src/hittable/instance/rotate_y.rs
@@ -0,0 +1,83 @@
+use std::sync::Arc;
+
+use crate::{hittable::{HitRecord, Hittable, AABB}, ray::Ray, util::degrees_to_radians, vec3::{Point3, Vec3}};
+
+pub struct RotateY {
+    hittable: Arc<dyn Hittable>,
+    sin_theta: f64,
+    cos_theta: f64,
+    aabb: Option<AABB>,
+}
+
+impl RotateY {
+    pub fn new(hittable: Arc<dyn Hittable>, angle: f64) -> Self {
+        let radians = degrees_to_radians(angle);
+        let sin_theta = radians.sin();
+        let cos_theta = radians.cos();
+        match hittable.bounding_box(0.0, 1.0) { // TODO: passing in 0.0 and 1.0 for time seems suspicious.
+            None => Self { hittable, sin_theta, cos_theta, aabb: None },
+            Some(aabb) => {
+                let mut min = Point3 { x: f64::INFINITY, y: f64::INFINITY, z: f64::INFINITY };
+                let mut max = Point3 { x: -f64::INFINITY, y: -f64::INFINITY, z: -f64::INFINITY };
+                for i in 0..2 {
+                    for j in 0..2 {
+                        for k in 0..2 {
+                            let x = i as f64 * aabb.maximum.x + (1.0 - i as f64) * aabb.minimum.x;
+                            let y = j as f64 * aabb.maximum.y + (1.0 - j as f64) * aabb.minimum.y;
+                            let z = k as f64 * aabb.maximum.z + (1.0 - k as f64) * aabb.minimum.z;
+                            let new_x = cos_theta * x + sin_theta * z;
+                            let new_z = -sin_theta * x + cos_theta * z;
+
+                            let tester = Vec3 { x: new_x, y, z: new_z };
+                            for c in 0..3 {
+                                *min.get_mut(c).unwrap() = min.get(c).unwrap().min(*tester.get(c).unwrap());
+                                *max.get_mut(c).unwrap() = max.get(c).unwrap().max(*tester.get(c).unwrap());
+                            }
+                        }
+                    }
+                }
+                let aabb = AABB { minimum: min, maximum: max };
+
+                Self {
+                    hittable,
+                    sin_theta,
+                    cos_theta,
+                    aabb: Some(aabb),
+                }
+            }
+        }
+    }
+}
+
+impl Hittable for RotateY {
+    fn hit(&self, ray: &Ray, t_min: f64, t_max: f64) -> Option<HitRecord> {
+        let mut origin = ray.origin.clone();
+        let mut direction = ray.direction.clone();
+
+        origin.x = self.cos_theta * ray.origin.x - self.sin_theta * ray.origin.z;
+        origin.z = self.sin_theta * ray.origin.x + self.cos_theta * ray.origin.z;
+        
+        direction.x = self.cos_theta * ray.direction.x - self.sin_theta * ray.direction.z;
+        direction.z = self.sin_theta * ray.direction.x + self.cos_theta * ray.direction.z;
+
+        let rotated_ray = Ray { origin, direction, time: ray.time };
+        let mut hit_record = self.hittable.hit(&rotated_ray, t_min, t_max)?;
+
+        let mut p = hit_record.p.clone();
+        let mut normal = hit_record.normal.clone();
+
+        p.x = self.cos_theta * hit_record.p.x + self.sin_theta * hit_record.p.z;
+        p.z = -self.sin_theta * hit_record.p.x + self.cos_theta * hit_record.p.z;
+
+        normal.x = self.cos_theta * hit_record.normal.x + self.sin_theta * hit_record.normal.z;
+        normal.z = -self.sin_theta * hit_record.normal.x + self.cos_theta * hit_record.normal.z;
+
+        hit_record.p = p;
+        hit_record.set_face_normal(&ray, &normal);
+        Some(hit_record)
+    }
+
+    fn bounding_box(&self, _: f64, _: f64) -> Option<AABB> {
+        self.aabb.clone()
+    }
+}
diff --git a/src/hittable/instance/rotate_z.rs b/src/hittable/instance/rotate_z.rs
new file mode 100644
index 0000000..119baca
--- /dev/null
+++ b/src/hittable/instance/rotate_z.rs
@@ -0,0 +1,83 @@
+use std::sync::Arc;
+
+use crate::{hittable::{HitRecord, Hittable, AABB}, ray::Ray, util::degrees_to_radians, vec3::{Point3, Vec3}};
+
+pub struct RotateZ {
+    hittable: Arc<dyn Hittable>,
+    sin_theta: f64,
+    cos_theta: f64,
+    aabb: Option<AABB>,
+}
+
+impl RotateZ {
+    pub fn new(hittable: Arc<dyn Hittable>, angle: f64) -> Self {
+        let radians = degrees_to_radians(angle);
+        let sin_theta = radians.sin();
+        let cos_theta = radians.cos();
+        match hittable.bounding_box(0.0, 1.0) { // TODO: passing in 0.0 and 1.0 for time seems suspicious.
+            None => Self { hittable, sin_theta, cos_theta, aabb: None },
+            Some(aabb) => {
+                let mut min = Point3 { x: f64::INFINITY, y: f64::INFINITY, z: f64::INFINITY };
+                let mut max = Point3 { x: -f64::INFINITY, y: -f64::INFINITY, z: -f64::INFINITY };
+                for i in 0..2 {
+                    for j in 0..2 {
+                        for k in 0..2 {
+                            let x = i as f64 * aabb.maximum.x + (1.0 - i as f64) * aabb.minimum.x;
+                            let y = j as f64 * aabb.maximum.y + (1.0 - j as f64) * aabb.minimum.y;
+                            let z = k as f64 * aabb.maximum.z + (1.0 - k as f64) * aabb.minimum.z;
+                            let new_x = cos_theta * x + sin_theta * y;
+                            let new_y = -sin_theta * x + cos_theta * y;
+
+                            let tester = Vec3 { x: new_x, z, y: new_y };
+                            for c in 0..3 {
+                                *min.get_mut(c).unwrap() = min.get(c).unwrap().min(*tester.get(c).unwrap());
+                                *max.get_mut(c).unwrap() = max.get(c).unwrap().max(*tester.get(c).unwrap());
+                            }
+                        }
+                    }
+                }
+                let aabb = AABB { minimum: min, maximum: max };
+
+                Self {
+                    hittable,
+                    sin_theta,
+                    cos_theta,
+                    aabb: Some(aabb),
+                }
+            }
+        }
+    }
+}
+
+impl Hittable for RotateZ {
+    fn hit(&self, ray: &Ray, t_min: f64, t_max: f64) -> Option<HitRecord> {
+        let mut origin = ray.origin.clone();
+        let mut direction = ray.direction.clone();
+
+        origin.x = self.cos_theta * ray.origin.x - self.sin_theta * ray.origin.y;
+        origin.y = self.sin_theta * ray.origin.x + self.cos_theta * ray.origin.y;
+        
+        direction.x = self.cos_theta * ray.direction.x - self.sin_theta * ray.direction.y;
+        direction.y = self.sin_theta * ray.direction.x + self.cos_theta * ray.direction.y;
+
+        let rotated_ray = Ray { origin, direction, time: ray.time };
+        let mut hit_record = self.hittable.hit(&rotated_ray, t_min, t_max)?;
+
+        let mut p = hit_record.p.clone();
+        let mut normal = hit_record.normal.clone();
+
+        p.x = self.cos_theta * hit_record.p.x + self.sin_theta * hit_record.p.y;
+        p.y = -self.sin_theta * hit_record.p.x + self.cos_theta * hit_record.p.y;
+
+        normal.x = self.cos_theta * hit_record.normal.x + self.sin_theta * hit_record.normal.y;
+        normal.y = -self.sin_theta * hit_record.normal.x + self.cos_theta * hit_record.normal.y;
+
+        hit_record.p = p;
+        hit_record.set_face_normal(&ray, &normal);
+        Some(hit_record)
+    }
+
+    fn bounding_box(&self, _: f64, _: f64) -> Option<AABB> {
+        self.aabb.clone()
+    }
+}
diff --git a/src/hittable/instance/translate.rs b/src/hittable/instance/translate.rs
new file mode 100644
index 0000000..a9c8162
--- /dev/null
+++ b/src/hittable/instance/translate.rs
@@ -0,0 +1,27 @@
+use std::sync::Arc;
+
+use crate::{hittable::{HitRecord, Hittable, AABB}, ray::Ray, vec3::Vec3};
+
+pub struct Translate {
+    pub hittable: Arc<dyn Hittable>,
+    pub offset: Vec3,
+}
+
+impl Hittable for Translate {
+    fn hit(&self, ray: &Ray, t_min: f64, t_max: f64) -> Option<HitRecord> {
+        let moved_ray = Ray { origin: &ray.origin - &self.offset, direction: ray.direction.clone(), time: ray.time };
+        let mut hit_record = self.hittable.hit(&moved_ray, t_min, t_max)?;
+        hit_record.p += self.offset.clone();
+        let normal = hit_record.normal.clone();
+        hit_record.set_face_normal(&moved_ray, &normal);
+        Some(hit_record)
+    }
+
+    fn bounding_box(&self, time_start: f64, time_end: f64) -> Option<AABB> {
+        let output_box = self.hittable.bounding_box(time_start, time_end)?;
+        Some(AABB {
+            minimum: &output_box.minimum + &self.offset,
+            maximum: &output_box.maximum + &self.offset,
+        })
+    }
+}
diff --git a/src/hittable/mod.rs b/src/hittable/mod.rs
new file mode 100644
index 0000000..e888c71
--- /dev/null
+++ b/src/hittable/mod.rs
@@ -0,0 +1,76 @@
+pub mod instance;
+mod bvh_node;
+pub use bvh_node::BVHNode;
+mod constant_medium;
+pub use constant_medium::ConstantMedium;
+mod hittable_box;
+pub use hittable_box::HittableBox;
+mod hittable_list;
+pub use hittable_list::HittableList;
+mod xy_rect;
+pub use xy_rect::XYRect;
+mod xz_rect;
+pub use xz_rect::XZRect;
+mod yz_rect;
+pub use yz_rect::YZRect;
+mod sphere;
+pub use sphere::Sphere;
+mod triangle;
+pub use triangle::Triangle;
+mod model;
+pub use model::Model;
+mod aabb;
+
+use std::sync::Arc;
+
+use crate::ray::Ray;
+use crate::vec3::{Point3, Vec3};
+use crate::material::Material;
+use aabb::AABB;
+
+#[derive(Clone)]
+pub struct HitRecord {
+    pub p: Point3,
+    pub normal: Vec3,
+    pub material: Option<Arc<dyn Material>>,
+    pub t: f64,
+    pub u: f64,
+    pub v: f64,
+    pub front_face: bool,
+}
+
+impl HitRecord {
+    pub fn new() -> HitRecord {
+        HitRecord {
+            p: Point3 {
+                x: 0.0,
+                y: 0.0,
+                z: 0.0,
+            },
+            normal: Vec3 {
+                x: 0.0,
+                y: 0.0,
+                z: 0.0,
+            },
+            material: None,
+            t: 0.0,
+            u: 0.0,
+            v: 0.0,
+            front_face: false,
+        }
+    }
+
+    pub fn set_face_normal(&mut self, ray: &Ray, outward_normal: &Vec3) {
+        self.front_face = ray.direction.dot(&outward_normal) < 0.0;
+        self.normal = if self.front_face {
+            outward_normal.clone()
+        } else {
+            -outward_normal
+        };
+    }
+}
+
+pub trait Hittable: Send + Sync {
+    fn hit(&self, ray: &Ray, t_min: f64, t_max: f64) -> Option<HitRecord>;
+    fn bounding_box(&self, time_start: f64, time_end: f64) -> Option<AABB>;
+}
diff --git a/src/hittable/model.rs b/src/hittable/model.rs
new file mode 100644
index 0000000..1a652be
--- /dev/null
+++ b/src/hittable/model.rs
@@ -0,0 +1,200 @@
+use std::sync::Arc;
+use std::vec::Vec;
+
+use crate::{hittable::{HitRecord, Hittable, AABB, HittableList, Triangle}, material::Material, ray::Ray, vec3::{Point3, Vec3}};
+
+pub struct Model {
+    faces: HittableList,
+}
+
+impl Model {
+    fn parse_face_triplet(triplet: &str) -> Option<(isize, Option<isize>, Option<isize>)> {
+        let mut triplet_iter = triplet.split("/");
+        Some((triplet_iter.next()?.parse::<isize>().ok()?, triplet_iter.next().and_then(|val| val.parse::<isize>().ok()) , triplet_iter.next().and_then(|val| val.parse::<isize>().ok())))
+    }
+    pub fn from_obj(obj_data: &str, material: Arc<dyn Material>) -> Self {
+        let mut geometric_vertices: Vec<Vec3> = Vec::new();
+        let mut texture_vertices: Vec<(f64, f64)> = Vec::new();
+        let mut vertex_normals: Vec<Vec3> = Vec::new();
+        // no free-form objects, so no parameter-space vertices!
+        let mut faces: Vec<Arc<dyn Hittable>> = Vec::new();
+        for entry in obj_data.lines() {
+            if entry.starts_with("#") {
+                // comment
+                continue;
+            }
+            let mut entry_iter = entry.split(' ');
+            let operator = match entry_iter.next() {
+                None => continue,
+                Some(val) => val,
+            };
+            match operator {
+                "v" | "vn" => {
+                    let x = match entry_iter.next() {
+                        None => {
+                            eprintln!("Malformed {} entry in OBJ: Missing x!", operator);
+                            continue;
+                        },
+                        Some(val) => {
+                            match val.parse::<f64>() {
+                                Err(_) => {
+                                    eprintln!("Malformed {} entry in OBJ: Malformed f64 x!", operator);
+                                    continue;
+                                },
+                                Ok(val) => val,
+                            }
+                        }
+                    };
+                    let y = match entry_iter.next() {
+                        None => {
+                            eprintln!("Malformed {} entry in OBJ: Missing y!", operator);
+                            continue;
+                        },
+                        Some(val) => {
+                            match val.parse::<f64>() {
+                                Err(_) => {
+                                    eprintln!("Malformed {} entry in OBJ: Malformed f64 y!", operator);
+                                    continue;
+                                },
+                                Ok(val) => val,
+                            }
+                        }
+                    };
+                    let z = match entry_iter.next() {
+                        None => {
+                            eprintln!("Malformed {} entry in OBJ: Missing z!", operator);
+                            continue;
+                        },
+                        Some(val) => {
+                            match val.parse::<f64>() {
+                                Err(_) => {
+                                    eprintln!("Malformed {} entry in OBJ: Malformed f64 z!", operator);
+                                    continue;
+                                },
+                                Ok(val) => val,
+                            }
+                        }
+                    };
+                    // who cares about w
+                    match operator {
+                        "v" => geometric_vertices.push(Vec3 {x, y, z}),
+                        "vn" => vertex_normals.push(Vec3 {x, y, z}),
+                        _ => panic!(),
+                    }
+                },
+                "vt" => {
+                    let u = match entry_iter.next() {
+                        None => {
+                            eprintln!("Malformed vt entry in OBJ: Missing u!");
+                            continue;
+                        },
+                        Some(val) => {
+                            match val.parse::<f64>() {
+                                Err(_) => {
+                                    eprintln!("Malformed vt entry in OBJ: Malformed f64 u!");
+                                    continue;
+                                },
+                                Ok(val) => val,
+                            }
+                        }
+                    };
+                    let v = match entry_iter.next() {
+                        None => {
+                            eprintln!("Malformed vt entry in OBJ: Missing v!");
+                            continue;
+                        },
+                        Some(val) => {
+                            match val.parse::<f64>() {
+                                Err(_) => {
+                                    eprintln!("Malformed v entry in OBJ: Malformed f64 v!");
+                                    continue;
+                                },
+                                Ok(val) => val,
+                            }
+                        }
+                    };
+                    // who cares about w
+                    texture_vertices.push((u, v));
+                },
+                "f" => {
+                    let mut triplets : Vec<(isize, Option<isize>, Option<isize>)> = Vec::new();
+                    for triplet in entry_iter {
+                        match Self::parse_face_triplet(triplet) {
+                            None => {
+                                eprintln!("Encountered malformed triplet in f operator!");
+                            },
+                            Some(val) => {
+                                triplets.push(val);
+                            }
+                        };
+                    }
+                    // only support faces with *exactly* three vertices. yeah, i know.
+                    if triplets.len() != 3 {
+                        eprintln!("Encountered face with unsupported vertex count!");
+                        continue;
+                    }
+                    let mut v0_index = triplets.get(0).unwrap().0;
+                    if v0_index < 0 {
+                        v0_index = geometric_vertices.len() as isize + v0_index;
+                    } else {
+                        v0_index = v0_index - 1;
+                    }
+                    let mut v1_index = triplets.get(1).unwrap().0;
+                    if v1_index < 0 {
+                        v1_index = geometric_vertices.len() as isize + v1_index;
+                    } else {
+                        v1_index = v1_index - 1;
+                    }
+                    let mut v2_index = triplets.get(2).unwrap().0;
+                    if v2_index < 0 {
+                        v2_index = geometric_vertices.len() as isize + v2_index;
+                    } else {
+                        v2_index = v2_index - 1;
+                    }
+                    let mut triangle = Triangle {
+                        v0: geometric_vertices.get(v0_index as usize).unwrap().clone(),
+                        v1: geometric_vertices.get(v1_index as usize).unwrap().clone(),
+                        v2: geometric_vertices.get(v2_index as usize).unwrap().clone(),
+                        material: material.clone(),
+                        custom_normal: None,
+                    };
+                    if let Some(vn0) = triplets.get(0).unwrap().2 {
+                        if let Some(vn1) = triplets.get(1).unwrap().2 {
+                            if let Some(vn2) = triplets.get(2).unwrap().2 {
+                                if vn0 != vn1 || vn1 != vn2 {
+                                    eprintln!("Unsupported geometry in OBJ file: Multiple normals for face!");
+                                    continue
+                                }
+                                let mut vn0 = vn0;
+                                if vn0 < 0 {
+                                    vn0 = vertex_normals.len() as isize + v0_index;
+                                } else {
+                                    vn0 = vn0 - 1;
+                                }
+                                triangle.custom_normal = Some(vertex_normals.get(vn0 as usize).unwrap().unit_vector());
+                            }
+                        }
+                    }
+                    faces.push(Arc::new(triangle));
+                },
+                _ => {
+                    eprintln!("Ignoring unknown operator {} in OBJ!", operator);
+                    continue;
+                },
+            }
+        }
+        Self {
+            faces: HittableList { objects: faces },
+        }
+    }
+}
+
+impl Hittable for Model {
+    fn hit(&self, ray: &Ray, t_min: f64, t_max: f64) -> Option<HitRecord> {
+        self.faces.hit(ray, t_min, t_max)
+    }
+
+    fn bounding_box(&self, time_start: f64, time_end: f64) -> Option<AABB> {
+        self.faces.bounding_box(time_start, time_end)
+    }
+}
diff --git a/src/hittable/sphere.rs b/src/hittable/sphere.rs
new file mode 100644
index 0000000..783b788
--- /dev/null
+++ b/src/hittable/sphere.rs
@@ -0,0 +1,61 @@
+use std::sync::Arc;
+use std::f64::consts;
+
+use crate::{hittable::{HitRecord, Hittable, AABB}, material::Material, vec3::Vec3};
+use crate::ray::Ray;
+use crate::vec3::Point3;
+
+pub struct Sphere {
+    pub center: Point3,
+    pub radius: f64,
+    pub material: Arc<dyn Material>,
+}
+
+impl Sphere {
+    pub fn get_sphere_uv(p: &Point3, u: &mut f64, v: &mut f64) {
+        let theta = (-p.y).acos();
+        let phi = -p.z.atan2(p.x) + consts::PI;
+
+        *u = phi / (2.0 * consts::PI);
+        *v = theta / consts::PI;
+    }
+}
+
+impl Hittable for Sphere {
+    fn hit(&self, ray: &Ray, t_min: f64, t_max: f64) -> Option<HitRecord> {
+        let oc = &ray.origin - &self.center;
+        let a = ray.direction.length_squared();
+        let half_b = oc.dot(&ray.direction);
+        let c = oc.length_squared() - self.radius * self.radius;
+        let discriminant = half_b * half_b - a * c;
+        if discriminant < 0.0 {
+            return None;
+        }
+
+        let sqrtd = discriminant.sqrt();
+        // Find the nearest root that lies within acceptable range
+        let mut root = (-half_b - sqrtd) / a;
+        if root < t_min || t_max < root {
+            root = (-half_b + sqrtd) / a;
+            if root < t_min || t_max < root {
+                return None;
+            }
+        }
+
+        let mut hit_record = HitRecord::new();
+        hit_record.t = root;
+        hit_record.p = ray.at(hit_record.t);
+        let outward_normal = (&hit_record.p - &self.center) / self.radius;
+        hit_record.set_face_normal(ray, &outward_normal);
+        Self::get_sphere_uv(&outward_normal, &mut hit_record.u, &mut hit_record.v);
+        hit_record.material = Some(self.material.clone());
+        Some(hit_record)
+    }
+
+    fn bounding_box(&self, _: f64, _: f64) -> Option<AABB> {
+        Some(AABB {
+            minimum: &self.center - Vec3 { x: self.radius, y: self.radius, z: self.radius },
+            maximum: &self.center + Vec3 { x: self.radius, y: self.radius, z: self.radius },
+        })
+    }
+}
diff --git a/src/hittable/triangle.rs b/src/hittable/triangle.rs
new file mode 100644
index 0000000..0fe5fc4
--- /dev/null
+++ b/src/hittable/triangle.rs
@@ -0,0 +1,79 @@
+use std::sync::Arc;
+
+use crate::{hittable::{HitRecord, Hittable, AABB}, material::Material, ray::Ray, vec3::{Point3, Vec3}};
+
+pub struct Triangle {
+    pub v0: Point3,
+    pub v1: Point3,
+    pub v2: Point3,
+    pub material: Arc<dyn Material>,
+    pub custom_normal: Option<Vec3>,
+}
+
+impl Triangle {
+    fn has_vertex_at_infinity(&self) -> bool {
+        self.v0.has_infinite_member() || self.v1.has_infinite_member() || self.v2.has_infinite_member()
+    }
+}
+
+impl Hittable for Triangle {
+    // https://en.wikipedia.org/wiki/M%C3%B6ller%E2%80%93Trumbore_intersection_algorithm
+    fn hit(&self, ray: &Ray, t_min: f64, t_max: f64) -> Option<HitRecord> {
+        let epsilon: f64 = 0.0000001;
+        let edge1 = &self.v1 - &self.v0;
+        let edge2 = &self.v2 - &self.v0;
+        let h = ray.direction.cross(&edge2);
+        let a = edge1.dot(&h);
+
+        if a > -epsilon && a < epsilon {
+            return None; // This ray is parallel to the triangle.
+        }
+        let f = 1.0 / a;
+        let s = &ray.origin - &self.v0;
+        let u = f * s.dot(&h);
+        if u < 0.0 || u > 1.0 {
+            return None;
+        }
+        let q = s.cross(&edge1);
+        let v = f * ray.direction.dot(&q);
+        if v < 0.0 || u + v > 1.0 {
+            return None;
+        }
+        // At this point, we can compute the point of intersection.
+        let t = f * edge2.dot(&q);
+        if t < t_min || t > t_max {
+            return None;
+        }
+        let mut hit_record = HitRecord::new();
+        hit_record.u = u;
+        hit_record.v = v;
+        hit_record.t = t;
+        hit_record.p = ray.at(t);
+        // TODO: i don't love this, but it allows for custom surface normals from OBJ data.
+        if let Some(normal) = &self.custom_normal {
+            hit_record.set_face_normal(ray, &normal);
+        } else {
+            let outward_normal = edge2.cross(&edge1).unit_vector();
+            hit_record.set_face_normal(ray, &outward_normal);
+        }
+        hit_record.material = Some(self.material.clone());
+        Some(hit_record)
+    }
+    fn bounding_box(&self, _: f64, _: f64) -> Option<AABB> {
+        match self.has_vertex_at_infinity() {
+            true => None,
+            false => Some(AABB {
+                minimum: Point3 {
+                    x: self.v0.x.min(self.v1.x).min(self.v2.x) - 0.0001,
+                    y: self.v0.y.min(self.v1.y).min(self.v2.y) - 0.0001,
+                    z: self.v0.z.min(self.v1.z).min(self.v2.z) - 0.0001,
+                },
+                maximum: Point3 {
+                    x: self.v0.x.max(self.v1.x).max(self.v2.x) + 0.0001,
+                    y: self.v0.y.max(self.v1.y).max(self.v2.y) + 0.0001,
+                    z: self.v0.z.max(self.v1.z).max(self.v2.z) + 0.0001,                    
+                },
+            })
+        }
+    }
+}
diff --git a/src/hittable/xy_rect.rs b/src/hittable/xy_rect.rs
new file mode 100644
index 0000000..8421bae
--- /dev/null
+++ b/src/hittable/xy_rect.rs
@@ -0,0 +1,48 @@
+use std::sync::Arc;
+
+use crate::{hittable::{HitRecord, Hittable, AABB}, material::Material, ray::Ray, vec3::{Point3, Vec3}};
+
+pub struct XYRect {
+    pub material: Arc<dyn Material>,
+    pub x0: f64,
+    pub x1: f64,
+    pub y0: f64,
+    pub y1: f64,
+    pub k: f64,
+}
+
+impl XYRect {
+    fn has_infinite_bounds(&self) -> bool {
+        self.x0.is_infinite() || self.x1.is_infinite() || self.y0.is_infinite() || self.y1.is_infinite()
+    }
+}
+
+impl Hittable for XYRect {
+    fn hit(&self, ray: &Ray, t_min: f64, t_max: f64) -> Option<HitRecord> {
+        let t = (self.k - ray.origin.z) / ray.direction.z;
+        if t < t_min || t > t_max {
+            return None;
+        }
+        let x = ray.origin.x + t * ray.direction.x;
+        let y = ray.origin.y + t * ray.direction.y;
+        if x < self.x0 || x > self.x1 || y < self.y0 || y > self.y1 {
+            return None;
+        }
+        let mut hit_record = HitRecord::new();
+        hit_record.u = (x - self.x0) / (self.x1 - self.x0);
+        hit_record.v = (y - self.y0) / (self.y1 - self.y0);
+        hit_record.t = t;
+        let outward_normal = Vec3 { x: 0.0, y: 0.0, z: 1.0 };
+        hit_record.set_face_normal(ray, &outward_normal);
+        hit_record.material = Some(self.material.clone());
+        hit_record.p = ray.at(t);
+        Some(hit_record)
+    }
+
+    fn bounding_box(&self, _: f64, _: f64) -> Option<AABB> {
+        match self.has_infinite_bounds() {
+            true => None,
+            false => Some(AABB { minimum: Point3 { x: self.x0, y: self.y0, z: self.k - 0.0001 }, maximum: Point3 { x: self.x1, y: self.y1, z: self.k + 0.0001 } }),
+        }
+    }
+}
diff --git a/src/hittable/xz_rect.rs b/src/hittable/xz_rect.rs
new file mode 100644
index 0000000..4761f36
--- /dev/null
+++ b/src/hittable/xz_rect.rs
@@ -0,0 +1,48 @@
+use std::sync::Arc;
+
+use crate::{hittable::{HitRecord, Hittable, AABB}, material::Material, ray::Ray, vec3::{Point3, Vec3}};
+
+pub struct XZRect {
+    pub material: Arc<dyn Material>,
+    pub x0: f64,
+    pub x1: f64,
+    pub z0: f64,
+    pub z1: f64,
+    pub k: f64,
+}
+
+impl XZRect {
+    fn has_infinite_bounds(&self) -> bool {
+        self.x0.is_infinite() || self.x1.is_infinite() || self.z0.is_infinite() || self.z1.is_infinite()
+    }
+}
+
+impl Hittable for XZRect {
+    fn hit(&self, ray: &Ray, t_min: f64, t_max: f64) -> Option<HitRecord> {
+        let t = (self.k - ray.origin.y) / ray.direction.y;
+        if t < t_min || t > t_max {
+            return None;
+        }
+        let x = ray.origin.x + t * ray.direction.x;
+        let z = ray.origin.z + t * ray.direction.z;
+        if x < self.x0 || x > self.x1 || z < self.z0 || z > self.z1 {
+            return None;
+        }
+        let mut hit_record = HitRecord::new();
+        hit_record.u = (x - self.x0) / (self.x1 - self.x0);
+        hit_record.v = (z - self.z0) / (self.z1 - self.z0);
+        hit_record.t = t;
+        let outward_normal = Vec3 { x: 0.0, y: 1.0, z: 0.0 };
+        hit_record.set_face_normal(ray, &outward_normal);
+        hit_record.material = Some(self.material.clone());
+        hit_record.p = ray.at(t);
+        Some(hit_record)
+    }
+
+    fn bounding_box(&self, _: f64, _: f64) -> Option<AABB> {
+        match self.has_infinite_bounds() {
+            true => None,
+            false => Some(AABB { minimum: Point3 { x: self.x0, y: self.k - 0.0001, z: self.z0 }, maximum: Point3 { x: self.x1, y: self.k + 0.0001, z: self.z1 } }),
+        }
+    }
+}
diff --git a/src/hittable/yz_rect.rs b/src/hittable/yz_rect.rs
new file mode 100644
index 0000000..dd90bdb
--- /dev/null
+++ b/src/hittable/yz_rect.rs
@@ -0,0 +1,48 @@
+use std::sync::Arc;
+
+use crate::{hittable::{HitRecord, Hittable, AABB}, material::Material, ray::Ray, vec3::{Point3, Vec3}};
+
+pub struct YZRect {
+    pub material: Arc<dyn Material>,
+    pub y0: f64,
+    pub y1: f64,
+    pub z0: f64,
+    pub z1: f64,
+    pub k: f64,
+}
+
+impl YZRect {
+    fn has_infinite_bounds(&self) -> bool {
+        self.y0.is_infinite() || self.y1.is_infinite() || self.z0.is_infinite() || self.z1.is_infinite()
+    }
+}
+
+impl Hittable for YZRect {
+    fn hit(&self, ray: &Ray, t_min: f64, t_max: f64) -> Option<HitRecord> {
+        let t = (self.k - ray.origin.x) / ray.direction.x;
+        if t < t_min || t > t_max {
+            return None;
+        }
+        let y = ray.origin.y + t * ray.direction.y;
+        let z = ray.origin.z + t * ray.direction.z;
+        if y < self.y0 || y > self.y1 || z < self.z0 || z > self.z1 {
+            return None;
+        }
+        let mut hit_record = HitRecord::new();
+        hit_record.u = (y - self.y0) / (self.y1 - self.y0);
+        hit_record.v = (z - self.z0) / (self.z1 - self.z0);
+        hit_record.t = t;
+        let outward_normal = Vec3 { x: 1.0, y: 0.0, z: 0.0 };
+        hit_record.set_face_normal(ray, &outward_normal);
+        hit_record.material = Some(self.material.clone());
+        hit_record.p = ray.at(t);
+        Some(hit_record)
+    }
+
+    fn bounding_box(&self, _: f64, _: f64) -> Option<AABB> {
+        match self.has_infinite_bounds() {
+            true => None,
+            false => Some(AABB { minimum: Point3 { x: self.k - 0.0001, y: self.y0, z: self.z0 }, maximum: Point3 { x: self.k + 0.0001, y: self.y1, z: self.z1 } }),
+        }
+    }
+}
-- 
cgit v1.2.3