Compare commits

..

No commits in common. "5f3bfd744eb310b2ec93143bc1acfca65718a446" and "ac4f5eb9a6c1245c08e3f1c213251170ce887601" have entirely different histories.

16 changed files with 29 additions and 888 deletions

View File

@ -1,3 +1,5 @@
use core::f32;
use anyhow::Result;
use rtchallenge::{

View File

@ -1,57 +0,0 @@
use anyhow::Result;
use rtchallenge::{
canvas::Canvas,
lights::PointLight,
materials::{lighting, Material},
rays::Ray,
spheres::{intersect, Sphere},
tuples::{Color, Tuple},
WHITE,
};
fn main() -> Result<()> {
let w = 640;
let h = w;
let bg = Color::new(0.2, 0.2, 0.2);
let mut c = Canvas::new(w, h, bg);
let ray_origin = Tuple::point(0., 0., -5.);
let wall_z = 10.;
let wall_size = 7.;
let pixel_size = wall_size / w as f32;
let half = wall_size / 2.;
let mut shape = Sphere::default();
shape.material = Material {
color: Color::new(1., 0.2, 1.),
specular: 0.5,
diffuse: 0.7,
shininess: 30.,
..Material::default()
};
let light_position = Tuple::point(-10., 10., -10.);
let light_color = WHITE;
let light = PointLight::new(light_position, light_color);
for y in 0..h {
let world_y = half - pixel_size * y as f32;
for x in 0..w {
let world_x = -half + pixel_size * x as f32;
let position = Tuple::point(world_x, world_y, wall_z);
let direction = (position - ray_origin).normalize();
let r = Ray::new(ray_origin, direction);
let xs = intersect(&shape, &r);
if let Some(hit) = xs.hit() {
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);
c.set(x, y, color);
}
}
}
let path = "/tmp/eoc6.png";
println!("saving output to {}", path);
c.write_to_file(path)?;
Ok(())
}

View File

@ -1,91 +0,0 @@
use std::{f32::consts::PI, time::Instant};
use anyhow::Result;
use rtchallenge::{
camera::Camera,
lights::PointLight,
materials::Material,
matrices::Matrix4x4,
spheres::Sphere,
transformations::view_transform,
tuples::{Color, Tuple},
world::World,
WHITE,
};
fn main() -> Result<()> {
let start = Instant::now();
let width = 640;
let height = 480;
let light_position = Tuple::point(-10., 10., -10.);
let light_color = WHITE;
let light = PointLight::new(light_position, light_color);
let mut camera = Camera::new(width, height, PI / 3.);
let from = Tuple::point(0., 1.5, -5.);
let to = Tuple::point(0., 1., 0.);
let up = Tuple::point(0., 1., 0.);
camera.transform = view_transform(from, to, up);
let mut floor = Sphere::default();
floor.transform = Matrix4x4::scaling(10., 0.01, 10.);
floor.material = Material {
color: Color::new(1., 0.9, 0.9),
specular: 0.,
..Material::default()
};
let mut left_wall = Sphere::default();
left_wall.transform = Matrix4x4::translation(0., 0., 5.)
* Matrix4x4::rotation_y(-PI / 4.)
* Matrix4x4::rotation_x(PI / 2.)
* Matrix4x4::scaling(10., 0.01, 10.);
left_wall.material = floor.material.clone();
let mut right_wall = Sphere::default();
right_wall.transform = Matrix4x4::translation(0., 0., 5.)
* Matrix4x4::rotation_y(PI / 4.)
* Matrix4x4::rotation_x(PI / 2.)
* Matrix4x4::scaling(10., 0.01, 10.);
right_wall.material = floor.material.clone();
let mut middle = Sphere::default();
middle.transform = Matrix4x4::translation(-0.5, 1., 0.5);
middle.material = Material {
color: Color::new(0.1, 1., 0.5),
diffuse: 0.7,
specular: 0.3,
..Material::default()
};
let mut right = Sphere::default();
right.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),
diffuse: 0.7,
specular: 0.3,
..Material::default()
};
let mut left = Sphere::default();
left.transform =
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),
diffuse: 0.7,
specular: 0.3,
..Material::default()
};
let mut world = World::default();
world.light = Some(light);
world.objects = vec![floor, left_wall, right_wall, middle, right, left];
let image = camera.render(&world);
let path = "/tmp/eoc7.png";
println!("saving output to {}", path);
image.write_to_file(path)?;
println!("Render time {:.2} seconds", start.elapsed().as_secs_f32());
Ok(())
}

View File

@ -1 +0,0 @@
format_code_in_doc_comments = true

View File

@ -1,159 +0,0 @@
use crate::{canvas::Canvas, matrices::Matrix4x4, rays::Ray, tuples::Tuple, world::World, BLACK};
pub struct Camera {
hsize: usize,
vsize: usize,
field_of_view: f32,
pub transform: Matrix4x4,
pixel_size: f32,
half_width: f32,
half_height: f32,
}
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 std::f32::consts::PI;
///
/// use rtchallenge::{camera::Camera, matrices::Matrix4x4};
///
/// 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_eq!(c.pixel_size(), 0.01);
///
/// // Pixel size for a horizontal canvas.
/// let c = Camera::new(150, 200, PI / 2.);
/// assert_eq!(c.pixel_size(), 0.01);
/// ```
pub fn new(hsize: usize, vsize: usize, field_of_view: f32) -> Camera {
let half_view = (field_of_view / 2.).tan();
let aspect = hsize as f32 / vsize as f32;
let (half_width, half_height) = if aspect >= 1. {
(half_view, half_view / aspect)
} else {
(half_view * aspect, half_view)
};
let pixel_size = 2. * half_width / hsize as f32;
Camera {
hsize,
vsize,
field_of_view,
transform: Matrix4x4::identity(),
pixel_size,
half_height,
half_width,
}
}
pub fn hsize(&self) -> usize {
self.hsize
}
pub fn vsize(&self) -> usize {
self.vsize
}
pub fn field_of_view(&self) -> f32 {
self.field_of_view
}
pub fn transform(&self) -> Matrix4x4 {
self.transform
}
pub fn pixel_size(&self) -> f32 {
self.pixel_size
}
/// Calculate ray that starts at the camera and passes through the (x,y)
/// pixel on the canvas.
///
/// # Examples
/// ```
/// use std::f32::consts::PI;
///
/// use rtchallenge::{camera::Camera, matrices::Matrix4x4, tuples::Tuple};
///
/// // 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.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_f32.sqrt() / 2., 0., -2_f32.sqrt() / 2.)
/// );
/// ```
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 f32 + 0.5) * self.pixel_size;
let yoffset = (py as f32 + 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 canves 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 std::f32::consts::PI;
///
/// use rtchallenge::{
/// camera::Camera,
/// 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.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 {
let mut image = Canvas::new(self.hsize, self.vsize, BLACK);
for y in 0..self.vsize {
for x in 0..self.hsize {
let ray = self.ray_for_pixel(x, y);
let color = w.color_at(&ray);
image.set(x, y, color);
}
}
image
}
}

View File

@ -39,7 +39,7 @@ impl Canvas {
}
self.pixels[x + y * self.width] = c;
}
pub fn get(&self, x: usize, y: usize) -> Color {
pub fn get(&mut self, x: usize, y: usize) -> Color {
self.pixels[x + y * self.width]
}
pub fn write_to_file<P>(&self, path: P) -> Result<(), CanvasError>
@ -74,16 +74,17 @@ impl Canvas {
mod tests {
use super::Canvas;
use crate::{tuples::Color, BLACK};
use crate::tuples::Color;
#[test]
fn create_canvas() {
let bg = BLACK;
let bg = Color::new(0.0, 0.0, 0.0);
let c = Canvas::new(10, 20, bg);
assert_eq!(c.width, 10);
assert_eq!(c.height, 20);
let black = Color::new(0., 0., 0.);
for (i, p) in c.pixels.iter().enumerate() {
assert_eq!(p, &BLACK, "pixel {} not {:?}: {:?}", i, &BLACK, p);
assert_eq!(p, &black, "pixel {} not {:?}: {:?}", i, &black, p);
}
}

View File

@ -1,10 +1,6 @@
use std::ops::Index;
use crate::{
rays::Ray,
spheres::Sphere,
tuples::{dot, Tuple},
};
use crate::spheres::Sphere;
#[derive(Debug, Clone, PartialEq)]
pub struct Intersection<'i> {
@ -118,86 +114,9 @@ impl<'i> Intersections<'i> {
}
}
impl<'i> IntoIterator for Intersections<'i> {
type Item = Intersection<'i>;
type IntoIter = std::vec::IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl<'i> Index<usize> for Intersections<'i> {
type Output = Intersection<'i>;
fn index(&self, idx: usize) -> &Self::Output {
&self.0[idx]
}
}
pub struct PrecomputedData<'i> {
pub t: f32,
pub object: &'i Sphere,
pub point: Tuple,
pub eyev: Tuple,
pub normalv: Tuple,
pub inside: bool,
}
/// Precomputes data common to all intersections.
///
/// # Examples
/// ```
/// use rtchallenge::{
/// intersections::{prepare_computations, Intersection, Intersections},
/// rays::Ray,
/// spheres::{intersect, Sphere},
/// tuples::Tuple,
/// };
///
/// // Precomputing the state of an intersection.
/// let r = Ray::new(Tuple::point(0., 0., -5.), Tuple::vector(0., 0., 1.));
/// let shape = Sphere::default();
/// 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 = Sphere::default();
/// 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 = Sphere::default();
/// 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.));
/// ```
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);
let eyev = -r.direction;
let (inside, normalv) = if dot(normalv, eyev) < 0. {
(true, -normalv)
} else {
(false, normalv)
};
PrecomputedData {
t: i.t,
object: i.object,
point,
normalv,
inside,
eyev,
}
}

View File

@ -1,17 +1,9 @@
pub mod camera;
pub mod canvas;
pub mod intersections;
pub mod lights;
pub mod materials;
pub mod matrices;
pub mod rays;
pub mod spheres;
pub mod transformations;
pub mod tuples;
pub mod world;
/// Value considered close enough for PartialEq implementations.
pub const EPSILON: f32 = 0.00001;
pub const BLACK: tuples::Color = tuples::Color::new(0., 0., 0.);
pub const WHITE: tuples::Color = tuples::Color::new(1., 1., 1.);

View File

@ -1,33 +0,0 @@
use crate::tuples::{Color, Tuple};
#[derive(Debug, PartialEq)]
pub struct PointLight {
pub position: Tuple,
pub intensity: Color,
}
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(position: Tuple, intensity: Color) -> PointLight {
PointLight {
position,
intensity,
}
}
}

View File

@ -1,132 +0,0 @@
use crate::{
lights::PointLight,
tuples::Color,
tuples::{dot, reflect, Tuple},
BLACK, WHITE,
};
#[derive(Debug, PartialEq, Clone)]
pub struct Material {
pub color: Color,
pub ambient: f32,
pub diffuse: f32,
pub specular: f32,
pub shininess: f32,
}
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,
ambient: 0.1,
diffuse: 0.9,
specular: 0.9,
shininess: 200.,
}
}
}
/// Compute lighting contributions using the Phong reflection model.
///
/// # Examples
/// ```
/// use rtchallenge::{
/// lights::PointLight,
/// materials::{lighting, Material},
/// tuples::{Color, Tuple},
/// WHITE,
/// };
///
/// 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);
/// 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_f32.sqrt() / 2., -2_f32.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);
/// 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);
/// 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_f32.sqrt() / 2., -2_f32.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);
/// assert_eq!(result, Color::new(1.6363853, 1.6363853, 1.6363853));
///
/// // 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);
/// assert_eq!(result, Color::new(0.1, 0.1, 0.1));
/// ```
pub fn lighting(
material: &Material,
light: &PointLight,
point: Tuple,
eyev: Tuple,
normalv: Tuple,
) -> Color {
// Combine the surface color with the light's color.
let effective_color = material.color * light.intensity;
// Find the direciton of the light source.
let lightv = (light.position - point).normalize();
// Compute the ambient distribution.
let ambient = effective_color * material.ambient;
// This is the cosine of the angle between the light vector an the normal
// vector. A negative number means the light is on the other side of the
// surface.
let light_dot_normal = dot(lightv, normalv);
let (diffuse, specular) = if light_dot_normal < 0. {
(BLACK, BLACK)
} else {
// Compute the diffuse contribution.
let diffuse = effective_color * material.diffuse * light_dot_normal;
// This represents the cosine of the angle between the relfection vector
// and the eye vector. A negative number means the light reflects away
// from the eye.
let reflectv = reflect(-lightv, normalv);
let reflect_dot_eye = dot(reflectv, eyev);
let specular = if reflect_dot_eye <= 0. {
BLACK
} else {
// Compute the specular contribution.
let factor = reflect_dot_eye.powf(material.shininess);
light.intensity * material.specular * factor
};
(diffuse, specular)
};
ambient + diffuse + specular
}

View File

@ -1,5 +1,5 @@
use std::fmt;
use std::ops::{Index, IndexMut, Mul, Sub};
use std::ops::{Index, IndexMut, Mul};
use crate::{tuples::Tuple, EPSILON};
@ -161,6 +161,7 @@ impl PartialEq for Matrix3x3 {
}
}
#[derive(Copy, Clone, Default)]
/// 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.
///
@ -193,7 +194,6 @@ impl PartialEq for Matrix3x3 {
/// let t = c * b * a;
/// assert_eq!(t * p, Tuple::point(15., 0., 7.));
/// ```
#[derive(Copy, Clone, Default)]
pub struct Matrix4x4 {
m: [[f32; 4]; 4],
}
@ -764,7 +764,7 @@ impl fmt::Debug for Matrix4x4 {
if f.alternate() {
write!(
f,
"\n {:8.5?}\n {:8.5?}\n {:8.5?}\n {:8.5?}",
"{:?}\n {:?}\n {:?}\n {:?}",
self.m[0], self.m[1], self.m[2], self.m[3]
)
} else {
@ -838,21 +838,6 @@ impl Mul<Tuple> for Matrix4x4 {
}
}
impl Sub for Matrix4x4 {
type Output = Matrix4x4;
fn sub(self, m2: Matrix4x4) -> Matrix4x4 {
let m1 = self;
let mut r: Matrix4x4 = Default::default();
for i in 0..4 {
for j in 0..4 {
r.m[i][j] = m1.m[i][j] - m2.m[i][j];
}
}
r
}
}
impl PartialEq for Matrix4x4 {
fn eq(&self, rhs: &Matrix4x4) -> bool {
let l = self.m;

View File

@ -1,9 +1,9 @@
use crate::{
intersections::{Intersection, Intersections},
materials::Material,
matrices::Matrix4x4,
rays::Ray,
tuples::{dot, Tuple},
EPSILON,
};
#[derive(Debug, PartialEq)]
@ -11,13 +11,12 @@ use crate::{
pub struct Sphere {
// TODO(wathiede): cache inverse to speed up intersect.
pub transform: Matrix4x4,
pub material: Material,
}
impl Default for Sphere {
/// # Examples
/// ```
/// use rtchallenge::{materials::Material, matrices::Matrix4x4, spheres::Sphere};
/// use rtchallenge::{matrices::Matrix4x4, spheres::Sphere};
///
/// // A sphere's default transform is the identity matrix.
/// let s = Sphere::default();
@ -28,21 +27,10 @@ impl Default for Sphere {
/// let t = Matrix4x4::translation(2., 3., 4.);
/// s.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 = Sphere::default();
/// let mut m = Material::default();
/// m.ambient = 1.;
/// s.material = m.clone();
/// assert_eq!(s.material, m);
/// ```
fn default() -> Sphere {
Sphere {
transform: Matrix4x4::identity(),
material: Material::default(),
}
}
}
@ -88,27 +76,21 @@ impl Sphere {
/// 3_f32.sqrt() / 3.,
/// ));
/// assert_eq!(n, n.normalize());
///
/// // Compute the normal on a translated sphere.
/// let mut s = Sphere::default();
/// s.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.
/// use std::f32::consts::PI;
///
/// let mut s = Sphere::default();
/// s.transform = Matrix4x4::scaling(1.,0.5,1.) * Matrix4x4::rotation_z(PI/5.);
/// let n = s.normal_at(Tuple::point(0., 2_f32.sqrt()/2., -2_f32.sqrt()/2.));
/// assert_eq!(n, Tuple::vector(0., 0.97014, -0.24254));
/// ```
pub fn normal_at(&self, world_point: Tuple) -> Tuple {
let object_point = self.transform.inverse() * world_point;
let object_normal = object_point - Tuple::point(0., 0., 0.);
let mut world_normal = self.transform.inverse().transpose() * object_normal;
world_normal.w = 0.;
world_normal.normalize()
/// ```should_panic
/// use rtchallenge::{spheres::Sphere, tuples::Tuple};
///
/// // In debug builds points not on the sphere should fail.
/// let s = Sphere::default();
/// let n = s.normal_at(Tuple::point(0., 0., 0.5));
/// ```
pub fn normal_at(&self, point: Tuple) -> Tuple {
debug_assert!(
((point - Tuple::point(0., 0., 0.)).magnitude() - 1.).abs() < EPSILON,
"{} != 1.",
(point - Tuple::point(0., 0., 0.)).magnitude()
);
(point - Tuple::point(0., 0., 0.)).normalize()
}
}

View File

@ -1,59 +0,0 @@
use crate::{
matrices::Matrix4x4,
tuples::{cross, Tuple},
};
/// 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());
let true_up = cross(left, forward);
Matrix4x4::new(
[left.x, left.y, left.z, 0.],
[true_up.x, true_up.y, true_up.z, 0.],
[-forward.x, -forward.y, -forward.z, 0.],
[0., 0., 0., 1.],
) * Matrix4x4::translation(-from.x, -from.y, -from.z)
}

View File

@ -46,28 +46,6 @@ impl Tuple {
}
}
/// Reflects vector v across normal n.
///
/// # Examples
/// ```
/// use rtchallenge::tuples::{reflect, Tuple};
///
/// // 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_f32.sqrt() / 2., 2_f32.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)
}
impl Add for Tuple {
type Output = Self;
fn add(self, other: Self) -> Self {
@ -158,24 +136,17 @@ pub fn cross(a: Tuple, b: Tuple) -> Tuple {
)
}
#[derive(Copy, Clone, Debug)]
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct Color {
pub red: f32,
pub green: f32,
pub blue: f32,
}
impl Color {
pub const fn new(red: f32, green: f32, blue: f32) -> Color {
pub fn new(red: f32, green: f32, blue: f32) -> Color {
Color { red, green, blue }
}
}
impl PartialEq for Color {
fn eq(&self, rhs: &Color) -> bool {
((self.red - rhs.red).abs() < EPSILON)
&& ((self.green - rhs.green).abs() < EPSILON)
&& ((self.blue - rhs.blue).abs() < EPSILON)
}
}
impl Add for Color {
type Output = Self;
fn add(self, other: Self) -> Self {

View File

@ -1,178 +0,0 @@
use crate::{
intersections::{prepare_computations, Intersections, PrecomputedData},
lights::PointLight,
materials::{lighting, Material},
matrices::Matrix4x4,
rays::Ray,
spheres::{intersect, Sphere},
tuples::{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.light, None);
/// ```
#[derive(Debug, Default)]
pub struct World {
// TODO(wathiede): make this a list of abstract Light traits.
pub light: Option<PointLight>,
pub objects: Vec<Sphere>,
}
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.light.is_some());
/// ```
pub fn test_world() -> World {
let light = PointLight::new(Tuple::point(-10., 10., -10.), WHITE);
let mut s1 = Sphere::default();
s1.material = Material {
color: Color::new(0.8, 1., 0.6),
diffuse: 0.7,
specular: 0.2,
..Material::default()
};
let mut s2 = Sphere::default();
s2.transform = Matrix4x4::scaling(0.5, 0.5, 0.5);
World {
light: Some(light),
objects: vec![s1, s2],
}
}
/// 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.);
/// ```
pub fn intersect(&self, r: &Ray) -> Intersections {
let mut xs: Vec<_> = self
.objects
.iter()
.map(|o| intersect(&o, &r))
.flatten()
.collect();
xs.sort_by(|i1, i2| {
i1.t.partial_cmp(&i2.t)
.expect("an intersection has a t value that is NaN")
});
Intersections::new(xs)
}
/// Compute shaded value for given precomputation.
///
/// # Examples
/// ```
/// use rtchallenge::{
/// intersections::{prepare_computations, Intersection},
/// lights::PointLight,
/// rays::Ray,
/// 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.light = Some(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));
/// ```
pub fn shade_hit(&self, comps: &PrecomputedData) -> Color {
// TODO(wathiede): support multiple light sources by iterating over all
// the light sources and summing the calls to lighting.
lighting(
&comps.object.material,
&self.light.as_ref().expect("World has no lights"),
comps.point,
comps.eyev,
comps.normalv,
)
}
/// Compute color for given ray fired at the world.
///
/// # Examples
/// ```
/// use rtchallenge::{
/// intersections::{prepare_computations, Intersection},
/// lights::PointLight,
/// rays::Ray,
/// 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];
/// 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() {
Some(hit) => {
let comps = prepare_computations(&hit, r);
self.shade_hit(&comps)
}
None => BLACK,
}
}
}

View File

@ -1 +0,0 @@
format_code_in_doc_comments = true