Compare commits
30 Commits
62ad827507
...
9befbd9ad2
| Author | SHA1 | Date | |
|---|---|---|---|
| 9befbd9ad2 | |||
| c882fc81e5 | |||
| 1c2caf2cc5 | |||
| 9006671a26 | |||
| 3838efd134 | |||
| e3d8988658 | |||
| e4846de25b | |||
| 4352c03d20 | |||
| 1d5e5a164b | |||
| f476822bcd | |||
| 135a519526 | |||
| 5d6b3e6d57 | |||
| 3aea76b35c | |||
| cd2a4770ca | |||
| 5debb16d10 | |||
| 42e8ebe3bd | |||
| 1d61f59935 | |||
| 7f36aecf5e | |||
| 0c7bbae4a3 | |||
| eaae65712b | |||
| 68709da6c2 | |||
| 77215193fa | |||
| 74fe69188a | |||
| bdcee49d5a | |||
| 2e4e8b3dcd | |||
| b9f2c3f0ec | |||
| 8b79876aee | |||
| bfa3282a37 | |||
| 3e383c4dbd | |||
| c158d92252 |
@ -8,7 +8,6 @@ edition = "2018"
|
||||
|
||||
[features]
|
||||
default = [ "float-as-double" ]
|
||||
disable-inverse-cache = []
|
||||
float-as-double = []
|
||||
|
||||
[dependencies]
|
||||
|
||||
168
rtchallenge/examples/eoc10.rs
Normal file
168
rtchallenge/examples/eoc10.rs
Normal 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(())
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@ -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(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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},
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 Gauss–Jordan 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
361
rtchallenge/src/patterns.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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.));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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.],
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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.));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user