Compare commits

..

30 Commits

Author SHA1 Message Date
9befbd9ad2 matrices: moving another doctest to unit
All checks were successful
continuous-integration/drone/push Build is passing
2021-07-30 21:59:40 -07:00
c882fc81e5 transformations: moving another doctest to unit 2021-07-30 21:58:32 -07:00
1c2caf2cc5 lights: moving another doctest to unit 2021-07-30 21:51:35 -07:00
9006671a26 intersections: moving another doctest to unit 2021-07-30 21:49:48 -07:00
3838efd134 camera: moving another doctest to unit 2021-07-30 21:44:58 -07:00
e3d8988658 matrices: moving another doctest to unit 2021-07-30 21:33:43 -07:00
e4846de25b rays: move tests from doctest to unit. 2021-07-30 21:28:13 -07:00
4352c03d20 tuples: move tests from doctest to unit. 2021-07-30 21:23:56 -07:00
1d5e5a164b materials: move tests from doctest to unit. 2021-07-30 21:21:51 -07:00
f476822bcd shapes: lint 2021-07-30 21:21:16 -07:00
135a519526 shapes: move tests from doctest to unit. 2021-07-30 20:58:05 -07:00
5d6b3e6d57 patterns: move tests from doctest to unit. 2021-07-30 20:00:08 -07:00
3aea76b35c matrices: move tests from doctest to unit. 2021-07-29 20:33:03 -07:00
cd2a4770ca world: move tests from doctest to unit. 2021-07-28 20:23:59 -07:00
5debb16d10 intersections: move tests from doctest to unit. 2021-07-27 21:51:26 -07:00
42e8ebe3bd Implement transparency, reflections and refraction. 2021-07-26 21:46:04 -07:00
1d61f59935 materials: add transparency and refractive_index to Material. 2021-07-25 16:33:03 -07:00
7f36aecf5e world: add reflection to ray tracer. 2021-07-25 16:28:34 -07:00
0c7bbae4a3 rtchallenge: remove disable-inverse-cache feature. 2021-07-25 14:54:00 -07:00
eaae65712b eoc10: example showing concepts from the chapter. 2021-07-25 14:51:33 -07:00
68709da6c2 patterns: implement checker pattern. 2021-07-25 14:46:26 -07:00
77215193fa patterns: implement ring pattern 2021-07-25 14:37:56 -07:00
74fe69188a patterns: add Gradient pattern. 2021-07-25 14:11:44 -07:00
bdcee49d5a patterns: add builder pattern for creating Patterns. 2021-07-25 13:50:13 -07:00
2e4e8b3dcd patterns: make From for Pattern a little generic. 2021-07-25 13:35:14 -07:00
b9f2c3f0ec patterns: create generic Pattern modeled after StripePattern.
Add TestPattern to validate generic implementation.
Make Material.color use Pattern.
2021-07-25 13:30:40 -07:00
8b79876aee patterns: implement object and pattern transformation awareness. 2021-07-25 11:22:36 -07:00
bfa3282a37 materials: add StripePattern as a Material color option. 2021-07-24 19:36:32 -07:00
3e383c4dbd patters: implemented basic stripe pattern. 2021-07-24 18:20:29 -07:00
c158d92252 eoc9: make width and height CLI flags. 2021-07-23 22:18:20 -07:00
19 changed files with 2426 additions and 1418 deletions

View File

@ -8,7 +8,6 @@ edition = "2018"
[features]
default = [ "float-as-double" ]
disable-inverse-cache = []
float-as-double = []
[dependencies]

View File

@ -0,0 +1,168 @@
use std::time::Instant;
use anyhow::Result;
use structopt::StructOpt;
use rtchallenge::prelude::*;
use rtchallenge::{
camera::RenderStrategy, float::consts::PI, patterns::test_pattern, BLACK, WHITE,
};
/// End of chapter 10 challenge.
#[derive(StructOpt, Debug)]
#[structopt(name = "eoc10")]
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., 2., -10.);
let to = point(2., 1., 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()
.material(
MaterialBuilder::default()
.color(
checkers_pattern(WHITE, BLACK)
.transform(translation(1., 0., 0.) * scaling(2., 2., 2.))
.build()?,
)
.specular(0.)
.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)
.build()?,
)
.build()?;
let x2y1 = sphere()
.transform(translation(2., 1., 0.) * sphere_size)
.material(
MaterialBuilder::default()
.color(stripe_pattern(WHITE, BLACK).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, BLACK)
.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, BLACK)
.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, BLACK)
.transform(scaling(0.5, 0.5, 0.5))
.build()?,
)
.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])
.build()?;
let image = camera.render(&world);
let path = "/tmp/eoc10.png";
println!("saving output to {}", path);
image.write_to_file(path)?;
println!("Render time {:.3} seconds", start.elapsed().as_secs_f32());
Ok(())
}

View File

@ -22,7 +22,7 @@ fn main() -> Result<()> {
let half = wall_size / 2.;
let mut shape = Shape::sphere();
shape.material = Material {
color: Color::new(1., 0.2, 1.),
color: Color::new(1., 0.2, 1.).into(),
specular: 0.5,
diffuse: 0.7,
shininess: 30.,
@ -44,7 +44,15 @@ fn main() -> Result<()> {
let point = r.position(hit.t);
let normal = hit.object.normal_at(point);
let eye = -r.direction;
let color = lighting(&hit.object.material, &light, point, eye, normal, in_shadow);
let color = lighting(
&hit.object.material,
&hit.object,
&light,
point,
eye,
normal,
in_shadow,
);
c.set(x, y, color);
}
}

View File

@ -42,7 +42,7 @@ fn main() -> Result<()> {
let mut floor = Shape::sphere();
floor.set_transform(Matrix4x4::scaling(10., 0.01, 10.));
floor.material = Material {
color: Color::new(1., 0.9, 0.9),
color: Color::new(1., 0.9, 0.9).into(),
specular: 0.,
..Material::default()
};
@ -68,7 +68,7 @@ fn main() -> Result<()> {
let mut middle = Shape::sphere();
middle.set_transform(Matrix4x4::translation(-0.5, 1., 0.5));
middle.material = Material {
color: Color::new(0.1, 1., 0.5),
color: Color::new(0.1, 1., 0.5).into(),
diffuse: 0.7,
specular: 0.3,
..Material::default()
@ -77,7 +77,7 @@ fn main() -> Result<()> {
let mut right = Shape::sphere();
right.set_transform(Matrix4x4::translation(1.5, 0.5, -0.5) * Matrix4x4::scaling(0.5, 0.5, 0.5));
right.material = Material {
color: Color::new(0.5, 1., 0.1),
color: Color::new(0.5, 1., 0.1).into(),
diffuse: 0.7,
specular: 0.3,
..Material::default()
@ -88,7 +88,7 @@ fn main() -> Result<()> {
Matrix4x4::translation(-1.5, 0.33, -0.75) * Matrix4x4::scaling(0.33, 0.33, 0.33),
);
left.material = Material {
color: Color::new(1., 0.8, 0.1),
color: Color::new(1., 0.8, 0.1).into(),
diffuse: 0.7,
specular: 0.3,
..Material::default()

View File

@ -54,7 +54,7 @@ fn main() -> Result<()> {
let mut floor = Shape::sphere();
floor.set_transform(Matrix4x4::scaling(10., 0.01, 10.));
floor.material = Material {
color: Color::new(1., 0.9, 0.9),
color: Color::new(1., 0.9, 0.9).into(),
specular: 0.,
..Material::default()
};
@ -80,7 +80,7 @@ fn main() -> Result<()> {
let mut middle = Shape::sphere();
middle.set_transform(Matrix4x4::translation(-0.5, 1., 0.5));
middle.material = Material {
color: Color::new(0.1, 1., 0.5),
color: Color::new(0.1, 1., 0.5).into(),
diffuse: 0.7,
specular: 0.3,
..Material::default()
@ -89,7 +89,7 @@ fn main() -> Result<()> {
let mut right = Shape::sphere();
right.set_transform(Matrix4x4::translation(1.5, 0.5, -0.5) * Matrix4x4::scaling(0.5, 0.5, 0.5));
right.material = Material {
color: Color::new(1., 1., 1.),
color: Color::new(1., 1., 1.).into(),
diffuse: 0.7,
specular: 0.0,
..Material::default()
@ -100,7 +100,7 @@ fn main() -> Result<()> {
Matrix4x4::translation(-1.5, 0.33, -0.75) * Matrix4x4::scaling(0.33, 0.33, 0.33),
);
left.material = Material {
color: Color::new(1., 0.8, 0.1),
color: Color::new(1., 0.8, 0.1).into(),
diffuse: 0.7,
specular: 0.3,
..Material::default()

View File

@ -11,19 +11,24 @@ use rtchallenge::{camera::RenderStrategy, float::consts::PI, WHITE};
#[derive(StructOpt, Debug)]
#[structopt(name = "eoc9")]
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 width = 2560;
let height = 1440;
let light1 = PointLightBuilder::default()
.position(point(-5., 5., -5.))
@ -42,8 +47,8 @@ fn main() -> Result<()> {
let to = point(0., 1., 0.);
let up = point(0., 1., 0.);
let camera = CameraBuilder::default()
.hsize(width)
.vsize(height)
.hsize(opt.width)
.vsize(opt.height)
.field_of_view(PI / 4.)
.transform(view_transform(from, to, up))
.render_strategy(opt.render_strategy)

View File

@ -20,19 +20,24 @@ use rtchallenge::{
#[derive(StructOpt, Debug)]
#[structopt(name = "eoc9")]
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 width = 2560;
let height = 1440;
let light_position = Tuple::point(-5., 5., -5.);
let light_color = WHITE;
@ -44,7 +49,7 @@ fn main() -> Result<()> {
let light_color = Color::new(0.2, 0.2, 0.1);
let light3 = PointLight::new(light_position, light_color);
let mut camera = Camera::new(width, height, PI / 4.);
let mut camera = Camera::new(opt.width, opt.height, PI / 4.);
let from = Tuple::point(0., 1.5, -5.);
let to = Tuple::point(0., 1., 0.);
let up = Tuple::point(0., 1., 0.);
@ -54,14 +59,14 @@ fn main() -> Result<()> {
let mut floor = Shape::plane();
floor.material = Material {
color: Color::new(1., 0.2, 0.2),
color: Color::new(1., 0.2, 0.2).into(),
specular: 0.,
..Material::default()
};
let mut ceiling = Shape::plane();
ceiling.set_transform(Matrix4x4::translation(0., 6., 0.) * Matrix4x4::rotation_x(PI));
ceiling.material = Material {
color: Color::new(0.6, 0.6, 0.8),
color: Color::new(0.6, 0.6, 0.8).into(),
specular: 0.2,
..Material::default()
};
@ -69,7 +74,7 @@ fn main() -> Result<()> {
let mut middle = Shape::sphere();
middle.set_transform(Matrix4x4::translation(-0.5, 0.5, 0.5));
middle.material = Material {
color: Color::new(0.1, 1., 0.5),
color: Color::new(0.1, 1., 0.5).into(),
diffuse: 0.7,
specular: 0.3,
..Material::default()
@ -78,7 +83,7 @@ fn main() -> Result<()> {
let mut right = Shape::sphere();
right.set_transform(Matrix4x4::translation(1.5, 0.5, -0.5) * Matrix4x4::scaling(0.5, 0.5, 0.5));
right.material = Material {
color: Color::new(1., 1., 1.),
color: Color::new(1., 1., 1.).into(),
diffuse: 0.7,
specular: 0.0,
..Material::default()
@ -89,7 +94,7 @@ fn main() -> Result<()> {
Matrix4x4::translation(-1.5, 0.33, -0.75) * Matrix4x4::scaling(0.33, 0.33, 0.33),
);
left.material = Material {
color: Color::new(1., 0.8, 0.1),
color: Color::new(1., 0.8, 0.1).into(),
diffuse: 0.7,
specular: 0.3,
..Material::default()

View File

@ -22,6 +22,8 @@ use crate::{
Float, BLACK,
};
const MAX_DEPTH_RECURSION: usize = 5;
#[derive(Copy, Clone, StructOpt, Debug, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum RenderStrategy {
@ -116,27 +118,6 @@ enum Response {
impl Camera {
/// Create a camera with a canvas of pixel hsize (height) and vsize (width)
/// with the given field of view (in radians).
///
/// # Examples
/// ```
/// use rtchallenge::{camera::Camera, float::consts::PI, matrices::Matrix4x4, EPSILON};
///
/// let hsize = 160;
/// let vsize = 120;
/// let field_of_view = PI / 2.;
/// let c = Camera::new(hsize, vsize, field_of_view);
/// assert_eq!(c.hsize(), 160);
/// assert_eq!(c.vsize(), 120);
/// assert_eq!(c.transform(), Matrix4x4::identity());
///
/// // Pixel size for a horizontal canvas.
/// let c = Camera::new(200, 150, PI / 2.);
/// assert!((c.pixel_size() - 0.010).abs() < EPSILON);
///
/// // Pixel size for a horizontal canvas.
/// let c = Camera::new(150, 200, PI / 2.);
/// assert!((c.pixel_size() - 0.010).abs() < EPSILON);
/// ```
pub fn new(hsize: usize, vsize: usize, field_of_view: Float) -> Camera {
let half_view = (field_of_view / 2.).tan();
let aspect = hsize as Float / vsize as Float;
@ -206,47 +187,17 @@ impl Camera {
/// Calculate ray that starts at the camera and passes through the (x,y)
/// pixel on the canvas.
///
/// # Examples
/// ```
/// use rtchallenge::{
/// camera::Camera, float::consts::PI, matrices::Matrix4x4, tuples::Tuple, Float,
/// };
///
/// // Constructing a ray through the center of the canvas.
/// let c = Camera::new(201, 101, PI / 2.);
/// let r = c.ray_for_pixel(100, 50);
/// assert_eq!(r.origin, Tuple::point(0., 0., 0.));
/// assert_eq!(r.direction, Tuple::vector(0., 0., -1.));
///
/// // Constructing a ray through the corner of the canvas.
/// let c = Camera::new(201, 101, PI / 2.);
/// let r = c.ray_for_pixel(0, 0);
/// assert_eq!(r.origin, Tuple::point(0., 0., 0.));
/// assert_eq!(r.direction, Tuple::vector(0.66519, 0.33259, -0.66851));
///
/// // Constructing a ray when the camera is transformed.
/// let mut c = Camera::new(201, 101, PI / 2.);
/// c.set_transform(Matrix4x4::rotation_y(PI / 4.) * Matrix4x4::translation(0., -2., 5.));
/// let r = c.ray_for_pixel(100, 50);
/// assert_eq!(r.origin, Tuple::point(0., 2., -5.));
/// assert_eq!(
/// r.direction,
/// Tuple::vector((2. as Float).sqrt() / 2., 0., -(2. as Float).sqrt() / 2.)
/// );
/// ```
#[cfg(not(feature = "disable-inverse-cache"))]
pub fn ray_for_pixel(&self, px: usize, py: usize) -> Ray {
// The offset from the edge of the canvas to the pixel's corner.
let xoffset = (px as Float + 0.5) * self.pixel_size;
let yoffset = (py as Float + 0.5) * self.pixel_size;
// The untransformed coordinates of the pixle in world space.
// The untransformed coordinates of the pixel in world space.
// (Remember that the camera looks toward -z, so +x is to the left.)
let world_x = self.half_width - xoffset;
let world_y = self.half_height - yoffset;
// Using the camera matrix, transofmrm the canvas point and the origin,
// Using the camera matrix, transform the canvas point and the origin,
// and then compute the ray's direction vector.
// (Remember that the canvas is at z>=-1).
let pixel = self.inverse_transform * Tuple::point(world_x, world_y, -1.);
@ -255,48 +206,8 @@ impl Camera {
Ray::new(origin, direction)
}
#[cfg(feature = "disable-inverse-cache")]
pub fn ray_for_pixel(&self, px: usize, py: usize) -> Ray {
// The offset from the edge of the canvas to the pixel's corner.
let xoffset = (px as Float + 0.5) * self.pixel_size;
let yoffset = (py as Float + 0.5) * self.pixel_size;
// The untransformed coordinates of the pixle in world space.
// (Remember that the camera looks toward -z, so +x is to the left.)
let world_x = self.half_width - xoffset;
let world_y = self.half_height - yoffset;
// Using the camera matrix, transofmrm the canvas point and the origin,
// and then compute the ray's direction vector.
// (Remember that the canvas is at z>=-1).
let pixel = self.transform.inverse() * Tuple::point(world_x, world_y, -1.);
let origin = self.transform.inverse() * Tuple::point(0., 0., 0.);
let direction = (pixel - origin).normalize();
Ray::new(origin, direction)
}
/// Use camera to render an image of the given world.
/// # Examples
/// ```
/// use rtchallenge::{
/// camera::Camera,
/// float::consts::PI,
/// transformations::view_transform,
/// tuples::{Color, Tuple},
/// world::World,
/// };
///
/// // Rendering a world with a camera.
/// let w = World::test_world();
/// let mut c = Camera::new(11, 11, PI / 2.);
/// let from = Tuple::point(0., 0., -5.);
/// let to = Tuple::point(0., 0., 0.);
/// let up = Tuple::vector(0., 1., 0.);
/// c.set_transform(view_transform(from, to, up));
/// let image = c.render(&w);
/// assert_eq!(image.get(5, 5), Color::new(0.38066, 0.47583, 0.2855));
/// ```
pub fn render(&self, w: &World) -> Canvas {
use RenderStrategy::*;
@ -416,12 +327,12 @@ impl Camera {
let color = self
.supersample_rays_for_pixel(x, y, self.samples_per_pixel)
.iter()
.map(|ray| w.color_at(&ray))
.map(|ray| w.color_at(&ray, MAX_DEPTH_RECURSION))
.fold(BLACK, |acc, c| acc + c);
color / self.samples_per_pixel as Float
} else {
let ray = self.ray_for_pixel(x, y);
w.color_at(&ray)
w.color_at(&ray, MAX_DEPTH_RECURSION)
}
}
}
@ -456,3 +367,70 @@ fn render_worker_task(
}
}
}
#[cfg(test)]
mod tests {
use crate::{
camera::Camera,
float::consts::PI,
matrices::Matrix4x4,
transformations::view_transform,
tuples::{point, vector},
world::World,
Float, EPSILON,
};
#[test]
fn new() {
let hsize = 160;
let vsize = 120;
let field_of_view = PI / 2.;
let c = Camera::new(hsize, vsize, field_of_view);
assert_eq!(c.hsize(), 160);
assert_eq!(c.vsize(), 120);
assert_eq!(c.transform(), Matrix4x4::identity());
// Pixel size for a horizontal canvas.
let c = Camera::new(200, 150, PI / 2.);
assert!((c.pixel_size() - 0.010).abs() < EPSILON);
// Pixel size for a horizontal canvas.
let c = Camera::new(150, 200, PI / 2.);
assert!((c.pixel_size() - 0.010).abs() < EPSILON);
}
#[test]
fn ray_for_pixel() {
// Constructing a ray through the center of the canvas.
let c = Camera::new(201, 101, PI / 2.);
let r = c.ray_for_pixel(100, 50);
assert_eq!(r.origin, point(0., 0., 0.));
assert_eq!(r.direction, vector(0., 0., -1.));
// Constructing a ray through the corner of the canvas.
let c = Camera::new(201, 101, PI / 2.);
let r = c.ray_for_pixel(0, 0);
assert_eq!(r.origin, point(0., 0., 0.));
assert_eq!(r.direction, vector(0.66519, 0.33259, -0.66851));
// Constructing a ray when the camera is transformed.
let mut c = Camera::new(201, 101, PI / 2.);
c.set_transform(Matrix4x4::rotation_y(PI / 4.) * Matrix4x4::translation(0., -2., 5.));
let r = c.ray_for_pixel(100, 50);
assert_eq!(r.origin, point(0., 2., -5.));
assert_eq!(
r.direction,
vector((2. as Float).sqrt() / 2., 0., -(2. as Float).sqrt() / 2.)
);
}
#[test]
fn render() {
// Rendering a world with a camera.
let w = World::test_world();
let mut c = Camera::new(11, 11, PI / 2.);
let from = point(0., 0., -5.);
let to = point(0., 0., 0.);
let up = vector(0., 1., 0.);
c.set_transform(view_transform(from, to, up));
let image = c.render(&w);
assert_eq!(image.get(5, 5), [0.38066, 0.47583, 0.2855].into());
}
}

View File

@ -3,7 +3,7 @@ use std::ops::Index;
use crate::{
rays::Ray,
shapes::Shape,
tuples::{dot, Tuple},
tuples::{dot, reflect, Tuple},
Float, EPSILON,
};
@ -20,50 +20,22 @@ impl<'i> PartialEq for Intersection<'i> {
impl<'i> Intersection<'i> {
/// Create new `Intersection` at the given `t` that hits the given `object`.
///
/// # Examples
/// ```
/// use rtchallenge::{intersections::Intersection, shapes::Shape};
///
/// // An intersection ecapsulates t and object.
/// let s = Shape::sphere();
/// let i = Intersection::new(3.5, &s);
/// assert_eq!(i.t, 3.5);
/// assert_eq!(i.object, &s);
/// ```
pub fn new(t: Float, object: &Shape) -> Intersection {
Intersection { t, object }
}
}
/// Aggregates `Intersection`s.
///
/// # Examples
/// ```
/// use rtchallenge::{
/// intersections::{Intersection, Intersections},
/// rays::Ray,
/// shapes::{intersect, Shape},
/// tuples::Tuple,
/// };
///
/// let s = Shape::sphere();
/// let i1 = Intersection::new(1., &s);
/// let i2 = Intersection::new(2., &s);
/// let xs = Intersections::new(vec![i1, i2]);
/// assert_eq!(xs.len(), 2);
/// assert_eq!(xs[0].t, 1.);
/// assert_eq!(xs[1].t, 2.);
///
/// let r = Ray::new(Tuple::point(0., 0., -5.), Tuple::vector(0., 0., 1.));
/// let xs = intersect(&s, &r);
/// assert_eq!(xs.len(), 2);
/// assert_eq!(xs[0].object, &s);
/// assert_eq!(xs[1].object, &s);
/// ```
#[derive(Debug, Default, PartialEq)]
pub struct Intersections<'i>(Vec<Intersection<'i>>);
/// Create an [Intersections] from a single [Intersection] to aid in tests.
impl<'i> From<Intersection<'i>> for Intersections<'i> {
fn from(i: Intersection<'i>) -> Intersections<'i> {
Intersections::new(vec![i])
}
}
impl<'i> Intersections<'i> {
pub fn new(xs: Vec<Intersection<'i>>) -> Intersections {
Intersections(xs)
@ -72,50 +44,6 @@ impl<'i> Intersections<'i> {
self.0.len()
}
/// Finds nearest hit for this collection of intersections.
///
/// # Examples
/// ```
/// use rtchallenge::{
/// intersections::{Intersection, Intersections},
/// rays::Ray,
/// shapes::{intersect, Shape},
/// tuples::Tuple,
/// };
///
/// // The hit, when all intersections have positive t.
/// let s = Shape::sphere();
/// let i1 = Intersection::new(1., &s);
/// let i2 = Intersection::new(2., &s);
/// let xs = Intersections::new(vec![i2, i1.clone()]);
/// let i = xs.hit();
/// assert_eq!(i, Some(&i1));
///
/// // The hit, when some intersections have negative t.
/// let s = Shape::sphere();
/// let i1 = Intersection::new(-1., &s);
/// let i2 = Intersection::new(1., &s);
/// let xs = Intersections::new(vec![i2.clone(), i1]);
/// let i = xs.hit();
/// assert_eq!(i, Some(&i2));
///
/// // The hit, when all intersections have negative t.
/// let s = Shape::sphere();
/// let i1 = Intersection::new(-2., &s);
/// let i2 = Intersection::new(-1., &s);
/// let xs = Intersections::new(vec![i2, i1]);
/// let i = xs.hit();
/// assert_eq!(i, None);
///
/// // The hit is always the lowest nonnegative intersection.
/// let s = Shape::sphere();
/// let i1 = Intersection::new(5., &s);
/// let i2 = Intersection::new(7., &s);
/// let i3 = Intersection::new(-3., &s);
/// let i4 = Intersection::new(2., &s);
/// let xs = Intersections::new(vec![i1, i2, i3, i4.clone()]);
/// let i = xs.hit();
/// assert_eq!(i, Some(&i4));
/// ```
pub fn hit(&self) -> Option<&Intersection> {
self.0.iter().filter(|i| i.t > 0.).min_by(|i1, i2| {
i1.t.partial_cmp(&i2.t)
@ -145,81 +73,358 @@ pub struct PrecomputedData<'i> {
pub t: Float,
pub object: &'i Shape,
pub point: Tuple,
pub under_point: Tuple,
pub over_point: Tuple,
pub eyev: Tuple,
pub normalv: Tuple,
pub reflectv: Tuple,
pub inside: bool,
pub n1: Float,
pub n2: Float,
}
/// Precomputes data common to all intersections.
///
/// # Examples
/// ```
/// use rtchallenge::{
/// intersections::{prepare_computations, Intersection, Intersections},
/// rays::Ray,
/// matrices::Matrix4x4,
/// shapes::{intersect, Shape},
/// tuples::Tuple,
/// EPSILON
/// };
///
/// // Precomputing the state of an intersection.
/// let r = Ray::new(Tuple::point(0., 0., -5.), Tuple::vector(0., 0., 1.));
/// let shape = Shape::sphere();
/// let i = Intersection::new(4., &shape);
/// let comps = prepare_computations(&i, &r);
/// assert_eq!(comps.t, i.t);
/// assert_eq!(comps.object, i.object);
/// assert_eq!(comps.point, Tuple::point(0., 0., -1.));
/// assert_eq!(comps.eyev, Tuple::vector(0., 0., -1.));
/// assert_eq!(comps.normalv, Tuple::vector(0., 0., -1.));
///
/// // The hit, when an intersection occurs on the outside.
/// let r = Ray::new(Tuple::point(0., 0., -5.), Tuple::vector(0., 0., 1.));
/// let shape = Shape::sphere();
/// let i = Intersection::new(4., &shape);
/// let comps = prepare_computations(&i, &r);
/// assert_eq!(comps.inside, false);
///
/// // The hit, when an intersection occurs on the inside.
/// let r = Ray::new(Tuple::point(0., 0., 0.), Tuple::vector(0., 0., 1.));
/// let shape = Shape::sphere();
/// let i = Intersection::new(1., &shape);
/// let comps = prepare_computations(&i, &r);
/// assert_eq!(comps.point, Tuple::point(0., 0., 1.));
/// assert_eq!(comps.eyev, Tuple::vector(0., 0., -1.));
/// assert_eq!(comps.inside, true);
//// // Normal would have been (0, 0, 1), but is inverted when inside.
/// assert_eq!(comps.normalv, Tuple::vector(0., 0., -1.));
///
/// // The hit should offset the point.
/// let r = Ray::new(Tuple::point(0., 0., -5.), Tuple::vector(0., 0., 1.));
/// let mut shape = Shape::sphere();
/// shape .set_transform(Matrix4x4::translation(0.,0.,1.));
/// let i = Intersection::new(5., &shape);
/// let comps = prepare_computations(&i, &r);
/// assert!(comps.over_point.z< -EPSILON/2.);
/// assert!(comps.point.z>comps.over_point.z);
/// ```
pub fn prepare_computations<'i>(i: &'i Intersection, r: &Ray) -> PrecomputedData<'i> {
let point = r.position(i.t);
let normalv = i.object.normal_at(point);
pub fn prepare_computations<'i>(
hit: &'i Intersection,
r: &Ray,
xs: &Intersections,
) -> PrecomputedData<'i> {
let point = r.position(hit.t);
let normalv = hit.object.normal_at(point);
let eyev = -r.direction;
let (inside, normalv) = if dot(normalv, eyev) < 0. {
(true, -normalv)
} else {
(false, normalv)
};
let reflectv = reflect(r.direction, normalv);
let mut n1 = -1.;
let mut n2 = -1.;
let mut containers: Vec<&Shape> = Vec::new();
for i in xs.0.iter() {
if hit == i {
if containers.is_empty() {
n1 = 1.;
} else {
n1 = containers.last().unwrap().material.refractive_index;
}
}
match containers.iter().position(|o| o == &i.object) {
Some(idx) => {
containers.remove(idx);
}
None => containers.push(i.object),
}
if hit == i {
if containers.is_empty() {
n2 = 1.;
} else {
n2 = containers.last().unwrap().material.refractive_index;
}
break;
}
}
let over_point = point + normalv * EPSILON;
let under_point = point - normalv * EPSILON;
PrecomputedData {
t: i.t,
object: i.object,
t: hit.t,
object: hit.object,
point,
over_point,
under_point,
normalv,
reflectv,
inside,
eyev,
n1,
n2,
}
}
/// Compute Schlick approximation to Fresnel's equations.
pub fn schlick(comps: &PrecomputedData) -> Float {
// Find the cosine of the angle between the eye and normal vectors.
let mut cos = dot(comps.eyev, comps.normalv);
// Total internal reflection can only occur if n1 > n2.
if comps.n1 > comps.n2 {
let n = comps.n1 / comps.n2;
let sin2_t = n * n * (1. - cos * cos);
if sin2_t > 1. {
return 1.;
}
// Compute cosine of theta_t using trig identity.
let cos_t = (1. - sin2_t).sqrt();
// When n1 > n2 use cos(theta_t) instead.
cos = cos_t;
}
let r0 = (comps.n1 - comps.n2) / (comps.n1 + comps.n2);
let r0 = r0 * r0;
r0 + (1. - r0) * (1. - cos) * (1. - cos) * (1. - cos) * (1. - cos) * (1. - cos)
}
#[cfg(test)]
mod tests {
use crate::{
intersections::{prepare_computations, schlick, Intersection, Intersections},
materials::MaterialBuilder,
matrices::{scaling, translation},
rays::Ray,
shapes::{glass_sphere, intersect, Shape},
tuples::{point, vector},
Float, EPSILON,
};
#[test]
fn intersection() {
// An intersection ecapsulates t and object.
let s = Shape::sphere();
let i = Intersection::new(3.5, &s);
assert_eq!(i.t, 3.5);
assert_eq!(i.object, &s);
}
#[test]
fn intersections() {
let s = Shape::sphere();
let i1 = Intersection::new(1., &s);
let i2 = Intersection::new(2., &s);
let xs = Intersections::new(vec![i1, i2]);
assert_eq!(xs.len(), 2);
assert_eq!(xs[0].t, 1.);
assert_eq!(xs[1].t, 2.);
let r = Ray::new(point(0., 0., -5.), vector(0., 0., 1.));
let xs = intersect(&s, &r);
assert_eq!(xs.len(), 2);
assert_eq!(xs[0].object, &s);
assert_eq!(xs[1].object, &s);
}
mod hit {
use super::*;
#[test]
fn when_all_intersections_have_positive_t() {
let s = Shape::sphere();
let i1 = Intersection::new(1., &s);
let i2 = Intersection::new(2., &s);
let xs = Intersections::new(vec![i2, i1.clone()]);
let i = xs.hit();
assert_eq!(i, Some(&i1));
}
#[test]
fn when_some_intersections_have_negative_t() {
let s = Shape::sphere();
let i1 = Intersection::new(-1., &s);
let i2 = Intersection::new(1., &s);
let xs = Intersections::new(vec![i2.clone(), i1]);
let i = xs.hit();
assert_eq!(i, Some(&i2));
}
#[test]
fn when_all_intersections_have_negative_t() {
let s = Shape::sphere();
let i1 = Intersection::new(-2., &s);
let i2 = Intersection::new(-1., &s);
let xs = Intersections::new(vec![i2, i1]);
let i = xs.hit();
assert_eq!(i, None);
}
#[test]
fn always_the_lowest_nonnegative_intersection() {
let s = Shape::sphere();
let i1 = Intersection::new(5., &s);
let i2 = Intersection::new(7., &s);
let i3 = Intersection::new(-3., &s);
let i4 = Intersection::new(2., &s);
let xs = Intersections::new(vec![i1, i2, i3, i4.clone()]);
let i = xs.hit();
assert_eq!(i, Some(&i4));
}
}
mod prepare_computations {
use super::*;
#[test]
fn precomputing_the_state_of_intersection() -> Result<(), Box<dyn std::error::Error>> {
let r = Ray::new(point(0., 0., -5.), vector(0., 0., 1.));
let shape = Shape::sphere();
let xs = Intersections::from(Intersection::new(4., &shape));
let comps = prepare_computations(&xs[0], &r, &xs);
assert_eq!(comps.t, xs[0].t);
assert_eq!(comps.object, xs[0].object);
assert_eq!(comps.point, point(0., 0., -1.));
assert_eq!(comps.eyev, vector(0., 0., -1.));
assert_eq!(comps.normalv, vector(0., 0., -1.));
Ok(())
}
#[test]
fn hit_when_intersection_occurs_outside() -> Result<(), Box<dyn std::error::Error>> {
// The hit, when an intersection occurs on the outside.
let r = Ray::new(point(0., 0., -5.), vector(0., 0., 1.));
let shape = Shape::sphere();
let xs = Intersections::from(Intersection::new(4., &shape));
let comps = prepare_computations(&xs[0], &r, &xs);
assert_eq!(comps.inside, false);
Ok(())
}
#[test]
fn hit_when_intersection_occurs_inside() -> Result<(), Box<dyn std::error::Error>> {
// The hit, when an intersection occurs on the inside.
let r = Ray::new(point(0., 0., 0.), vector(0., 0., 1.));
let shape = Shape::sphere();
let xs = Intersections::from(Intersection::new(1., &shape));
let comps = prepare_computations(&xs[0], &r, &xs);
assert_eq!(comps.point, point(0., 0., 1.));
assert_eq!(comps.eyev, vector(0., 0., -1.));
assert_eq!(comps.inside, true);
// Normal would have been (0, 0, 1), but is inverted when inside.
assert_eq!(comps.normalv, vector(0., 0., -1.));
Ok(())
}
#[test]
fn hit_should_offset_the_point() -> Result<(), Box<dyn std::error::Error>> {
// The hit should offset the point.
let r = Ray::new(point(0., 0., -5.), vector(0., 0., 1.));
let mut shape = Shape::sphere();
shape.set_transform(translation(0., 0., 1.));
let xs = Intersections::from(Intersection::new(5., &shape));
let comps = prepare_computations(&xs[0], &r, &xs);
assert!(comps.over_point.z < -EPSILON / 2.);
assert!(comps.point.z > comps.over_point.z);
Ok(())
}
#[test]
fn precomputing_the_reflection_vector() -> Result<(), Box<dyn std::error::Error>> {
// Precomputing the reflection vector.
let shape = Shape::plane();
let r = Ray::new(
point(0., 1., -1.),
vector(0., -(2 as Float).sqrt() / 2., (2 as Float).sqrt() / 2.),
);
let xs = Intersections::from(Intersection::new((2 as Float).sqrt(), &shape));
let comps = prepare_computations(&xs[0], &r, &xs);
assert_eq!(
comps.reflectv,
vector(0., (2 as Float).sqrt() / 2., (2 as Float).sqrt() / 2.)
);
Ok(())
}
#[test]
fn finding_n1_and_n2() -> Result<(), Box<dyn std::error::Error>> {
// Finding n1 and n2 at various intersections.
let a = glass_sphere()
.transform(scaling(2., 2., 2.))
.material(
MaterialBuilder::default()
.transparency(1.)
.refractive_index(1.5)
.build()?,
)
.build()?;
let b = glass_sphere()
.transform(translation(0., 0., -0.25))
.material(
MaterialBuilder::default()
.transparency(1.)
.refractive_index(2.)
.build()?,
)
.build()?;
let c = glass_sphere()
.transform(translation(0., 0., 0.25))
.material(
MaterialBuilder::default()
.transparency(1.)
.refractive_index(2.5)
.build()?,
)
.build()?;
let r = Ray::new(point(0., 0., -4.), vector(0., 0., 1.));
let xs = Intersections::new(vec![
Intersection::new(2., &a),
Intersection::new(2.75, &b),
Intersection::new(3.25, &c),
Intersection::new(4.75, &b),
Intersection::new(5.25, &c),
Intersection::new(6., &a),
]);
for (index, n1, n2) in &[
(0, 1.0, 1.5),
(1, 1.5, 2.0),
(2, 2.0, 2.5),
(3, 2.5, 2.5),
(4, 2.5, 1.5),
(5, 1.5, 1.0),
] {
let comps = prepare_computations(&xs[*index], &r, &xs);
assert_eq!(comps.n1, *n1);
assert_eq!(comps.n2, *n2);
}
Ok(())
}
#[test]
fn under_point_offset_below_surface() -> Result<(), Box<dyn std::error::Error>> {
// The under point is offset below the surface.
let r = Ray::new(point(0., 0., -5.), vector(0., 0., 1.));
let shape = glass_sphere().transform(translation(0., 0., 1.)).build()?;
let xs = Intersections::from(Intersection::new(5., &shape));
let comps = prepare_computations(&xs[0], &r, &xs);
assert!(comps.under_point.z > EPSILON / 2.);
assert!(comps.point.z < comps.under_point.z);
Ok(())
}
}
mod schlick {
use super::*;
#[test]
fn under_total_reflection() -> Result<(), Box<dyn std::error::Error>> {
let shape = glass_sphere().build()?;
let r = Ray::new(point(0., 0., (2 as Float).sqrt() / 2.), vector(0., 1., 0.));
let xs = Intersections::new(vec![
Intersection::new(-(2 as Float).sqrt() / 2., &shape),
Intersection::new((2 as Float).sqrt() / 2., &shape),
]);
let comps = prepare_computations(&xs[1], &r, &xs);
let reflectance = schlick(&comps);
assert_eq!(reflectance, 1.);
Ok(())
}
#[test]
fn perpendicular_viewing_angle() -> Result<(), Box<dyn std::error::Error>> {
let shape = glass_sphere().build()?;
let r = Ray::new(point(0., 0., 0.), vector(0., 1., 0.));
let xs = Intersections::new(vec![
Intersection::new(-1., &shape),
Intersection::new(1., &shape),
]);
let comps = prepare_computations(&xs[1], &r, &xs);
let reflectance = schlick(&comps);
assert!((reflectance - 0.04).abs() < EPSILON);
Ok(())
}
#[test]
fn small_angle_n2_greater_n1() -> Result<(), Box<dyn std::error::Error>> {
let shape = glass_sphere().build()?;
let r = Ray::new(point(0., 0.99, -2.), vector(0., 0., 1.));
let xs = Intersections::new(vec![Intersection::new(1.8589, &shape)]);
let comps = prepare_computations(&xs[0], &r, &xs);
let reflectance = schlick(&comps);
assert!((reflectance - 0.48873).abs() < EPSILON);
Ok(())
}
}
}

View File

@ -4,6 +4,7 @@ pub mod intersections;
pub mod lights;
pub mod materials;
pub mod matrices;
pub mod patterns;
pub mod rays;
pub mod shapes;
pub mod transformations;
@ -44,6 +45,7 @@ pub mod prelude {
lights::{PointLight, PointLightBuilder},
materials::{Material, MaterialBuilder},
matrices::{identity, rotation_x, rotation_y, rotation_z, scaling, shearing, translation},
patterns::{checkers_pattern, gradient_pattern, ring_pattern, stripe_pattern},
shapes::{plane, sphere, test_shape},
transformations::view_transform,
tuples::{point, vector, Color},

View File

@ -13,21 +13,6 @@ pub struct PointLight {
impl PointLight {
/// Creates a new `PositionLight` at the given `position` and with the given
/// `intensity`.
///
/// # Examples
/// ```
/// use rtchallenge::{
/// lights::PointLight,
/// tuples::{Color, Tuple},
/// WHITE,
/// };
///
/// let intensity = WHITE;
/// let position = Tuple::point(0., 0., 0.);
/// let light = PointLight::new(position, intensity);
/// assert_eq!(light.position, position);
/// assert_eq!(light.intensity, intensity);
/// ```
pub fn new<C>(position: Tuple, intensity: C) -> PointLight
where
C: Into<Color>,
@ -38,3 +23,17 @@ impl PointLight {
}
}
}
#[cfg(test)]
mod tests {
use crate::{lights::PointLight, tuples::point, WHITE};
#[test]
fn new() {
let intensity = WHITE;
let position = point(0., 0., 0.);
let light = PointLight::new(position, intensity);
assert_eq!(light.position, position);
assert_eq!(light.intensity, intensity);
}
}

View File

@ -2,8 +2,9 @@ use derive_builder::Builder;
use crate::{
lights::PointLight,
tuples::Color,
tuples::{dot, reflect, Tuple},
patterns::Pattern,
shapes::Shape,
tuples::{dot, reflect, Color, Tuple},
Float, BLACK, WHITE,
};
@ -11,103 +12,36 @@ use crate::{
#[builder(default)]
pub struct Material {
#[builder(setter(into))]
pub color: Color,
pub color: Pattern,
pub ambient: Float,
pub diffuse: Float,
pub specular: Float,
pub shininess: Float,
pub reflective: Float,
pub transparency: Float,
pub refractive_index: Float,
}
impl Default for Material {
/// Creates the default material.
///
/// # Examples
/// ```
/// use rtchallenge::{materials::Material, tuples::Color, WHITE};
///
/// let m = Material::default();
/// assert_eq!(
/// m,
/// Material {
/// color: WHITE,
/// ambient: 0.1,
/// diffuse: 0.9,
/// specular: 0.9,
/// shininess: 200.,
/// }
/// );
/// ```
fn default() -> Material {
Material {
color: WHITE,
color: WHITE.into(),
ambient: 0.1,
diffuse: 0.9,
specular: 0.9,
shininess: 200.,
reflective: 0.0,
transparency: 0.0,
refractive_index: 1.0,
}
}
}
/// Compute lighting contributions using the Phong reflection model.
///
/// # Examples
/// ```
/// use rtchallenge::{
/// lights::PointLight,
/// materials::{lighting, Material},
/// tuples::{Color, Tuple},
/// Float, WHITE,
/// };
///
/// let in_shadow = false;
/// let m = Material::default();
/// let position = Tuple::point(0., 0., 0.);
///
/// // Lighting with the eye between the light and the surface.
/// let eyev = Tuple::vector(0., 0., -1.);
/// let normalv = Tuple::vector(0., 0., -1.);
/// let light = PointLight::new(Tuple::point(0., 0., -10.), WHITE);
/// let result = lighting(&m, &light, position, eyev, normalv, in_shadow);
/// assert_eq!(result, Color::new(1.9, 1.9, 1.9));
///
/// // Lighting with the eye between the light and the surface, eye offset 45°.
/// let eyev = Tuple::vector(0., (2. as Float).sqrt() / 2., -(2. as Float).sqrt() / 2.);
/// let normalv = Tuple::vector(0., 0., -1.);
/// let light = PointLight::new(Tuple::point(0., 0., -10.), WHITE);
/// let result = lighting(&m, &light, position, eyev, normalv, in_shadow);
/// assert_eq!(result, WHITE);
///
/// // Lighting with the eye opposite surface, light offset 45°.
/// let eyev = Tuple::vector(0., 0., -1.);
/// let normalv = Tuple::vector(0., 0., -1.);
/// let light = PointLight::new(Tuple::point(0., 10., -10.), WHITE);
/// let result = lighting(&m, &light, position, eyev, normalv, in_shadow);
/// assert_eq!(result, Color::new(0.7364, 0.7364, 0.7364));
///
/// // Lighting with the eye in the path of the reflection vector.
/// let eyev = Tuple::vector(0., -(2.0 as Float).sqrt() / 2., -(2.0 as Float).sqrt() / 2.);
/// let normalv = Tuple::vector(0., 0., -1.);
/// let light = PointLight::new(Tuple::point(0., 10., -10.), WHITE);
/// let result = lighting(&m, &light, position, eyev, normalv, in_shadow);
/// assert_eq!(result, Color::new(1.63639, 1.63639, 1.63639));
///
/// // Lighting with the light behind the surface.
/// let eyev = Tuple::vector(0., 0., -1.);
/// let normalv = Tuple::vector(0., 0., -1.);
/// let light = PointLight::new(Tuple::point(0., 0., 10.), WHITE);
/// let result = lighting(&m, &light, position, eyev, normalv, in_shadow);
/// assert_eq!(result, Color::new(0.1, 0.1, 0.1));
///
/// // Lighting with the surface in shadow.
/// let in_shadow = true;
/// let eyev = Tuple::vector(0., 0., -1.);
/// let normalv = Tuple::vector(0., 0., -1.);
/// let light = PointLight::new(Tuple::point(0., 0., -10.), WHITE);
/// let result = lighting(&m, &light, position, eyev, normalv, in_shadow);
/// assert_eq!(result, Color::new(0.1, 0.1, 0.1));
/// ```
pub fn lighting(
material: &Material,
object: &Shape,
light: &PointLight,
point: Tuple,
eyev: Tuple,
@ -115,7 +49,8 @@ pub fn lighting(
in_shadow: bool,
) -> Color {
// Combine the surface color with the light's color.
let effective_color = material.color * light.intensity;
let color = material.color.pattern_at_object(object, point);
let effective_color = color * light.intensity;
// Find the direciton of the light source.
let lightv = (light.position - point).normalize();
// Compute the ambient distribution.
@ -149,3 +84,153 @@ pub fn lighting(
ambient + diffuse + specular
}
}
#[cfg(test)]
mod tests {
use crate::{materials::Material, WHITE};
#[test]
fn default() {
let m = Material::default();
assert_eq!(
m,
Material {
color: WHITE.into(),
ambient: 0.1,
diffuse: 0.9,
specular: 0.9,
shininess: 200.,
reflective: 0.0,
transparency: 0.0,
refractive_index: 1.0,
}
);
}
mod lighting {
use crate::{
lights::PointLight,
materials::{lighting, Material},
patterns::Pattern,
shapes::Shape,
tuples::{point, vector, Color},
Float, BLACK, WHITE,
};
#[test]
fn eye_between_light_and_surface() {
let in_shadow = false;
let m = Material::default();
let position = point(0., 0., 0.);
let object = Shape::sphere();
// Lighting with the eye between the light and the surface.
let eyev = vector(0., 0., -1.);
let normalv = vector(0., 0., -1.);
let light = PointLight::new(point(0., 0., -10.), WHITE);
let result = lighting(&m, &object, &light, position, eyev, normalv, in_shadow);
assert_eq!(result, Color::new(1.9, 1.9, 1.9));
}
#[test]
fn eye_between_light_and_surface_offset_45() {
// Lighting with the eye between the light and the surface, eye offset 45°.
let in_shadow = false;
let m = Material::default();
let position = point(0., 0., 0.);
let object = Shape::sphere();
let eyev = vector(0., (2. as Float).sqrt() / 2., -(2. as Float).sqrt() / 2.);
let normalv = vector(0., 0., -1.);
let light = PointLight::new(point(0., 0., -10.), WHITE);
let result = lighting(&m, &object, &light, position, eyev, normalv, in_shadow);
assert_eq!(result, WHITE);
}
#[test]
fn eye_opposite_surface_light_offset_45() {
// Lighting with the eye opposite surface, light offset 45°.
let in_shadow = false;
let m = Material::default();
let position = point(0., 0., 0.);
let object = Shape::sphere();
let eyev = vector(0., 0., -1.);
let normalv = vector(0., 0., -1.);
let light = PointLight::new(point(0., 10., -10.), WHITE);
let result = lighting(&m, &object, &light, position, eyev, normalv, in_shadow);
assert_eq!(result, Color::new(0.7364, 0.7364, 0.7364));
}
#[test]
fn eye_in_path_of_reflection_vector() {
// Lighting with the eye in the path of the reflection vector.
let in_shadow = false;
let m = Material::default();
let position = point(0., 0., 0.);
let object = Shape::sphere();
let eyev = vector(0., -(2.0 as Float).sqrt() / 2., -(2.0 as Float).sqrt() / 2.);
let normalv = vector(0., 0., -1.);
let light = PointLight::new(point(0., 10., -10.), WHITE);
let result = lighting(&m, &object, &light, position, eyev, normalv, in_shadow);
assert_eq!(result, Color::new(1.63639, 1.63639, 1.63639));
}
#[test]
fn light_behind_surface() {
// Lighting with the light behind the surface.
let in_shadow = false;
let m = Material::default();
let position = point(0., 0., 0.);
let object = Shape::sphere();
let eyev = vector(0., 0., -1.);
let normalv = vector(0., 0., -1.);
let light = PointLight::new(point(0., 0., 10.), WHITE);
let result = lighting(&m, &object, &light, position, eyev, normalv, in_shadow);
assert_eq!(result, Color::new(0.1, 0.1, 0.1));
}
#[test]
fn surface_in_shadow() {
// Lighting with the surface in shadow.
let m = Material::default();
let position = point(0., 0., 0.);
let object = Shape::sphere();
let in_shadow = true;
let eyev = vector(0., 0., -1.);
let normalv = vector(0., 0., -1.);
let light = PointLight::new(point(0., 0., -10.), WHITE);
let result = lighting(&m, &object, &light, position, eyev, normalv, in_shadow);
assert_eq!(result, Color::new(0.1, 0.1, 0.1));
}
#[test]
fn pattern_applied() {
// Lighting with a pattern applied.
let object = Shape::sphere();
let m = Material {
color: Pattern::stripe(WHITE, BLACK),
ambient: 1.,
diffuse: 0.,
specular: 0.,
..Material::default()
};
let eyev = vector(0., 0., -1.);
let normalv = vector(0., 0., -1.);
let light = PointLight::new(point(0., 0., -10.), WHITE);
let c1 = lighting(
&m,
&object,
&light,
point(0.9, 0., 0.),
eyev,
normalv,
false,
);
let c2 = lighting(
&m,
&object,
&light,
point(1.1, 0., 0.),
eyev,
normalv,
false,
);
assert_eq!(c1, WHITE);
assert_eq!(c2, BLACK);
}
}
}

View File

@ -4,82 +4,30 @@ use std::ops::{Index, IndexMut, Mul, Sub};
use crate::{tuples::Tuple, Float, EPSILON};
/// Short hand for creating a Matrix4x4 set to the identity matrix.
///
/// # Examples
/// ```
/// use rtchallenge::matrices::{identity, Matrix4x4};
///
/// assert_eq!(identity(), Matrix4x4::identity());
/// ```
pub fn identity() -> Matrix4x4 {
Matrix4x4::identity()
}
/// Short hand for creating a Matrix4x4 for rotating around the X-axis.
///
/// # Examples
/// ```
/// use rtchallenge::matrices::{rotation_x, Matrix4x4};
///
/// assert_eq!(rotation_x(10.), Matrix4x4::rotation_x(10.));
/// ```
pub fn rotation_x(radians: Float) -> Matrix4x4 {
Matrix4x4::rotation_x(radians)
}
/// Short hand for creating a Matrix4x4 for rotating around the Y-axis.
///
/// # Examples
/// ```
/// use rtchallenge::matrices::{rotation_y, Matrix4x4};
///
/// assert_eq!(rotation_y(10.), Matrix4x4::rotation_y(10.));
/// ```
pub fn rotation_y(radians: Float) -> Matrix4x4 {
Matrix4x4::rotation_y(radians)
}
/// Short hand for creating a Matrix4x4 for rotating around the Z-axis.
///
/// # Examples
/// ```
/// use rtchallenge::matrices::{rotation_z, Matrix4x4};
///
/// assert_eq!(rotation_z(10.), Matrix4x4::rotation_z(10.));
/// ```
pub fn rotation_z(radians: Float) -> Matrix4x4 {
Matrix4x4::rotation_z(radians)
}
/// Short hand for creating a Matrix4x4 that scales in the given x,y,z axis.
///
/// # Examples
/// ```
/// use rtchallenge::matrices::{scaling, Matrix4x4};
///
/// assert_eq!(scaling(1., 2., 3.), Matrix4x4::scaling(1., 2., 3.));
/// ```
pub fn scaling(x: Float, y: Float, z: Float) -> Matrix4x4 {
Matrix4x4::scaling(x, y, z)
}
/// Short hand for creating a Matrix4x4 that shears across the given axis pairs.
///
/// # Examples
/// ```
/// use rtchallenge::matrices::{shearing, Matrix4x4};
///
/// assert_eq!(
/// shearing(1., 2., 3., 4., 5., 6.),
/// Matrix4x4::shearing(1., 2., 3., 4., 5., 6.)
/// );
/// ```
pub fn shearing(xy: Float, xz: Float, yx: Float, yz: Float, zx: Float, zy: Float) -> Matrix4x4 {
Matrix4x4::shearing(xy, xz, yx, yz, zx, zy)
}
/// Short hand for creating a Matrix4x4 that translations along the given x,y,z axis.
///
/// # Examples
/// ```
/// use rtchallenge::matrices::{translation, Matrix4x4};
///
/// assert_eq!(translation(1., 2., 3.), Matrix4x4::translation(1., 2., 3.));
/// ```
pub fn translation(x: Float, y: Float, z: Float) -> Matrix4x4 {
Matrix4x4::translation(x, y, z)
}
@ -95,16 +43,6 @@ impl Matrix2x2 {
}
/// Calculate the determinant of a 2x2.
///
/// # Examples
///
/// ```
/// use rtchallenge::matrices::Matrix2x2;
///
/// let a = Matrix2x2::new([1., 5.], [-3., 2.]);
///
/// assert_eq!(a.determinant(), 17.);
/// ```
pub fn determinant(&self) -> Float {
let m = self;
m[(0, 0)] * m[(1, 1)] - m[(0, 1)] * m[(1, 0)]
@ -142,16 +80,6 @@ impl Matrix3x3 {
Matrix3x3 { m: [r0, r1, r2] }
}
/// submatrix extracts a 2x2 matrix ignoring the 0-based `row` and `col` given.
///
/// # Examples
/// ```
/// use rtchallenge::matrices::{Matrix2x2, Matrix3x3};
///
/// assert_eq!(
/// Matrix3x3::new([1., 5., 0.], [-3., 2., 7.], [0., 6., -3.],).submatrix(0, 2),
/// Matrix2x2::new([-3., 2.], [0., 6.])
/// );
/// ```
pub fn submatrix(&self, row: usize, col: usize) -> Matrix2x2 {
assert!(row < 3);
assert!(col < 3);
@ -172,49 +100,17 @@ impl Matrix3x3 {
}
/// Compute minor of a 3x3 matrix.
///
/// # Examples
/// ```
/// use rtchallenge::matrices::Matrix3x3;
///
/// let a = Matrix3x3::new([3., 5., 0.], [2., -1., -7.], [6., -1., 5.]);
/// let b = a.submatrix(1, 0);
/// assert_eq!(b.determinant(), 25.0);
/// assert_eq!(b.determinant(), a.minor(1, 0));
/// ```
pub fn minor(&self, row: usize, col: usize) -> Float {
self.submatrix(row, col).determinant()
}
/// Compute cofactor of a 3x3 matrix.
///
/// # Examples
/// ```
/// use rtchallenge::matrices::Matrix3x3;
///
/// let a = Matrix3x3::new([3., 5., 0.], [2., -1., -7.], [6., -1., 5.]);
/// assert_eq!(a.minor(0, 0), -12.);
/// assert_eq!(a.cofactor(0, 0), -12.);
/// assert_eq!(a.minor(1, 0), 25.);
/// assert_eq!(a.cofactor(1, 0), -25.);
/// ```
pub fn cofactor(&self, row: usize, col: usize) -> Float {
let negate = if (row + col) % 2 == 0 { 1. } else { -1. };
self.submatrix(row, col).determinant() * negate
}
/// Compute determinant of a 3x3 matrix.
///
/// # Examples
/// ```
/// use rtchallenge::matrices::Matrix3x3;
///
/// let a = Matrix3x3::new([1., 2., 6.], [-5., 8., -4.], [2., 6., 4.]);
/// assert_eq!(a.cofactor(0, 0), 56.);
/// assert_eq!(a.cofactor(0, 1), 12.);
/// assert_eq!(a.cofactor(0, 2), -46.);
/// assert_eq!(a.determinant(), -196.);
/// ```
pub fn determinant(&self) -> Float {
(0..3).map(|i| self.cofactor(0, i) * self[(0, i)]).sum()
}
@ -244,34 +140,6 @@ impl PartialEq for Matrix3x3 {
/// Matrix4x4 represents a 4x4 matrix in row-major form. So, element `m[i][j]` corresponds to m<sub>i,j</sub>
/// where `i` is the row number and `j` is the column number.
///
/// # Examples
/// ```
/// use rtchallenge::{float::consts::PI, matrices::Matrix4x4, tuples::Tuple};
///
/// // Individual transformations are applied in sequence.
/// let p = Tuple::point(1., 0., 1.);
/// let a = Matrix4x4::rotation_x(PI / 2.);
/// let b = Matrix4x4::scaling(5., 5., 5.);
/// let c = Matrix4x4::translation(10., 5., 7.);
/// // Apply rotation first.
/// let p2 = a * p;
/// assert_eq!(p2, Tuple::point(1., -1., 0.));
/// // Then apply scaling.
/// let p3 = b * p2;
/// assert_eq!(p3, Tuple::point(5., -5., 0.));
/// // Then apply translation.
/// let p4 = c * p3;
/// assert_eq!(p4, Tuple::point(15., 0., 7.));
///
/// // Chained transformations must be applied in reverse order.
/// let p = Tuple::point(1., 0., 1.);
/// let a = Matrix4x4::rotation_x(PI / 2.);
/// let b = Matrix4x4::scaling(5., 5., 5.);
/// let c = Matrix4x4::translation(10., 5., 7.);
/// let t = c * b * a;
/// assert_eq!(t * p, Tuple::point(15., 0., 7.));
/// ```
#[derive(Copy, Clone, Default)]
pub struct Matrix4x4 {
m: [[Float; 4]; 4],
@ -292,21 +160,6 @@ impl From<[Float; 16]> for Matrix4x4 {
impl Matrix4x4 {
/// Create a `Matrix4x4` containing the identity, all zeros with ones along the diagonal.
/// # Examples
///
/// ```
/// use rtchallenge::matrices::Matrix4x4;
///
/// let a = Matrix4x4::new(
/// [0., 1., 2., 3.],
/// [1., 2., 4., 8.],
/// [2., 4., 8., 16.],
/// [4., 8., 16., 32.],
/// );
/// let i = Matrix4x4::identity();
///
/// assert_eq!(a * i, a);
/// ```
pub fn identity() -> Matrix4x4 {
Matrix4x4::new(
[1., 0., 0., 0.],
@ -324,22 +177,6 @@ impl Matrix4x4 {
}
/// Creates a 4x4 matrix representing a translation of x,y,z.
///
/// # Examples
///
/// ```
/// use rtchallenge::{matrices::Matrix4x4, tuples::Tuple};
///
/// let transform = Matrix4x4::translation(5., -3., 2.);
/// let p = Tuple::point(-3., 4., 5.);
/// assert_eq!(transform * p, Tuple::point(2., 1., 7.));
///
/// let inv = transform.inverse();
/// assert_eq!(inv * p, Tuple::point(-8., 7., 3.));
///
/// let v = Tuple::vector(-3., 4., 5.);
/// assert_eq!(transform * v, v);
/// ```
pub fn translation(x: Float, y: Float, z: Float) -> Matrix4x4 {
Matrix4x4::new(
[1., 0., 0., x],
@ -350,30 +187,6 @@ impl Matrix4x4 {
}
/// Creates a 4x4 matrix representing a scaling of x,y,z.
///
/// # Examples
///
/// ```
/// use rtchallenge::{matrices::Matrix4x4, tuples::Tuple};
///
/// // A scaling matrix applied to a point.
/// let transform = Matrix4x4::scaling(2., 3., 4.);
/// let p = Tuple::point(-4., 6., 8.);
/// assert_eq!(transform * p, Tuple::point(-8., 18., 32.));
///
/// // A scaling matrix applied to a vector.
/// let v = Tuple::vector(-4., 6., 8.);
/// assert_eq!(transform * v, Tuple::vector(-8., 18., 32.));
///
/// // Multiplying by the inverse of a scaling matrix.
/// let inv = transform.inverse();
/// assert_eq!(inv * v, Tuple::vector(-2., 2., 2.));
///
/// // Reflection is scaling by a negative value.
/// let transform = Matrix4x4::scaling(-1., 1., 1.);
/// let p = Tuple::point(2., 3., 4.);
/// assert_eq!(transform * p, Tuple::point(-2., 3., 4.));
/// ```
pub fn scaling(x: Float, y: Float, z: Float) -> Matrix4x4 {
Matrix4x4::new(
[x, 0., 0., 0.],
@ -384,23 +197,6 @@ impl Matrix4x4 {
}
/// Creates a 4x4 matrix representing a rotation around the x-axis.
///
/// # Examples
///
/// ```
/// use rtchallenge::{float::consts::PI, matrices::Matrix4x4, tuples::Tuple, Float};
///
/// // A scaling matrix applied to a point.
/// let p = Tuple::point(0., 1., 0.);
/// let half_quarter = Matrix4x4::rotation_x(PI / 4.);
/// let full_quarter = Matrix4x4::rotation_x(PI / 2.);
///
/// assert_eq!(
/// half_quarter * p,
/// Tuple::point(0., (2.0 as Float).sqrt() / 2., (2.0 as Float).sqrt() / 2.)
/// );
/// assert_eq!(full_quarter * p, Tuple::point(0., 0., 1.),);
/// ```
pub fn rotation_x(radians: Float) -> Matrix4x4 {
let r = radians;
Matrix4x4::new(
@ -412,23 +208,6 @@ impl Matrix4x4 {
}
/// Creates a 4x4 matrix representing a rotation around the y-axis.
///
/// # Examples
///
/// ```
/// use rtchallenge::{float::consts::PI, matrices::Matrix4x4, tuples::Tuple, Float};
///
/// // A scaling matrix applied to a point.
/// let p = Tuple::point(0., 0., 1.);
/// let half_quarter = Matrix4x4::rotation_y(PI / 4.);
/// let full_quarter = Matrix4x4::rotation_y(PI / 2.);
///
/// assert_eq!(
/// half_quarter * p,
/// Tuple::point((2.0 as Float).sqrt() / 2., 0., (2.0 as Float).sqrt() / 2.)
/// );
/// assert_eq!(full_quarter * p, Tuple::point(1., 0., 0.,),);
/// ```
pub fn rotation_y(radians: Float) -> Matrix4x4 {
let r = radians;
Matrix4x4::new(
@ -440,23 +219,6 @@ impl Matrix4x4 {
}
/// Creates a 4x4 matrix representing a rotation around the z-axis.
///
/// # Examples
///
/// ```
/// use rtchallenge::{float::consts::PI, matrices::Matrix4x4, tuples::Tuple, Float};
///
/// // A scaling matrix applied to a point.
/// let p = Tuple::point(0., 1., 0.);
/// let half_quarter = Matrix4x4::rotation_z(PI / 4.);
/// let full_quarter = Matrix4x4::rotation_z(PI / 2.);
///
/// assert_eq!(
/// half_quarter * p,
/// Tuple::point(-(2.0 as Float).sqrt() / 2., (2.0 as Float).sqrt() / 2., 0.)
/// );
/// assert_eq!(full_quarter * p, Tuple::point(-1., 0., 0.,),);
/// ```
pub fn rotation_z(radians: Float) -> Matrix4x4 {
let r = radians;
Matrix4x4::new(
@ -468,26 +230,6 @@ impl Matrix4x4 {
}
/// Transpose self, returning a new matrix that has been reflected across the diagonal.
/// # Examples
///
/// ```
/// use rtchallenge::matrices::Matrix4x4;
///
/// let m = Matrix4x4::new(
/// [2., 0., 0., 0.],
/// [3., 1., 0., 0.],
/// [4., 0., 1., 0.],
/// [5., 6., 7., 1.],
/// );
/// let m_t = Matrix4x4::new(
/// [2., 3., 4., 5.],
/// [0., 1., 0., 6.],
/// [0., 0., 1., 7.],
/// [0., 0., 0., 1.],
/// );
/// assert_eq!(m.transpose(), m_t);
///
/// assert_eq!(Matrix4x4::identity(), Matrix4x4::identity().transpose());
pub fn transpose(&self) -> Matrix4x4 {
let m = self.m;
Matrix4x4 {
@ -501,40 +243,6 @@ impl Matrix4x4 {
}
/// Create a transform matrix that will shear (skew) points.
/// # Examples
///
/// ```
/// use rtchallenge::{matrices::Matrix4x4, tuples::Tuple};
///
/// // A shearing transform moves x in proportion to y.
/// let transform = Matrix4x4::shearing(1.,0.,0.,0.,0.,0.);
/// let p = Tuple::point(2.,3.,4.);
/// assert_eq!(transform * p, Tuple::point(5.,3.,4.));
///
/// // A shearing transform moves x in proportion to z.
/// let transform = Matrix4x4::shearing(0.,1.,0.,0.,0.,0.);
/// let p = Tuple::point(2.,3.,4.);
/// assert_eq!(transform * p, Tuple::point(6.,3.,4.));
///
/// // A shearing transform moves y in proportion to x.
/// let transform = Matrix4x4::shearing(0.,0.,1.,0.,0.,0.);
/// let p = Tuple::point(2.,3.,4.);
/// assert_eq!(transform * p, Tuple::point(2.,5.,4.));
///
/// // A shearing transform moves y in proportion to z.
/// let transform = Matrix4x4::shearing(0.,0.,0.,1.,0.,0.);
/// let p = Tuple::point(2.,3.,4.);
/// assert_eq!(transform * p, Tuple::point(2.,7.,4.));
///
/// // A shearing transform moves z in proportion to x.
/// let transform = Matrix4x4::shearing(0.,0.,0.,0.,1.,0.);
/// let p = Tuple::point(2.,3.,4.);
/// assert_eq!(transform * p, Tuple::point(2.,3.,6.));
///
/// // A shearing transform moves z in proportion to y.
/// let transform = Matrix4x4::shearing(0.,0.,0.,0.,0.,1.);
/// let p = Tuple::point(2.,3.,4.);
/// assert_eq!(transform * p, Tuple::point(2.,3.,7.));
pub fn shearing(xy: Float, xz: Float, yx: Float, yz: Float, zx: Float, zy: Float) -> Matrix4x4 {
Matrix4x4::new(
[1., xy, xz, 0.],
@ -547,24 +255,6 @@ impl Matrix4x4 {
/// Returns a new matrix that is the inverse of self. If self is A, inverse returns A<sup>-1</sup>, where
/// AA<sup>-1</sup> = I.
/// This implementation uses a numerically stable GaussJordan elimination routine to compute the inverse.
///
/// # Examples
///
/// ```
/// use rtchallenge::matrices::Matrix4x4;
///
/// let i = Matrix4x4::identity();
/// assert_eq!(i.inverse_rtiow() * i, i);
///
/// let m = Matrix4x4::new(
/// [2., 0., 0., 0.],
/// [0., 3., 0., 0.],
/// [0., 0., 4., 0.],
/// [0., 0., 0., 1.],
/// );
/// assert_eq!(m.inverse_rtiow() * m, i);
/// assert_eq!(m * m.inverse_rtiow(), i);
/// ```
pub fn inverse_rtiow(&self) -> Matrix4x4 {
// TODO(wathiede): how come the C++ version doesn't need to deal with non-invertable
// matrix.
@ -639,22 +329,6 @@ impl Matrix4x4 {
Matrix4x4 { m: minv }
}
/// submatrix extracts a 3x3 matrix ignoring the 0-based `row` and `col` given.
///
/// # Examples
/// ```
/// use rtchallenge::matrices::{Matrix3x3, Matrix4x4};
///
/// assert_eq!(
/// Matrix4x4::new(
/// [-6., 1., 1., 6.],
/// [-8., 5., 8., 6.],
/// [-1., 0., 8., 2.],
/// [-7., 1., -1., 1.],
/// )
/// .submatrix(2, 1),
/// Matrix3x3::new([-6., 1., 6.], [-8., 8., 6.], [-7., -1., 1.],)
/// );
/// ```
pub fn submatrix(&self, row: usize, col: usize) -> Matrix3x3 {
assert!(row < 4);
assert!(col < 4);
@ -688,133 +362,16 @@ impl Matrix4x4 {
self.submatrix(row, col).determinant() * negate
}
/// Compute determinant of a 4x4 matrix.
///
/// # Examples
/// ```
/// use rtchallenge::matrices::Matrix4x4;
///
/// let a = Matrix4x4::new(
/// [-2., -8., 3., 5.],
/// [-3., 1., 7., 3.],
/// [1., 2., -9., 6.],
/// [-6., 7., 7., -9.],
/// );
/// assert_eq!(a.cofactor(0, 0), 690.);
/// assert_eq!(a.cofactor(0, 1), 447.);
/// assert_eq!(a.cofactor(0, 2), 210.);
/// assert_eq!(a.cofactor(0, 3), 51.);
/// assert_eq!(a.determinant(), -4071.);
/// ```
pub fn determinant(&self) -> Float {
(0..4).map(|i| self.cofactor(0, i) * self[(0, i)]).sum()
}
/// Compute invertibility of matrix (i.e. non-zero determinant.
///
/// # Examples
/// ```
/// use rtchallenge::matrices::Matrix4x4;
///
/// let a = Matrix4x4::new(
/// [6., 4., 4., 4.],
/// [5., 5., 7., 6.],
/// [4., -9., 3., -7.],
/// [9., 1., 7., -6.],
/// );
/// assert_eq!(a.determinant(), -2120.);
/// assert_eq!(a.invertable(), true);
///
/// let a = Matrix4x4::new(
/// [-4., 2., -2., -3.],
/// [9., 6., 2., 6.],
/// [0., -5., 1., -5.],
/// [0., 0., 0., 0.],
/// );
/// assert_eq!(a.determinant(), 0.);
/// assert_eq!(a.invertable(), false);
/// ```
pub fn invertable(&self) -> bool {
self.determinant() != 0.
}
/// Compute the inverse of a 4x4 matrix.
///
/// # Examples
/// ```
/// use rtchallenge::matrices::Matrix4x4;
///
/// let a = Matrix4x4::new(
/// [-5., 2., 6., -8.],
/// [1., -5., 1., 8.],
/// [7., 7., -6., -7.],
/// [1., -3., 7., 4.],
/// );
/// let b = a.inverse();
///
/// assert_eq!(a.determinant(), 532.);
/// assert_eq!(a.cofactor(2, 3), -160.);
/// assert_eq!(b[(3, 2)], -160. / 532.);
/// assert_eq!(a.cofactor(3, 2), 105.);
/// assert_eq!(b[(2, 3)], 105. / 532.);
/// assert_eq!(
/// b,
/// Matrix4x4::new(
/// [0.21804512, 0.45112783, 0.24060151, -0.04511278],
/// [-0.8082707, -1.456767, -0.44360903, 0.5206767],
/// [-0.078947365, -0.2236842, -0.05263158, 0.19736843],
/// [-0.52255636, -0.81390977, -0.30075186, 0.30639097]
/// )
/// );
///
/// // Second test case
/// assert_eq!(
/// Matrix4x4::new(
/// [8., -5., 9., 2.],
/// [7., 5., 6., 1.],
/// [-6., 0., 9., 6.],
/// [-3., 0., -9., -4.],
/// )
/// .inverse(),
/// Matrix4x4::new(
/// [-0.15384616, -0.15384616, -0.2820513, -0.53846157],
/// [-0.07692308, 0.12307692, 0.025641026, 0.03076923],
/// [0.35897437, 0.35897437, 0.43589744, 0.9230769],
/// [-0.6923077, -0.6923077, -0.7692308, -1.9230769]
/// ),
/// );
///
/// // Third test case
/// assert_eq!(
/// Matrix4x4::new(
/// [9., 3., 0., 9.],
/// [-5., -2., -6., -3.],
/// [-4., 9., 6., 4.],
/// [-7., 6., 6., 2.],
/// )
/// .inverse(),
/// Matrix4x4::new(
/// [-0.04074074, -0.07777778, 0.14444445, -0.22222222],
/// [-0.07777778, 0.033333335, 0.36666667, -0.33333334],
/// [-0.029012345, -0.14629629, -0.10925926, 0.12962963],
/// [0.17777778, 0.06666667, -0.26666668, 0.33333334]
/// ),
/// );
///
/// let a = Matrix4x4::new(
/// [3., -9., 7., 3.],
/// [3., -8., 2., -9.],
/// [-4., 4., 4., 1.],
/// [-6., 5., -1., 1.],
/// );
/// let b = Matrix4x4::new(
/// [8., 2., 2., 2.],
/// [3., -1., 7., 0.],
/// [7., 0., 5., 4.],
/// [6., -2., 0., 5.],
/// );
/// let c = a * b;
/// assert_eq!(c * b.inverse(), a);
/// ```
pub fn inverse(&self) -> Matrix4x4 {
self.inverse_rtc()
}
@ -857,17 +414,6 @@ impl Mul<Matrix4x4> for Matrix4x4 {
type Output = Matrix4x4;
/// Implement matrix multiplication for `Matrix4x4`.
///
/// # Examples
/// ```
/// use rtchallenge::matrices::Matrix4x4;
///
/// let i = Matrix4x4::identity();
/// let m1 = Matrix4x4::identity();
/// let m2 = Matrix4x4::identity();
///
/// assert_eq!(m1 * m2, i);
/// ```
fn mul(self, m2: Matrix4x4) -> Matrix4x4 {
let m1 = self;
let mut r: Matrix4x4 = Default::default();
@ -887,22 +433,6 @@ impl Mul<Tuple> for Matrix4x4 {
type Output = Tuple;
/// Implement matrix multiplication for `Matrix4x4` * `Tuple`.
///
/// # Examples
/// ```
/// use rtchallenge::matrices::Matrix4x4;
/// use rtchallenge::tuples::Tuple;
///
/// let a = Matrix4x4::new(
/// [1., 2., 3., 4.],
/// [2., 4., 4., 2.],
/// [8., 6., 4., 1.],
/// [0., 0., 0., 1.],
/// );
/// let b = Tuple::new(1., 2., 3., 1.);
///
/// assert_eq!(a * b, Tuple::new(18., 24., 33., 1.));
/// ```
fn mul(self, t: Tuple) -> Tuple {
let m = self;
Tuple {
@ -962,6 +492,235 @@ impl IndexMut<(usize, usize)> for Matrix4x4 {
mod tests {
use super::*;
use crate::{
float::consts::PI,
tuples::{point, vector},
};
#[test]
fn example4x4() {
// Individual transformations are applied in sequence.
let p = point(1., 0., 1.);
let a = Matrix4x4::rotation_x(PI / 2.);
let b = Matrix4x4::scaling(5., 5., 5.);
let c = Matrix4x4::translation(10., 5., 7.);
// Apply rotation first.
let p2 = a * p;
assert_eq!(p2, point(1., -1., 0.));
// Then apply scaling.
let p3 = b * p2;
assert_eq!(p3, point(5., -5., 0.));
// Then apply translation.
let p4 = c * p3;
assert_eq!(p4, point(15., 0., 7.));
// Chained transformations must be applied in reverse order.
let p = point(1., 0., 1.);
let a = Matrix4x4::rotation_x(PI / 2.);
let b = Matrix4x4::scaling(5., 5., 5.);
let c = Matrix4x4::translation(10., 5., 7.);
let t = c * b * a;
assert_eq!(t * p, point(15., 0., 7.));
}
#[test]
fn translation() {
let transform = Matrix4x4::translation(5., -3., 2.);
let p = point(-3., 4., 5.);
assert_eq!(transform * p, point(2., 1., 7.));
let inv = transform.inverse();
assert_eq!(inv * p, point(-8., 7., 3.));
let v = vector(-3., 4., 5.);
assert_eq!(transform * v, v);
}
#[test]
fn scaling() {
// A scaling matrix applied to a point.
let transform = Matrix4x4::scaling(2., 3., 4.);
let p = point(-4., 6., 8.);
assert_eq!(transform * p, point(-8., 18., 32.));
// A scaling matrix applied to a vector.
let v = vector(-4., 6., 8.);
assert_eq!(transform * v, vector(-8., 18., 32.));
// Multiplying by the inverse of a scaling matrix.
let inv = transform.inverse();
assert_eq!(inv * v, vector(-2., 2., 2.));
// Reflection is scaling by a negative value.
let transform = Matrix4x4::scaling(-1., 1., 1.);
let p = point(2., 3., 4.);
assert_eq!(transform * p, point(-2., 3., 4.));
}
#[test]
fn rotation_x() {
// A scaling matrix applied to a point.
let p = point(0., 1., 0.);
let half_quarter = Matrix4x4::rotation_x(PI / 4.);
let full_quarter = Matrix4x4::rotation_x(PI / 2.);
assert_eq!(
half_quarter * p,
point(0., (2.0 as Float).sqrt() / 2., (2.0 as Float).sqrt() / 2.)
);
assert_eq!(full_quarter * p, point(0., 0., 1.),);
}
#[test]
fn rotation_y() {
// A scaling matrix applied to a point.
let p = point(0., 0., 1.);
let half_quarter = Matrix4x4::rotation_y(PI / 4.);
let full_quarter = Matrix4x4::rotation_y(PI / 2.);
assert_eq!(
half_quarter * p,
point((2.0 as Float).sqrt() / 2., 0., (2.0 as Float).sqrt() / 2.)
);
assert_eq!(full_quarter * p, point(1., 0., 0.,),);
}
#[test]
fn rotation_z() {
// A scaling matrix applied to a point.
let p = point(0., 1., 0.);
let half_quarter = Matrix4x4::rotation_z(PI / 4.);
let full_quarter = Matrix4x4::rotation_z(PI / 2.);
assert_eq!(
half_quarter * p,
point(-(2.0 as Float).sqrt() / 2., (2.0 as Float).sqrt() / 2., 0.)
);
assert_eq!(full_quarter * p, point(-1., 0., 0.,),);
}
#[test]
fn transpose() {
let m = Matrix4x4::new(
[2., 0., 0., 0.],
[3., 1., 0., 0.],
[4., 0., 1., 0.],
[5., 6., 7., 1.],
);
let m_t = Matrix4x4::new(
[2., 3., 4., 5.],
[0., 1., 0., 6.],
[0., 0., 1., 7.],
[0., 0., 0., 1.],
);
assert_eq!(m.transpose(), m_t);
assert_eq!(Matrix4x4::identity(), Matrix4x4::identity().transpose());
}
#[test]
fn shearing() {
// A shearing transform moves x in proportion to y.
let transform = Matrix4x4::shearing(1., 0., 0., 0., 0., 0.);
let p = point(2., 3., 4.);
assert_eq!(transform * p, point(5., 3., 4.));
// A shearing transform moves x in proportion to z.
let transform = Matrix4x4::shearing(0., 1., 0., 0., 0., 0.);
let p = point(2., 3., 4.);
assert_eq!(transform * p, point(6., 3., 4.));
// A shearing transform moves y in proportion to x.
let transform = Matrix4x4::shearing(0., 0., 1., 0., 0., 0.);
let p = point(2., 3., 4.);
assert_eq!(transform * p, point(2., 5., 4.));
// A shearing transform moves y in proportion to z.
let transform = Matrix4x4::shearing(0., 0., 0., 1., 0., 0.);
let p = point(2., 3., 4.);
assert_eq!(transform * p, point(2., 7., 4.));
// A shearing transform moves z in proportion to x.
let transform = Matrix4x4::shearing(0., 0., 0., 0., 1., 0.);
let p = point(2., 3., 4.);
assert_eq!(transform * p, point(2., 3., 6.));
// A shearing transform moves z in proportion to y.
let transform = Matrix4x4::shearing(0., 0., 0., 0., 0., 1.);
let p = point(2., 3., 4.);
assert_eq!(transform * p, point(2., 3., 7.));
}
#[test]
fn inverse_rtiow() {
let i = Matrix4x4::identity();
assert_eq!(i.inverse_rtiow() * i, i);
let m = Matrix4x4::new(
[2., 0., 0., 0.],
[0., 3., 0., 0.],
[0., 0., 4., 0.],
[0., 0., 0., 1.],
);
assert_eq!(m.inverse_rtiow() * m, i);
assert_eq!(m * m.inverse_rtiow(), i);
}
#[test]
fn determinant_2x2() {
let a = Matrix2x2::new([1., 5.], [-3., 2.]);
assert_eq!(a.determinant(), 17.);
}
#[test]
fn determinant_3x3() {
let a = Matrix3x3::new([1., 2., 6.], [-5., 8., -4.], [2., 6., 4.]);
assert_eq!(a.cofactor(0, 0), 56.);
assert_eq!(a.cofactor(0, 1), 12.);
assert_eq!(a.cofactor(0, 2), -46.);
assert_eq!(a.determinant(), -196.);
}
#[test]
fn determinant_4x4() {
let a = Matrix4x4::new(
[-2., -8., 3., 5.],
[-3., 1., 7., 3.],
[1., 2., -9., 6.],
[-6., 7., 7., -9.],
);
assert_eq!(a.cofactor(0, 0), 690.);
assert_eq!(a.cofactor(0, 1), 447.);
assert_eq!(a.cofactor(0, 2), 210.);
assert_eq!(a.cofactor(0, 3), 51.);
assert_eq!(a.determinant(), -4071.);
}
#[test]
fn submatrix_3x3() {
assert_eq!(
Matrix3x3::new([1., 5., 0.], [-3., 2., 7.], [0., 6., -3.],).submatrix(0, 2),
Matrix2x2::new([-3., 2.], [0., 6.])
);
}
#[test]
fn submatrix_4x4() {
assert_eq!(
Matrix4x4::new(
[-6., 1., 1., 6.],
[-8., 5., 8., 6.],
[-1., 0., 8., 2.],
[-7., 1., -1., 1.],
)
.submatrix(2, 1),
Matrix3x3::new([-6., 1., 6.], [-8., 8., 6.], [-7., -1., 1.],)
);
}
#[test]
fn minor_3x3() {
let a = Matrix3x3::new([3., 5., 0.], [2., -1., -7.], [6., -1., 5.]);
let b = a.submatrix(1, 0);
assert_eq!(b.determinant(), 25.0);
assert_eq!(b.determinant(), a.minor(1, 0));
}
#[test]
fn cofactor_3x3() {
let a = Matrix3x3::new([3., 5., 0.], [2., -1., -7.], [6., -1., 5.]);
assert_eq!(a.minor(0, 0), -12.);
assert_eq!(a.cofactor(0, 0), -12.);
assert_eq!(a.minor(1, 0), 25.);
assert_eq!(a.cofactor(1, 0), -25.);
}
#[test]
fn construct2x2() {
let m = Matrix2x2::new([-3., 5.], [1., -2.]);
@ -980,6 +739,100 @@ mod tests {
assert_eq!(m[(2, 2)], 1.);
}
#[test]
fn invertable() {
let a = Matrix4x4::new(
[6., 4., 4., 4.],
[5., 5., 7., 6.],
[4., -9., 3., -7.],
[9., 1., 7., -6.],
);
assert_eq!(a.determinant(), -2120.);
assert_eq!(a.invertable(), true);
let a = Matrix4x4::new(
[-4., 2., -2., -3.],
[9., 6., 2., 6.],
[0., -5., 1., -5.],
[0., 0., 0., 0.],
);
assert_eq!(a.determinant(), 0.);
assert_eq!(a.invertable(), false);
}
#[test]
fn inverse() {
let a = Matrix4x4::new(
[-5., 2., 6., -8.],
[1., -5., 1., 8.],
[7., 7., -6., -7.],
[1., -3., 7., 4.],
);
let b = a.inverse();
assert_eq!(a.determinant(), 532.);
assert_eq!(a.cofactor(2, 3), -160.);
assert_eq!(b[(3, 2)], -160. / 532.);
assert_eq!(a.cofactor(3, 2), 105.);
assert_eq!(b[(2, 3)], 105. / 532.);
assert_eq!(
b,
Matrix4x4::new(
[0.21804512, 0.45112783, 0.24060151, -0.04511278],
[-0.8082707, -1.456767, -0.44360903, 0.5206767],
[-0.078947365, -0.2236842, -0.05263158, 0.19736843],
[-0.52255636, -0.81390977, -0.30075186, 0.30639097]
)
);
// Second test case
assert_eq!(
Matrix4x4::new(
[8., -5., 9., 2.],
[7., 5., 6., 1.],
[-6., 0., 9., 6.],
[-3., 0., -9., -4.],
)
.inverse(),
Matrix4x4::new(
[-0.15384616, -0.15384616, -0.2820513, -0.53846157],
[-0.07692308, 0.12307692, 0.025641026, 0.03076923],
[0.35897437, 0.35897437, 0.43589744, 0.9230769],
[-0.6923077, -0.6923077, -0.7692308, -1.9230769]
),
);
// Third test case
assert_eq!(
Matrix4x4::new(
[9., 3., 0., 9.],
[-5., -2., -6., -3.],
[-4., 9., 6., 4.],
[-7., 6., 6., 2.],
)
.inverse(),
Matrix4x4::new(
[-0.04074074, -0.07777778, 0.14444445, -0.22222222],
[-0.07777778, 0.033333335, 0.36666667, -0.33333334],
[-0.029012345, -0.14629629, -0.10925926, 0.12962963],
[0.17777778, 0.06666667, -0.26666668, 0.33333334]
),
);
let a = Matrix4x4::new(
[3., -9., 7., 3.],
[3., -8., 2., -9.],
[-4., 4., 4., 1.],
[-6., 5., -1., 1.],
);
let b = Matrix4x4::new(
[8., 2., 2., 2.],
[3., -1., 7., 0.],
[7., 0., 5., 4.],
[6., -2., 0., 5.],
);
let c = a * b;
assert_eq!(c * b.inverse(), a);
}
#[test]
fn construct4x4() {
let m = Matrix4x4::new(
[1., 2., 3., 4.],
@ -1052,4 +905,16 @@ mod tests {
)
);
}
#[test]
fn mul4x4_tuple() {
let a = Matrix4x4::new(
[1., 2., 3., 4.],
[2., 4., 4., 2.],
[8., 6., 4., 1.],
[0., 0., 0., 1.],
);
let b = Tuple::new(1., 2., 3., 1.);
assert_eq!(a * b, Tuple::new(18., 24., 33., 1.));
}
}

361
rtchallenge/src/patterns.rs Normal file
View File

@ -0,0 +1,361 @@
use derive_builder::Builder;
use crate::{
matrices::Matrix4x4,
shapes::Shape,
tuples::{Color, Tuple},
WHITE,
};
#[derive(Debug, PartialEq, Copy, Clone)]
pub enum ColorMapper {
/// TestPattern the color returned is the pattern space point after going through world->object and object->pattern space translation.
TestPattern,
/// Solid color, the same sampled every where.
Constant(Color),
/// Pattern that alternates between the given colors along each unit of the X-axis. The strip
/// extends infinitely in the positive and negative Y and Z axes.
Stripe { a: Color, b: Color },
/// Linear blend between `a` and `b` along the X-axis.
Gradient { a: Color, b: Color },
/// Bullseye pattern in the XZ plane.
Ring { a: Color, b: Color },
/// Traditional ray tracer tile floor pattern.
Checkers { a: Color, b: Color },
}
#[derive(Builder, Debug, PartialEq, Clone)]
#[builder(default, pattern = "owned")]
pub struct Pattern {
pub color: ColorMapper,
transform: Matrix4x4,
#[builder(private, default = "self.default_inverse_transform()?")]
inverse_transform: Matrix4x4,
}
impl PatternBuilder {
fn default_inverse_transform(&self) -> Result<Matrix4x4, String> {
Ok(self.transform.unwrap_or(Matrix4x4::identity()).inverse())
}
}
impl Default for Pattern {
fn default() -> Pattern {
Pattern {
color: ColorMapper::Constant(WHITE),
transform: Matrix4x4::identity(),
inverse_transform: Matrix4x4::identity(),
}
}
}
/// Creates a [Pattern] with a color type of [ColorMapper::Constant] from the given [Color]
impl<C> From<C> for Pattern
where
C: Into<Color>,
{
fn from(c: C) -> Self {
Pattern {
color: ColorMapper::Constant(c.into()),
..Pattern::default()
}
}
}
/// Builder for creating a material pattern used for testing. The color returned is the pattern space point
/// after going through world->object and object->pattern space translation.
pub fn test_pattern() -> PatternBuilder {
PatternBuilder::default().color(ColorMapper::TestPattern)
}
/// Builder for creating a material pattern that alternates between the given colors along each unit of the
/// X-axis. The strip extends infinitely in the positive and negative Y and Z axes.
pub fn stripe_pattern(a: Color, b: Color) -> PatternBuilder {
PatternBuilder::default().color(ColorMapper::Stripe { a, b })
}
/// Builder for creating a material pattern that gradually blends between the given colors along
/// the X-axis.
pub fn gradient_pattern(a: Color, b: Color) -> PatternBuilder {
PatternBuilder::default().color(ColorMapper::Gradient { a, b })
}
/// Builder for creating a material pattern that alternates between the given colors in a ring
/// shape in the XZ plane.
pub fn ring_pattern(a: Color, b: Color) -> PatternBuilder {
PatternBuilder::default().color(ColorMapper::Ring { a, b })
}
/// Builder for creating a material pattern that alternates between the given colors along the X, Y
/// and Z pattern. Creates traditional ray tracer tile floor pattern.
pub fn checkers_pattern(a: Color, b: Color) -> PatternBuilder {
PatternBuilder::default().color(ColorMapper::Checkers { a, b })
}
/// Generic implementation for mapping points to colors according to the given [ColorMapper].
impl Pattern {
/// Create a pattern used for testing. The color returned is the pattern space point
/// after going through world->object and object->pattern space translation.
pub fn test() -> Pattern {
Pattern {
color: ColorMapper::TestPattern,
..Pattern::default()
}
}
/// Create a pattern that alternates between the given colors along each unit of the
/// X-axis. The strip extends infinitely in the positive and negative Y and Z axes.
pub fn stripe(a: Color, b: Color) -> Pattern {
Pattern {
color: ColorMapper::Stripe { a, b },
..Pattern::default()
}
}
/// Create a pattern that gradually blends between the given colors along the X-axis.
pub fn gradient(a: Color, b: Color) -> Pattern {
Pattern {
color: ColorMapper::Gradient { a, b },
..Pattern::default()
}
}
/// Create a pattern that alternates between the given colors in a ring in the XZ plane.
pub fn ring(a: Color, b: Color) -> Pattern {
Pattern {
color: ColorMapper::Ring { a, b },
..Pattern::default()
}
}
/// Create a pattern that alternates between the given colors along the X, Y and Z axis.
pub fn checkers(a: Color, b: Color) -> Pattern {
Pattern {
color: ColorMapper::Checkers { a, b },
..Pattern::default()
}
}
/// Sample the color at the given point in untranslated object space.
pub fn pattern_at(&self, point: Tuple) -> Color {
match self.color {
ColorMapper::TestPattern => [point.x, point.y, point.z].into(),
ColorMapper::Constant(c) => c,
ColorMapper::Stripe { a, b } => {
let x = point.x.floor();
if x % 2. == 0. {
a
} else {
b
}
}
ColorMapper::Gradient { a, b } => {
let distance = b - a;
let fraction = point.x - point.x.floor();
a + distance * fraction
}
ColorMapper::Ring { a, b } => {
let px = point.x;
let pz = point.z;
if (px * px + pz * pz).sqrt().floor() % 2. == 0. {
a
} else {
b
}
}
ColorMapper::Checkers { a, b } => {
let d = point.x.floor() + point.y.floor() + point.z.floor();
if d % 2. == 0. {
a
} else {
b
}
}
}
}
/// Sample the color at the given world point on the given object.
/// This function respects the object and the pattern's transform matrix.
pub fn pattern_at_object(&self, object: &Shape, world_point: Tuple) -> Color {
let object_point = object.inverse_transform() * world_point;
let pattern_point = self.inverse_transform * object_point;
self.pattern_at(pattern_point)
}
pub fn transform(&self) -> Matrix4x4 {
self.transform
}
pub fn inverse_transform(&self) -> Matrix4x4 {
self.inverse_transform
}
pub fn set_transform(&mut self, t: Matrix4x4) {
self.transform = t;
self.inverse_transform = t.inverse();
}
}
#[cfg(test)]
mod tests {
use crate::{
matrices::identity,
patterns::{ColorMapper, Pattern},
BLACK, WHITE,
};
#[test]
fn test_create() {
let pattern = Pattern::test();
assert_eq!(pattern.transform(), identity());
}
#[test]
fn stripe_create() {
let pattern = Pattern::stripe(BLACK, WHITE);
assert_eq!(pattern.color, ColorMapper::Stripe { a: BLACK, b: WHITE });
}
#[test]
fn gradient_create() {
let pattern = Pattern::gradient(BLACK, WHITE);
assert_eq!(pattern.color, ColorMapper::Gradient { a: BLACK, b: WHITE });
}
#[test]
fn ring_create() {
let pattern = Pattern::ring(BLACK, WHITE);
assert_eq!(pattern.color, ColorMapper::Ring { a: BLACK, b: WHITE });
}
#[test]
fn checkers_create() {
let pattern = Pattern::checkers(BLACK, WHITE);
assert_eq!(pattern.color, ColorMapper::Checkers { a: BLACK, b: WHITE });
}
mod pattern_at {
use super::*;
use crate::tuples::point;
#[test]
fn test_returns_coordinates() {
// A test returns the pattern space coordinates of the point.
let pattern = Pattern::test();
let p = point(1., 2., 3.);
assert_eq!(pattern.pattern_at(p), [p.x, p.y, p.z].into());
}
#[test]
fn stripe_alternates_between_two_colors() {
// A stripe alternates between two colors.
let pattern = Pattern::stripe(WHITE, BLACK);
for (p, want) in &[
// A stripe pattern is constant in y.
(point(0., 0., 0.), WHITE),
(point(0., 1., 0.), WHITE),
(point(0., 2., 0.), WHITE),
// A stripe pattern is constant in z.
(point(0., 0., 0.), WHITE),
(point(0., 0., 1.), WHITE),
(point(0., 0., 2.), WHITE),
// A stripe pattern alternates in z.
(point(0., 0., 0.), WHITE),
(point(0.9, 0., 0.), WHITE),
(point(1., 0., 0.), BLACK),
(point(-0.1, 0., 0.), BLACK),
(point(-1., 0., 0.), BLACK),
(point(-1.1, 0., 0.), WHITE),
] {
assert_eq!(pattern.pattern_at(*p), *want, "{:?}", p);
}
}
#[test]
fn gradient_linearly_interpolates_between_colors() {
// A gradient linearly interpolates between two colors.
let pattern = Pattern::gradient(WHITE, BLACK);
assert_eq!(pattern.pattern_at(point(0., 0., 0.)), WHITE);
assert_eq!(
pattern.pattern_at(point(0.25, 0., 0.)),
[0.75, 0.75, 0.75].into()
);
assert_eq!(
pattern.pattern_at(point(0.5, 0., 0.)),
[0.5, 0.5, 0.5].into()
);
assert_eq!(
pattern.pattern_at(point(0.75, 0., 0.)),
[0.25, 0.25, 0.25].into()
);
}
#[test]
fn ring_extend_in_x_and_z() {
// A ring should extend both in x and z.
let pattern = Pattern::ring(WHITE, BLACK);
assert_eq!(pattern.pattern_at(point(0., 0., 0.)), WHITE);
assert_eq!(pattern.pattern_at(point(1., 0., 0.)), BLACK);
assert_eq!(pattern.pattern_at(point(0., 0., 1.)), BLACK);
// 0.708 is slight more than 2.sqrt()/2.
assert_eq!(pattern.pattern_at(point(0.708, 0., 0.708)), BLACK);
}
#[test]
fn checkers_repeat_along_x_axis() {
// Checkers should repeat along X-axis.
let pattern = Pattern::checkers(WHITE, BLACK);
assert_eq!(pattern.pattern_at(point(0., 0., 0.)), WHITE);
assert_eq!(pattern.pattern_at(point(0.99, 0., 0.)), WHITE);
assert_eq!(pattern.pattern_at(point(1.01, 0., 0.)), BLACK);
}
#[test]
fn checkers_repeat_along_y_axis() {
// Checkers should repeat along Y-axis.
let pattern = Pattern::checkers(WHITE, BLACK);
assert_eq!(pattern.pattern_at(point(0., 0., 0.)), WHITE);
assert_eq!(pattern.pattern_at(point(0., 0.99, 0.)), WHITE);
assert_eq!(pattern.pattern_at(point(0., 1.01, 0.)), BLACK);
}
#[test]
fn checkers_repeat_along_z_axis() {
// Checkers should repeat along Z-axis.
let pattern = Pattern::checkers(WHITE, BLACK);
assert_eq!(pattern.pattern_at(point(0., 0., 0.)), WHITE);
assert_eq!(pattern.pattern_at(point(0., 0., 0.99)), WHITE);
assert_eq!(pattern.pattern_at(point(0., 0., 1.01)), BLACK);
}
}
mod pattern_at_object {
use std::error::Error;
use crate::{
matrices::scaling,
patterns::stripe_pattern,
shapes::{Shape, ShapeBuilder},
tuples::point,
BLACK, WHITE,
};
#[test]
fn stripes_with_an_object_transformation() -> Result<(), Box<dyn Error>> {
// Stripes with an object transformation.
let object = ShapeBuilder::sphere()
.transform(scaling(2., 2., 2.))
.build()?;
let pattern = stripe_pattern(WHITE, BLACK).build()?;
let c = pattern.pattern_at_object(&object, point(1.5, 0., 0.));
assert_eq!(c, WHITE);
Ok(())
}
#[test]
fn stripes_with_a_pattern_transformation() -> Result<(), Box<dyn Error>> {
// Stripes with a pattern transformation.
let object = Shape::sphere();
let mut pattern = stripe_pattern(WHITE, BLACK).build()?;
pattern.set_transform(scaling(2., 2., 2.));
let c = pattern.pattern_at_object(&object, point(1.5, 0., 0.));
assert_eq!(c, WHITE);
Ok(())
}
}
}

View File

@ -10,17 +10,6 @@ pub struct Ray {
impl Ray {
/// Create a ray with the given origin point and direction vector.
/// Will panic if origin not a point or direction not a vector.
///
/// # Examples
/// ```
/// use rtchallenge::{rays::Ray, tuples::Tuple};
///
/// let origin = Tuple::point(1., 2., 3.);
/// let direction = Tuple::vector(4., 5., 6.);
/// let r = Ray::new(origin, direction);
/// assert_eq!(r.origin, origin);
/// assert_eq!(r.direction, direction);
/// ```
pub fn new(origin: Tuple, direction: Tuple) -> Ray {
assert!(origin.is_point(), "Ray origin must be a point");
assert!(direction.is_vector(), "Ray direction must be a vector");
@ -28,41 +17,11 @@ impl Ray {
}
/// Compute a point from the given distance along the `Ray`.
///
/// # Examples
/// ```
/// use rtchallenge::{rays::Ray, tuples::Tuple};
///
/// let r = Ray::new(Tuple::point(2., 3., 4.), Tuple::vector(1., 0., 0.));
/// assert_eq!(r.position(0.), Tuple::point(2., 3., 4.));
/// assert_eq!(r.position(1.), Tuple::point(3., 3., 4.));
/// assert_eq!(r.position(-1.), Tuple::point(1., 3., 4.));
/// assert_eq!(r.position(2.5), Tuple::point(4.5, 3., 4.));
/// ```
pub fn position(&self, t: Float) -> Tuple {
self.origin + self.direction * t
}
/// Apply Matrix4x4 transforms to Ray.
///
/// # Examples
/// ```
/// use rtchallenge::{matrices::Matrix4x4, rays::Ray, tuples::Tuple};
///
/// // Translating a ray
/// let r = Ray::new(Tuple::point(1., 2., 3.), Tuple::vector(0., 1., 0.));
/// let m = Matrix4x4::translation(3., 4., 5.);
/// let r2 = r.transform(m);
/// assert_eq!(r2.origin, Tuple::point(4., 6., 8.));
/// assert_eq!(r2.direction, Tuple::vector(0., 1., 0.));
///
/// // Scaling a ray
/// let r = Ray::new(Tuple::point(1., 2., 3.), Tuple::vector(0., 1., 0.));
/// let m = Matrix4x4::scaling(2., 3., 4.);
/// let r2 = r.transform(m);
/// assert_eq!(r2.origin, Tuple::point(2., 6., 12.));
/// assert_eq!(r2.direction, Tuple::vector(0., 3., 0.));
/// ```
pub fn transform(&self, m: Matrix4x4) -> Ray {
Ray {
origin: m * self.origin,
@ -70,3 +29,46 @@ impl Ray {
}
}
}
#[cfg(test)]
mod tests {
use crate::{
matrices::Matrix4x4,
rays::Ray,
tuples::{point, vector},
};
#[test]
fn create() {
let origin = point(1., 2., 3.);
let direction = vector(4., 5., 6.);
let r = Ray::new(origin, direction);
assert_eq!(r.origin, origin);
assert_eq!(r.direction, direction);
}
#[test]
fn position() {
let r = Ray::new(point(2., 3., 4.), vector(1., 0., 0.));
assert_eq!(r.position(0.), point(2., 3., 4.));
assert_eq!(r.position(1.), point(3., 3., 4.));
assert_eq!(r.position(-1.), point(1., 3., 4.));
assert_eq!(r.position(2.5), point(4.5, 3., 4.));
}
#[test]
fn transform_translating_ray() {
// Translating a ray
let r = Ray::new(point(1., 2., 3.), vector(0., 1., 0.));
let m = Matrix4x4::translation(3., 4., 5.);
let r2 = r.transform(m);
assert_eq!(r2.origin, point(4., 6., 8.));
assert_eq!(r2.direction, vector(0., 1., 0.));
}
#[test]
fn transform_scaling_ray() {
// Scaling a ray
let r = Ray::new(point(1., 2., 3.), vector(0., 1., 0.));
let m = Matrix4x4::scaling(2., 3., 4.);
let r2 = r.transform(m);
assert_eq!(r2.origin, point(2., 6., 12.));
assert_eq!(r2.direction, vector(0., 3., 0.));
}
}

View File

@ -3,7 +3,10 @@ use std::sync::{Arc, Mutex};
use derive_builder::Builder;
use crate::{
intersections::Intersections, materials::Material, matrices::Matrix4x4, rays::Ray,
intersections::Intersections,
materials::{Material, MaterialBuilder},
matrices::Matrix4x4,
rays::Ray,
tuples::Tuple,
};
@ -54,87 +57,39 @@ pub struct Shape {
}
/// Short hand for creating a ShapeBuilder with a plane geometry.
///
/// # Examples
/// ```
/// use rtchallenge::shapes::{plane, Shape};
/// # fn main() -> Result<(), Box<std::error::Error>> {
/// assert_eq!(plane().build()?, Shape::plane());
/// # Ok(())
/// # }
/// ```
pub fn plane() -> ShapeBuilder {
ShapeBuilder::plane()
}
/// Short hand for creating a ShapeBuilder with a sphere geometry.
///
/// # Examples
/// ```
/// use rtchallenge::shapes::{sphere, Shape};
///
/// # fn main() -> Result<(), Box<std::error::Error>> {
/// assert_eq!(sphere().build()?, Shape::sphere());
/// # Ok(())
/// # }
/// ```
pub fn sphere() -> ShapeBuilder {
ShapeBuilder::sphere()
}
/// Short hand for creating a ShapeBuilder with a test shape geometry.
///
/// # Examples
/// ```
/// use rtchallenge::shapes::{test_shape, Shape};
///
/// # fn main() -> Result<(), Box<std::error::Error>> {
/// assert_eq!(test_shape().build()?, Shape::test_shape());
/// # Ok(())
/// # }
/// ```
pub fn test_shape() -> ShapeBuilder {
ShapeBuilder::test_shape()
}
/// Helper for producing a sphere with a glassy material.
pub fn glass_sphere() -> ShapeBuilder {
ShapeBuilder::sphere().material(
MaterialBuilder::default()
.transparency(1.)
.refractive_index(1.5)
.build()
.unwrap(),
)
}
impl ShapeBuilder {
/// Short hand for creating a ShapeBuilder with a plane geometry.
///
/// # Examples
/// ```
/// use rtchallenge::shapes::{plane, Shape};
///
/// # fn main() -> Result<(), Box<std::error::Error>> {
/// assert_eq!(plane().build()?, Shape::plane());
/// # Ok(())
/// # }
/// ```
pub fn plane() -> ShapeBuilder {
ShapeBuilder::default().geometry(Geometry::Plane)
}
/// Short hand for creating a ShapeBuilder with a sphere geometry.
///
/// # Examples
/// ```
/// use rtchallenge::shapes::{sphere, Shape};
///
/// # fn main() -> Result<(), Box<std::error::Error>> {
/// assert_eq!(sphere().build()?, Shape::sphere());
/// # Ok(())
/// # }
/// ```
pub fn sphere() -> ShapeBuilder {
ShapeBuilder::default().geometry(Geometry::Sphere)
}
/// Short hand for creating a ShapeBuilder with a test shape geometry.
///
/// # Examples
/// ```
/// use rtchallenge::shapes::{test_shape, Shape};
///
/// # fn main() -> Result<(), Box<std::error::Error>> {
/// assert_eq!(test_shape().build()?, Shape::test_shape());
/// # Ok(())
/// # }
/// ```
pub fn test_shape() -> ShapeBuilder {
ShapeBuilder::default().geometry(Geometry::TestShape(Arc::new(Mutex::new(
TestData::default(),
@ -158,24 +113,6 @@ impl Default for Shape {
impl Shape {
/// Create a test shape useful for debugging.
///
/// # Examples
/// ```
/// use rtchallenge::{materials::Material, matrices::Matrix4x4, shapes::Shape};
///
/// let mut s = Shape::test_shape();
/// // The default transform.
/// assert_eq!(s.transform(), Matrix4x4::identity());
/// // The default material.
/// assert_eq!(s.material, Material::default());
/// // Assigning a material.
/// let mut m = Material {
/// ambient: 1.,
/// ..Material::default()
/// };
/// s.material = m.clone();
/// assert_eq!(s.material, m);
/// ```
pub fn test_shape() -> Shape {
Shape {
transform: Matrix4x4::identity(),
@ -185,28 +122,6 @@ impl Shape {
}
}
/// # Examples
/// ```
/// use rtchallenge::{materials::Material, matrices::Matrix4x4, shapes::Shape};
///
/// // A sphere's default transform is the identity matrix.
/// let s = Shape::sphere();
/// assert_eq!(s.transform(), Matrix4x4::identity());
///
/// // It can be changed by directly setting the transform member.
/// let mut s = Shape::sphere();
/// let t = Matrix4x4::translation(2., 3., 4.);
/// s.set_transform(t.clone());
/// assert_eq!(s.transform(), t);
///
/// // Default Sphere has the default material.
/// assert_eq!(s.material, Material::default());
/// // It can be overridden.
/// let mut s = Shape::sphere();
/// let mut m = Material::default();
/// m.ambient = 1.;
/// s.material = m.clone();
/// assert_eq!(s.material, m);
/// ```
pub fn sphere() -> Shape {
Shape {
transform: Matrix4x4::identity(),
@ -224,101 +139,6 @@ impl Shape {
}
}
/// Find the normal at the point on the sphere.
///
/// # Examples
/// ```
/// use rtchallenge::{
/// float::consts::PI, materials::Material, matrices::Matrix4x4, shapes::Shape, tuples::Tuple,
/// Float,
/// };
///
/// // Computing the normal on a translated shape.
/// let mut s = Shape::test_shape();
/// s.set_transform(Matrix4x4::translation(0., 1., 0.));
/// let n = s.normal_at(Tuple::point(0., 1.70711, -0.70711));
/// assert_eq!(n, Tuple::vector(0., 0.70711, -0.70711));
///
/// // Computing the normal on a transform shape.
/// let mut s = Shape::test_shape();
/// s.set_transform(Matrix4x4::scaling(1., 0.5, 1.) * Matrix4x4::rotation_z(PI / 5.));
/// let n = s.normal_at(Tuple::point(
/// 0.,
/// (2. as Float).sqrt() / 2.,
/// -(2. as Float).sqrt() / 2.,
/// ));
/// assert_eq!(n, Tuple::vector(0., 0.97014, -0.24254));
///
/// // Normal on X-axis
/// let s = Shape::sphere();
/// let n = s.normal_at(Tuple::point(1., 0., 0.));
/// assert_eq!(n, Tuple::vector(1., 0., 0.));
///
/// // Normal on Y-axis
/// let s = Shape::sphere();
/// let n = s.normal_at(Tuple::point(0., 1., 0.));
/// assert_eq!(n, Tuple::vector(0., 1., 0.));
///
/// // Normal on Z-axis
/// let s = Shape::sphere();
/// let n = s.normal_at(Tuple::point(0., 0., 1.));
/// assert_eq!(n, Tuple::vector(0., 0., 1.));
///
/// // Normal on a sphere at a nonaxial point.
/// let s = Shape::sphere();
/// let n = s.normal_at(Tuple::point(
/// (3. as Float).sqrt() / 3.,
/// (3. as Float).sqrt() / 3.,
/// (3. as Float).sqrt() / 3.,
/// ));
/// assert_eq!(
/// n,
/// Tuple::vector(
/// (3. as Float).sqrt() / 3.,
/// (3. as Float).sqrt() / 3.,
/// (3. as Float).sqrt() / 3.,
/// )
/// );
/// // Normals returned are normalized.
/// let s = Shape::sphere();
/// let n = s.normal_at(Tuple::point(
/// (3. as Float).sqrt() / 3.,
/// (3. as Float).sqrt() / 3.,
/// (3. as Float).sqrt() / 3.,
/// ));
/// assert_eq!(n, n.normalize());
///
/// // Compute the normal on a translated sphere.
/// let mut s = Shape::sphere();
/// s.set_transform(Matrix4x4::translation(0., 1., 0.));
/// let n = s.normal_at(Tuple::point(0., 1.70711, -0.70711));
/// assert_eq!(n, Tuple::vector(0., 0.70711, -0.70711));
///
/// // Compute the normal on a transformed sphere.
/// let mut s = Shape::sphere();
/// s.set_transform(Matrix4x4::scaling(1., 0.5, 1.) * Matrix4x4::rotation_z(PI / 5.));
/// let n = s.normal_at(Tuple::point(
/// 0.,
/// (2. as Float).sqrt() / 2.,
/// -(2. as Float).sqrt() / 2.,
/// ));
/// assert_eq!(n, Tuple::vector(0., 0.97014, -0.24254));
///
/// // Normal of a plane is constant everywhere.
/// let p = Shape::plane();
/// assert_eq!(
/// p.normal_at(Tuple::point(0., 0., 0.)),
/// Tuple::vector(0., 1., 0.)
/// );
/// assert_eq!(
/// p.normal_at(Tuple::point(10., 0., -10.)),
/// Tuple::vector(0., 1., 0.)
/// );
/// assert_eq!(
/// p.normal_at(Tuple::point(-5., 0., 150.)),
/// Tuple::vector(0., 1., 0.)
/// );
/// ```
#[cfg(not(feature = "disable-inverse-cache"))]
pub fn normal_at(&self, world_point: Tuple) -> Tuple {
let object_point = self.inverse_transform * world_point;
let object_normal = match self.geometry {
@ -330,23 +150,15 @@ impl Shape {
world_normal.w = 0.;
world_normal.normalize()
}
#[cfg(feature = "disable-inverse-cache")]
pub fn normal_at(&self, world_point: Tuple) -> Tuple {
let object_point = self.transform.inverse() * world_point;
let object_normal = match self.geometry {
Geometry::Sphere => object_point - Tuple::point(0., 0., 0.),
Geometry::Plane => Tuple::vector(0., 1., 0.),
Geometry::TestShape(_) => todo!("test shape normal"),
};
let mut world_normal = self.transform.inverse().transpose() * object_normal;
world_normal.w = 0.;
world_normal.normalize()
}
pub fn transform(&self) -> Matrix4x4 {
self.transform
}
pub fn inverse_transform(&self) -> Matrix4x4 {
self.inverse_transform
}
pub fn set_transform(&mut self, t: Matrix4x4) {
self.transform = t;
self.inverse_transform = t.inverse();
@ -357,123 +169,6 @@ impl Shape {
}
/// Intersect a ray with a shapes.
///
/// # Examples
/// ```
/// use rtchallenge::{
/// intersections::{Intersection, Intersections},
/// matrices::Matrix4x4,
/// rays::Ray,
/// shapes::{intersect, Geometry, Shape},
/// tuples::Tuple,
/// };
///
/// // Intersecting a scaled shape with a ray.
/// let r = Ray::new(Tuple::point(0., 0., -5.), Tuple::vector(0., 0., 1.));
/// let mut s = Shape::test_shape();
/// s.set_transform(Matrix4x4::scaling(2., 2., 2.));
/// let xs = intersect(&s, &r);
/// if let Geometry::TestShape(data) = s.geometry() {
/// if let Some(ray) = &data.lock().unwrap().saved_ray {
/// assert_eq!(ray.origin, Tuple::point(0., 0., -2.5));
/// assert_eq!(ray.direction, Tuple::vector(0., 0., 0.5));
/// } else {
/// panic!("ray wasn't set");
/// };
/// } else {
/// panic!("test_shape returned a non-TestShape geometry")
/// };
///
/// // Intersecting a translated shape with a ray.
/// let r = Ray::new(Tuple::point(0., 0., -5.), Tuple::vector(0., 0., 1.));
/// let mut s = Shape::test_shape();
/// s.set_transform(Matrix4x4::translation(5., 0., 0.));
/// let xs = intersect(&s, &r);
/// if let Geometry::TestShape(data) = s.geometry() {
/// if let Some(ray) = &data.lock().unwrap().saved_ray {
/// assert_eq!(ray.origin, Tuple::point(-5., 0., -5.));
/// assert_eq!(ray.direction, Tuple::vector(0., 0., 1.));
/// } else {
/// panic!("ray wasn't set");
/// };
/// } else {
/// panic!("test_shape returned a non-TestShape geometry")
/// };
///
/// // A ray intersects a sphere in two points.
/// let r = Ray::new(Tuple::point(0., 0., -5.), Tuple::vector(0., 0., 1.));
/// let s = Shape::sphere();
/// let xs = intersect(&s, &r);
/// assert_eq!(
/// xs,
/// Intersections::new(vec![Intersection::new(4., &s), Intersection::new(6., &s)])
/// );
///
/// // A ray intersects a sphere at a tangent.
/// let r = Ray::new(Tuple::point(0., 2., -5.), Tuple::vector(0., 0., 1.));
/// let s = Shape::sphere();
/// let xs = intersect(&s, &r);
/// assert_eq!(xs, Intersections::default());
///
/// // A ray originates inside a sphere.
/// let r = Ray::new(Tuple::point(0., 0., 0.), Tuple::vector(0., 0., 1.));
/// let s = Shape::sphere();
/// let xs = intersect(&s, &r);
/// assert_eq!(
/// xs,
/// Intersections::new(vec![Intersection::new(-1., &s), Intersection::new(1., &s)])
/// );
///
/// // A sphere is behind a ray.
/// let r = Ray::new(Tuple::point(0., 0., 5.), Tuple::vector(0., 0., 1.));
/// let s = Shape::sphere();
/// let xs = intersect(&s, &r);
/// assert_eq!(
/// xs,
/// Intersections::new(vec![Intersection::new(-6., &s), Intersection::new(-4., &s)])
/// );
///
/// // Intersect a scaled sphere with a ray.
/// let r = Ray::new(Tuple::point(0., 0., -5.), Tuple::vector(0., 0., 1.));
/// let mut s = Shape::sphere();
/// s.set_transform(Matrix4x4::scaling(2., 2., 2.));
/// let xs = intersect(&s, &r);
/// assert_eq!(xs.len(), 2, "xs {:?}", xs);
/// assert_eq!(xs[0].t, 3., "xs {:?}", xs);
/// assert_eq!(xs[1].t, 7., "xs {:?}", xs);
///
/// // Intersect a translated sphere with a ray.
/// let r = Ray::new(Tuple::point(0., 0., -5.), Tuple::vector(0., 0., 1.));
/// let mut s = Shape::sphere();
/// s.set_transform(Matrix4x4::translation(5., 0., 0.));
/// let xs = intersect(&s, &r);
/// assert_eq!(xs.len(), 0);
///
/// // Intersect with a ray parallel to the plane.
/// let p = Shape::plane();
/// let r = Ray::new(Tuple::point(0., 10., 0.), Tuple::vector(0., 0., 1.));
/// let xs = intersect(&p, &r);
/// assert_eq!(xs.len(), 0);
///
/// // Intersect with a coplanar.
/// let r = Ray::new(Tuple::point(0., 0., 0.), Tuple::vector(0., 0., 1.));
/// let xs = intersect(&p, &r);
/// assert_eq!(xs.len(), 0);
///
/// // A ray intersecting a plane from above.
/// let r = Ray::new(Tuple::point(0., 1., 0.), Tuple::vector(0., -1., 0.));
/// let xs = intersect(&p, &r);
/// assert_eq!(xs.len(), 1);
/// assert_eq!(xs[0].t, 1.);
/// assert_eq!(xs[0].object, &p);
///
/// // A ray intersecting a plane from below.
/// let r = Ray::new(Tuple::point(0., -1., 0.), Tuple::vector(0., 1., 0.));
/// let xs = intersect(&p, &r);
/// assert_eq!(xs.len(), 1);
/// assert_eq!(xs[0].t, 1.);
/// assert_eq!(xs[0].object, &p);
/// ```
pub fn intersect<'s>(shape: &'s Shape, ray: &Ray) -> Intersections<'s> {
let local_ray = ray.transform(shape.inverse_transform);
match shape.geometry {
@ -539,3 +234,325 @@ mod plane {
)])
}
}
#[cfg(test)]
mod tests {
mod shape_builder {
use std::error::Error;
use crate::shapes::{plane, sphere, test_shape, Shape};
#[test]
fn plane_builder() -> Result<(), Box<dyn Error>> {
assert_eq!(plane().build()?, Shape::plane());
Ok(())
}
#[test]
fn sphere_builder() -> Result<(), Box<dyn Error>> {
assert_eq!(sphere().build()?, Shape::sphere());
Ok(())
}
#[test]
fn test_shape_builder() -> Result<(), Box<dyn Error>> {
assert_eq!(test_shape().build()?, Shape::test_shape());
Ok(())
}
}
mod shape {
use crate::{
materials::Material,
matrices::{identity, translation},
shapes::Shape,
};
#[test]
fn test_shape() {
let mut s = Shape::test_shape();
// The default transform.
assert_eq!(s.transform(), identity());
// The default material.
assert_eq!(s.material, Material::default());
// Assigning a material.
let m = Material {
ambient: 1.,
..Material::default()
};
s.material = m.clone();
assert_eq!(s.material, m);
}
#[test]
fn sphere() {
// A sphere's default transform is the identity matrix.
let s = Shape::sphere();
assert_eq!(s.transform(), identity());
// It can be changed by directly setting the transform member.
let mut s = Shape::sphere();
let t = translation(2., 3., 4.);
s.set_transform(t.clone());
assert_eq!(s.transform(), t);
// Default Sphere has the default material.
assert_eq!(s.material, Material::default());
// It can be overridden.
let mut s = Shape::sphere();
let mut m = Material::default();
m.ambient = 1.;
s.material = m.clone();
assert_eq!(s.material, m);
}
}
mod normal_at {
use crate::{float::consts::PI, matrices::Matrix4x4, shapes::Shape, tuples::Tuple, Float};
#[test]
fn compute_normal_on_translated_shape() {
// Computing the normal on a translated shape.
let mut s = Shape::test_shape();
s.set_transform(Matrix4x4::translation(0., 1., 0.));
let n = s.normal_at(Tuple::point(0., 1.70711, -0.70711));
assert_eq!(n, Tuple::vector(0., 0.70711, -0.70711));
}
#[test]
fn compute_normal_on_scaled_shape() {
// Computing the normal on a scaled shape.
let mut s = Shape::test_shape();
s.set_transform(Matrix4x4::scaling(1., 0.5, 1.) * Matrix4x4::rotation_z(PI / 5.));
let n = s.normal_at(Tuple::point(
0.,
(2. as Float).sqrt() / 2.,
-(2. as Float).sqrt() / 2.,
));
assert_eq!(n, Tuple::vector(0., 0.97014, -0.24254));
}
#[test]
fn sphere_normal_on_x_axis() {
// Normal on X-axis
let s = Shape::sphere();
let n = s.normal_at(Tuple::point(1., 0., 0.));
assert_eq!(n, Tuple::vector(1., 0., 0.));
}
#[test]
fn sphere_normal_on_y_axis() {
// Normal on Y-axis
let s = Shape::sphere();
let n = s.normal_at(Tuple::point(0., 1., 0.));
assert_eq!(n, Tuple::vector(0., 1., 0.));
}
#[test]
fn sphere_normal_on_z_axis() {
// Normal on Z-axis
let s = Shape::sphere();
let n = s.normal_at(Tuple::point(0., 0., 1.));
assert_eq!(n, Tuple::vector(0., 0., 1.));
}
#[test]
fn sphere_normal_non_axial() {
// Normal on a sphere at a nonaxial point.
let s = Shape::sphere();
let n = s.normal_at(Tuple::point(
(3. as Float).sqrt() / 3.,
(3. as Float).sqrt() / 3.,
(3. as Float).sqrt() / 3.,
));
assert_eq!(
n,
Tuple::vector(
(3. as Float).sqrt() / 3.,
(3. as Float).sqrt() / 3.,
(3. as Float).sqrt() / 3.,
)
);
}
#[test]
fn normals_are_normalized() {
// Normals returned are normalized.
let s = Shape::sphere();
let n = s.normal_at(Tuple::point(
(3. as Float).sqrt() / 3.,
(3. as Float).sqrt() / 3.,
(3. as Float).sqrt() / 3.,
));
assert_eq!(n, n.normalize());
}
#[test]
fn compute_normal_on_translated_sphere() {
// Compute the normal on a translated sphere.
let mut s = Shape::sphere();
s.set_transform(Matrix4x4::translation(0., 1., 0.));
let n = s.normal_at(Tuple::point(0., 1.70711, -0.70711));
assert_eq!(n, Tuple::vector(0., 0.70711, -0.70711));
}
#[test]
fn compute_normal_on_scaled_sphere() {
// Compute the normal on a transformed sphere.
let mut s = Shape::sphere();
s.set_transform(Matrix4x4::scaling(1., 0.5, 1.) * Matrix4x4::rotation_z(PI / 5.));
let n = s.normal_at(Tuple::point(
0.,
(2. as Float).sqrt() / 2.,
-(2. as Float).sqrt() / 2.,
));
assert_eq!(n, Tuple::vector(0., 0.97014, -0.24254));
}
#[test]
fn nomal_of_plane_constant() {
// Normal of a plane is constant everywhere.
let p = Shape::plane();
assert_eq!(
p.normal_at(Tuple::point(0., 0., 0.)),
Tuple::vector(0., 1., 0.)
);
assert_eq!(
p.normal_at(Tuple::point(10., 0., -10.)),
Tuple::vector(0., 1., 0.)
);
assert_eq!(
p.normal_at(Tuple::point(-5., 0., 150.)),
Tuple::vector(0., 1., 0.)
);
}
}
mod intersect {
use crate::{
intersections::{Intersection, Intersections},
matrices::Matrix4x4,
rays::Ray,
shapes::{intersect, Geometry, Shape},
tuples::Tuple,
};
#[test]
fn scaled_shape_with_ray() {
// Intersecting a scaled shape with a ray.
let r = Ray::new(Tuple::point(0., 0., -5.), Tuple::vector(0., 0., 1.));
let mut s = Shape::test_shape();
s.set_transform(Matrix4x4::scaling(2., 2., 2.));
let _xs = intersect(&s, &r);
if let Geometry::TestShape(data) = s.geometry() {
if let Some(ray) = &data.lock().unwrap().saved_ray {
assert_eq!(ray.origin, Tuple::point(0., 0., -2.5));
assert_eq!(ray.direction, Tuple::vector(0., 0., 0.5));
} else {
panic!("ray wasn't set");
};
} else {
panic!("test_shape returned a non-TestShape geometry")
};
}
#[test]
fn translated_shape_with_ray() {
// Intersecting a translated shape with a ray.
let r = Ray::new(Tuple::point(0., 0., -5.), Tuple::vector(0., 0., 1.));
let mut s = Shape::test_shape();
s.set_transform(Matrix4x4::translation(5., 0., 0.));
let _xs = intersect(&s, &r);
if let Geometry::TestShape(data) = s.geometry() {
if let Some(ray) = &data.lock().unwrap().saved_ray {
assert_eq!(ray.origin, Tuple::point(-5., 0., -5.));
assert_eq!(ray.direction, Tuple::vector(0., 0., 1.));
} else {
panic!("ray wasn't set");
};
} else {
panic!("test_shape returned a non-TestShape geometry")
};
}
#[test]
fn ray_intersects_sphere_two_points() {
// A ray intersects a sphere in two points.
let r = Ray::new(Tuple::point(0., 0., -5.), Tuple::vector(0., 0., 1.));
let s = Shape::sphere();
let xs = intersect(&s, &r);
assert_eq!(
xs,
Intersections::new(vec![Intersection::new(4., &s), Intersection::new(6., &s)])
);
}
#[test]
fn ray_intersects_at_tangent() {
// A ray intersects a sphere at a tangent.
let r = Ray::new(Tuple::point(0., 2., -5.), Tuple::vector(0., 0., 1.));
let s = Shape::sphere();
let xs = intersect(&s, &r);
assert_eq!(xs, Intersections::default());
}
#[test]
fn ray_originates_inside_sphere() {
// A ray originates inside a sphere.
let r = Ray::new(Tuple::point(0., 0., 0.), Tuple::vector(0., 0., 1.));
let s = Shape::sphere();
let xs = intersect(&s, &r);
assert_eq!(
xs,
Intersections::new(vec![Intersection::new(-1., &s), Intersection::new(1., &s)])
);
}
#[test]
fn sphere_behind_ray() {
// A sphere is behind a ray.
let r = Ray::new(Tuple::point(0., 0., 5.), Tuple::vector(0., 0., 1.));
let s = Shape::sphere();
let xs = intersect(&s, &r);
assert_eq!(
xs,
Intersections::new(vec![Intersection::new(-6., &s), Intersection::new(-4., &s)])
);
}
#[test]
fn ray_intersects_scaled_sphere() {
// Intersect a scaled sphere with a ray.
let r = Ray::new(Tuple::point(0., 0., -5.), Tuple::vector(0., 0., 1.));
let mut s = Shape::sphere();
s.set_transform(Matrix4x4::scaling(2., 2., 2.));
let xs = intersect(&s, &r);
assert_eq!(xs.len(), 2, "xs {:?}", xs);
assert_eq!(xs[0].t, 3., "xs {:?}", xs);
assert_eq!(xs[1].t, 7., "xs {:?}", xs);
}
#[test]
fn ray_intersects_translated_sphere() {
// Intersect a translated sphere with a ray.
let r = Ray::new(Tuple::point(0., 0., -5.), Tuple::vector(0., 0., 1.));
let mut s = Shape::sphere();
s.set_transform(Matrix4x4::translation(5., 0., 0.));
let xs = intersect(&s, &r);
assert_eq!(xs.len(), 0);
}
#[test]
fn ray_parallel_to_plane() {
// Intersect with a ray parallel to the plane.
let p = Shape::plane();
let r = Ray::new(Tuple::point(0., 10., 0.), Tuple::vector(0., 0., 1.));
let xs = intersect(&p, &r);
assert_eq!(xs.len(), 0);
}
#[test]
fn ray_coplanar_to_plane() {
// Intersect with a coplanar.
let p = Shape::plane();
let r = Ray::new(Tuple::point(0., 0., 0.), Tuple::vector(0., 0., 1.));
let xs = intersect(&p, &r);
assert_eq!(xs.len(), 0);
}
#[test]
fn ray_intersects_plane_from_above() {
// A ray intersecting a plane from above.
let p = Shape::plane();
let r = Ray::new(Tuple::point(0., 1., 0.), Tuple::vector(0., -1., 0.));
let xs = intersect(&p, &r);
assert_eq!(xs.len(), 1);
assert_eq!(xs[0].t, 1.);
assert_eq!(xs[0].object, &p);
}
#[test]
fn ray_intersects_plane_from_below() {
// A ray intersecting a plane from below.
let p = Shape::plane();
let r = Ray::new(Tuple::point(0., -1., 0.), Tuple::vector(0., 1., 0.));
let xs = intersect(&p, &r);
assert_eq!(xs.len(), 1);
assert_eq!(xs[0].t, 1.);
assert_eq!(xs[0].object, &p);
}
}
}

View File

@ -5,47 +5,6 @@ use crate::{
/// Create a matrix representing a eye at `from` looking at `to`, with an `up`
/// as the up vector.
///
/// # Examples
/// ```
/// use rtchallenge::{matrices::Matrix4x4, transformations::view_transform, tuples::Tuple};
///
/// // The transofrmation matrix for the default orientation.
/// let from = Tuple::point(0., 0., 0.);
/// let to = Tuple::point(0., 0., -1.);
/// let up = Tuple::vector(0., 1., 0.);
/// let t = view_transform(from, to, up);
/// assert_eq!(t, Matrix4x4::identity());
///
/// // A view transformation matrix looking in positive z direction.
/// let from = Tuple::point(0., 0., 0.);
/// let to = Tuple::point(0., 0., 1.);
/// let up = Tuple::vector(0., 1., 0.);
/// let t = view_transform(from, to, up);
/// assert_eq!(t, Matrix4x4::scaling(-1., 1., -1.));
///
/// // The view tranformation moves the world.
/// let from = Tuple::point(0., 0., 8.);
/// let to = Tuple::point(0., 0., 0.);
/// let up = Tuple::vector(0., 1., 0.);
/// let t = view_transform(from, to, up);
/// assert_eq!(t, Matrix4x4::translation(0., 0., -8.));
///
/// // An arbitrary view transformation.
/// let from = Tuple::point(1., 3., 2.);
/// let to = Tuple::point(4., -2., 8.);
/// let up = Tuple::vector(1., 1., 0.);
/// let t = view_transform(from, to, up);
/// assert_eq!(
/// t,
/// Matrix4x4::new(
/// [-0.50709, 0.50709, 0.67612, -2.36643],
/// [0.76772, 0.60609, 0.12122, -2.82843],
/// [-0.35857, 0.59761, -0.71714, 0.],
/// [0., 0., 0., 1.],
/// )
/// );
/// ```
pub fn view_transform(from: Tuple, to: Tuple, up: Tuple) -> Matrix4x4 {
let forward = (to - from).normalize();
let left = cross(forward, up.normalize());
@ -57,3 +16,56 @@ pub fn view_transform(from: Tuple, to: Tuple, up: Tuple) -> Matrix4x4 {
[0., 0., 0., 1.],
) * Matrix4x4::translation(-from.x, -from.y, -from.z)
}
#[cfg(test)]
mod tests {
use crate::{
matrices::{identity, scaling, translation, Matrix4x4},
transformations::view_transform,
tuples::{point, vector},
};
#[test]
fn default_orientation() {
// The transformation matrix for the default orientation.
let from = point(0., 0., 0.);
let to = point(0., 0., -1.);
let up = vector(0., 1., 0.);
let t = view_transform(from, to, up);
assert_eq!(t, identity());
}
#[test]
fn looking_positive_z() {
// A view transformation matrix looking in positive z direction.
let from = point(0., 0., 0.);
let to = point(0., 0., 1.);
let up = vector(0., 1., 0.);
let t = view_transform(from, to, up);
assert_eq!(t, scaling(-1., 1., -1.));
}
#[test]
fn transformation_moves_world() {
// The view transformation moves the world.
let from = point(0., 0., 8.);
let to = point(0., 0., 0.);
let up = vector(0., 1., 0.);
let t = view_transform(from, to, up);
assert_eq!(t, translation(0., 0., -8.));
}
#[test]
fn arbitrary_view() {
// An arbitrary view transformation.
let from = point(1., 3., 2.);
let to = point(4., -2., 8.);
let up = vector(1., 1., 0.);
let t = view_transform(from, to, up);
assert_eq!(
t,
Matrix4x4::new(
[-0.50709, 0.50709, 0.67612, -2.36643],
[0.76772, 0.60609, 0.12122, -2.82843],
[-0.35857, 0.59761, -0.71714, 0.],
[0., 0., 0., 1.],
)
);
}
}

View File

@ -3,27 +3,11 @@ use std::ops::{Add, Div, Mul, Neg, Sub};
use crate::{Float, EPSILON};
/// Short hand for creating a Tuple that represents a point, w=1.
///
/// # Examples
/// ```
/// use rtchallenge::tuples::{point, Tuple};
///
/// assert_eq!(point(1., 2., 3.), Tuple::point(1., 2., 3.));
/// assert_eq!(point(1., 2., 3.), Tuple::new(1., 2., 3., 1.));
/// ```
pub fn point(x: Float, y: Float, z: Float) -> Tuple {
Tuple::point(x, y, z)
}
/// Short hand for creating a Tuple that represents a vector, w=0.
///
/// # Examples
/// ```
/// use rtchallenge::tuples::{vector, Tuple};
///
/// assert_eq!(vector(1., 2., 3.), Tuple::vector(1., 2., 3.));
/// assert_eq!(vector(1., 2., 3.), Tuple::new(1., 2., 3., 0.));
/// ```
pub fn vector(x: Float, y: Float, z: Float) -> Tuple {
Tuple::vector(x, y, z)
}
@ -73,26 +57,6 @@ impl Tuple {
}
/// Reflects vector v across normal n.
///
/// # Examples
/// ```
/// use rtchallenge::{
/// tuples::{reflect, Tuple},
/// Float,
/// };
///
/// // Reflecting a vector approaching at 45°
/// let v = Tuple::vector(1., -1., 0.);
/// let n = Tuple::vector(0., 1., 0.);
/// let r = reflect(v, n);
/// assert_eq!(r, Tuple::vector(1., 1., 0.));
///
/// // Reflecting off a slanted surface.
/// let v = Tuple::vector(0., -1., 0.);
/// let n = Tuple::vector((2. as Float).sqrt() / 2., (2. as Float).sqrt() / 2., 0.);
/// let r = reflect(v, n);
/// assert_eq!(r, Tuple::vector(1., 0., 0.));
/// ```
pub fn reflect(v: Tuple, n: Tuple) -> Tuple {
v - n * 2. * dot(v, n)
}
@ -297,7 +261,7 @@ impl Sub for Color {
#[cfg(test)]
mod tests {
use super::{cross, dot, Color, Float, Tuple, EPSILON};
use super::{cross, dot, reflect, Color, Float, Tuple, EPSILON};
#[test]
fn is_point() {
// A tuple with w = 1 is a point
@ -456,4 +420,20 @@ mod tests {
let c2 = Color::new(0.9, 1., 0.1);
assert_eq!(c1 * c2, Color::new(1.0 * 0.9, 0.2 * 1., 0.4 * 0.1));
}
#[test]
fn reflect_approaching_at_45() {
// Reflecting a vector approaching at 45°
let v = Tuple::vector(1., -1., 0.);
let n = Tuple::vector(0., 1., 0.);
let r = reflect(v, n);
assert_eq!(r, Tuple::vector(1., 1., 0.));
}
#[test]
fn reflect_slanted_surface() {
// Reflecting off a slanted surface.
let v = Tuple::vector(0., -1., 0.);
let n = Tuple::vector((2. as Float).sqrt() / 2., (2. as Float).sqrt() / 2., 0.);
let r = reflect(v, n);
assert_eq!(r, Tuple::vector(1., 0., 0.));
}
}

View File

@ -1,27 +1,17 @@
use derive_builder::Builder;
use crate::{
intersections::{prepare_computations, Intersections, PrecomputedData},
intersections::{prepare_computations, schlick, Intersections, PrecomputedData},
lights::PointLight,
materials::{lighting, Material},
matrices::Matrix4x4,
rays::Ray,
shapes::{intersect, Shape},
tuples::{Color, Tuple},
tuples::{dot, Color, Tuple},
BLACK, WHITE,
};
/// World holds all drawable objects and the light(s) that illuminate them.
///
/// # Examples
/// ```
/// use rtchallenge::world::World;
///
/// let w = World::default();
/// assert!(w.objects.is_empty());
/// assert_eq!(w.lights.len(), 0);
/// ```
#[derive(Builder, Clone, Debug, Default)]
#[builder(default)]
pub struct World {
@ -31,20 +21,11 @@ pub struct World {
impl World {
/// Creates a world suitable for use across multiple tests in from the book.
///
/// # Examples
/// ```
/// use rtchallenge::world::World;
///
/// let w = World::test_world();
/// assert_eq!(w.objects.len(), 2);
/// assert!(!w.lights.is_empty());
/// ```
pub fn test_world() -> World {
let light = PointLight::new(Tuple::point(-10., 10., -10.), WHITE);
let mut s1 = Shape::sphere();
s1.material = Material {
color: Color::new(0.8, 1., 0.6),
color: [0.8, 1., 0.6].into(),
diffuse: 0.7,
specular: 0.2,
..Material::default()
@ -57,21 +38,7 @@ impl World {
}
}
/// Intesects the ray with this world.
///
/// # Examples
/// ```
/// use rtchallenge::{rays::Ray, tuples::Tuple, world::World};
///
/// let w = World::test_world();
/// let r = Ray::new(Tuple::point(0., 0., -5.), Tuple::vector(0., 0., 1.));
/// let xs = w.intersect(&r);
/// assert_eq!(xs.len(), 4);
/// assert_eq!(xs[0].t, 4.);
/// assert_eq!(xs[1].t, 4.5);
/// assert_eq!(xs[2].t, 5.5);
/// assert_eq!(xs[3].t, 6.);
/// ```
/// Intersects the ray with this world.
pub fn intersect(&self, r: &Ray) -> Intersections {
let mut xs: Vec<_> = self
.objects
@ -87,147 +54,46 @@ impl World {
}
/// Compute shaded value for given precomputation.
///
/// # Examples
/// ```
/// use rtchallenge::{
/// intersections::{prepare_computations, Intersection},
/// lights::PointLight,
/// matrices::Matrix4x4,
/// rays::Ray,
/// shapes::Shape,
/// tuples::{Color, Tuple},
/// world::World,
/// WHITE,
/// };
///
/// // Shading an intersection.
/// let w = World::test_world();
/// let r = Ray::new(Tuple::point(0., 0., -5.), Tuple::vector(0., 0., 1.));
/// let s = &w.objects[0];
/// let i = Intersection::new(4., &s);
/// let comps = prepare_computations(&i, &r);
/// let c = w.shade_hit(&comps);
/// assert_eq!(c, Color::new(0.38066, 0.47583, 0.2855));
///
/// // Shading an intersection from the inside.
/// let mut w = World::test_world();
/// w.lights = vec![PointLight::new(Tuple::point(0., 0.25, 0.), WHITE)];
/// let r = Ray::new(Tuple::point(0., 0., 0.), Tuple::vector(0., 0., 1.));
/// let s = &w.objects[1];
/// let i = Intersection::new(0.5, &s);
/// let comps = prepare_computations(&i, &r);
/// let c = w.shade_hit(&comps);
/// assert_eq!(c, Color::new(0.90498, 0.90498, 0.90498));
///
/// // Shading with an intersection in shadow.
/// let mut w = World::default();
/// w.lights = vec![PointLight::new(Tuple::point(0., 0., -10.), WHITE)];
/// let s1 = Shape::sphere();
/// let mut s2 = Shape::sphere();
/// s2.set_transform(Matrix4x4::translation(0., 0., 10.));
/// w.objects = vec![s1, s2.clone()];
/// let r = Ray::new(Tuple::point(0., 0., 5.), Tuple::vector(0., 0., 1.));
/// let i = Intersection::new(4., &s2);
/// let comps = prepare_computations(&i, &r);
/// let c = w.shade_hit(&comps);
/// assert_eq!(c, Color::new(0.1, 0.1, 0.1));
/// ```
pub fn shade_hit(&self, comps: &PrecomputedData) -> Color {
let c = self
pub fn shade_hit(&self, comps: &PrecomputedData, remaining: usize) -> Color {
let surface = self
.lights
.iter()
.fold(Color::new(0., 0., 0.), |acc, light| {
let shadowed = self.is_shadowed(comps.over_point, light);
acc + lighting(
let surface = lighting(
&comps.object.material,
&comps.object,
light,
comps.over_point,
comps.eyev,
comps.normalv,
shadowed,
)
);
acc + surface
});
c
let reflected = self.reflected_color(comps, remaining);
let refracted = self.refracted_color(comps, remaining);
let material = &comps.object.material;
if material.reflective > 0. && material.transparency > 0. {
let reflectance = schlick(comps);
surface + reflected * reflectance + refracted * (1. - reflectance)
} else {
surface + reflected + refracted
}
}
/// Compute color for given ray fired at the world.
///
/// # Examples
/// ```
/// use rtchallenge::{
/// intersections::{prepare_computations, Intersection},
/// lights::PointLight,
/// rays::Ray,
/// shapes::Shape,
/// tuples::{Color, Tuple},
/// world::World,
/// BLACK,
/// };
///
/// // The color when a ray misses.
/// let w = World::test_world();
/// let r = Ray::new(Tuple::point(0., 0., -5.), Tuple::vector(0., 1., 0.));
/// let c = w.color_at(&r);
/// assert_eq!(c, BLACK);
///
/// // The color when a ray hits.
/// let w = World::test_world();
/// let r = Ray::new(Tuple::point(0., 0., -5.), Tuple::vector(0., 0., 1.));
/// let c = w.color_at(&r);
/// assert_eq!(c, Color::new(0.38066, 0.47583, 0.2855));
///
/// // The color with an intersection behind the ray.
/// let w = {
/// let mut w = World::test_world();
/// let mut outer = &mut w.objects[0];
/// let m = outer.material.ambient = 1.;
/// let inner = &mut w.objects[1];
/// inner.material.ambient = 1.;
/// w
/// };
/// let inner = &w.objects[1];
/// let r = Ray::new(Tuple::point(0., 0., 0.75), Tuple::vector(0., 0., -1.));
/// let c = w.color_at(&r);
/// assert_eq!(c, inner.material.color);
/// ```
pub fn color_at(&self, r: &Ray) -> Color {
match self.intersect(r).hit() {
pub fn color_at(&self, r: &Ray, remaining: usize) -> Color {
let xs = self.intersect(r);
match xs.hit() {
Some(hit) => {
let comps = prepare_computations(&hit, r);
self.shade_hit(&comps)
let comps = prepare_computations(&hit, r, &xs);
self.shade_hit(&comps, remaining)
}
None => BLACK,
}
}
/// Determine if point in world is in a shadow.
///
/// # Examples
/// ```
/// use rtchallenge::{
/// tuples::{ Tuple},
/// shapes::Shape,
/// world::World,
/// };
///
/// let w = World::test_world();
/// let light = &w.lights[0];
///
/// // There is no shadow when nothing is collinear with point and light.
/// let p = Tuple::point(0.,10.,0.);
/// assert_eq!(w.is_shadowed(p,light), false);
///
/// // There shadow when an object is between the point and the light.
/// let p = Tuple::point(10.,-10.,10.);
/// assert_eq!(w.is_shadowed(p,light), true);
///
/// // There is no shadow when an object is behind the light.
/// let p = Tuple::point(-20.,20.,-20.);
/// assert_eq!(w.is_shadowed(p,light), false);
///
/// // There is no shadow when an object is behind the point.
/// let p = Tuple::point(-2.,2.,-2.);
/// assert_eq!(w.is_shadowed(p,light), false);
pub fn is_shadowed(&self, point: Tuple, light: &PointLight) -> bool {
let v = light.position - point;
let distance = v.magnitude();
@ -240,4 +106,455 @@ impl World {
}
false
}
/// Compute reflected color for the given precomputation.
pub fn reflected_color(&self, comps: &PrecomputedData, remaining: usize) -> Color {
if remaining == 0 {
return BLACK;
}
if comps.object.material.reflective == 0. {
return BLACK;
}
// Fire reflected ray to figure out color.
let reflect_ray = Ray::new(comps.over_point, comps.reflectv);
let color = self.color_at(&reflect_ray, remaining - 1);
color * comps.object.material.reflective
}
/// Compute refracted color for the given precomputation.
pub fn refracted_color(&self, comps: &PrecomputedData, remaining: usize) -> Color {
if remaining == 0 {
return BLACK;
}
if comps.object.material.transparency == 0. {
return BLACK;
}
// Find the ratio of the first index of refraction to the secon
// (This is inverted from the definition of Snell's Law.)
let n_ratio = comps.n1 / comps.n2;
// cos(theta_i) is the same as the dot product of the two vectors
// TODO(wathiede): is cosine faster than doc productings?
let cos_i = dot(comps.eyev, comps.normalv);
// Find the sin(theta_t)^2 via trigonometric identity.
let sin2_t = n_ratio * n_ratio * (1. - cos_i * cos_i);
if sin2_t > 1. {
// Total internal reflection.
return BLACK;
}
// Find cos(theta_t) via trigonometric identity.
let cos_t = (1. - sin2_t).sqrt();
// Compute the direction of the refracted ray.
let direction = comps.normalv * (n_ratio * cos_i - cos_t) - comps.eyev * n_ratio;
// Create the refracted ray.
let refract_ray = Ray::new(comps.under_point, direction);
// Find he color of the refracted ray, making sure to multiply by the transparency value to
// account for any opacity.
self.color_at(&refract_ray, remaining - 1) * comps.object.material.transparency
}
}
#[cfg(test)]
mod tests {
use crate::{
float::consts::SQRT_2,
intersections::{prepare_computations, Intersection, Intersections},
lights::PointLight,
materials::MaterialBuilder,
matrices::translation,
patterns::test_pattern,
rays::Ray,
shapes::{plane, sphere, Shape, ShapeBuilder},
tuples::{point, vector},
world::{World, WorldBuilder},
Float, BLACK, WHITE,
};
mod intersect {
use super::*;
#[test]
fn intersection_with_test_world() {
let w = World::test_world();
let r = Ray::new(point(0., 0., -5.), vector(0., 0., 1.));
let xs = w.intersect(&r);
assert_eq!(xs.len(), 4);
assert_eq!(xs[0].t, 4.);
assert_eq!(xs[1].t, 4.5);
assert_eq!(xs[2].t, 5.5);
assert_eq!(xs[3].t, 6.);
}
}
mod world {
use super::*;
#[test]
fn default_world() {
let w = World::default();
assert!(w.objects.is_empty());
assert_eq!(w.lights.len(), 0);
}
#[test]
fn test_world() {
let w = World::test_world();
assert_eq!(w.objects.len(), 2);
assert!(!w.lights.is_empty());
}
}
mod shade_hit {
use super::*;
#[test]
fn shading_an_intersection() {
// Shading an intersection.
let w = World::test_world();
let r = Ray::new(point(0., 0., -5.), vector(0., 0., 1.));
let s = &w.objects[0];
let xs = Intersections::from(Intersection::new(4., &s));
let comps = prepare_computations(&xs[0], &r, &xs);
let c = w.shade_hit(&comps, 1);
assert_eq!(c, [0.38066, 0.47583, 0.2855].into());
}
#[test]
fn shading_an_intersection_from_inside() {
// Shading an intersection from the inside.
let mut w = World::test_world();
w.lights = vec![PointLight::new(point(0., 0.25, 0.), WHITE)];
let r = Ray::new(point(0., 0., 0.), vector(0., 0., 1.));
let s = &w.objects[1];
let xs = Intersections::from(Intersection::new(0.5, &s));
let comps = prepare_computations(&xs[0], &r, &xs);
let c = w.shade_hit(&comps, 1);
assert_eq!(c, [0.90498, 0.90498, 0.90498].into());
}
#[test]
fn shading_an_intersection_in_shadow() {
// Shading with an intersection in shadow.
let mut w = World::default();
w.lights = vec![PointLight::new(point(0., 0., -10.), WHITE)];
let s1 = Shape::sphere();
let mut s2 = Shape::sphere();
s2.set_transform(translation(0., 0., 10.));
w.objects = vec![s1, s2.clone()];
let r = Ray::new(point(0., 0., 5.), vector(0., 0., 1.));
let xs = Intersections::from(Intersection::new(4., &s2));
let comps = prepare_computations(&xs[0], &r, &xs);
let c = w.shade_hit(&comps, 1);
assert_eq!(c, [0.1, 0.1, 0.1].into());
}
#[test]
fn shading_with_a_reflective_material() -> Result<(), Box<dyn std::error::Error>> {
// Shading with a reflective material.
let mut w = World::test_world();
let shape = ShapeBuilder::plane()
.material(MaterialBuilder::default().reflective(0.5).build()?)
.transform(translation(0., -1., 0.))
.build()?;
w.objects.push(shape.clone());
let r = Ray::new(
point(0., 0., -3.),
vector(0., -(2 as Float).sqrt() / 2., (2 as Float).sqrt() / 2.),
);
let xs = Intersections::from(Intersection::new((2 as Float).sqrt(), &shape));
let comps = prepare_computations(&xs[0], &r, &xs);
let c = w.shade_hit(&comps, 1);
assert_eq!(c, [0.87676, 0.92434, 0.82917].into());
Ok(())
}
#[test]
fn shading_with_a_transparent_material() -> Result<(), Box<dyn std::error::Error>> {
// shade_hit() with a transparent material.
let mut w = World::test_world();
let floor = plane()
.transform(translation(0., -1., 0.))
.material(
MaterialBuilder::default()
.transparency(0.5)
.refractive_index(1.5)
.build()?,
)
.build()?;
w.objects.push(floor.clone());
let ball = sphere()
.material(
MaterialBuilder::default()
.color([1., 0., 0.])
.ambient(0.5)
.build()?,
)
.transform(translation(0., -3.5, -0.5))
.build()?;
w.objects.push(ball);
let r = Ray::new(
point(0., 0., -3.),
vector(0., -(2 as Float).sqrt() / 2., (2 as Float).sqrt() / 2.),
);
let xs = Intersections::new(vec![Intersection::new((2 as Float).sqrt(), &floor)]);
let comps = prepare_computations(&xs[0], &r, &xs);
let c = w.shade_hit(&comps, 5);
assert_eq!(c, [0.93642, 0.68642, 0.68643].into());
Ok(())
}
#[test]
fn reflective_transparent_material() -> Result<(), Box<dyn std::error::Error>> {
let mut w = World::test_world();
let floor = plane()
.transform(translation(0., -1., 0.))
.material(
MaterialBuilder::default()
.reflective(0.5)
.transparency(0.5)
.refractive_index(1.5)
.build()?,
)
.build()?;
w.objects.push(floor.clone());
let ball = sphere()
.transform(translation(0., -3.5, -0.5))
.material(
MaterialBuilder::default()
.color([1., 0., 0.])
.ambient(0.5)
.build()?,
)
.build()?;
w.objects.push(ball);
let r = Ray::new(point(0., 0., -3.), vector(0., -SQRT_2 / 2., SQRT_2 / 2.));
let xs = Intersections::new(vec![Intersection::new(SQRT_2, &floor)]);
let comps = prepare_computations(&xs[0], &r, &xs);
let color = w.shade_hit(&comps, 5);
assert_eq!(color, [0.93391, 0.69643, 0.69243].into());
Ok(())
}
}
mod color_at {
use super::*;
#[test]
fn color_when_ray_misses() {
// The color when a ray misses.
let w = World::test_world();
let r = Ray::new(point(0., 0., -5.), vector(0., 1., 0.));
let c = w.color_at(&r, 1);
assert_eq!(c, BLACK);
}
#[test]
fn color_when_ray_hits() {
// The color when a ray hits.
let w = World::test_world();
let r = Ray::new(point(0., 0., -5.), vector(0., 0., 1.));
let c = w.color_at(&r, 1);
assert_eq!(c, [0.38066, 0.47583, 0.2855].into());
}
#[test]
fn color_with_an_intersection_behind_ray() {
// The color with an intersection behind the ray.
let w = {
let mut w = World::test_world();
let mut outer = &mut w.objects[0];
outer.material.ambient = 1.;
let inner = &mut w.objects[1];
inner.material.ambient = 1.;
w
};
let r = Ray::new(point(0., 0., 0.75), vector(0., 0., -1.));
let c = w.color_at(&r, 1);
// inner.material.color is WHITE
assert_eq!(c, WHITE);
}
#[test]
fn ensure_mutually_reflective_surfaces_terminate() -> Result<(), Box<dyn std::error::Error>>
{
// Ensure mutually reflective surfaces don't infinitely recurse.
let lower = ShapeBuilder::plane()
.material(MaterialBuilder::default().reflective(1.).build()?)
.transform(translation(0., -1., 0.))
.build()?;
let upper = ShapeBuilder::plane()
.material(MaterialBuilder::default().reflective(1.).build()?)
.transform(translation(0., 1., 0.))
.build()?;
let w = WorldBuilder::default()
.lights(vec![PointLight::new(point(0., 0., 0.), WHITE)])
.objects(vec![lower, upper])
.build()?;
let r = Ray::new(point(0., 0., 0.), vector(0., 1., 0.));
// This should complete without stack overflow.
w.color_at(&r, 1);
Ok(())
}
}
mod is_shadowed {
use super::*;
#[test]
fn no_shadow_when_nothing_collinear_with_point_and_light() {
// There is no shadow when nothing is collinear with point and light.
let w = World::test_world();
let light = &w.lights[0];
let p = point(0., 10., 0.);
assert_eq!(w.is_shadowed(p, light), false);
}
#[test]
fn shadow_when_object_between_point_and_light() {
// The shadow when an object is between the point and the light.
let w = World::test_world();
let light = &w.lights[0];
let p = point(10., -10., 10.);
assert_eq!(w.is_shadowed(p, light), true);
}
#[test]
fn no_shadow_when_object_behind_light() {
// There is no shadow when an object is behind the light.
let w = World::test_world();
let light = &w.lights[0];
let p = point(-20., 20., -20.);
assert_eq!(w.is_shadowed(p, light), false);
}
#[test]
fn no_shadow_when_object_behind_point() {
// There is no shadow when an object is behind the point.
let w = World::test_world();
let light = &w.lights[0];
let p = point(-2., 2., -2.);
assert_eq!(w.is_shadowed(p, light), false);
}
}
mod reflected_color {
use super::*;
#[test]
fn reflected_color_for_nonreflective_material() {
// The reflected color for a nonreflective material.
let r = Ray::new(point(0., 0., 0.), vector(0., 0., 1.));
let mut w = World::test_world();
w.objects[1].material.ambient = 1.;
let xs = Intersections::from(Intersection::new(1., &w.objects[1]));
let comps = prepare_computations(&xs[0], &r, &xs);
let c = w.reflected_color(&comps, 1);
assert_eq!(c, BLACK);
}
#[test]
fn reflected_color_for_reflective_material() -> Result<(), Box<dyn std::error::Error>> {
// The reflected color for a reflective material.
let mut w = World::test_world();
let shape = ShapeBuilder::plane()
.material(MaterialBuilder::default().reflective(0.5).build()?)
.transform(translation(0., -1., 0.))
.build()?;
w.objects.push(shape.clone());
let r = Ray::new(
point(0., 0., -3.),
vector(0., -(2 as Float).sqrt() / 2., (2 as Float).sqrt() / 2.),
);
let xs = Intersections::from(Intersection::new((2 as Float).sqrt(), &shape));
let comps = prepare_computations(&xs[0], &r, &xs);
let c = w.reflected_color(&comps, 1);
assert_eq!(c, [0.19033, 0.23791, 0.14274].into());
Ok(())
}
#[test]
fn reflected_color_at_maximum_recursion_depth() -> Result<(), Box<dyn std::error::Error>> {
// The reflected color at the maximum recursion depth.
let mut w = World::test_world();
let shape = ShapeBuilder::plane()
.material(MaterialBuilder::default().reflective(0.5).build()?)
.transform(translation(0., -1., 0.))
.build()?;
w.objects.push(shape.clone());
let r = Ray::new(
point(0., 0., -3.),
vector(0., -(2 as Float).sqrt() / 2., (2 as Float).sqrt() / 2.),
);
let xs = Intersections::from(Intersection::new((2 as Float).sqrt(), &shape));
let comps = prepare_computations(&xs[0], &r, &xs);
let _ = w.reflected_color(&comps, 0);
// Just needs to get here without infinite recursion.
Ok(())
}
}
mod refracted_color {
use super::*;
#[test]
fn refracted_color_with_opaque_surface() {
// The refracted color with an opaque surface.
let w = World::test_world();
let shape = &w.objects[0];
let r = Ray::new(point(0., 0., -5.), vector(0., 0., 1.));
let xs = Intersections::new(vec![
Intersection::new(4., &shape),
Intersection::new(6., &shape),
]);
let comps = prepare_computations(&xs[0], &r, &xs);
let c = w.refracted_color(&comps, 5);
assert_eq!(c, BLACK);
}
#[test]
fn refracted_color_at_maximum_recursion_depth() {
// The refracted color at the maximum recursive depth.
let mut w = World::test_world();
w.objects[0].material.transparency = 1.0;
w.objects[0].material.refractive_index = 1.5;
let shape = &w.objects[0];
let r = Ray::new(point(0., 0., -5.), vector(0., 0., 1.));
let xs = Intersections::new(vec![
Intersection::new(4., &shape),
Intersection::new(6., &shape),
]);
let comps = prepare_computations(&xs[0], &r, &xs);
let c = w.refracted_color(&comps, 0);
assert_eq!(c, BLACK);
}
#[test]
fn refracted_color_under_total_internal_reflection() {
// The refracted color under total internal reflection.
let mut w = World::test_world();
w.objects[0].material.transparency = 1.0;
w.objects[0].material.refractive_index = 1.5;
let shape = &w.objects[0];
let r = Ray::new(point(0., 0., (2 as Float).sqrt() / 2.), vector(0., 1., 0.));
let xs = Intersections::new(vec![
Intersection::new(-(2 as Float).sqrt() / 2., &shape),
Intersection::new((2 as Float).sqrt() / 2., &shape),
]);
let comps = prepare_computations(&xs[1], &r, &xs);
let c = w.refracted_color(&comps, 5);
assert_eq!(c, BLACK);
}
#[test]
fn refracted_color_with_a_refracted_ray() -> Result<(), Box<dyn std::error::Error>> {
// The refracted color with a refracted ray.
let mut w = World::test_world();
w.objects[0].material.ambient = 1.;
w.objects[0].material.color = test_pattern().build()?;
w.objects[1].material.transparency = 1.;
w.objects[1].material.refractive_index = 1.5;
let r = Ray::new(point(0., 0., 0.1), vector(0., 1., 0.));
let a = &w.objects[0];
let b = &w.objects[1];
let xs = Intersections::new(vec![
Intersection::new(-0.9899, &a),
Intersection::new(-0.4899, &b),
Intersection::new(0.4899, &b),
Intersection::new(0.9899, &a),
]);
let comps = prepare_computations(&xs[2], &r, &xs);
let c = w.refracted_color(&comps, 5);
assert_eq!(c, [0., 0.99887, 0.04721].into());
Ok(())
}
}
}