commit 5cc0b49cd9eb81d8d28d4ecb2c598adf348fa417 Author: Robert Garrett Date: Sat Jun 3 09:48:54 2023 -0500 Mid-Material save point I get lazy with commits when following guided material. There's a design challenge to approach, now. I'm saving here so I can revert changes in case it goes sideways. Materials are proving to be a little complicated in Rust semantics. The Ray Tracing in a Weekend book uses shared_ptr's, but Rust doesn't really like that. I'm doing references with lifetime annotations. Hopefully I can get that the right way around to work out. The materials themselves look like reasonable candidates for describing as Enums. This takes away the ability to add new ones by simply impl'ing a trait, but that was never gonna happen anyway. The code would be modified and recompiled. There's no difference in maintenance cost if that's a new struct impl'ing a trait, or adding enum members. diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..c91eba2 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "rustpt" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +rand = { version = "0.8.5", features = ["small_rng"] } + diff --git a/src/camera.rs b/src/camera.rs new file mode 100644 index 0000000..4804641 --- /dev/null +++ b/src/camera.rs @@ -0,0 +1,52 @@ + +/* + * let viewport = (aspect_ratio * 2.0, 2.0); + let focal_length = 1.0; + + let origin = Vec3::new(0.0, 0.0, 0.0); + let horizontal = Vec3::new(viewport.0, 0.0, 0.0); + let vertical = Vec3::new(0.0, viewport.1, 0.0); + + let lower_left_corner = origin - horizontal/2.0 - vertical/2.0 - Vec3::new(0.0, 0.0, focal_length); + */ + +use crate::vec3::Vec3; +use crate::ray::Ray; + +pub struct Camera { + pub origin: Vec3, + pub lower_left_corner: Vec3, + pub horizontal: Vec3, + pub vertical: Vec3, +} + +impl Camera { + pub fn new() -> Camera { + let aspect_ratio = 16.0 / 9.0; + let vp_height = 2.0; + let vp_width = aspect_ratio * vp_height; + let focal_length = 1.0; + + let horiz = Vec3::new(vp_width, 0.0, 0.0); + let verti = Vec3::new(0.0, vp_height, 0.0); + let orig = Vec3::zero(); + + Camera{ + origin: orig, + lower_left_corner: orig - horiz/2.0 - verti/2.0 - Vec3::new(0.0, 0.0, focal_length), + horizontal: horiz, + vertical: verti, + } + } + + pub fn get_ray(&self, u: f32, v: f32) -> Ray { + let dir = self.lower_left_corner + + self.horizontal * u + + self.vertical * v + - self.origin; + Ray{ + orig: self.origin, + dir, + } + } +} diff --git a/src/hittable.rs b/src/hittable.rs new file mode 100644 index 0000000..9e88cb6 --- /dev/null +++ b/src/hittable.rs @@ -0,0 +1,66 @@ + +use crate::vec3::Vec3; +use crate::ray::Ray; +use crate::material::Material; + +pub struct HitRecord<'a>{ + pub p: Vec3, + pub normal: Vec3, + pub material: Option<&'a Material>, + pub t: f32, + pub front_face: bool, +} + +impl<'a> HitRecord<'a>{ + pub fn set_face_normal(&mut self, r: Ray, outward_normal: Vec3) -> (){ + self.front_face = Vec3::dot(r.dir, outward_normal) < 0.0; + self.normal = if self.front_face { outward_normal } else { -outward_normal }; + } +} + +pub trait Hittable { + fn hit(&self, r: Ray, t_min: f32, t_max: f32) -> Option; +} + +pub struct HittableList{ + hittables: Vec>, +} + +impl HittableList{ + pub fn new() -> HittableList { + HittableList { + hittables: Vec::>::new() + } + } + pub fn add(&mut self, hittable: Box ) -> () { + self.hittables.push(hittable); + } + pub fn clear(&mut self) -> () { + self.hittables.clear(); + } +} + +impl Hittable for HittableList{ + fn hit(&self, r: Ray, t_min: f32, t_max: f32) -> Option{ + let mut might_return = HitRecord { + p: Vec3::zero(), + normal: Vec3::zero(), + material: None, + t: t_max, + front_face: false, + }; + let mut hit_anything = false; + + for item in &self.hittables { + if let Some(record) = item.hit(r, t_min, might_return.t){ + hit_anything = true; + might_return = record; + } + } + if hit_anything{ + return Some(might_return); + } else { return None; } + } +} + + diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..5ecee08 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,116 @@ + + +mod vec3; +mod ray; +mod camera; +mod material; +mod hittable; +mod sphere; + +use crate::vec3::Vec3; +use crate::ray::Ray; +use crate::sphere::Sphere; +use crate::hittable::{ + Hittable, + HittableList, +}; + +use crate::camera::Camera; +use rand::{Rng, SeedableRng}; +use rand::rngs::SmallRng; +use rand::distributions::Uniform; + +fn main() { + // image + let aspect_ratio = 16.0 / 9.0; + let image = ( + 400, + (400.0 / aspect_ratio) as i32 + ); + let samples_per_pixel = 100; + let max_depth = 50; + + // world + + let mut world = HittableList::new(); + world.add( + Box::new( + Sphere{ + center: Vec3{ x: 0.0, y: 0.0, z: -1.0}, + radius: 0.5 + material: None, + } + ) + ); + world.add( + Box::new( + Sphere{ + center: Vec3{ x: 0.0, y: -100.5, z: -1.0 }, + radius: 100.0, + material: None, + } + ) + ); + // camera + + let cam = Camera::new(); + + // render + let mut small_rng = SmallRng::from_entropy(); + let distrib = Uniform::new(0.0, 1.0); + println!("P3\n{} {}\n255", image.0, image.1); + for y in (0..image.1).rev() { + eprintln!("Scanlines remaining: {}", image.1 - y); + for x in 0..image.0 { + let mut color = Vec3::zero(); + for _ in 0..samples_per_pixel { + let u = ((x as f32) + small_rng.sample(distrib)) / ((image.0 - 1) as f32); + let v = ((y as f32) + small_rng.sample(distrib)) / ((image.1 - 1) as f32); + let ray = cam.get_ray(u, v); + color+= ray_color(ray, &world, max_depth, &mut small_rng, distrib); + } + println!("{}", color.print_ppm(samples_per_pixel)); + } + } + eprintln!("Done!"); +} + +fn ray_color(r: Ray, world: &HittableList, depth: u32, srng: &mut SmallRng, distrib: Uniform ) -> Vec3 { + // recursion depth guard + if depth == 0 { + return Vec3::new(0.0, 0.0, 0.0); + } + + if let Some(rec) = world.hit(r, 0.001, f32::INFINITY){ + let target = rec.p + rec.normal + Vec3::rand_unit_vector(srng, distrib); + return ray_color( + Ray{ + orig: rec.p, + dir: target - rec.p, + }, + world, depth, srng, distrib + ) * 0.5; + } + let unitdir = Vec3::as_unit(&r.dir); + let t = 0.5 * (unitdir.y + 1.0); + return Vec3::ones() * (1.0 - t) + Vec3::new(0.5, 0.7, 1.0) * t +} + +fn degrees_to_radians(degrees: f32) -> f32 { + degrees * std::f32::consts::PI / 180.0 +} + +fn hit_sphere(center: Vec3, radius: f32, ray: &Ray) -> f32{ + let oc = ray.orig - center; + let a = ray.dir.length_squared(); + let half_b = Vec3::dot(oc, ray.dir); + let c = oc.length_squared() - radius*radius; + let discriminant = half_b*half_b - a*c; + + if discriminant < 0.0 { + return -1.0; + } else { + return (-half_b - discriminant.sqrt()) / a; + } +} + diff --git a/src/material.rs b/src/material.rs new file mode 100644 index 0000000..6b1db74 --- /dev/null +++ b/src/material.rs @@ -0,0 +1,13 @@ + +use crate::ray::Ray; +use crate::hittable::HitRecord; +use crate::Vec3; + +pub struct Material; + +pub Scatter { + fn scatter( + ray_in: Ray, rec: HitRecord, attenuation: Vec3, scattered: Ray + ) -> bool; +} + diff --git a/src/ray.rs b/src/ray.rs new file mode 100644 index 0000000..314f6a5 --- /dev/null +++ b/src/ray.rs @@ -0,0 +1,37 @@ + +use crate::vec3::Vec3; +use crate::hittable::{ + Hittable, + HitRecord, +}; + +#[derive(Copy)] +#[derive(Clone)] +pub struct Ray{ + pub orig: Vec3, + pub dir: Vec3, +} + +impl Ray{ + pub fn at(&self, t: f32) -> Vec3 { + self.orig + self.dir*t + } +} + +#[cfg(test)] +mod test{ + use super::*; + + #[test] + fn check_lerp(){ + let ray = Ray{ + orig: Vec3::new(0.0, 0.0, 0.0), + dir: Vec3::new(1.0, 1.0, 0.0) + }; + let half = ray.at(0.5); + assert_eq!( + half, + Vec3::new(0.5, 0.5, 0.0) + ); + } +} diff --git a/src/sphere.rs b/src/sphere.rs new file mode 100644 index 0000000..719f27d --- /dev/null +++ b/src/sphere.rs @@ -0,0 +1,51 @@ + +use crate::vec3::Vec3; +use crate::hittable::{ + Hittable, + HitRecord, +}; +use crate::material::Material; + +use crate::ray::Ray; + +pub struct Sphere<'a>{ + pub center: Vec3, + pub radius: f32, + pub material: &'a Material, +} + +impl<'a> Hittable for Sphere<'a> { + fn hit(&self, r: Ray, t_min: f32, t_max: f32) -> Option{ + let oc = r.orig - self.center; + let a = r.dir.length_squared(); + let half_b = Vec3::dot(oc, r.dir); + 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(); + + // nearest root that lies within tolerance + let root = (-half_b - sqrtd) / a; + if root < t_min || root > t_max { + let root = (-half_b + sqrtd) / a; + if root < t_min || root > t_max { + return None; + } + } + + let mut record = HitRecord{ + p: r.at(root), + normal: (r.at(root) - self.center) / self.radius, + material: Some(self.material), + t: root, + front_face: false, + }; + let outward_normal = (record.p - self.center) / self.radius; + record.set_face_normal(r, outward_normal); + Some(record) + } +} + diff --git a/src/vec3.rs b/src/vec3.rs new file mode 100644 index 0000000..748b1ce --- /dev/null +++ b/src/vec3.rs @@ -0,0 +1,485 @@ + +use std::ops::{ + Add, + AddAssign, + Sub, + SubAssign, + Mul, + MulAssign, + Div, + DivAssign, + Neg, +}; +use std::fmt; +use std::fmt::Display; + +use rand::Rng; +use rand::rngs::SmallRng; +use rand::distributions::Uniform; + +#[derive(Copy)] +#[derive(Clone)] +#[derive(PartialEq)] +#[derive(PartialOrd)] +#[derive(Debug)] +pub struct Vec3{ + pub x: f32, + pub y: f32, + pub z: f32, +} + +impl Vec3{ + pub fn new(x: f32, y: f32, z: f32) -> Vec3{ + Vec3{x, y, z} + } + + pub fn zero() -> Vec3{ + Vec3{ + x: 0.0, + y: 0.0, + z: 0.0, + } + } + + pub fn ones() -> Vec3{ + Vec3 { + x: 1.0, + y: 1.0, + z: 1.0 + } + } + + pub fn rand(srng: &mut SmallRng, distrib: Uniform) -> Vec3 { + Vec3{ + x: srng.sample(distrib), + y: srng.sample(distrib), + z: srng.sample(distrib), + } + } + + pub fn rand_in_unit_sphere(srng: &mut SmallRng, distrib: Uniform) -> Vec3 { + loop { + let p = Vec3::rand(srng, distrib); + if p.length_squared() >= 1.0 { continue; } + else { return p; } + } + } + + pub fn rand_unit_vector(srng: &mut SmallRng, distrib: Uniform) -> Vec3 { + return Vec3::as_unit(&Vec3::rand_in_unit_sphere(srng, distrib)); + } + + pub fn length(&self) -> f32 { + self.length_squared().sqrt() + } + + pub fn length_squared(&self) -> f32 { + (self.x*self.x) + (self.y*self.y) + (self.z*self.z) + } + + // roughly equivalent to the `void write_color(...)` in the book + pub fn print_ppm(&self, samples_per_pixel: u32) -> String { + + let scale = 1.0 / samples_per_pixel as f32; + + // now with gamma correction + let r = (self.x * scale).sqrt(); + let g = (self.y * scale).sqrt(); + let b = (self.y * scale).sqrt(); + + let ir = (Vec3::clamp(r, 0.0, 0.999) * 256.0) as i32; + let ig = (Vec3::clamp(g, 0.0, 0.999) * 256.0) as i32; + let ib = (Vec3::clamp(b, 0.0, 0.999) * 256.0) as i32; + format!("{} {} {}", ir, ig, ib) + } + + pub fn dot(left: Vec3, right: Vec3) -> f32{ + left.x * right.x + + left.y * right.y + + left.z * right.z + } + + pub fn cross(u: Vec3, v: Vec3) -> Vec3{ + Vec3{ + x: u.y * v.z - u.z * v.y, + y: u.z * v.x - u.x * v.z, + z: u.x * v.y - u.y * v.x + } + } + + pub fn as_unit(v: &Vec3) -> Vec3 { + let len = v.length(); + *v / len + } + + fn clamp(input: f32, min: f32, max: f32) -> f32 { + if input < min { + return min; + } else if input > max { + return max; + } else { + return input; + } + } +} +impl Add for Vec3 { + type Output = Vec3; + fn add(self, other: Vec3) -> Vec3 { + Vec3{ + x: self.x + other.x, + y: self.y + other.y, + z: self.z + other.z, + } + } +} + +impl AddAssign for Vec3 { + fn add_assign(&mut self, other: Vec3){ + *self = Self { + x: self.x + other.x, + y: self.y + other.y, + z: self.z + other.z + }; + } +} + +impl Sub for Vec3 { + type Output = Vec3; + fn sub(self, other: Vec3) -> Vec3 { + Vec3 { + x: self.x - other.x, + y: self.y - other.y, + z: self.z - other.z, + } + } +} + +impl SubAssign for Vec3 { + fn sub_assign(&mut self, other: Vec3){ + *self = Self { + x: self.x - other.x, + y: self.y - other.y, + z: self.z - other.z + }; + } +} + +impl Mul for Vec3 { + type Output = Vec3; + fn mul(self, other: Vec3) -> Vec3 { + Vec3 { + x: self.x * other.x, + y: self.y * other.y, + z: self.z * other.z, + } + } +} + +impl Mul for Vec3{ + type Output = Vec3; + fn mul(self, other: f32) -> Vec3 { + Vec3 { + x: self.x * other, + y: self.y * other, + z: self.z * other, + } + } + +} + +impl MulAssign for Vec3 { + fn mul_assign(&mut self, other: Vec3){ + *self = Self { + x: self.x * other.x, + y: self.y * other.y, + z: self.z * other.z + }; + } +} + +impl MulAssign for Vec3{ + fn mul_assign(&mut self, other: f32){ + *self = Self { + x: self.x * other, + y: self.y * other, + z: self.z * other + }; + } +} + +impl Div for Vec3 { + type Output = Vec3; + fn div(self, other: Vec3) -> Vec3 { + Vec3 { + x: self.x / other.x, + y: self.y / other.y, + z: self.z / other.z, + } + } +} + +impl Div for Vec3 { + type Output = Vec3; + fn div(self, other: f32) -> Vec3 { + Vec3 { + x: self.x / other, + y: self.y / other, + z: self.z / other, + } + } +} + +impl DivAssign for Vec3 { + fn div_assign(&mut self, other: Vec3){ + *self = Self { + x: self.x / other.x, + y: self.y / other.y, + z: self.z / other.z + }; + } +} + +impl DivAssign for Vec3 { + fn div_assign(&mut self, other: f32){ + *self = Self { + x: self.x / other, + y: self.y / other, + z: self.z / other + }; + } +} + +impl Neg for Vec3{ + type Output = Self; + fn neg(self) -> Self::Output { + Vec3{ + x: -self.x, + y: -self.y, + z: -self.z, + } + } +} + +impl Display for Vec3 { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + let str = format!("{} {} {}", self.x, self.y, self.z); + fmt.write_str(&str)?; + Ok(()) + + } +} + + +#[cfg(test)] +mod test{ + use super::*; + + #[test] + fn test_add(){ + let v1 = Vec3::new(1.0, 1.0, 0.0); + let v2 = Vec3::new(0.0, 0.0, 1.0); + + let expected = Vec3::new(1.0, 1.0, 1.0); + + assert_eq!( v1+v2, expected ); + } + + #[test] + fn test_add_assign(){ + let mut v1 = Vec3::new(0.0, 1.0, 1.0); + let v2 = Vec3::new(1.0, 0.0, 0.0); + + let expected = Vec3::new(1.0, 1.0, 1.0); + + v1+=v2; + assert_eq!( v1, expected ); + } + + #[test] + fn test_sub(){ + let v1 = Vec3::new(1.0, 1.0, 0.0); + let v2 = Vec3::new(0.0, 0.0, 1.0); + + let expected = Vec3::new(1.0, 1.0, -1.0); + + assert_eq!( v1-v2, expected ); + } + + #[test] + fn test_sub_assign(){ + let mut v1 = Vec3::new(0.0, 1.0, 1.0); + let v2 = Vec3::new(1.0, 0.0, 0.0); + + let expected = Vec3::new(-1.0, 1.0, 1.0); + + v1-=v2; + assert_eq!( v1, expected ); + } + + #[test] + fn test_mul_vec(){ + let v1 = Vec3::new(0.1, 0.5, 0.7); + let v2 = Vec3::new(1.0, 2.0, 1.0); + + let expected = Vec3::new(0.1, 1.0, 0.7); + + assert_eq!( v1*v2, expected ); + } + + #[test] + fn test_mul_float(){ + let v1 = Vec3::new(0.1, 0.5, 0.7); + let f1 = 0.5; + + let expected = Vec3::new(0.05, 0.25, 0.35); + + assert_eq!( v1*f1, expected ); + } + + #[test] + fn test_mul_vec_assign(){ + let mut v1 = Vec3::new(0.1, 0.5, 0.7); + let v2 = Vec3::new(1.0, 2.0, 1.0); + + let expected = Vec3::new(0.1, 1.0, 0.7); + + v1*=v2; + assert_eq!( v1, expected ); + } + + #[test] + fn test_mul_float_assign(){ + let mut v1 = Vec3::new(0.1, 0.5, 0.7); + let f1 = 0.5; + + let expected = Vec3::new(0.05, 0.25, 0.35); + + v1*=f1; + assert_eq!( v1, expected ); + } + + #[test] + fn test_div_vec(){ + let v1 = Vec3::new(0.1, 0.5, 0.7); + let v2 = Vec3::new(0.5, 2.0, 1.0); + + let expected = Vec3::new(0.2, 0.25, 0.7); + + assert_eq!( v1/v2, expected ); + } + + #[test] + fn test_div_float(){ + let v1 = Vec3::new(0.1, 0.5, 0.7); + let f1 = 0.5; + + let expected = Vec3::new(0.2, 1.0, 1.4); + + assert_eq!( v1/f1, expected ); + } + + #[test] + fn test_div_vec_assign(){ + let mut v1 = Vec3::new(0.1, 0.5, 0.7); + let v2 = Vec3::new(1.0, 2.0, 1.0); + + let expected = Vec3::new(0.1, 0.25, 0.7); + + v1/=v2; + assert_eq!( v1, expected ); + } + + #[test] + fn test_div_float_assign(){ + let mut v1 = Vec3::new(0.1, 0.5, 0.7); + let f1 = 0.5; + + let expected = Vec3::new(0.2, 1., 1.4); + + v1/=f1; + assert_eq!( v1, expected ); + } + + #[test] + fn test_length_squared(){ + let v = Vec3::new(2.0, 0.0, 2.0); + let len = v.length_squared(); + assert_eq!(len, 8.0); + } + + #[test] + fn test_length(){ + let v = Vec3::new(3.0, 4.0, 0.0); + let len = v.length(); + assert_eq!(len, 5.0) + } + + #[test] + fn test_dot_perpendicular(){ + let v1 = Vec3::new(1.0, 0.0, 0.0); + let v2 = Vec3::new(0.0, 1.0, 0.0); + assert_eq!(Vec3::dot(v1, v2), 0.0); + } + + #[test] + fn test_dot_parallel(){ + let v1 = Vec3::new(1.0, 0.0, 0.0); + let v2 = Vec3::new(1.0, 0.0, 0.0); + assert_eq!(Vec3::dot(v1, v2), 1.0); + } + + #[test] + fn test_cross_perpendicular(){ + let v1 = Vec3::new(1.0, 0.0, 0.0); + let v2 = Vec3::new(0.0, 1.0, 0.0); + + let expected = Vec3::new(0.0, 0.0, 1.0); + assert_eq!(Vec3::cross(v1, v2), expected); + } + + #[test] + fn test_cross_parallel(){ + let v1 = Vec3::new(1.0, 0.0, 0.0); + let v2 = Vec3::new(1.0, 0.0, 0.0); + + let expected = Vec3::new(0.0, 0.0, 0.0); + + assert_eq!(Vec3::cross(v1, v2), expected); + } + + #[test] + fn test_cross_111(){ + let v1 = Vec3::new(1.0, 1.0, 1.0); + let v2 = Vec3::new(0.0, 1.0, 0.0); + + let expected = Vec3::new(-1.0, 0.0, 1.0); + + assert_eq!(Vec3::cross(v1, v2), expected); + } + + #[test] + fn test_unit_shorten(){ + let v = Vec3::new(2.0, 0.0, 0.0); + let expected = Vec3::new(1.0, 0.0, 0.0); + + assert_eq!(Vec3::as_unit(&v), expected); + } + + #[test] + fn test_unit_lengthen(){ + let v = Vec3::new(0.5, 0.0, 0.0); + let expected = Vec3::new(1.0, 0.0, 0.0); + + assert_eq!(Vec3::as_unit(&v), expected); + } + + #[test] + fn test_unit_111(){ + let v = Vec3::new(1.0, 1.0, 1.0); + let expected = Vec3::new(0.577350269,0.577350269,0.577350269); + + assert!(Vec3::as_unit(&v) <= expected * 1.001); // within very small under-estimate + assert!(Vec3::as_unit(&v) >= expected * 0.999); // within very small over-estimate + } +} +