From e574cdb5921cbf44217df149c812c9733260ee77 Mon Sep 17 00:00:00 2001 From: Bill Thiede Date: Sat, 11 Jun 2022 17:46:26 -0700 Subject: [PATCH] Random changes. --- rtchallenge/examples/eoc14.rs | 232 ++++++++++++++++++++++++++++++++++ rtchallenge/examples/glass.rs | 115 +++++++++++++++++ rtchallenge/src/camera.rs | 2 +- rtchallenge/src/lib.rs | 1 + rtchallenge/src/shapes.rs | 133 ++++++++++++++++++- 5 files changed, 477 insertions(+), 6 deletions(-) create mode 100644 rtchallenge/examples/eoc14.rs create mode 100644 rtchallenge/examples/glass.rs diff --git a/rtchallenge/examples/eoc14.rs b/rtchallenge/examples/eoc14.rs new file mode 100644 index 0000000..05c1065 --- /dev/null +++ b/rtchallenge/examples/eoc14.rs @@ -0,0 +1,232 @@ +use std::time::Instant; + +use anyhow::Result; +use structopt::StructOpt; + +use rtchallenge::prelude::*; + +use rtchallenge::{ + camera::RenderStrategy, + float::consts::PI, + patterns::{test_pattern, BLACK_PAT, WHITE_PAT}, + WHITE, +}; + +/// End of chapter 14 challenge. +#[derive(StructOpt, Debug)] +#[structopt(name = "eoc14")] +struct Opt { + /// Strategy for casting rays into image. + #[structopt(long, default_value = "rayon")] + render_strategy: RenderStrategy, + /// Number of samples per pixel. 0 renders from the center of the pixel, 1 or more samples N + /// times randomly across the pixel. + #[structopt(short, long, default_value = "0")] + samples: usize, + /// Rendered image width in pixels. + #[structopt(short, long, default_value = "2560")] + width: usize, + /// Rendered image height in pixels. + #[structopt(short, long, default_value = "1440")] + height: usize, +} + +fn main() -> Result<()> { + let start = Instant::now(); + let opt = Opt::from_args(); + + let light1 = PointLightBuilder::default() + .position(point(-5., 5., -5.)) + .intensity(WHITE) + .build()?; + let light2 = PointLightBuilder::default() + .position(point(5., 5., -5.)) + .intensity([0.2, 0.2, 0.6]) + .build()?; + let light3 = PointLightBuilder::default() + .position(point(0., 2., -5.)) + .intensity([0.2, 0.2, 0.1]) + .build()?; + + let from = point(2., 8., -10.); + let to = point(2., 2., -1.); + let up = point(0., 1., 0.); + let camera = CameraBuilder::default() + .hsize(opt.width) + .vsize(opt.height) + .field_of_view(PI / 12.) + .transform(view_transform(from, to, up)) + .render_strategy(opt.render_strategy) + .samples_per_pixel(opt.samples) + .build()?; + + let floor = plane() + .material( + MaterialBuilder::default() + .color( + checkers_pattern( + ring_pattern([0.8, 0.8, 0.2].into(), [0.8, 0.2, 0.8].into()) + .transform(scaling(1. / 8., 1. / 8., 1. / 8.)) + .build()?, + ring_pattern([0.2, 0.8, 0.2].into(), [0.8, 0.2, 0.2].into()) + .transform(scaling(1. / 4., 1. / 4., 1. / 4.)) + .build()?, + ) + .transform(translation(0., 1., 0.) * scaling(2., 2., 2.)) + .build()?, + ) + .specular(0.) + .reflective(0.5) + .build()?, + ) + .build()?; + let sphere_size = scaling(0.5, 0.5, 0.5); + + let x1y1 = sphere() + .transform(translation(1., 1., 0.) * sphere_size) + .material( + MaterialBuilder::default() + .color( + gradient_pattern([0., 0., 1.].into(), [1., 1., 0.].into()) + .transform(scaling(2., 1., 1.) * translation(-0.5, 0., 0.)) + .build()?, + ) + .diffuse(0.7) + .specular(0.3) + .reflective(0.5) + .build()?, + ) + .build()?; + + let x2y1 = sphere() + .transform(translation(2., 1., 0.) * sphere_size) + .material( + MaterialBuilder::default() + .color(stripe_pattern(WHITE_PAT, BLACK_PAT).build()?) + .diffuse(0.7) + .specular(0.3) + .build()?, + ) + .build()?; + + let x3y1 = sphere() + .transform(translation(3., 1., 0.) * sphere_size) + .material( + MaterialBuilder::default() + .color( + stripe_pattern(WHITE_PAT, BLACK_PAT) + .transform(scaling(0.2, 1., 1.)) + .build()?, + ) + .diffuse(0.7) + .specular(0.0) + .build()?, + ) + .build()?; + + let x1y2 = sphere() + .transform(translation(1., 2., 0.) * sphere_size) + .material( + MaterialBuilder::default() + .color(test_pattern().build()?) + .diffuse(0.7) + .specular(0.3) + .build()?, + ) + .build()?; + + let x2y2 = sphere() + .transform(translation(2., 2., 0.) * sphere_size) + .material( + MaterialBuilder::default() + .color( + ring_pattern(WHITE_PAT, BLACK_PAT) + .transform(scaling(0.2, 0.2, 0.2)) + .build()?, + ) + .diffuse(0.7) + .specular(0.3) + .build()?, + ) + .build()?; + + let x3y2 = sphere() + .transform(translation(3., 2., 0.) * sphere_size) + .material( + MaterialBuilder::default() + .color( + checkers_pattern(WHITE_PAT, BLACK_PAT) + .transform(scaling(0.5, 0.5, 0.5)) + .build()?, + ) + .diffuse(0.7) + .specular(0.3) + .build()?, + ) + .build()?; + + let x1y2z1 = sphere() + .transform(translation(1., 2., -1.) * sphere_size) + .material( + MaterialBuilder::default() + .color([0.8, 0.2, 0.2]) + .diffuse(0.7) + .specular(0.3) + .transparency(0.5) + .build()?, + ) + .build()?; + + let x2y2z1 = sphere() + .transform(translation(2., 2., -1.) * sphere_size) + .material( + MaterialBuilder::default() + .color([0.8, 0.2, 0.2]) + .diffuse(0.7) + .specular(0.3) + .transparency(0.9) + .refractive_index(1.5) + .build()?, + ) + .build()?; + + let x3y2z1 = cube() + .transform( + translation(3., 2., -1.) * (sphere_size * scaling(0.5, 0.5, 0.5)) * rotation_y(PI / 4.), + ) + .material( + MaterialBuilder::default() + .color([0.8, 0.8, 0.2]) + .diffuse(0.7) + .specular(0.3) + .reflective(0.5) + .build()?, + ) + .build()?; + + let x2y4z1 = sphere() + .transform(translation(2., 4., 0.) * sphere_size) + .material( + MaterialBuilder::default() + .color([0.2, 0.2, 0.8]) + .diffuse(0.7) + .specular(0.3) + .build()?, + ) + .build()?; + + let world = WorldBuilder::default() + .lights(vec![light1, light2, light3]) + .objects(vec![ + floor, x1y1, x2y1, x3y1, x1y2, x2y2, x3y2, x1y2z1, x2y2z1, x2y4z1, x3y2z1, + ]) + .build()?; + + let image = camera.render(&world); + + let path = "/tmp/eoc14.png"; + println!("saving output to {}", path); + image.write_to_file(path)?; + println!("Render time {:.3} seconds", start.elapsed().as_secs_f32()); + Ok(()) +} diff --git a/rtchallenge/examples/glass.rs b/rtchallenge/examples/glass.rs new file mode 100644 index 0000000..150b01f --- /dev/null +++ b/rtchallenge/examples/glass.rs @@ -0,0 +1,115 @@ +use std::time::Instant; + +use anyhow::Result; +use structopt::StructOpt; + +use rtchallenge::prelude::*; + +use rtchallenge::{ + camera::RenderStrategy, + float::consts::PI, + patterns::{test_pattern, BLACK_PAT, WHITE_PAT}, + WHITE, +}; + +/// Experiment with transparency. +#[derive(StructOpt, Debug)] +#[structopt(name = "glass")] +struct Opt { + /// Strategy for casting rays into image. + #[structopt(long, default_value = "rayon")] + render_strategy: RenderStrategy, + /// Number of samples per pixel. 0 renders from the center of the pixel, 1 or more samples N + /// times randomly across the pixel. + #[structopt(short, long, default_value = "0")] + samples: usize, + /// Rendered image width in pixels. + #[structopt(short, long, default_value = "2560")] + width: usize, + /// Rendered image height in pixels. + #[structopt(short, long, default_value = "1440")] + height: usize, +} + +fn main() -> Result<()> { + let start = Instant::now(); + let opt = Opt::from_args(); + + let light1 = PointLightBuilder::default() + .position(point(-5., 5., -5.)) + .intensity(WHITE) + .build()?; + let light2 = PointLightBuilder::default() + .position(point(5., 5., -5.)) + .intensity([0.2, 0.2, 0.6]) + .build()?; + let light3 = PointLightBuilder::default() + .position(point(0., 2., -5.)) + .intensity([0.2, 0.2, 0.1]) + .build()?; + + let from = point(0., 0., -10.); + let to = point(0., 0., 0.); + let up = point(0., 1., 0.); + let camera = CameraBuilder::default() + .hsize(opt.width) + .vsize(opt.height) + .field_of_view(PI / 4.) + .transform(view_transform(from, to, up)) + .render_strategy(opt.render_strategy) + .samples_per_pixel(opt.samples) + .build()?; + + let floor = plane() + .transform(rotation_x(PI / 2.)) + .material( + MaterialBuilder::default() + .color(checkers_pattern(BLACK_PAT, WHITE_PAT).build()?) + .reflective(0.5) + .build()?, + ) + .build()?; + let ceiling = plane() + .transform(translation(0., 0., -20.) * rotation_x(PI / 2.)) + .material(MaterialBuilder::default().color([0.2, 0.2, 0.8]).build()?) + .build()?; + + let mut objects = Vec::new(); + for x in 0..5 { + for y in 0..5 { + let trans = y as Float / 5.; + let index = 1. + x as Float / 5.; + dbg!(x, y, trans, index); + objects.push( + sphere() + .transform( + translation(x as Float - 2., y as Float - 2., -2.) * scaling(0.5, 0.5, 0.5), + ) + .material( + MaterialBuilder::default() + .color([0.5, 0.5, 0.5]) + .transparency(trans) + .refractive_index(index) + .build()?, + ) + .build()?, + ); + } + } + + objects.push(floor); + objects.push(ceiling); + + let world = WorldBuilder::default() + .lights(vec![light1, light2, light3]) + .objects(objects) + .build()?; + + let image = camera.render(&world); + + let path = "/tmp/glass.png"; + println!("saving output to {}", path); + image.write_to_file(path)?; + println!("Render time {:.3} seconds", start.elapsed().as_secs_f32()); + Ok(()) +} diff --git a/rtchallenge/src/camera.rs b/rtchallenge/src/camera.rs index f7ddb4f..dcc42ad 100644 --- a/rtchallenge/src/camera.rs +++ b/rtchallenge/src/camera.rs @@ -22,7 +22,7 @@ use crate::{ Float, BLACK, }; -const MAX_DEPTH_RECURSION: usize = 5; +const MAX_DEPTH_RECURSION: usize = 10; #[derive(Copy, Clone, StructOpt, Debug, Deserialize)] #[serde(rename_all = "kebab-case")] diff --git a/rtchallenge/src/lib.rs b/rtchallenge/src/lib.rs index 563870a..f8c752b 100644 --- a/rtchallenge/src/lib.rs +++ b/rtchallenge/src/lib.rs @@ -50,5 +50,6 @@ pub mod prelude { transformations::view_transform, tuples::{point, vector, Color}, world::{World, WorldBuilder}, + Float, }; } diff --git a/rtchallenge/src/shapes.rs b/rtchallenge/src/shapes.rs index 22aed99..0f1428d 100644 --- a/rtchallenge/src/shapes.rs +++ b/rtchallenge/src/shapes.rs @@ -5,7 +5,7 @@ use derive_builder::Builder; use crate::{ intersections::Intersections, materials::{Material, MaterialBuilder}, - matrices::Matrix4x4, + matrices::{identity, Matrix4x4}, rays::Ray, tuples::Tuple, }; @@ -25,6 +25,8 @@ pub enum Geometry { Plane, /// AABB cube at origin from -1,1 in each direction. Cube, + /// Takes shape from children held within. + Group(Vec), } impl Default for Geometry { @@ -41,6 +43,7 @@ impl PartialEq for Geometry { (Sphere, Sphere) => true, (Plane, Plane) => true, (Cube, Cube) => true, + (Group(v1), Group(v2)) => v1 == v2, _ => false, } } @@ -50,10 +53,9 @@ impl PartialEq for Geometry { /// many different shapes based on the value of it's geometry field. Users chose the shape by /// calling the appropriate constructor, i.e. [Shape::sphere]. #[derive(Builder, Debug, Clone, PartialEq)] -#[builder(default, pattern = "owned")] +#[builder(default, pattern = "owned", build_fn(skip))] pub struct Shape { transform: Matrix4x4, - #[builder(private, default = "self.default_inverse_transform()?")] inverse_transform: Matrix4x4, pub material: Material, geometry: Geometry, @@ -78,6 +80,11 @@ pub fn cube() -> ShapeBuilder { ShapeBuilder::cube() } +/// Short hand for creating a ShapeBuilder with a group geometry. +pub fn group() -> ShapeBuilder { + ShapeBuilder::group() +} + /// Helper for producing a sphere with a glassy material. pub fn glass_sphere() -> ShapeBuilder { ShapeBuilder::sphere().material( @@ -107,9 +114,38 @@ impl ShapeBuilder { pub fn cube() -> ShapeBuilder { ShapeBuilder::default().geometry(Geometry::Cube) } + /// Short hand for creating a ShapeBuilder with a group geometry. + pub fn group() -> ShapeBuilder { + ShapeBuilder::default().geometry(Geometry::Group(Vec::new())) + } - fn default_inverse_transform(&self) -> Result { - Ok(self.transform.unwrap_or(Matrix4x4::identity()).inverse()) + /// Add child shapes, only valid for Group::Geometry. + pub fn add_child(mut self, child: Shape) -> ShapeBuilder { + if let Some(Geometry::Group(ref mut children)) = &mut self.geometry { + children.push(child); + } + self + } + + pub fn build(&self) -> Result { + let mut s = Shape::default(); + + if let Some(transform) = &self.transform { + s.set_transform(transform.clone()); + } + if let Some(material) = &self.material { + s.material = material.clone(); + }; + if let Some(geometry) = &self.geometry { + s.geometry = geometry.clone(); + }; + let transform = s.transform().clone(); + if let Geometry::Group(ref mut children) = s.geometry { + children + .into_iter() + .for_each(|c| c.set_transform(transform * c.transform())); + } + Ok(s) } } @@ -158,6 +194,14 @@ impl Shape { geometry: Geometry::Cube, } } + pub fn group() -> Shape { + Shape { + transform: Matrix4x4::identity(), + inverse_transform: Matrix4x4::identity(), + material: Material::default(), + geometry: Geometry::Group(Vec::new()), + } + } /// Find the normal at the point on the sphere. pub fn normal_at(&self, world_point: Tuple) -> Tuple { let object_point = self.inverse_transform * world_point; @@ -166,6 +210,7 @@ impl Shape { Geometry::Plane => Tuple::vector(0., 1., 0.), Geometry::TestShape(_) => object_point, Geometry::Cube => cube::local_normal_at(object_point), + Geometry::Group(_) => todo!("normal_at"), }; let mut world_normal = self.inverse_transform.transpose() * object_normal; world_normal.w = 0.; @@ -197,6 +242,7 @@ pub fn intersect<'s>(shape: &'s Shape, ray: &Ray) -> Intersections<'s> { Geometry::Plane => plane::intersect(shape, &local_ray), Geometry::TestShape(_) => test_shape::intersect(shape, &local_ray), Geometry::Cube => cube::intersect(shape, &local_ray), + Geometry::Group(_) => group::intersect(shape, &ray), } } @@ -380,6 +426,83 @@ mod cube { } } +mod group { + use crate::{intersections::Intersections, rays::Ray, shapes::Shape}; + + use super::Geometry; + + pub fn intersect<'s>(shape: &'s Shape, ray: &Ray) -> Intersections<'s> { + if let Geometry::Group(children) = &shape.geometry { + let mut intersections: Vec<_> = children + .iter() + .map(|c| super::intersect(c, ray)) + .flatten() + .collect(); + intersections.sort_by(|a, b| { + a.t.partial_cmp(&b.t) + .expect("an intersection has a t value that is NaN") + }); + return Intersections::new(intersections); + } + unreachable!(); + } + #[cfg(test)] + mod tests { + use crate::{ + matrices::{scaling, translation}, + rays::Ray, + shapes::{intersect, Shape, ShapeBuilder}, + tuples::{point, vector}, + }; + + #[test] + fn intersecting_empty_group() { + let g = Shape::group(); + let r = Ray::new(point(0., 0., 0.), vector(0., 0., 1.)); + let xs = intersect(&g, &r); + assert_eq!(xs.len(), 0); + } + + #[test] + fn intersecting_nonempty_group() -> Result<(), Box> { + let s1 = Shape::sphere(); + let s2 = ShapeBuilder::sphere() + .transform(translation(0., 0., -3.)) + .build()?; + let s3 = ShapeBuilder::sphere() + .transform(translation(5., 0., 0.)) + .build()?; + let g = ShapeBuilder::group() + .add_child(s1.clone()) + .add_child(s2.clone()) + .add_child(s3.clone()) + .build()?; + let r = Ray::new(point(0., 0., -5.), vector(0., 0., 1.)); + let xs = intersect(&g, &r); + assert_eq!(xs.len(), 4); + assert_eq!(xs[0].object, &s2); + assert_eq!(xs[1].object, &s2); + assert_eq!(xs[2].object, &s1); + assert_eq!(xs[3].object, &s1); + Ok(()) + } + #[test] + fn intersecting_transformed_group() -> Result<(), Box> { + let g = ShapeBuilder::group() + .transform(scaling(2., 2., 2.)) + .add_child( + ShapeBuilder::sphere() + .transform(translation(5., 0., 0.)) + .build()?, + ) + .build()?; + let r = Ray::new(point(10., 0., -10.), vector(0., 0., 1.)); + let xs = intersect(&g, &r); + assert_eq!(xs.len(), 2); + Ok(()) + } + } +} #[cfg(test)] mod tests { mod shape_builder {