This commit is contained in:
parent
270a7ec349
commit
e574cdb592
232
rtchallenge/examples/eoc14.rs
Normal file
232
rtchallenge/examples/eoc14.rs
Normal file
@ -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(())
|
||||
}
|
||||
115
rtchallenge/examples/glass.rs
Normal file
115
rtchallenge/examples/glass.rs
Normal file
@ -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(())
|
||||
}
|
||||
@ -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")]
|
||||
|
||||
@ -50,5 +50,6 @@ pub mod prelude {
|
||||
transformations::view_transform,
|
||||
tuples::{point, vector, Color},
|
||||
world::{World, WorldBuilder},
|
||||
Float,
|
||||
};
|
||||
}
|
||||
|
||||
@ -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<Shape>),
|
||||
}
|
||||
|
||||
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<Matrix4x4, String> {
|
||||
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<Shape, ShapeBuilderError> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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 {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user