Compare commits
29 Commits
ac4f5eb9a6
...
5f3bfd744e
| Author | SHA1 | Date | |
|---|---|---|---|
| 5f3bfd744e | |||
| e752536430 | |||
| 059f710706 | |||
| ad02d7e945 | |||
| 5911610064 | |||
| 39f7f77b74 | |||
| 125c96c25f | |||
| 249a2915d9 | |||
| 9f00485256 | |||
| b37398ac40 | |||
| 86d052d38b | |||
| b3737dcd5f | |||
| 72c6944ab9 | |||
| 9924330f98 | |||
| 2316df896a | |||
| 2c79132ebc | |||
| e9f2ef0118 | |||
| eebdc270eb | |||
| 3bc9f6f924 | |||
| cbecaa70ef | |||
| 6863b4ecd6 | |||
| bad54bb433 | |||
| d8e5476806 | |||
| 385ed70d88 | |||
| 1cbfbc8641 | |||
| 81540cd484 | |||
| b4428f924c | |||
| bcf847660a | |||
| e529710d5d |
@ -1,5 +1,3 @@
|
|||||||
use core::f32;
|
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
use rtchallenge::{
|
use rtchallenge::{
|
||||||
|
|||||||
57
rtchallenge/examples/eoc6.rs
Normal file
57
rtchallenge/examples/eoc6.rs
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
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(())
|
||||||
|
}
|
||||||
91
rtchallenge/examples/eoc7.rs
Normal file
91
rtchallenge/examples/eoc7.rs
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
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(())
|
||||||
|
}
|
||||||
1
rtchallenge/rustfmt.toml
Normal file
1
rtchallenge/rustfmt.toml
Normal file
@ -0,0 +1 @@
|
|||||||
|
format_code_in_doc_comments = true
|
||||||
159
rtchallenge/src/camera.rs
Normal file
159
rtchallenge/src/camera.rs
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -39,7 +39,7 @@ impl Canvas {
|
|||||||
}
|
}
|
||||||
self.pixels[x + y * self.width] = c;
|
self.pixels[x + y * self.width] = c;
|
||||||
}
|
}
|
||||||
pub fn get(&mut self, x: usize, y: usize) -> Color {
|
pub fn get(&self, x: usize, y: usize) -> Color {
|
||||||
self.pixels[x + y * self.width]
|
self.pixels[x + y * self.width]
|
||||||
}
|
}
|
||||||
pub fn write_to_file<P>(&self, path: P) -> Result<(), CanvasError>
|
pub fn write_to_file<P>(&self, path: P) -> Result<(), CanvasError>
|
||||||
@ -74,17 +74,16 @@ impl Canvas {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::Canvas;
|
use super::Canvas;
|
||||||
|
|
||||||
use crate::tuples::Color;
|
use crate::{tuples::Color, BLACK};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn create_canvas() {
|
fn create_canvas() {
|
||||||
let bg = Color::new(0.0, 0.0, 0.0);
|
let bg = BLACK;
|
||||||
let c = Canvas::new(10, 20, bg);
|
let c = Canvas::new(10, 20, bg);
|
||||||
assert_eq!(c.width, 10);
|
assert_eq!(c.width, 10);
|
||||||
assert_eq!(c.height, 20);
|
assert_eq!(c.height, 20);
|
||||||
let black = Color::new(0., 0., 0.);
|
|
||||||
for (i, p) in c.pixels.iter().enumerate() {
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,10 @@
|
|||||||
use std::ops::Index;
|
use std::ops::Index;
|
||||||
|
|
||||||
use crate::spheres::Sphere;
|
use crate::{
|
||||||
|
rays::Ray,
|
||||||
|
spheres::Sphere,
|
||||||
|
tuples::{dot, Tuple},
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub struct Intersection<'i> {
|
pub struct Intersection<'i> {
|
||||||
@ -114,9 +118,86 @@ 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> {
|
impl<'i> Index<usize> for Intersections<'i> {
|
||||||
type Output = Intersection<'i>;
|
type Output = Intersection<'i>;
|
||||||
fn index(&self, idx: usize) -> &Self::Output {
|
fn index(&self, idx: usize) -> &Self::Output {
|
||||||
&self.0[idx]
|
&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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,9 +1,17 @@
|
|||||||
|
pub mod camera;
|
||||||
pub mod canvas;
|
pub mod canvas;
|
||||||
pub mod intersections;
|
pub mod intersections;
|
||||||
|
pub mod lights;
|
||||||
|
pub mod materials;
|
||||||
pub mod matrices;
|
pub mod matrices;
|
||||||
pub mod rays;
|
pub mod rays;
|
||||||
pub mod spheres;
|
pub mod spheres;
|
||||||
|
pub mod transformations;
|
||||||
pub mod tuples;
|
pub mod tuples;
|
||||||
|
pub mod world;
|
||||||
|
|
||||||
/// Value considered close enough for PartialEq implementations.
|
/// Value considered close enough for PartialEq implementations.
|
||||||
pub const EPSILON: f32 = 0.00001;
|
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.);
|
||||||
|
|||||||
33
rtchallenge/src/lights.rs
Normal file
33
rtchallenge/src/lights.rs
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
132
rtchallenge/src/materials.rs
Normal file
132
rtchallenge/src/materials.rs
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::ops::{Index, IndexMut, Mul};
|
use std::ops::{Index, IndexMut, Mul, Sub};
|
||||||
|
|
||||||
use crate::{tuples::Tuple, EPSILON};
|
use crate::{tuples::Tuple, EPSILON};
|
||||||
|
|
||||||
@ -161,7 +161,6 @@ 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>
|
/// 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.
|
/// where `i` is the row number and `j` is the column number.
|
||||||
///
|
///
|
||||||
@ -194,6 +193,7 @@ impl PartialEq for Matrix3x3 {
|
|||||||
/// let t = c * b * a;
|
/// let t = c * b * a;
|
||||||
/// assert_eq!(t * p, Tuple::point(15., 0., 7.));
|
/// assert_eq!(t * p, Tuple::point(15., 0., 7.));
|
||||||
/// ```
|
/// ```
|
||||||
|
#[derive(Copy, Clone, Default)]
|
||||||
pub struct Matrix4x4 {
|
pub struct Matrix4x4 {
|
||||||
m: [[f32; 4]; 4],
|
m: [[f32; 4]; 4],
|
||||||
}
|
}
|
||||||
@ -764,7 +764,7 @@ impl fmt::Debug for Matrix4x4 {
|
|||||||
if f.alternate() {
|
if f.alternate() {
|
||||||
write!(
|
write!(
|
||||||
f,
|
f,
|
||||||
"{:?}\n {:?}\n {:?}\n {:?}",
|
"\n {:8.5?}\n {:8.5?}\n {:8.5?}\n {:8.5?}",
|
||||||
self.m[0], self.m[1], self.m[2], self.m[3]
|
self.m[0], self.m[1], self.m[2], self.m[3]
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@ -838,6 +838,21 @@ 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 {
|
impl PartialEq for Matrix4x4 {
|
||||||
fn eq(&self, rhs: &Matrix4x4) -> bool {
|
fn eq(&self, rhs: &Matrix4x4) -> bool {
|
||||||
let l = self.m;
|
let l = self.m;
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
intersections::{Intersection, Intersections},
|
intersections::{Intersection, Intersections},
|
||||||
|
materials::Material,
|
||||||
matrices::Matrix4x4,
|
matrices::Matrix4x4,
|
||||||
rays::Ray,
|
rays::Ray,
|
||||||
tuples::{dot, Tuple},
|
tuples::{dot, Tuple},
|
||||||
EPSILON,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
@ -11,12 +11,13 @@ use crate::{
|
|||||||
pub struct Sphere {
|
pub struct Sphere {
|
||||||
// TODO(wathiede): cache inverse to speed up intersect.
|
// TODO(wathiede): cache inverse to speed up intersect.
|
||||||
pub transform: Matrix4x4,
|
pub transform: Matrix4x4,
|
||||||
|
pub material: Material,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Sphere {
|
impl Default for Sphere {
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```
|
/// ```
|
||||||
/// use rtchallenge::{matrices::Matrix4x4, spheres::Sphere};
|
/// use rtchallenge::{materials::Material, matrices::Matrix4x4, spheres::Sphere};
|
||||||
///
|
///
|
||||||
/// // A sphere's default transform is the identity matrix.
|
/// // A sphere's default transform is the identity matrix.
|
||||||
/// let s = Sphere::default();
|
/// let s = Sphere::default();
|
||||||
@ -27,10 +28,21 @@ impl Default for Sphere {
|
|||||||
/// let t = Matrix4x4::translation(2., 3., 4.);
|
/// let t = Matrix4x4::translation(2., 3., 4.);
|
||||||
/// s.transform = t.clone();
|
/// s.transform = t.clone();
|
||||||
/// assert_eq!(s.transform, t);
|
/// 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 {
|
fn default() -> Sphere {
|
||||||
Sphere {
|
Sphere {
|
||||||
transform: Matrix4x4::identity(),
|
transform: Matrix4x4::identity(),
|
||||||
|
material: Material::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -76,21 +88,27 @@ impl Sphere {
|
|||||||
/// 3_f32.sqrt() / 3.,
|
/// 3_f32.sqrt() / 3.,
|
||||||
/// ));
|
/// ));
|
||||||
/// assert_eq!(n, n.normalize());
|
/// assert_eq!(n, n.normalize());
|
||||||
/// ```
|
|
||||||
/// ```should_panic
|
|
||||||
/// use rtchallenge::{spheres::Sphere, tuples::Tuple};
|
|
||||||
///
|
///
|
||||||
/// // In debug builds points not on the sphere should fail.
|
/// // Compute the normal on a translated sphere.
|
||||||
/// let s = Sphere::default();
|
/// let mut s = Sphere::default();
|
||||||
/// let n = s.normal_at(Tuple::point(0., 0., 0.5));
|
/// 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, point: Tuple) -> Tuple {
|
pub fn normal_at(&self, world_point: Tuple) -> Tuple {
|
||||||
debug_assert!(
|
let object_point = self.transform.inverse() * world_point;
|
||||||
((point - Tuple::point(0., 0., 0.)).magnitude() - 1.).abs() < EPSILON,
|
let object_normal = object_point - Tuple::point(0., 0., 0.);
|
||||||
"{} != 1.",
|
let mut world_normal = self.transform.inverse().transpose() * object_normal;
|
||||||
(point - Tuple::point(0., 0., 0.)).magnitude()
|
world_normal.w = 0.;
|
||||||
);
|
world_normal.normalize()
|
||||||
(point - Tuple::point(0., 0., 0.)).normalize()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
59
rtchallenge/src/transformations.rs
Normal file
59
rtchallenge/src/transformations.rs
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
@ -46,6 +46,28 @@ 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 {
|
impl Add for Tuple {
|
||||||
type Output = Self;
|
type Output = Self;
|
||||||
fn add(self, other: Self) -> Self {
|
fn add(self, other: Self) -> Self {
|
||||||
@ -136,17 +158,24 @@ pub fn cross(a: Tuple, b: Tuple) -> Tuple {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
#[derive(Copy, Clone, Debug)]
|
||||||
pub struct Color {
|
pub struct Color {
|
||||||
pub red: f32,
|
pub red: f32,
|
||||||
pub green: f32,
|
pub green: f32,
|
||||||
pub blue: f32,
|
pub blue: f32,
|
||||||
}
|
}
|
||||||
impl Color {
|
impl Color {
|
||||||
pub fn new(red: f32, green: f32, blue: f32) -> Color {
|
pub const fn new(red: f32, green: f32, blue: f32) -> Color {
|
||||||
Color { red, green, blue }
|
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 {
|
impl Add for Color {
|
||||||
type Output = Self;
|
type Output = Self;
|
||||||
fn add(self, other: Self) -> Self {
|
fn add(self, other: Self) -> Self {
|
||||||
|
|||||||
178
rtchallenge/src/world.rs
Normal file
178
rtchallenge/src/world.rs
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
rtiow/rustfmt.toml
Normal file
1
rtiow/rustfmt.toml
Normal file
@ -0,0 +1 @@
|
|||||||
|
format_code_in_doc_comments = true
|
||||||
Loading…
x
Reference in New Issue
Block a user