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 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;
|
||||
}
|
||||
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]
|
||||
}
|
||||
pub fn write_to_file<P>(&self, path: P) -> Result<(), CanvasError>
|
||||
@ -74,17 +74,16 @@ impl Canvas {
|
||||
mod tests {
|
||||
use super::Canvas;
|
||||
|
||||
use crate::tuples::Color;
|
||||
use crate::{tuples::Color, BLACK};
|
||||
|
||||
#[test]
|
||||
fn create_canvas() {
|
||||
let bg = Color::new(0.0, 0.0, 0.0);
|
||||
let bg = BLACK;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
use std::ops::Index;
|
||||
|
||||
use crate::spheres::Sphere;
|
||||
use crate::{
|
||||
rays::Ray,
|
||||
spheres::Sphere,
|
||||
tuples::{dot, Tuple},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
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> {
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,9 +1,17 @@
|
||||
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.);
|
||||
|
||||
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::ops::{Index, IndexMut, Mul};
|
||||
use std::ops::{Index, IndexMut, Mul, Sub};
|
||||
|
||||
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>
|
||||
/// 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;
|
||||
/// 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 {:?}\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]
|
||||
)
|
||||
} 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 {
|
||||
fn eq(&self, rhs: &Matrix4x4) -> bool {
|
||||
let l = self.m;
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
use crate::{
|
||||
intersections::{Intersection, Intersections},
|
||||
materials::Material,
|
||||
matrices::Matrix4x4,
|
||||
rays::Ray,
|
||||
tuples::{dot, Tuple},
|
||||
EPSILON,
|
||||
};
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
@ -11,12 +11,13 @@ 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::{matrices::Matrix4x4, spheres::Sphere};
|
||||
/// use rtchallenge::{materials::Material, matrices::Matrix4x4, spheres::Sphere};
|
||||
///
|
||||
/// // A sphere's default transform is the identity matrix.
|
||||
/// let s = Sphere::default();
|
||||
@ -27,10 +28,21 @@ 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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -76,21 +88,27 @@ impl Sphere {
|
||||
/// 3_f32.sqrt() / 3.,
|
||||
/// ));
|
||||
/// assert_eq!(n, n.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));
|
||||
/// // 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, 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()
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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 {
|
||||
type Output = 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 red: f32,
|
||||
pub green: f32,
|
||||
pub blue: f32,
|
||||
}
|
||||
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 }
|
||||
}
|
||||
}
|
||||
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 {
|
||||
|
||||
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