aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorlamp2023-03-05 21:45:56 +0000
committerlamp2023-03-05 21:45:56 +0000
commit78ddaff5855bf8446adef9e18eb0d7b7ddcee52a (patch)
tree0d0e93cfa28751a2f96518eeb231cf715958e1fa /src
init
Diffstat (limited to 'src')
-rw-r--r--src/camera.rs58
-rw-r--r--src/hittable/aabb.rs46
-rw-r--r--src/hittable/bvh_node.rs90
-rw-r--r--src/hittable/constant_medium.rs65
-rw-r--r--src/hittable/hittable_box.rs40
-rw-r--r--src/hittable/hittable_list.rs52
-rw-r--r--src/hittable/instance/mod.rs10
-rw-r--r--src/hittable/instance/moving.rs40
-rw-r--r--src/hittable/instance/rotate_x.rs83
-rw-r--r--src/hittable/instance/rotate_y.rs83
-rw-r--r--src/hittable/instance/rotate_z.rs83
-rw-r--r--src/hittable/instance/translate.rs27
-rw-r--r--src/hittable/mod.rs76
-rw-r--r--src/hittable/model.rs200
-rw-r--r--src/hittable/sphere.rs61
-rw-r--r--src/hittable/triangle.rs79
-rw-r--r--src/hittable/xy_rect.rs48
-rw-r--r--src/hittable/xz_rect.rs48
-rw-r--r--src/hittable/yz_rect.rs48
-rw-r--r--src/image.rs77
-rw-r--r--src/main.rs115
-rw-r--r--src/material/dielectric.rs39
-rw-r--r--src/material/diffuse_light.rs29
-rw-r--r--src/material/isotropic.rs33
-rw-r--r--src/material/lambertian.rs34
-rw-r--r--src/material/metal.rs18
-rw-r--r--src/material/mod.rs21
-rw-r--r--src/ray.rs21
-rw-r--r--src/scenes.rs252
-rw-r--r--src/texture/checker_texture.rs29
-rw-r--r--src/texture/image_texture.rs63
-rw-r--r--src/texture/mod.rs15
-rw-r--r--src/texture/noise_texture.rs22
-rw-r--r--src/texture/perlin.rs98
-rw-r--r--src/texture/solid_color.rs20
-rw-r--r--src/util.rs3
-rw-r--r--src/vec3.rs201
37 files changed, 2327 insertions, 0 deletions
diff --git a/src/camera.rs b/src/camera.rs
new file mode 100644
index 0000000..55bf387
--- /dev/null
+++ b/src/camera.rs
@@ -0,0 +1,58 @@
+use crate::ray::Ray;
+use crate::vec3::{Point3, Vec3};
+use crate::util::degrees_to_radians;
+
+pub struct Camera {
+ origin: Point3,
+ lower_left_corner: Point3,
+ horizontal: Vec3,
+ vertical: Vec3,
+ w: Vec3,
+ u: Vec3,
+ v: Vec3,
+ lens_radius: f64,
+ time_start: f64,
+ time_end: f64,
+}
+
+impl Camera {
+ pub fn new(lookfrom: Point3, lookat: Point3, vup: Vec3, vfov: f64, aspect_ratio: f64, aperture: f64, focus_dist: f64, time_start: f64, time_end: f64) -> Camera {
+ let theta = degrees_to_radians(vfov);
+ let h = (theta / 2.0).tan();
+ let viewport_height = 2.0 * h;
+ let viewport_width = aspect_ratio * viewport_height;
+
+ let w = (&lookfrom - &lookat).unit_vector();
+ let u = vup.cross(&w).unit_vector();
+ let v = w.cross(&u);
+
+ let origin = lookfrom;
+ let horizontal = focus_dist * viewport_width * &u;
+ let vertical = focus_dist * viewport_height * &v;
+ Camera {
+ lower_left_corner: &origin
+ - &horizontal / 2.0
+ - &vertical / 2.0
+ - focus_dist * &w,
+ origin,
+ horizontal,
+ vertical,
+ w,
+ u,
+ v,
+ lens_radius: aperture / 2.0,
+ time_start,
+ time_end,
+ }
+ }
+
+ pub fn get_ray(&self, s: f64, t: f64) -> Ray {
+ let rd = self.lens_radius * Vec3::random_in_unit_disk();
+ let offset = &self.u * rd.x + &self.v * rd.y;
+ Ray {
+ origin: &self.origin + &offset,
+ direction: &self.lower_left_corner + s * &self.horizontal + t * &self.vertical - &self.origin - &offset,
+ time: self.time_start + (self.time_end - self.time_start) * rand::random::<f64>(),
+ }
+ }
+}
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 } }),
+ }
+ }
+}
diff --git a/src/image.rs b/src/image.rs
new file mode 100644
index 0000000..e24c14b
--- /dev/null
+++ b/src/image.rs
@@ -0,0 +1,77 @@
+use std::io::Write;
+
+use crate::vec3::Color;
+
+pub struct Image {
+ width: usize,
+ height: usize,
+ data: Vec<Pixel>,
+}
+
+#[derive(Clone)]
+struct Pixel {
+ color: Color,
+ sample_count: u32,
+}
+
+impl Image {
+ pub fn new(width: usize, height: usize) -> Image {
+ let data = vec![
+ Pixel {
+ color: Color {
+ x: 0.0,
+ y: 0.0,
+ z: 0.0
+ },
+ sample_count: 0
+ };
+ width * height
+ ];
+ Image {
+ width,
+ height,
+ data,
+ }
+ }
+
+ pub fn add_sample(&mut self, x: usize, y: usize, color: Color) {
+ self.data
+ .get_mut((y * self.width) + x)
+ .unwrap()
+ .update(color);
+ }
+
+ pub fn write(&self, output: &mut impl Write) {
+ output.write_fmt(format_args!("P3\n{} {}\n255\n", self.width, self.height)).unwrap();
+ for y in (0..self.height).rev() {
+ for x in 0..self.width {
+ let pixel = self.data.get((y * self.width) + x).unwrap();
+ let mut r = pixel.color.x;
+ let mut g = pixel.color.y;
+ let mut b = pixel.color.z;
+
+ // Divide by the number of samples and perform gamma correction for gamma 2
+ let scale = 1.0 / pixel.sample_count as f64;
+ r = (r * scale).sqrt();
+ g = (g * scale).sqrt();
+ b = (b * scale).sqrt();
+
+ output
+ .write_fmt(format_args!(
+ "{} {} {}\n",
+ (256.0 * r.clamp(0.0, 0.999)) as u32,
+ (256.0 * g.clamp(0.0, 0.999)) as u32,
+ (256.0 * b.clamp(0.0, 0.999)) as u32,
+ ))
+ .unwrap();
+ }
+ }
+ }
+}
+
+impl Pixel {
+ pub fn update(&mut self, color: Color) {
+ self.color += color;
+ self.sample_count += 1;
+ }
+}
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..7ec82e6
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,115 @@
+mod camera;
+mod hittable;
+mod material;
+mod ray;
+mod util;
+mod vec3;
+mod image;
+mod texture;
+mod scenes;
+
+use std::{sync::{Arc, mpsc}, thread};
+
+use camera::Camera;
+use hittable::Hittable;
+use image::Image;
+use ray::Ray;
+use vec3::{Vec3, Color};
+use scenes::get_scene;
+
+struct PixelUpdate {
+ color: Color,
+ x: usize,
+ y: usize,
+}
+
+fn ray_color(ray: &Ray, background: &Color, world: &dyn Hittable, depth: u32) -> Color {
+ if depth <= 0 {
+ return Color {
+ x: 0.0,
+ y: 0.0,
+ z: 0.0,
+ };
+ }
+ match world.hit(ray, 0.001, f64::INFINITY) {
+ None => background.clone(),
+ Some(rec) => {
+ let mut scattered = Ray::new();
+ let mut attenuation = Color::new();
+ if let Some(material) = &rec.material {
+ let emitted = material.emitted(rec.u, rec.v, &rec.p);
+ if !material.scatter(&ray, &rec, &mut attenuation, &mut scattered) {
+ emitted
+ } else {
+ emitted + attenuation * ray_color(&scattered, background, world, depth - 1)
+ }
+ } else {
+ Color { x: 0.0, y: 0.0, z: 0.0 }
+ }
+ },
+ }
+}
+
+fn render(image_width: u32, image_height: u32, samples_per_pixel: u32, max_depth: u32, world: Arc<dyn Hittable>, background: Color, camera: Arc<Camera>, tx: mpsc::Sender<PixelUpdate>) {
+ for j in (0..image_height).rev() {
+ for i in 0..image_width {
+ for _ in 0..samples_per_pixel {
+ let u = ((i as f64) + rand::random::<f64>()) / ((image_width - 1) as f64);
+ let v = ((j as f64) + rand::random::<f64>()) / ((image_height - 1) as f64);
+ let ray = camera.get_ray(u, v);
+
+ tx.send(PixelUpdate { color: ray_color(&ray, &background, world.as_ref(), max_depth), x: i as usize, y: j as usize}).unwrap();
+ }
+ }
+ }
+}
+
+fn main() {
+ // Image
+ const ASPECT_RATIO: f64 = 16.0 / 9.0;
+ //const ASPECT_RATIO: f64 = 1.0;
+ const IMAGE_WIDTH: u32 = 600;
+ const IMAGE_HEIGHT: u32 = (IMAGE_WIDTH as f64 / ASPECT_RATIO) as u32;
+ const SAMPLES_PER_PIXEL: u32 = 30;
+ const MAX_DEPTH: u32 = 50;
+ const THREAD_COUNT: u32 = 8;
+ const TIME_START: f64 = 0.0;
+ const TIME_END: f64 = 1.0;
+ // World
+ let (world, lookfrom, lookat, vfov, aperture, background) = get_scene(std::env::args().nth(1).unwrap_or("0".to_string()).trim().parse().unwrap_or(0));
+
+ // Camera
+ let vup = Vec3 { x: 0.0, y: 1.0, z: 0.0 };
+ let dist_to_focus = 10.0;
+ let cam = Arc::new(Camera::new(lookfrom, lookat, vup, vfov, ASPECT_RATIO, aperture, dist_to_focus, TIME_START, TIME_END));
+ // Render
+ let mut final_image = Image::new(IMAGE_WIDTH as usize, IMAGE_HEIGHT as usize);
+ let (tx, rx) = mpsc::channel::<PixelUpdate>();
+ for _ in 0..THREAD_COUNT {
+ let sender = tx.clone();
+ let world_ref = world.clone();
+ let camera_ref = cam.clone();
+ let background_clone = background.clone();
+ thread::spawn( || {
+ render(IMAGE_WIDTH, IMAGE_HEIGHT, SAMPLES_PER_PIXEL / THREAD_COUNT, MAX_DEPTH, world_ref, background_clone, camera_ref, sender);
+ });
+ }
+ let expected_updates: u64 = (SAMPLES_PER_PIXEL / THREAD_COUNT) as u64 * THREAD_COUNT as u64 * IMAGE_HEIGHT as u64 * IMAGE_WIDTH as u64;
+ let print_frequency: u64 = (SAMPLES_PER_PIXEL / THREAD_COUNT) as u64 * THREAD_COUNT as u64 * IMAGE_WIDTH as u64;
+ let mut update_count: u64 = 0;
+ loop {
+ if let Ok(update) = rx.try_recv() {
+ update_count += 1;
+ final_image.add_sample(update.x, update.y, update.color);
+ if update_count % print_frequency == 0 {
+ eprint!("\rCurrent completion: {:.2}%", (update_count as f64 / expected_updates as f64) * 100.0)
+ }
+ } else {
+ if Arc::strong_count(&world) == 1 {
+ break
+ }
+ }
+ }
+ final_image.write(&mut std::io::stdout());
+ eprintln!("\nDone.");
+}
diff --git a/src/material/dielectric.rs b/src/material/dielectric.rs
new file mode 100644
index 0000000..bcacb78
--- /dev/null
+++ b/src/material/dielectric.rs
@@ -0,0 +1,39 @@
+use super::Material;
+use crate::{hittable::HitRecord, vec3::Vec3};
+use crate::vec3::Color;
+use crate::ray::Ray;
+
+pub struct Dielectric {
+ pub index_of_refraction: f64,
+}
+
+impl Dielectric {
+ fn reflectance(cosine: f64, ref_idx: f64) -> f64 {
+ // Using Schlick's Approximation:
+ let mut r0 = (1.0 - ref_idx) / (1.0 + ref_idx);
+ r0 *= r0;
+ r0 + (1.0 - r0) * (1.0 - cosine).powi(5)
+ }
+}
+
+impl Material for Dielectric {
+ fn scatter(&self, ray_in: &Ray, hit_record: &HitRecord, attenuation: &mut Color, scattered: &mut Ray) -> bool {
+ *attenuation = Color { x: 1.0, y: 1.0, z: 1.0 };
+ let refraction_ratio = if hit_record.front_face { 1.0 / self.index_of_refraction } else { self.index_of_refraction };
+ let unit_direction = ray_in.direction.unit_vector();
+ let cos_theta = hit_record.normal.dot(&-&unit_direction).min(1.0);
+ let sin_theta = (1.0 - cos_theta * cos_theta).sqrt();
+
+ let cannot_refract = refraction_ratio * sin_theta > 1.0;
+ let direction: Vec3;
+
+ if cannot_refract || Self::reflectance(cos_theta, refraction_ratio) > rand::random::<f64>() {
+ direction = unit_direction.reflect(&hit_record.normal)
+ } else {
+ direction = unit_direction.refract(&hit_record.normal, refraction_ratio)
+ }
+
+ *scattered = Ray { origin: hit_record.p.clone(), direction, time: ray_in.time };
+ true
+ }
+}
diff --git a/src/material/diffuse_light.rs b/src/material/diffuse_light.rs
new file mode 100644
index 0000000..fecbcff
--- /dev/null
+++ b/src/material/diffuse_light.rs
@@ -0,0 +1,29 @@
+use std::sync::Arc;
+
+use super::Material;
+use crate::{hittable::HitRecord, texture::Texture, vec3::Point3};
+use crate::vec3::Color;
+use crate::texture::SolidColor;
+use crate::ray::Ray;
+
+pub struct DiffuseLight {
+ emit: Arc<dyn Texture>,
+}
+
+impl DiffuseLight {
+ pub fn from_color(color: Color) -> Self {
+ Self {
+ emit: Arc::new(SolidColor::from_color(color)),
+ }
+ }
+}
+
+impl Material for DiffuseLight {
+ fn scatter(&self, _: &Ray, _: &HitRecord, _: &mut Color, _: &mut Ray) -> bool {
+ false
+ }
+
+ fn emitted(&self, u: f64, v: f64, point: &Point3) -> Color {
+ self.emit.value(u, v, point)
+ }
+}
diff --git a/src/material/isotropic.rs b/src/material/isotropic.rs
new file mode 100644
index 0000000..f59fa7d
--- /dev/null
+++ b/src/material/isotropic.rs
@@ -0,0 +1,33 @@
+use std::sync::Arc;
+
+use super::Material;
+use crate::{hittable::HitRecord, texture::Texture, vec3::Vec3};
+use crate::vec3::Color;
+use crate::texture::SolidColor;
+use crate::ray::Ray;
+
+pub struct Isotropic {
+ albedo: Arc<dyn Texture>,
+}
+
+impl Isotropic {
+ pub fn from_color(color: Color) -> Self {
+ Self {
+ albedo: Arc::new(SolidColor::from_color(color)),
+ }
+ }
+
+ pub fn from_texture(texture: Arc<dyn Texture>) -> Self {
+ Self {
+ albedo: texture,
+ }
+ }
+}
+
+impl Material for Isotropic {
+ fn scatter(&self, ray_in: &Ray, hit_record: &HitRecord, attenuation: &mut Color, scattered: &mut Ray) -> bool {
+ *scattered = Ray { origin: hit_record.p.clone(), direction: Vec3::random_in_unit_sphere(), time: ray_in.time };
+ *attenuation = self.albedo.value(hit_record.u, hit_record.v, &hit_record.p);
+ true
+ }
+}
diff --git a/src/material/lambertian.rs b/src/material/lambertian.rs
new file mode 100644
index 0000000..95f698e
--- /dev/null
+++ b/src/material/lambertian.rs
@@ -0,0 +1,34 @@
+use std::sync::Arc;
+
+use super::Material;
+use crate::{hittable::HitRecord, texture::Texture, vec3::Vec3};
+use crate::vec3::Color;
+use crate::texture::SolidColor;
+use crate::ray::Ray;
+
+pub struct Lambertian {
+ pub albedo: Arc<dyn Texture>,
+}
+
+impl Lambertian {
+ pub fn from_color(color: Color) -> Self {
+ Self {
+ albedo: Arc::new(SolidColor { color_value: color.clone() }),
+ }
+ }
+}
+
+impl Material for Lambertian {
+ fn scatter(&self, ray_in : &Ray, hit_record: &HitRecord, attenuation: &mut Color, scattered: &mut Ray) -> bool {
+ let mut scatter_direction = &hit_record.normal + Vec3::random_unit_vector();
+
+ // Catch zero-vector scatter directions that will generate issues later
+ if scatter_direction.near_zero() {
+ scatter_direction = hit_record.normal.clone();
+ }
+
+ *scattered = Ray { origin: hit_record.p.clone(), direction: scatter_direction, time: ray_in.time };
+ *attenuation = self.albedo.value(hit_record.u, hit_record.v, &hit_record.p);
+ true
+ }
+}
diff --git a/src/material/metal.rs b/src/material/metal.rs
new file mode 100644
index 0000000..2865a2f
--- /dev/null
+++ b/src/material/metal.rs
@@ -0,0 +1,18 @@
+use super::Material;
+use crate::{hittable::HitRecord, vec3::Vec3};
+use crate::vec3::Color;
+use crate::ray::Ray;
+
+pub struct Metal {
+ pub albedo: Color,
+ pub fuzz: f64, // TODO: This should have value 0.0 - 1.0; this is not enforced
+}
+
+impl Material for Metal {
+ fn scatter(&self, ray_in: &Ray, hit_record: &HitRecord, attenuation: &mut Color, scattered: &mut Ray) -> bool {
+ let reflected = ray_in.direction.unit_vector().reflect(&hit_record.normal);
+ *scattered = Ray { origin: hit_record.p.clone(), direction: reflected + self.fuzz * Vec3::random_in_unit_sphere(), time: ray_in.time };
+ *attenuation = self.albedo.clone();
+ scattered.direction.dot(&hit_record.normal) > 0.0
+ }
+}
diff --git a/src/material/mod.rs b/src/material/mod.rs
new file mode 100644
index 0000000..bf1eb24
--- /dev/null
+++ b/src/material/mod.rs
@@ -0,0 +1,21 @@
+mod lambertian;
+pub use lambertian::Lambertian;
+mod metal;
+pub use metal::Metal;
+mod dielectric;
+pub use dielectric::Dielectric;
+mod diffuse_light;
+pub use diffuse_light::DiffuseLight;
+mod isotropic;
+pub use isotropic::Isotropic;
+
+use crate::{hittable::HitRecord, vec3::Point3};
+use crate::vec3::Color;
+use crate::ray::Ray;
+
+pub trait Material: Send + Sync {
+ fn emitted(&self, _: f64, _: f64, _: &Point3) -> Color {
+ Color { x: 0.0, y: 0.0, z: 0.0 }
+ }
+ fn scatter(&self, ray_in: &Ray, hit_record: &HitRecord, attenuation: &mut Color, scattered: &mut Ray) -> bool;
+}
diff --git a/src/ray.rs b/src/ray.rs
new file mode 100644
index 0000000..0a4fc3c
--- /dev/null
+++ b/src/ray.rs
@@ -0,0 +1,21 @@
+use crate::vec3::{Point3, Vec3};
+
+pub struct Ray {
+ pub origin: Point3,
+ pub direction: Vec3,
+ pub time: f64,
+}
+
+impl Ray {
+ pub fn new() -> Ray {
+ Ray {
+ origin: Point3 { x: 0.0, y: 0.0, z: 0.0 },
+ direction: Vec3 { x: 0.0, y:0.0, z: 0.0},
+ time: 0.0
+ }
+ }
+
+ pub fn at(&self, t: f64) -> Point3 {
+ &self.origin + t * &self.direction
+ }
+}
diff --git a/src/scenes.rs b/src/scenes.rs
new file mode 100644
index 0000000..7cdce86
--- /dev/null
+++ b/src/scenes.rs
@@ -0,0 +1,252 @@
+use std::sync::Arc;
+
+use crate::hittable::{ConstantMedium, Hittable};
+use crate::hittable::HittableBox;
+use crate::hittable::HittableList;
+use crate::material::{Dielectric, DiffuseLight, Lambertian, Material, Metal};
+use crate::hittable::instance::RotateY;
+use crate::hittable::instance::RotateX;
+use crate::hittable::instance::RotateZ;
+use crate::hittable::Sphere;
+use crate::hittable::instance::Translate;
+use crate::vec3::{Point3, Vec3, Color};
+use crate::hittable::BVHNode;
+use crate::texture::{CheckerTexture, ImageTexture, NoiseTexture, SolidColor};
+use crate::hittable::XYRect;
+use crate::hittable::XZRect;
+use crate::hittable::YZRect;
+use crate::hittable::Triangle;
+use crate::hittable::Model;
+use crate::hittable::instance::Moving;
+
+pub fn get_scene(id: u32) -> (Arc<dyn Hittable>, Point3, Point3, f64, f64, Color) {
+ match id {
+ 2 => two_spheres(),
+ 3 => two_perlin_spheres(),
+ 4 => earth(),
+ 5 => simple_light(),
+ 6 => cornell_box(),
+ 7 => cornell_smoke(),
+ 8 => final_scene(),
+ 9 => test_scene(),
+ 10 => triangle_scene(),
+ _ => random_scene(),
+ }
+}
+
+fn random_scene() -> (Arc<dyn Hittable>, Point3, Point3, f64, f64, Color) {
+ let mut world = HittableList::new();
+
+ let checker = Arc::new(CheckerTexture::from_colors(Color { x: 0.2, y: 0.3, z: 0.1 } , Color { x: 0.9, y: 0.9, z: 0.9 }));
+ let ground_material = Arc::new(Lambertian { albedo: checker });
+ world.add(Arc::new(Sphere{ center: Point3 { x: 0.0, y: -1000.0, z: 0.0 }, radius: 1000.0, material: ground_material }));
+
+ for a in -11..11 {
+ for b in -11..11 {
+ let choose_mat = rand::random::<f64>();
+ let center = Point3 { x: (a as f64) + 0.9 * rand::random::<f64>(), y: 0.2, z: (b as f64) + 0.9 * rand::random::<f64>() };
+
+ if (&center - Point3 { x: 4.0, y: 0.3, z: 0.0 }).length() > 0.9 {
+ let sphere_material: Arc<dyn Material>;
+
+ if choose_mat < 0.8 {
+ let albedo = Arc::new(SolidColor::from_color(Color::random() * Color::random()));
+ sphere_material = Arc::new(Lambertian { albedo });
+ world.add(Arc::new(Sphere { center, radius: 0.2, material: sphere_material }));
+ } else if choose_mat < 0.95 {
+ let albedo = Color::random_in_range(0.5, 1.0);
+ let fuzz = rand::random::<f64>() / 2.0;
+ sphere_material = Arc::new(Metal { albedo, fuzz });
+ world.add(Arc::new(Sphere { center, radius: 0.2, material: sphere_material }));
+ } else {
+ sphere_material = Arc::new(Dielectric { index_of_refraction: 1.5 });
+ world.add(Arc::new(Sphere { center, radius: 0.2, material: sphere_material }));
+ }
+ }
+ }
+ }
+
+ let material1 = Arc::new(Dielectric { index_of_refraction: 1.5 });
+ world.add(Arc::new(Sphere { center: Point3 { x: 0.0, y: 1.0, z: 0.0 }, radius: 1.0, material: material1 }));
+
+ let material2 = Arc::new(Lambertian { albedo: Arc::new(SolidColor::from_color(Color { x: 0.4, y: 0.2, z: 0.1 })) });
+ world.add(Arc::new(Sphere { center: Point3 { x: -4.0, y: 1.0, z: 0.0 }, radius: 1.0, material: material2 }));
+
+ let material3 = Arc::new(Metal { albedo: Color { x: 0.7, y: 0.6, z: 0.5 }, fuzz: 0.0 });
+ world.add(Arc::new(Sphere { center: Point3 { x: 4.0, y: 1.0, z: 0.0 }, radius: 1.0, material: material3 }));
+
+ (Arc::new(BVHNode::new(&world, 0.0, 1.0)), Point3 { x: 13.0, y: 2.0, z: 3.0}, Point3::new(), 20.0, 0.1, Color { x: 0.7, y: 0.8, z: 1.0 })
+}
+
+fn two_spheres() -> (Arc<dyn Hittable>, Point3, Point3, f64, f64, Color) {
+ let mut objects = HittableList::new();
+ let checker = Arc::new(Lambertian { albedo: Arc::new(CheckerTexture::from_colors(Color { x: 0.2, y: 0.3, z: 0.1 } , Color { x: 0.9, y: 0.9, z: 0.9 })) });
+ objects.add(Arc::new(Sphere { center: Point3 { x: 0.0, y: -10.0, z: 0.0 }, radius: 10.0, material: checker.clone() }));
+ objects.add(Arc::new(Sphere { center: Point3 { x: 0.0, y: 10.0, z: 0.0 }, radius: 10.0, material: checker }));
+ (Arc::new(BVHNode::new(&objects, 0.0, 1.0)), Point3 { x: 13.0, y: 2.0, z: 3.0}, Point3::new(), 20.0, 0.0, Color { x: 0.7, y: 0.8, z: 1.0 })
+}
+
+fn two_perlin_spheres() -> (Arc<dyn Hittable>, Point3, Point3, f64, f64, Color) {
+ let mut objects = HittableList::new();
+ let pertext = Arc::new(Lambertian { albedo: Arc::new(NoiseTexture::new(4.0)) });
+ objects.add(Arc::new(Sphere { center: Point3 { x: 0.0, y: -1000.0, z: 0.0 }, radius: 1000.0, material: pertext.clone() }));
+ objects.add(Arc::new(Sphere { center: Point3 { x: 0.0, y: 2.0, z: 0.0 }, radius: 2.0, material: pertext.clone() }));
+ (Arc::new(BVHNode::new(&objects, 0.0, 1.0)), Point3 { x: 13.0, y: 2.0, z: 3.0}, Point3::new(), 20.0, 0.0, Color { x: 0.7, y: 0.8, z: 1.0 })
+}
+
+fn earth() -> (Arc<dyn Hittable>, Point3, Point3, f64, f64, Color) {
+ let mut objects = HittableList::new();
+ let earth_texture = Arc::new(ImageTexture::from_bmp_data(&include_bytes!("../res/earthmap.bmp").to_vec()));
+ let earth_surface = Arc::new(Lambertian { albedo: earth_texture });
+ objects.add(Arc::new(Sphere { center: Point3 { x: 0.0, y: 0.0, z: 0.0 }, radius: 2.0, material: earth_surface }));
+ (Arc::new(BVHNode::new(&objects, 0.0, 1.0)), Point3 { x: 13.0, y: 2.0, z: 3.0}, Point3::new(), 20.0, 0.0, Color { x: 0.7, y: 0.8, z: 1.0 })
+}
+
+fn simple_light() -> (Arc<dyn Hittable>, Point3, Point3, f64, f64, Color) {
+ let mut objects = HittableList::new();
+ let pertext = Arc::new(Lambertian { albedo: Arc::new(NoiseTexture::new(4.0)) });
+ objects.add(Arc::new(Sphere { center: Point3 { x: 0.0, y: -1000.0, z: 0.0 }, radius: 1000.0, material: pertext.clone() }));
+ objects.add(Arc::new(Sphere { center: Point3 { x: 0.0, y: 2.0, z: 0.0 }, radius: 2.0, material: pertext.clone() }));
+ let diff_light = Arc::new(DiffuseLight::from_color(Color { x: 4.0, y: 4.0, z: 4.0 }));
+ objects.add(Arc::new(XYRect { material: diff_light, x0: 3.0, x1: 5.0, y0: 1.0, y1: 3.0, k: -2.0 }));
+ (Arc::new(BVHNode::new(&objects, 0.0, 1.0)), Point3 { x: 26.0, y: 3.0, z: 6.0}, Point3 { x: 0.0, y: 2.0, z: 0.0}, 20.0, 0.0, Color::new())
+}
+
+fn cornell_box() -> (Arc<dyn Hittable>, Point3, Point3, f64, f64, Color) {
+ let mut objects = HittableList::new();
+ let red = Arc::new(Lambertian::from_color(Color { x: 0.65, y: 0.05, z: 0.05 }));
+ let white = Arc::new(Lambertian::from_color(Color { x: 0.73, y: 0.73, z: 0.73 }));
+ let green = Arc::new(Lambertian::from_color(Color { x: 0.12, y: 0.45, z: 0.15 }));
+ let light = Arc::new(DiffuseLight::from_color(Color { x: 15.0, y: 15.0, z: 15.0 }));
+
+ objects.add(Arc::new(YZRect { material: green, y0: 0.0, y1: 555.0, z0: 0.0, z1: 555.0, k: 555.0 }));
+ objects.add(Arc::new(YZRect { material: red, y0: 0.0, y1: 555.0, z0: 0.0, z1: 555.0, k: 0.0 }));
+ objects.add(Arc::new(XZRect { material: light, x0: 213.0, x1: 343.0, z0: 227.0, z1: 332.0, k: 554.0 }));
+ objects.add(Arc::new(XZRect { material: white.clone(), x0: 0.0, x1: 555.0, z0: 0.0, z1: 555.0, k: 0.0 }));
+ objects.add(Arc::new(XZRect { material: white.clone(), x0: 0.0, x1: 555.0, z0: 0.0, z1: 555.0, k: 555.0 }));
+ objects.add(Arc::new(XYRect { material: white.clone(), x0: 0.0, x1: 555.0, y0: 0.0, y1: 555.0, k: 555.0 }));
+
+ //objects.add(Arc::new(HittableBox::new(Point3 { x: 130.0, y: 0.0, z: 65.0 }, Point3 { x: 295.0, y: 165.0, z: 230.0 }, white.clone())));
+ //objects.add(Arc::new(HittableBox::new(Point3 { x: 265.0, y: 0.0, z: 295.0 }, Point3 { x: 430.0, y: 330.0, z: 460.0 }, white.clone())));
+ let box_1 = Arc::new(HittableBox::new(Point3 { x: 0.0, y: 0.0, z: 0.0 }, Point3 { x: 165.0, y: 330.0, z: 165.0 }, white.clone()));
+ let box_1 = Arc::new(RotateY::new(box_1, 15.0));
+ let box_1 = Arc::new(Translate { hittable: box_1, offset: Point3 { x: 265.0, y: 0.0, z: 295.0 } });
+ objects.add(box_1);
+ let box_2 = Arc::new(HittableBox::new(Point3 { x: 0.0, y: 0.0, z: 0.0 }, Point3 { x: 165.0, y: 165.0, z: 165.0 }, white.clone()));
+ let box_2 = Arc::new(RotateY::new(box_2, -18.0));
+ let box_2 = Arc::new(Translate { hittable: box_2, offset: Point3 { x: 130.0, y: 0.0, z: 65.0 } });
+ objects.add(box_2);
+
+ (Arc::new(BVHNode::new(&objects, 0.0, 1.0)), Point3 { x: 278.0, y: 278.0, z: -800.0}, Point3 { x: 278.0, y: 278.0, z: 0.0}, 40.0, 0.0, Color::new())
+}
+
+fn cornell_smoke() -> (Arc<dyn Hittable>, Point3, Point3, f64, f64, Color) {
+ let mut objects = HittableList::new();
+ let red = Arc::new(Lambertian::from_color(Color { x: 0.65, y: 0.05, z: 0.05 }));
+ let white = Arc::new(Lambertian::from_color(Color { x: 0.73, y: 0.73, z: 0.73 }));
+ let green = Arc::new(Lambertian::from_color(Color { x: 0.12, y: 0.45, z: 0.15 }));
+ let light = Arc::new(DiffuseLight::from_color(Color { x: 7.0, y: 7.0, z: 7.0 }));
+
+ objects.add(Arc::new(YZRect { material: green, y0: 0.0, y1: 555.0, z0: 0.0, z1: 555.0, k: 555.0 }));
+ objects.add(Arc::new(YZRect { material: red, y0: 0.0, y1: 555.0, z0: 0.0, z1: 555.0, k: 0.0 }));
+ objects.add(Arc::new(XZRect { material: light, x0: 113.0, x1: 443.0, z0: 127.0, z1: 432.0, k: 554.0 }));
+ objects.add(Arc::new(XZRect { material: white.clone(), x0: 0.0, x1: 555.0, z0: 0.0, z1: 555.0, k: 0.0 }));
+ objects.add(Arc::new(XZRect { material: white.clone(), x0: 0.0, x1: 555.0, z0: 0.0, z1: 555.0, k: 555.0 }));
+ objects.add(Arc::new(XYRect { material: white.clone(), x0: 0.0, x1: 555.0, y0: 0.0, y1: 555.0, k: 555.0 }));
+
+ //objects.add(Arc::new(HittableBox::new(Point3 { x: 130.0, y: 0.0, z: 65.0 }, Point3 { x: 295.0, y: 165.0, z: 230.0 }, white.clone())));
+ //objects.add(Arc::new(HittableBox::new(Point3 { x: 265.0, y: 0.0, z: 295.0 }, Point3 { x: 430.0, y: 330.0, z: 460.0 }, white.clone())));
+ let box_1 = Arc::new(HittableBox::new(Point3 { x: 0.0, y: 0.0, z: 0.0 }, Point3 { x: 165.0, y: 330.0, z: 165.0 }, white.clone()));
+ let box_1 = Arc::new(RotateY::new(box_1, 15.0));
+ let box_1 = Arc::new(Translate { hittable: box_1, offset: Point3 { x: 265.0, y: 0.0, z: 295.0 } });
+ let box_2 = Arc::new(HittableBox::new(Point3 { x: 0.0, y: 0.0, z: 0.0 }, Point3 { x: 165.0, y: 165.0, z: 165.0 }, white.clone()));
+ let box_2 = Arc::new(RotateY::new(box_2, -18.0));
+ let box_2 = Arc::new(Translate { hittable: box_2, offset: Point3 { x: 130.0, y: 0.0, z: 65.0 } });
+
+ objects.add(Arc::new(ConstantMedium::new(box_1, 0.01, Arc::new(SolidColor::from_color(Color { x: 0.0, y: 0.0, z: 0.0 })))));
+ objects.add(Arc::new(ConstantMedium::new(box_2, 0.01, Arc::new(SolidColor::from_color(Color { x: 1.0, y: 1.0, z: 1.0 })))));
+
+ (Arc::new(BVHNode::new(&objects, 0.0, 1.0)), Point3 { x: 278.0, y: 278.0, z: -800.0}, Point3 { x: 278.0, y: 278.0, z: 0.0}, 40.0, 0.0, Color::new())
+}
+
+fn final_scene() -> (Arc<dyn Hittable>, Point3, Point3, f64, f64, Color) {
+ let mut boxes_1 = HittableList::new();
+ let ground = Arc::new(Lambertian::from_color(Color { x: 0.48, y: 0.83, z: 0.53 }));
+
+ const BOXES_PER_SIDE: usize = 20;
+ for i in 0..BOXES_PER_SIDE {
+ for j in 0..BOXES_PER_SIDE {
+ let w = 100.0;
+ let x0 = -1000.0 + (i as f64) * w;
+ let z0 = -1000.0 + (j as f64) * w;
+ let y0 = 0.0;
+ let x1 = x0 + w;
+ let y1 = 1.0 + 100.0 * rand::random::<f64>();
+ let z1 = z0 + w;
+ boxes_1.add(Arc::new(HittableBox::new(Point3 { x: x0, y: y0, z: z0 }, Point3 { x: x1, y: y1, z: z1 }, ground.clone())));
+ }
+ }
+
+ let mut objects = HittableList::new();
+ objects.add(Arc::new(BVHNode::new(&boxes_1, 0.0, 1.0)));
+
+ let light = Arc::new(DiffuseLight::from_color(Color { x: 7.0, y: 7.0, z: 7.0 }));
+ objects.add(Arc::new(XZRect { material: light.clone(), x0: 123.0, x1: 423.0, z0: 147.0, z1: 412.0, k: 554.0 }));
+
+ let center_1 = Point3 { x: 400.0, y: 400.0, z: 200.0 };
+ let center_2 = &center_1 + Vec3 { x: 30.0, y: 0.0, z: 0.0 };
+
+ let moving_sphere_material = Arc::new(Lambertian::from_color(Color { x: 0.7, y: 0.3, z: 0.1 }));
+ objects.add(Arc::new(Moving { hittable: Arc::new(Sphere { center: Point3::new(), radius: 50.0, material: moving_sphere_material }), offset_start: center_1, offset_end: center_2, time_start: 0.0, time_end: 1.0, }));
+
+ objects.add(Arc::new(Sphere { center: Point3 { x: 260.0, y: 150.0, z: 45.0 }, radius: 50.0, material: Arc::new(Dielectric { index_of_refraction: 1.5 }) }));
+ objects.add(Arc::new(Sphere { center: Point3 { x: 0.0, y: 150.0, z: 145.0 }, radius: 50.0, material: Arc::new(Metal { albedo: Color { x: 0.8, y: 0.8, z: 0.9 }, fuzz: 1.0 }) }));
+
+ let boundary = Arc::new(Sphere { center: Point3 { x: 360.0, y: 150.0, z: 145.0 }, radius: 70.0, material: Arc::new(Dielectric { index_of_refraction: 1.5 }) });
+ objects.add(boundary.clone());
+ objects.add(Arc::new(ConstantMedium::new(boundary.clone(), 0.2, Arc::new(SolidColor::from_color(Color { x: 0.2, y: 0.4, z: 0.9 })))));
+ let boundary = Arc::new(Sphere { center: Point3 { x: 0.0, y: 0.0, z: 0.0 }, radius: 5000.0, material: Arc::new(Dielectric { index_of_refraction: 1.5 }) });
+ objects.add(Arc::new(ConstantMedium::new(boundary.clone(), 0.0001, Arc::new(SolidColor::from_color(Color { x: 1.0, y: 1.0, z: 1.0 })))));
+
+ let emat = Arc::new(Lambertian { albedo: Arc::new(ImageTexture::from_bmp_data(&include_bytes!("../res/earthmap.bmp").to_vec())) });
+ objects.add(Arc::new(Sphere { center: Point3 { x: 400.0, y: 200.0, z: 400.0 }, radius: 100.0, material: emat }));
+ let pertext = Arc::new(Lambertian { albedo: Arc::new(NoiseTexture::new(0.1)) });
+ objects.add(Arc::new(Sphere { center: Point3 { x: 220.0, y: 280.0, z: 300.0 }, radius: 80.0, material: pertext }));
+
+ let mut boxes_2 = HittableList::new();
+ let white = Arc::new(Lambertian::from_color(Color { x: 0.73, y: 0.73, z: 0.73 }));
+ for _ in 0..1000 {
+ boxes_2.add(Arc::new(Sphere { center: Point3::random_in_range(0.0, 165.0), radius: 10.0, material: white.clone() }));
+ }
+
+ objects.add(Arc::new(Translate { hittable: Arc::new(RotateY::new(Arc::new(BVHNode::new(&boxes_2, 0.0, 1.0)), 15.0)), offset: Vec3 { x: -100.0, y: 270.0, z: 395.0 } }));
+ (Arc::new(objects), Point3 { x: 478.0, y: 278.0, z: -600.0}, Point3 { x: 278.0, y: 278.0, z: 0.0}, 40.0, 0.0, Color::new())
+}
+
+fn test_scene() -> (Arc<dyn Hittable>, Point3, Point3, f64, f64, Color) {
+ let mut objects = HittableList::new();
+ //let earth_texture = Arc::new(ImageTexture::from_bmp_data(&include_bytes!("../res/earthmap.bmp").to_vec()));
+ //let earth_surface = Arc::new(Lambertian { albedo: earth_texture });
+ let pertext = Arc::new(Lambertian { albedo: Arc::new(NoiseTexture::new(4.0)) });
+ objects.add(Arc::new(XZRect { material: pertext, x0: -f64::INFINITY, x1: f64::INFINITY, z0: -f64::INFINITY, z1: f64::INFINITY, k: 0.0 }));
+ //(Arc::new(BVHNode::new(&objects, 0.0, 1.0)), Point3 { x: 13.0, y: 2.0, z: 3.0}, Point3::new(), 20.0, 0.0, Color { x: 0.7, y: 0.8, z: 1.0 })
+ (Arc::new(objects), Point3 { x: 13.0, y: 2.0, z: 3.0}, Point3::new(), 20.0, 0.0, Color { x: 0.7, y: 0.8, z: 1.0 })
+}
+
+fn triangle_scene() -> (Arc<dyn Hittable>, Point3, Point3, f64, f64, Color) {
+ let mut objects = HittableList::new();
+ let checker = Arc::new(CheckerTexture::from_colors(Color { x: 0.2, y: 0.3, z: 0.1 } , Color { x: 0.9, y: 0.9, z: 0.9 }));
+ let ground_material = Arc::new(Lambertian { albedo: checker });
+ objects.add(Arc::new(Sphere{ center: Point3 { x: 0.0, y: -1001.0, z: 0.0 }, radius: 1000.0, material: ground_material }));
+
+ //let pertext = Arc::new(Lambertian { albedo: Arc::new(NoiseTexture::new(4.0)) });
+ //let pertext = Arc::new(Lambertian { albedo: Arc::new(SolidColor::from_color(Color {x: 0.0, y: 0.0, z: 0.0}) ) });
+ //objects.add(Arc::new(Triangle { material: Arc::new(Metal { albedo: Color { x: 0.7, y: 0.6, z: 0.5 }, fuzz: 0.0 }), v0 : Vec3::new(), v1: Vec3 { x: 0.0, y: 0.0, z: 1.0 }, v2: Vec3 { x: 0.0, y: 1.0, z: 0.5 } }));
+ //let monkey = Arc::new(Model::from_obj(include_str!("../res/monkey.obj"), Arc::new(Dielectric { index_of_refraction: 1.5 })));
+ let monkey = Arc::new(Model::from_obj(include_str!("../res/monkey.obj"), Arc::new(Metal { albedo: Color { x: 0.7, y: 0.6, z: 0.5 }, fuzz: 0.5 })));
+ let monkey = Arc::new(RotateX::new(monkey, 45.0));
+ let monkey = Arc::new(RotateZ::new(monkey, 45.0));
+ objects.add(monkey);
+ //(Arc::new(objects), Point3 { x: 5.0, y: 5.0, z: 5.0}, Point3::new(), 20.0, 0.0, Color { x: 0.7, y: 0.8, z: 1.0 })
+ (Arc::new(BVHNode::new(&objects, 0.0, 1.0)), Point3 { x: 5.0, y: 5.0, z: 5.0}, Point3 { x: 0.0, y: 0.0, z: 0.0}, 20.0, 0.0, Color { x: 0.7, y: 0.8, z: 1.0 })
+}
diff --git a/src/texture/checker_texture.rs b/src/texture/checker_texture.rs
new file mode 100644
index 0000000..385017b
--- /dev/null
+++ b/src/texture/checker_texture.rs
@@ -0,0 +1,29 @@
+use std::sync::Arc;
+
+use super::{Texture, SolidColor};
+use crate::{vec3::Color, vec3::Point3};
+
+pub struct CheckerTexture {
+ odd: Arc<dyn Texture>,
+ even: Arc<dyn Texture>,
+}
+
+impl CheckerTexture {
+ pub fn from_colors(odd: Color, even: Color) -> Self {
+ Self {
+ odd: Arc::new(SolidColor::from_color(odd)),
+ even: Arc::new(SolidColor::from_color(even)),
+ }
+ }
+}
+
+impl Texture for CheckerTexture {
+ fn value(&self, u: f64, v: f64, p: &Point3) -> Color {
+ let sines = (10.0 * p.x).sin() * (10.0 * p.y).sin() * (10.0 * p.z).sin();
+ if sines < 0.0 {
+ self.odd.value(u, v, p)
+ } else {
+ self.even.value(u, v, p)
+ }
+ }
+}
diff --git a/src/texture/image_texture.rs b/src/texture/image_texture.rs
new file mode 100644
index 0000000..763c3f0
--- /dev/null
+++ b/src/texture/image_texture.rs
@@ -0,0 +1,63 @@
+use super::Texture;
+use crate::{vec3::Color, vec3::Point3};
+
+// assume 24 bit depth
+const BYTES_PER_PIXEL: usize = 3;
+pub struct ImageTexture {
+ data: Vec<u8>,
+ width: usize,
+ height: usize,
+ bytes_per_scanline: usize,
+}
+
+impl ImageTexture {
+ pub fn from_bmp_data(bmp_data: &Vec<u8>) -> Self {
+ let data_position = u32::from_le_bytes([
+ bmp_data[0x0A],
+ bmp_data[0x0B],
+ bmp_data[0x0C],
+ bmp_data[0x0D],
+ ]);
+ // assuming windows BITMAPINFOHEADER, these are i32
+ let width = i32::from_le_bytes([
+ bmp_data[0x12],
+ bmp_data[0x13],
+ bmp_data[0x14],
+ bmp_data[0x15],
+ ]) as usize;
+ let height = i32::from_le_bytes([
+ bmp_data[0x16],
+ bmp_data[0x17],
+ bmp_data[0x18],
+ bmp_data[0x19],
+ ]) as usize;
+ Self {
+ data: bmp_data[(data_position as usize)..bmp_data.len()].to_vec(),
+ height,
+ width,
+ bytes_per_scanline: BYTES_PER_PIXEL * width,
+ }
+ }
+}
+
+impl Texture for ImageTexture {
+ fn value(&self, u: f64, v: f64, _: &Point3) -> Color {
+ let u = u.clamp(0.0, 1.0);
+ // This is a deviation from the book, where v gets flipped.
+ // This is probably because the BMP loader loads in stuff upside down.
+ //let v = 1.0 - v.clamp(0.0, 1.0);
+ let v = v.clamp(0.0, 1.0);
+ let mut i = (u * self.width as f64) as usize;
+ let mut j = (v * self.height as f64) as usize;
+
+ if i >= self.width { i = self.width - 1 };
+ if j >= self.height { j = self.height - 1 };
+ let color_scale = 1.0 / 255.0;
+ let pixel = j * self.bytes_per_scanline + i * BYTES_PER_PIXEL;
+ Color {
+ x: color_scale * *self.data.get(pixel + 2).unwrap() as f64,
+ y: color_scale * *self.data.get(pixel + 1).unwrap() as f64,
+ z: color_scale * *self.data.get(pixel).unwrap() as f64,
+ }
+ }
+}
diff --git a/src/texture/mod.rs b/src/texture/mod.rs
new file mode 100644
index 0000000..55bc9cf
--- /dev/null
+++ b/src/texture/mod.rs
@@ -0,0 +1,15 @@
+mod perlin;
+mod solid_color;
+pub use solid_color::SolidColor;
+mod checker_texture;
+pub use checker_texture::CheckerTexture;
+mod noise_texture;
+pub use noise_texture::NoiseTexture;
+mod image_texture;
+pub use image_texture::ImageTexture;
+
+use crate::{vec3::Color, vec3::Point3};
+
+pub trait Texture: Send + Sync {
+ fn value(&self, u: f64, v: f64, p: &Point3) -> Color;
+}
diff --git a/src/texture/noise_texture.rs b/src/texture/noise_texture.rs
new file mode 100644
index 0000000..92ac166
--- /dev/null
+++ b/src/texture/noise_texture.rs
@@ -0,0 +1,22 @@
+use super::{Texture, perlin::Perlin};
+use crate::{vec3::Color, vec3::Point3};
+
+pub struct NoiseTexture {
+ noise: Perlin,
+ scale: f64,
+}
+
+impl NoiseTexture {
+ pub fn new(scale: f64) -> Self {
+ Self {
+ noise: Perlin::new(),
+ scale,
+ }
+ }
+}
+
+impl Texture for NoiseTexture {
+ fn value(&self, _: f64, _: f64, p: &Point3) -> Color {
+ Color { x: 1.0, y: 1.0, z: 1.0 } * 0.5 * (1.0 + (self.scale * p.z + 10.0 * self.noise.turb(p, 7)).sin())
+ }
+}
diff --git a/src/texture/perlin.rs b/src/texture/perlin.rs
new file mode 100644
index 0000000..43d8f64
--- /dev/null
+++ b/src/texture/perlin.rs
@@ -0,0 +1,98 @@
+use crate::vec3::{Vec3, Point3};
+
+const POINT_COUNT: usize = 256;
+
+pub struct Perlin {
+ ranvec: Vec<Vec3>,
+ perm_x: Vec<usize>,
+ perm_y: Vec<usize>,
+ perm_z: Vec<usize>,
+}
+
+impl Perlin {
+ pub fn new() -> Self {
+ let mut ranvec = Vec::with_capacity(POINT_COUNT);
+ for _ in 0..POINT_COUNT {
+ ranvec.push(Vec3::random_in_range(-1.0, 1.0).unit_vector());
+ }
+ Self {
+ ranvec,
+ perm_x: Self::generate_perm(),
+ perm_y: Self::generate_perm(),
+ perm_z: Self::generate_perm(),
+ }
+ }
+
+ pub fn turb(&self, point: &Point3, depth: u32) -> f64 {
+ let mut accum = 0.0;
+ let mut temp_point = point.clone();
+ let mut weight = 1.0;
+
+ for _ in 0..depth {
+ accum += weight * self.noise(&temp_point);
+ weight *= 0.5;
+ temp_point *= 2.0;
+ }
+ accum.abs()
+ }
+
+ pub fn noise(&self, point: &Point3) -> f64 {
+ let u = point.x - point.x.floor();
+ let v = point.y - point.y.floor();
+ let w = point.z - point.z.floor();
+ let i = point.x.floor() as i32;
+ let j = point.y.floor() as i32;
+ let k = point.z.floor() as i32;
+ let mut c: [[[Vec3; 2]; 2]; 2] = Default::default();
+ for di in 0..2 {
+ for dj in 0..2 {
+ for dk in 0..2 {
+ c[di][dj][dk] = self
+ .ranvec
+ .get(
+ (self.perm_x.get(((i + di as i32) & 255) as usize).unwrap()
+ ^ self.perm_y.get(((j + dj as i32) & 255) as usize).unwrap()
+ ^ self.perm_z.get(((k + dk as i32) & 255) as usize).unwrap())
+ as usize,
+ )
+ .unwrap().clone();
+ }
+ }
+ }
+ Self::trilinear_interpolate(c, u, v, w)
+ }
+
+ fn trilinear_interpolate(c: [[[Vec3; 2]; 2]; 2], u: f64, v: f64, w: f64) -> f64 {
+ let uu = u * u * (3.0 - 2.0 * u);
+ let vv = v * v * (3.0 - 2.0 * v);
+ let ww = w * w * (3.0 - 2.0 * w);
+ let mut accum: f64 = 0.0;
+ for i in 0..2 {
+ for j in 0..2 {
+ for k in 0..2 {
+ let i_f = i as f64;
+ let j_f = j as f64;
+ let k_f = k as f64;
+ let weight_v = Vec3 { x: u - i_f, y: v - j_f, z: w - k_f };
+ accum += (i_f * uu + (1.0 - i_f) * (1.0 - uu)) *
+ (j_f * vv + (1.0 - j_f) * (1.0 - vv)) *
+ (k_f * ww + (1.0 - k_f) * (1.0 - ww)) *
+ c[i][j][k].dot(&weight_v);
+ }
+ }
+ }
+ accum
+ }
+
+ fn generate_perm() -> Vec<usize> {
+ let mut p = (0..POINT_COUNT).collect();
+ Self::permute(&mut p, POINT_COUNT);
+ p
+ }
+
+ fn permute(p: &mut Vec<usize>, n: usize) {
+ for i in (1..n).rev() {
+ p.swap(i, rand::random::<usize>() % i);
+ }
+ }
+}
diff --git a/src/texture/solid_color.rs b/src/texture/solid_color.rs
new file mode 100644
index 0000000..3af46ca
--- /dev/null
+++ b/src/texture/solid_color.rs
@@ -0,0 +1,20 @@
+use super::Texture;
+use crate::{vec3::Color, vec3::Point3};
+
+pub struct SolidColor {
+ pub color_value: Color,
+}
+
+impl SolidColor {
+ pub fn from_color(color_value: Color) -> Self {
+ Self {
+ color_value,
+ }
+ }
+}
+
+impl Texture for SolidColor {
+ fn value(&self, _: f64, _: f64, _: &Point3) -> Color {
+ self.color_value.clone()
+ }
+}
diff --git a/src/util.rs b/src/util.rs
new file mode 100644
index 0000000..cbd5813
--- /dev/null
+++ b/src/util.rs
@@ -0,0 +1,3 @@
+pub fn degrees_to_radians(degrees: f64) -> f64 {
+ degrees * std::f64::consts::PI / 180.0
+}
diff --git a/src/vec3.rs b/src/vec3.rs
new file mode 100644
index 0000000..8525fdd
--- /dev/null
+++ b/src/vec3.rs
@@ -0,0 +1,201 @@
+use auto_ops::{impl_op_ex, impl_op_ex_commutative};
+use std::fmt;
+
+pub type Point3 = Vec3;
+pub type Color = Vec3;
+
+#[derive(Clone, Default)]
+pub struct Vec3 {
+ pub x: f64,
+ pub y: f64,
+ pub z: f64,
+}
+
+impl Vec3 {
+ pub fn new() -> Vec3 {
+ Vec3 {
+ x: 0.0,
+ y: 0.0,
+ z: 0.0,
+ }
+ }
+
+ pub fn get(&self, index: usize) -> Option<&f64> {
+ match index {
+ 0 => Some(&self.x),
+ 1 => Some(&self.y),
+ 2 => Some(&self.z),
+ _ => None,
+ }
+ }
+
+ pub fn get_mut(&mut self, index: usize) -> Option<&mut f64> {
+ match index {
+ 0 => Some(&mut self.x),
+ 1 => Some(&mut self.y),
+ 2 => Some(&mut self.z),
+ _ => None,
+ }
+ }
+
+ pub fn length(&self) -> f64 {
+ self.length_squared().sqrt()
+ }
+
+ pub fn length_squared(&self) -> f64 {
+ self.x * self.x + self.y * self.y + self.z * self.z
+ }
+
+ pub fn dot(&self, other: &Vec3) -> f64 {
+ self.x * other.x + self.y * other.y + self.z * other.z
+ }
+
+ pub fn cross(&self, other: &Vec3) -> Vec3 {
+ Vec3 {
+ x: self.y * other.z - self.z * other.y,
+ y: self.z * other.x - self.x * other.z,
+ z: self.x * other.y - self.y * other.x,
+ }
+ }
+
+ pub fn unit_vector(&self) -> Vec3 {
+ self / self.length()
+ }
+
+ pub fn random() -> Vec3 {
+ Vec3 {
+ x: rand::random::<f64>(),
+ y: rand::random::<f64>(),
+ z: rand::random::<f64>(),
+ }
+ }
+
+ pub fn random_in_range(min: f64, max: f64) -> Vec3 {
+ Vec3 {
+ x: min + (max - min) * rand::random::<f64>(),
+ y: min + (max - min) * rand::random::<f64>(),
+ z: min + (max - min) * rand::random::<f64>(),
+ }
+ }
+
+ pub fn random_in_unit_sphere() -> Vec3 {
+ loop {
+ let p = Vec3 {
+ x: 2.0 * rand::random::<f64>() - 1.0,
+ y: 2.0 * rand::random::<f64>() - 1.0,
+ z: 2.0 * rand::random::<f64>() - 1.0,
+ };
+ if p.length_squared() < 1.0 {
+ return p;
+ }
+ };
+ }
+
+ pub fn random_unit_vector() -> Vec3 {
+ Self::random_in_unit_sphere().unit_vector()
+ }
+
+ pub fn random_in_unit_disk() -> Vec3 {
+ loop {
+ let p = Vec3 {
+ x: 2.0 * rand::random::<f64>() -1.0,
+ y: 2.0 * rand::random::<f64>() -1.0,
+ z: 0.0,
+ };
+ if p.length_squared() < 1.0 {
+ return p;
+ }
+ }
+ }
+
+ pub fn near_zero(&self) -> bool {
+ const S: f64 = 1e-8;
+ self.x.abs() < S && self.y.abs() < S && self.z.abs() < S
+ }
+
+ pub fn reflect(&self, normal: &Vec3) -> Vec3 {
+ self - 2.0 * self.dot(normal) * normal
+ }
+
+ pub fn refract(&self, normal: &Vec3, etai_over_etat: f64) -> Vec3 {
+ let cos_theta = normal.dot(&-self).min(1.0);
+ let r_out_perp = etai_over_etat * (self + cos_theta * normal);
+ let r_out_parallel = -((1.0 - r_out_perp.length_squared()).abs().sqrt()) * normal;
+ r_out_perp + r_out_parallel
+ }
+
+ pub fn has_infinite_member(&self) -> bool {
+ self.x.is_infinite() || self.y.is_infinite() || self.z.is_infinite()
+ }
+}
+
+impl fmt::Display for Vec3 {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(f, "{} {} {}", self.x, self.y, self.z)
+ }
+}
+
+impl<'a> IntoIterator for &'a Vec3 {
+ type Item = f64;
+ type IntoIter = Vec3Iterator<'a>;
+
+ fn into_iter(self) -> Self::IntoIter {
+ Vec3Iterator {
+ vec3: self,
+ index: 0,
+ }
+ }
+}
+
+pub struct Vec3Iterator<'a> {
+ vec3: &'a Vec3,
+ index: usize,
+}
+
+impl<'a> Iterator for Vec3Iterator<'a> {
+ type Item = f64;
+ fn next(&mut self) -> Option<f64> {
+ let result = match self.index {
+ 0 => self.vec3.x,
+ 1 => self.vec3.y,
+ 2 => self.vec3.z,
+ _ => return None,
+ };
+ self.index += 1;
+ Some(result)
+ }
+}
+
+impl_op_ex!(- |a: &Vec3| -> Vec3 {
+ Vec3 {
+ x: -a.x,
+ y: -a.y,
+ z: -a.z,
+ }
+});
+impl_op_ex!(+= |lhs: &mut Vec3, rhs: Vec3| { *lhs = Vec3 { x: lhs.x + rhs.x, y: lhs.y + rhs.y, z: lhs.z + rhs.z } });
+impl_op_ex!(*= |lhs: &mut Vec3, rhs: &f64| { *lhs = Vec3 { x: lhs.x * rhs, y: lhs.y * rhs, z: lhs.z * rhs } });
+impl_op_ex!(/= |lhs: &mut Vec3, rhs: &f64| { *lhs *= 1.0 / rhs });
+impl_op_ex!(+ |lhs: &Vec3, rhs: &Vec3| -> Vec3 { Vec3 { x: lhs.x + rhs.x, y: lhs.y + rhs.y, z: lhs.z + rhs.z } });
+impl_op_ex!(-|lhs: &Vec3, rhs: &Vec3| -> Vec3 {
+ Vec3 {
+ x: lhs.x - rhs.x,
+ y: lhs.y - rhs.y,
+ z: lhs.z - rhs.z,
+ }
+});
+impl_op_ex!(*|lhs: &Vec3, rhs: &Vec3| -> Vec3 {
+ Vec3 {
+ x: lhs.x * rhs.x,
+ y: lhs.y * rhs.y,
+ z: lhs.z * rhs.z,
+ }
+});
+impl_op_ex_commutative!(*|lhs: &Vec3, rhs: &f64| -> Vec3 {
+ Vec3 {
+ x: lhs.x * rhs,
+ y: lhs.y * rhs,
+ z: lhs.z * rhs,
+ }
+});
+impl_op_ex!(/ |lhs: &Vec3, rhs: &f64| -> Vec3 { lhs * (1.0/rhs) });