827 lines
28 KiB
Rust
827 lines
28 KiB
Rust
use std::sync::{Arc, Mutex};
|
|
|
|
use derive_builder::Builder;
|
|
|
|
use crate::{
|
|
intersections::Intersections,
|
|
materials::{Material, MaterialBuilder},
|
|
matrices::{identity, Matrix4x4},
|
|
rays::Ray,
|
|
tuples::Tuple,
|
|
};
|
|
|
|
#[derive(Default, PartialEq, Debug, Clone)]
|
|
pub struct TestData {
|
|
pub saved_ray: Option<Ray>,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub enum Geometry {
|
|
/// Shape with predictable normals useful for debugging.
|
|
TestShape(Arc<Mutex<TestData>>),
|
|
/// Sphere represents the unit-sphere (radius of unit 1.) at the origin 0., 0., 0.
|
|
Sphere,
|
|
/// Flat surface that extends infinitely in the XZ axes.
|
|
Plane,
|
|
/// AABB cube at origin from -1,1 in each direction.
|
|
Cube,
|
|
/// Takes shape from children held within.
|
|
Group(Vec<Shape>),
|
|
}
|
|
|
|
impl Default for Geometry {
|
|
fn default() -> Geometry {
|
|
Geometry::Sphere
|
|
}
|
|
}
|
|
|
|
impl PartialEq for Geometry {
|
|
fn eq(&self, rhs: &Geometry) -> bool {
|
|
use Geometry::*;
|
|
match (self, rhs) {
|
|
(TestShape(l), TestShape(r)) => *l.lock().unwrap() == *r.lock().unwrap(),
|
|
(Sphere, Sphere) => true,
|
|
(Plane, Plane) => true,
|
|
(Cube, Cube) => true,
|
|
(Group(v1), Group(v2)) => v1 == v2,
|
|
_ => false,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Shape represents visible objects. A signal instance of Shape can generically represent one of
|
|
/// many different shapes based on the value of it's geometry field. Users chose the shape by
|
|
/// calling the appropriate constructor, i.e. [Shape::sphere].
|
|
#[derive(Builder, Debug, Clone, PartialEq)]
|
|
#[builder(default, pattern = "owned", build_fn(skip))]
|
|
pub struct Shape {
|
|
transform: Matrix4x4,
|
|
inverse_transform: Matrix4x4,
|
|
pub material: Material,
|
|
geometry: Geometry,
|
|
}
|
|
|
|
/// Short hand for creating a ShapeBuilder with a plane geometry.
|
|
pub fn plane() -> ShapeBuilder {
|
|
ShapeBuilder::plane()
|
|
}
|
|
/// Short hand for creating a ShapeBuilder with a sphere geometry.
|
|
pub fn sphere() -> ShapeBuilder {
|
|
ShapeBuilder::sphere()
|
|
}
|
|
|
|
/// Short hand for creating a ShapeBuilder with a test shape geometry.
|
|
pub fn test_shape() -> ShapeBuilder {
|
|
ShapeBuilder::test_shape()
|
|
}
|
|
|
|
/// Short hand for creating a ShapeBuilder with a cube geometry.
|
|
pub fn cube() -> ShapeBuilder {
|
|
ShapeBuilder::cube()
|
|
}
|
|
|
|
/// Short hand for creating a ShapeBuilder with a group geometry.
|
|
pub fn group() -> ShapeBuilder {
|
|
ShapeBuilder::group()
|
|
}
|
|
|
|
/// Helper for producing a sphere with a glassy material.
|
|
pub fn glass_sphere() -> ShapeBuilder {
|
|
ShapeBuilder::sphere().material(
|
|
MaterialBuilder::default()
|
|
.transparency(1.)
|
|
.refractive_index(1.5)
|
|
.build()
|
|
.unwrap(),
|
|
)
|
|
}
|
|
impl ShapeBuilder {
|
|
/// Short hand for creating a ShapeBuilder with a plane geometry.
|
|
pub fn plane() -> ShapeBuilder {
|
|
ShapeBuilder::default().geometry(Geometry::Plane)
|
|
}
|
|
/// Short hand for creating a ShapeBuilder with a sphere geometry.
|
|
pub fn sphere() -> ShapeBuilder {
|
|
ShapeBuilder::default().geometry(Geometry::Sphere)
|
|
}
|
|
/// Short hand for creating a ShapeBuilder with a test shape geometry.
|
|
pub fn test_shape() -> ShapeBuilder {
|
|
ShapeBuilder::default().geometry(Geometry::TestShape(Arc::new(Mutex::new(
|
|
TestData::default(),
|
|
))))
|
|
}
|
|
/// Short hand for creating a ShapeBuilder with a cube geometry.
|
|
pub fn cube() -> ShapeBuilder {
|
|
ShapeBuilder::default().geometry(Geometry::Cube)
|
|
}
|
|
/// Short hand for creating a ShapeBuilder with a group geometry.
|
|
pub fn group() -> ShapeBuilder {
|
|
ShapeBuilder::default().geometry(Geometry::Group(Vec::new()))
|
|
}
|
|
|
|
/// Add child shapes, only valid for Group::Geometry.
|
|
pub fn add_child(mut self, child: Shape) -> ShapeBuilder {
|
|
if let Some(Geometry::Group(ref mut children)) = &mut self.geometry {
|
|
children.push(child);
|
|
}
|
|
self
|
|
}
|
|
|
|
pub fn build(&self) -> Result<Shape, ShapeBuilderError> {
|
|
let mut s = Shape::default();
|
|
|
|
if let Some(transform) = &self.transform {
|
|
s.set_transform(transform.clone());
|
|
}
|
|
if let Some(material) = &self.material {
|
|
s.material = material.clone();
|
|
};
|
|
if let Some(geometry) = &self.geometry {
|
|
s.geometry = geometry.clone();
|
|
};
|
|
let transform = s.transform().clone();
|
|
if let Geometry::Group(ref mut children) = s.geometry {
|
|
children
|
|
.into_iter()
|
|
.for_each(|c| c.set_transform(transform * c.transform()));
|
|
}
|
|
Ok(s)
|
|
}
|
|
}
|
|
|
|
impl Default for Shape {
|
|
fn default() -> Shape {
|
|
Shape {
|
|
transform: Matrix4x4::identity(),
|
|
inverse_transform: Matrix4x4::identity(),
|
|
material: Material::default(),
|
|
geometry: Geometry::default(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Shape {
|
|
/// Create a test shape useful for debugging.
|
|
pub fn test_shape() -> Shape {
|
|
Shape {
|
|
transform: Matrix4x4::identity(),
|
|
inverse_transform: Matrix4x4::identity(),
|
|
material: Material::default(),
|
|
geometry: Geometry::TestShape(Arc::new(Mutex::new(TestData::default()))),
|
|
}
|
|
}
|
|
pub fn sphere() -> Shape {
|
|
Shape {
|
|
transform: Matrix4x4::identity(),
|
|
inverse_transform: Matrix4x4::identity(),
|
|
material: Material::default(),
|
|
geometry: Geometry::Sphere,
|
|
}
|
|
}
|
|
pub fn plane() -> Shape {
|
|
Shape {
|
|
transform: Matrix4x4::identity(),
|
|
inverse_transform: Matrix4x4::identity(),
|
|
material: Material::default(),
|
|
geometry: Geometry::Plane,
|
|
}
|
|
}
|
|
pub fn cube() -> Shape {
|
|
Shape {
|
|
transform: Matrix4x4::identity(),
|
|
inverse_transform: Matrix4x4::identity(),
|
|
material: Material::default(),
|
|
geometry: Geometry::Cube,
|
|
}
|
|
}
|
|
pub fn group() -> Shape {
|
|
Shape {
|
|
transform: Matrix4x4::identity(),
|
|
inverse_transform: Matrix4x4::identity(),
|
|
material: Material::default(),
|
|
geometry: Geometry::Group(Vec::new()),
|
|
}
|
|
}
|
|
/// Find the normal at the point on the sphere.
|
|
pub fn normal_at(&self, world_point: Tuple) -> Tuple {
|
|
let object_point = self.inverse_transform * world_point;
|
|
let object_normal = match self.geometry {
|
|
Geometry::Sphere => object_point - Tuple::point(0., 0., 0.),
|
|
Geometry::Plane => Tuple::vector(0., 1., 0.),
|
|
Geometry::TestShape(_) => object_point,
|
|
Geometry::Cube => cube::local_normal_at(object_point),
|
|
Geometry::Group(_) => todo!("normal_at"),
|
|
};
|
|
let mut world_normal = self.inverse_transform.transpose() * object_normal;
|
|
world_normal.w = 0.;
|
|
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();
|
|
}
|
|
pub fn geometry(&self) -> &Geometry {
|
|
&self.geometry
|
|
}
|
|
}
|
|
|
|
/// Intersect a ray with a shapes.
|
|
pub fn intersect<'s>(shape: &'s Shape, ray: &Ray) -> Intersections<'s> {
|
|
let local_ray = ray.transform(shape.inverse_transform);
|
|
match shape.geometry {
|
|
Geometry::Sphere => sphere::intersect(shape, &local_ray),
|
|
Geometry::Plane => plane::intersect(shape, &local_ray),
|
|
Geometry::TestShape(_) => test_shape::intersect(shape, &local_ray),
|
|
Geometry::Cube => cube::intersect(shape, &local_ray),
|
|
Geometry::Group(_) => group::intersect(shape, &ray),
|
|
}
|
|
}
|
|
|
|
mod test_shape {
|
|
use crate::{
|
|
intersections::Intersections,
|
|
rays::Ray,
|
|
shapes::{Geometry, Shape},
|
|
};
|
|
pub fn intersect<'s>(shape: &'s Shape, ray: &Ray) -> Intersections<'s> {
|
|
if let Geometry::TestShape(s) = &shape.geometry {
|
|
s.lock()
|
|
.expect("couldn't grab mutex for TestData")
|
|
.saved_ray = Some(ray.clone());
|
|
}
|
|
Intersections::default()
|
|
}
|
|
}
|
|
|
|
mod sphere {
|
|
use crate::{
|
|
intersections::{Intersection, Intersections},
|
|
rays::Ray,
|
|
shapes::Shape,
|
|
tuples::{dot, Tuple},
|
|
};
|
|
pub fn intersect<'s>(shape: &'s Shape, ray: &Ray) -> Intersections<'s> {
|
|
let sphere_to_ray = ray.origin - Tuple::point(0., 0., 0.);
|
|
let a = dot(ray.direction, ray.direction);
|
|
let b = 2. * dot(ray.direction, sphere_to_ray);
|
|
let c = dot(sphere_to_ray, sphere_to_ray) - 1.;
|
|
let discriminant = b * b - 4. * a * c;
|
|
if discriminant < 0. {
|
|
return Intersections::default();
|
|
}
|
|
Intersections::new(vec![
|
|
Intersection::new((-b - discriminant.sqrt()) / (2. * a), &shape),
|
|
Intersection::new((-b + discriminant.sqrt()) / (2. * a), &shape),
|
|
])
|
|
}
|
|
}
|
|
|
|
mod plane {
|
|
use crate::{
|
|
intersections::{Intersection, Intersections},
|
|
rays::Ray,
|
|
shapes::Shape,
|
|
EPSILON,
|
|
};
|
|
pub fn intersect<'s>(shape: &'s Shape, ray: &Ray) -> Intersections<'s> {
|
|
if (ray.direction.y).abs() < EPSILON {
|
|
return Intersections::default();
|
|
}
|
|
Intersections::new(vec![Intersection::new(
|
|
-ray.origin.y / ray.direction.y,
|
|
&shape,
|
|
)])
|
|
}
|
|
}
|
|
|
|
mod cube {
|
|
use crate::{
|
|
intersections::{Intersection, Intersections},
|
|
rays::Ray,
|
|
shapes::Shape,
|
|
tuples::{vector, Tuple},
|
|
Float, EPSILON,
|
|
};
|
|
|
|
fn check_axis(origin: Float, direction: Float) -> (Float, Float) {
|
|
let tmin_numerator = -1. - origin;
|
|
let tmax_numerator = 1. - origin;
|
|
|
|
let (tmin, tmax) = if direction.abs() >= EPSILON {
|
|
(tmin_numerator / direction, tmax_numerator / direction)
|
|
} else {
|
|
(
|
|
tmin_numerator * Float::INFINITY,
|
|
tmax_numerator * Float::INFINITY,
|
|
)
|
|
};
|
|
|
|
if tmin > tmax {
|
|
return (tmax, tmin);
|
|
}
|
|
(tmin, tmax)
|
|
}
|
|
|
|
pub fn intersect<'s>(shape: &'s Shape, ray: &Ray) -> Intersections<'s> {
|
|
let (xtmin, xtmax) = check_axis(ray.origin.x, ray.direction.x);
|
|
let (ytmin, ytmax) = check_axis(ray.origin.y, ray.direction.y);
|
|
let (ztmin, ztmax) = check_axis(ray.origin.z, ray.direction.z);
|
|
|
|
let tmin = xtmin.max(ytmin).max(ztmin);
|
|
let tmax = xtmax.min(ytmax).min(ztmax);
|
|
|
|
if tmin > tmax {
|
|
return Intersections::default();
|
|
}
|
|
Intersections::new(vec![
|
|
Intersection::new(tmin, &shape),
|
|
Intersection::new(tmax, &shape),
|
|
])
|
|
}
|
|
pub fn local_normal_at(point: Tuple) -> Tuple {
|
|
let x = point.x.abs();
|
|
let y = point.y.abs();
|
|
let z = point.z.abs();
|
|
let maxc = x.max(y).max(z);
|
|
if maxc == x {
|
|
return vector(point.x, 0., 0.);
|
|
}
|
|
if maxc == y {
|
|
return vector(0., point.y, 0.);
|
|
}
|
|
vector(0., 0., point.z)
|
|
}
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use crate::{
|
|
rays::Ray,
|
|
shapes::Shape,
|
|
tuples::{point, vector},
|
|
EPSILON,
|
|
};
|
|
|
|
use super::{intersect, local_normal_at};
|
|
|
|
#[test]
|
|
fn ray_intersects_cube() {
|
|
let c = Shape::cube();
|
|
for (name, o, d, t1, t2) in [
|
|
("+x", point(5., 0.5, 0.), vector(-1., 0., 0.), 4., 6.),
|
|
("-x", point(-5., 0.5, 0.), vector(1., 0., 0.), 4., 6.),
|
|
("+y", point(0.5, 5., 0.), vector(0., -1., 0.), 4., 6.),
|
|
("-y", point(0.5, -5., 0.), vector(0., 1., 0.), 4., 6.),
|
|
("+z", point(0.5, 0., 5.), vector(0., 0., -1.), 4., 6.),
|
|
("-z", point(0.5, 0., -5.), vector(0., 0., 1.), 4., 6.),
|
|
("inside", point(0., 0.5, 0.), vector(0., 0., 1.), -1., 1.),
|
|
] {
|
|
let r = Ray::new(o, d);
|
|
let xs = intersect(&c, &r);
|
|
assert_eq!(xs.len(), 2, "{}", name);
|
|
assert!((xs[0].t - t1).abs() < EPSILON, "{} t1 {}", name, xs[0].t);
|
|
assert!((xs[1].t - t2).abs() < EPSILON, "{} t2 {}", name, xs[1].t);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn ray_misses_cube() {
|
|
let c = Shape::cube();
|
|
for (o, d) in [
|
|
(point(-2., 0., 0.), vector(0.2673, 0.5345, 0.8018)),
|
|
(point(0., -2., 0.), vector(0.8018, 0.2673, 0.5345)),
|
|
(point(0., 0., -2.), vector(0.5345, 0.8018, 0.2673)),
|
|
(point(2., 0., 2.), vector(0., 0., -1.)),
|
|
(point(0., 2., 2.), vector(0., -1., 0.)),
|
|
(point(2., 2., -5.), vector(-1., 0., 0.)),
|
|
] {
|
|
let r = Ray::new(o, d);
|
|
let xs = intersect(&c, &r);
|
|
assert_eq!(xs.len(), 0, "({:?}, {:?})", o, d);
|
|
}
|
|
}
|
|
#[test]
|
|
fn normal_cube_surface() {
|
|
for (p, n) in [
|
|
(point(1., 0.8, -0.8), vector(1., 0., 0.)),
|
|
(point(-1., -0.2, 0.9), vector(-1., 0., 0.)),
|
|
(point(-0.4, 1., -0.1), vector(0., 1., 0.)),
|
|
(point(0.3, -1., -0.7), vector(0., -1., 0.)),
|
|
(point(-0.6, 0.3, 1.), vector(0., 0., 1.)),
|
|
(point(0.4, 0.4, -1.), vector(0., 0., -1.)),
|
|
(point(1., 1., 1.), vector(1., 0., 0.)),
|
|
(point(-1., -1., -1.), vector(-1., 0., 0.)),
|
|
] {
|
|
let normal = local_normal_at(p);
|
|
assert_eq!(n, normal);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
mod group {
|
|
use crate::{intersections::Intersections, rays::Ray, shapes::Shape};
|
|
|
|
use super::Geometry;
|
|
|
|
pub fn intersect<'s>(shape: &'s Shape, ray: &Ray) -> Intersections<'s> {
|
|
if let Geometry::Group(children) = &shape.geometry {
|
|
let mut intersections: Vec<_> = children
|
|
.iter()
|
|
.map(|c| super::intersect(c, ray))
|
|
.flatten()
|
|
.collect();
|
|
intersections.sort_by(|a, b| {
|
|
a.t.partial_cmp(&b.t)
|
|
.expect("an intersection has a t value that is NaN")
|
|
});
|
|
return Intersections::new(intersections);
|
|
}
|
|
unreachable!();
|
|
}
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use crate::{
|
|
matrices::{scaling, translation},
|
|
rays::Ray,
|
|
shapes::{intersect, Shape, ShapeBuilder},
|
|
tuples::{point, vector},
|
|
};
|
|
|
|
#[test]
|
|
fn intersecting_empty_group() {
|
|
let g = Shape::group();
|
|
let r = Ray::new(point(0., 0., 0.), vector(0., 0., 1.));
|
|
let xs = intersect(&g, &r);
|
|
assert_eq!(xs.len(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn intersecting_nonempty_group() -> Result<(), Box<dyn std::error::Error>> {
|
|
let s1 = Shape::sphere();
|
|
let s2 = ShapeBuilder::sphere()
|
|
.transform(translation(0., 0., -3.))
|
|
.build()?;
|
|
let s3 = ShapeBuilder::sphere()
|
|
.transform(translation(5., 0., 0.))
|
|
.build()?;
|
|
let g = ShapeBuilder::group()
|
|
.add_child(s1.clone())
|
|
.add_child(s2.clone())
|
|
.add_child(s3.clone())
|
|
.build()?;
|
|
let r = Ray::new(point(0., 0., -5.), vector(0., 0., 1.));
|
|
let xs = intersect(&g, &r);
|
|
assert_eq!(xs.len(), 4);
|
|
assert_eq!(xs[0].object, &s2);
|
|
assert_eq!(xs[1].object, &s2);
|
|
assert_eq!(xs[2].object, &s1);
|
|
assert_eq!(xs[3].object, &s1);
|
|
Ok(())
|
|
}
|
|
#[test]
|
|
fn intersecting_transformed_group() -> Result<(), Box<dyn std::error::Error>> {
|
|
let g = ShapeBuilder::group()
|
|
.transform(scaling(2., 2., 2.))
|
|
.add_child(
|
|
ShapeBuilder::sphere()
|
|
.transform(translation(5., 0., 0.))
|
|
.build()?,
|
|
)
|
|
.build()?;
|
|
let r = Ray::new(point(10., 0., -10.), vector(0., 0., 1.));
|
|
let xs = intersect(&g, &r);
|
|
assert_eq!(xs.len(), 2);
|
|
Ok(())
|
|
}
|
|
}
|
|
}
|
|
#[cfg(test)]
|
|
mod tests {
|
|
mod shape_builder {
|
|
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);
|
|
}
|
|
}
|
|
}
|