Compare commits

..

13 Commits

7 changed files with 356 additions and 8 deletions

View File

@ -24,20 +24,18 @@ fn main() -> Result<()> {
let w = 200;
let h = w;
let mut c = Canvas::new(w, h);
let t = Matrix4x4::translate(0., 0.4, 0.);
let t = Matrix4x4::translation(0., 0.4, 0.);
let p = Tuple::point(0., 0., 0.);
let rot_hour = Matrix4x4::rotation_z(-PI / 6.);
let mut p = t * p;
let w = w as f32;
let h = h as f32;
let h_w = w / 2.0;
let h_h = h / 2.0;
// The 'world' exists between -0.5 - 0.5 in X-Y plane.
// To convert to screen space, we translate by 0.5, scale to canvas size,
// and invert the Y-axis.
let world_to_screen =
Matrix4x4::scaling(w as f32, -h as f32, 1.0) * Matrix4x4::translate(0.5, -0.5, 0.);
Matrix4x4::scaling(w as f32, -h as f32, 1.0) * Matrix4x4::translation(0.5, -0.5, 0.);
for _ in 0..12 {
let canvas_pixel = world_to_screen * p;
draw_dot(&mut c, canvas_pixel.x as usize, canvas_pixel.y as usize);

View File

@ -0,0 +1,41 @@
use core::f32;
use anyhow::Result;
use rtchallenge::{
canvas::Canvas,
rays::Ray,
spheres::{intersect, Sphere},
tuples::{Color, Tuple},
};
fn main() -> Result<()> {
let w = 200;
let h = w;
let mut c = Canvas::new(w, h);
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 color = Color::new(1., 0., 0.);
let shape = Sphere::default();
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 r = Ray::new(ray_origin, (position - ray_origin).normalize());
let xs = intersect(&shape, &r);
if xs.hit().is_some() {
c.set(x, y, color);
}
}
}
let path = "/tmp/eoc5.png";
println!("saving output to {}", path);
c.write_to_file(path)?;
Ok(())
}

View File

@ -0,0 +1,122 @@
use std::ops::Index;
use crate::spheres::Sphere;
#[derive(Debug, Clone, PartialEq)]
pub struct Intersection<'i> {
pub t: f32,
pub object: &'i Sphere,
}
impl<'i> Intersection<'i> {
/// Create new `Intersection` at the given `t` that hits the given `object`.
///
/// # Examples
/// ```
/// use rtchallenge::{intersections::Intersection, spheres::Sphere};
///
/// // An intersection ecapsulates t and object.
/// let s = Sphere::default();
/// let i = Intersection::new(3.5, &s);
/// assert_eq!(i.t, 3.5);
/// assert_eq!(i.object, &s);
/// ```
pub fn new(t: f32, object: &Sphere) -> Intersection {
Intersection { t, object }
}
}
/// Aggregates `Intersection`s.
///
/// # Examples
/// ```
/// use rtchallenge::{
/// intersections::{Intersection, Intersections},
/// rays::Ray,
/// spheres::{intersect, Sphere},
/// tuples::Tuple,
/// };
///
/// let s = Sphere::default();
/// 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>>);
impl<'i> Intersections<'i> {
pub fn new(xs: Vec<Intersection<'i>>) -> Intersections {
Intersections(xs)
}
pub fn len(&self) -> usize {
self.0.len()
}
/// Finds nearest hit for this collection of intersections.
///
/// # Examples
/// ```
/// use rtchallenge::{
/// intersections::{Intersection, Intersections},
/// rays::Ray,
/// spheres::{intersect, Sphere},
/// tuples::Tuple,
/// };
///
/// // The hit, when all intersections have positive t.
/// let s = Sphere::default();
/// 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 = Sphere::default();
/// 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 = Sphere::default();
/// 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 = Sphere::default();
/// 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)
.expect("an intersection has a t value that is NaN")
})
}
}
impl<'i> Index<usize> for Intersections<'i> {
type Output = Intersection<'i>;
fn index(&self, idx: usize) -> &Self::Output {
&self.0[idx]
}
}

View File

@ -1,5 +1,8 @@
pub mod canvas;
pub mod intersections;
pub mod matrices;
pub mod rays;
pub mod spheres;
pub mod tuples;
/// Value considered close enough for PartialEq implementations.

View File

@ -175,7 +175,7 @@ impl PartialEq for Matrix3x3 {
/// let p = Tuple::point(1., 0., 1.);
/// let a = Matrix4x4::rotation_x(PI / 2.);
/// let b = Matrix4x4::scaling(5., 5., 5.);
/// let c = Matrix4x4::translate(10., 5., 7.);
/// let c = Matrix4x4::translation(10., 5., 7.);
/// // Apply rotation first.
/// let p2 = a * p;
/// assert_eq!(p2, Tuple::point(1., -1., 0.));
@ -190,7 +190,7 @@ impl PartialEq for Matrix3x3 {
/// let p = Tuple::point(1., 0., 1.);
/// let a = Matrix4x4::rotation_x(PI / 2.);
/// let b = Matrix4x4::scaling(5., 5., 5.);
/// let c = Matrix4x4::translate(10., 5., 7.);
/// let c = Matrix4x4::translation(10., 5., 7.);
/// let t = c * b * a;
/// assert_eq!(t * p, Tuple::point(15., 0., 7.));
/// ```
@ -251,7 +251,7 @@ impl Matrix4x4 {
/// ```
/// use rtchallenge::{matrices::Matrix4x4, tuples::Tuple};
///
/// let transform = Matrix4x4::translate(5., -3., 2.);
/// let transform = Matrix4x4::translation(5., -3., 2.);
/// let p = Tuple::point(-3., 4., 5.);
/// assert_eq!(transform * p, Tuple::point(2., 1., 7.));
///
@ -261,7 +261,7 @@ impl Matrix4x4 {
/// let v = Tuple::vector(-3., 4., 5.);
/// assert_eq!(transform * v, v);
/// ```
pub fn translate(x: f32, y: f32, z: f32) -> Matrix4x4 {
pub fn translation(x: f32, y: f32, z: f32) -> Matrix4x4 {
Matrix4x4::new(
[1., 0., 0., x],
[0., 1., 0., y],

71
rtchallenge/src/rays.rs Normal file
View File

@ -0,0 +1,71 @@
use crate::{matrices::Matrix4x4, tuples::Tuple};
/// Rays have an origin and a direction. This datatype is the 'ray' in 'raytracer'.
pub struct Ray {
pub origin: Tuple,
pub direction: Tuple,
}
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");
Ray { origin, direction }
}
/// 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: f32) -> 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,
direction: m * self.direction,
}
}
}

113
rtchallenge/src/spheres.rs Normal file
View File

@ -0,0 +1,113 @@
use crate::{
intersections::{Intersection, Intersections},
matrices::Matrix4x4,
rays::Ray,
tuples::{dot, Tuple},
};
#[derive(Debug, PartialEq)]
/// Sphere represents the unit-sphere (radius of unit 1.) at the origin 0., 0., 0.
pub struct Sphere {
// TODO(wathiede): cache inverse to speed up intersect.
pub transform: Matrix4x4,
}
impl Default for Sphere {
/// # Examples
/// ```
/// use rtchallenge::{matrices::Matrix4x4, spheres::Sphere};
///
/// // A sphere's default transform is the identity matrix.
/// let s = Sphere::default();
/// assert_eq!(s.transform, Matrix4x4::identity());
///
/// // It can be changed by directly setting the transform member.
/// let mut s = Sphere::default();
/// let t = Matrix4x4::translation(2., 3., 4.);
/// s.transform = t.clone();
/// assert_eq!(s.transform, t);
/// ```
fn default() -> Sphere {
Sphere {
transform: Matrix4x4::identity(),
}
}
}
/// Intersect a ray with a sphere.
///
/// # Examples
/// ```
/// use rtchallenge::{
/// intersections::{Intersection, Intersections},
/// matrices::Matrix4x4,
/// rays::Ray,
/// spheres::{intersect, Sphere},
/// tuples::Tuple,
/// };
///
/// // A ray intersects a sphere in two points.
/// let r = Ray::new(Tuple::point(0., 0., -5.), Tuple::vector(0., 0., 1.));
/// let s = Sphere::default();
/// 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 = Sphere::default();
/// 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 = Sphere::default();
/// 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 = Sphere::default();
/// 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 = Sphere::default();
/// s.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 = Sphere::default();
/// s.transform = Matrix4x4::translation(5., 0., 0.);
/// let xs = intersect(&s, &r);
/// assert_eq!(xs.len(), 0);
/// ```
pub fn intersect<'s>(sphere: &'s Sphere, ray: &Ray) -> Intersections<'s> {
let ray = ray.transform(sphere.transform.inverse());
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. {
Intersections::default()
} else {
Intersections::new(vec![
Intersection::new((-b - discriminant.sqrt()) / (2. * a), &sphere),
Intersection::new((-b + discriminant.sqrt()) / (2. * a), &sphere),
])
}
}