Compare commits

..

18 Commits

Author SHA1 Message Date
839642b886 camera: make supersampling configurable, wire it up in eoc8.
All checks were successful
continuous-integration/drone/push Build is passing
2021-07-18 20:30:06 -07:00
c4f10126e3 git-hooks: build and test with --no-default-features too.
Stores output in separate target/ subdir, so it shouldn't get clobbered.
2021-07-18 20:16:34 -07:00
7a8ed15017 eoc8: zoom the fov. 2021-07-18 20:01:40 -07:00
5d8024a485 features: rename s/_/-/g and make double sizes Floats default. 2021-07-18 17:29:30 -07:00
ecf7cd7bdc eoc: Updates to work with new Float abstraction. 2021-07-18 17:27:52 -07:00
1c06833658 Fixes for test when using Float == f64. 2021-07-18 17:27:31 -07:00
95de5863cc all: s/f32/Float/g now using customer Float alias. 2021-07-18 17:18:08 -07:00
5d57304d95 eoc8: implement the end of chapter 8 challenge. Note:
This implementation looks like crap and I'm not sure why.
2021-07-18 16:55:29 -07:00
599f484dff camera: protoype supersampling for rayon render pass. 2021-07-18 16:31:36 -07:00
ce998c68eb matrices: rename inverse_old to inverse_rtiow to indicate origin. 2021-07-18 16:30:38 -07:00
37048ddd52 spheres: implement rtiow Sphere::intersect for comparison. 2021-07-18 16:29:45 -07:00
19b2ef6ded intersections: derive Debug for PrecomputedData. 2021-07-18 16:29:21 -07:00
7e450d664e world & intersections: handle shadows. 2021-07-18 13:08:03 -07:00
09047eb713 world: implement World::is_shadowed. 2021-07-18 12:54:18 -07:00
1065702a5d materials: make lighting calculation shadow aware 2021-07-18 12:46:49 -07:00
efdc963a48 camera: update default render strategy to match CLI default 2021-07-18 12:37:29 -07:00
02ef8634c3 camera: update docstring to reflect new findings 2021-07-18 12:36:15 -07:00
7e19d7e61b camera: lint cleanup 2021-07-18 12:14:13 -07:00
18 changed files with 554 additions and 181 deletions

View File

@ -1,5 +1,7 @@
#!/usr/bin/env bash
set -e
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
pushd () {
command pushd "$@" > /dev/null
}
@ -18,9 +20,14 @@ RT=rtchallenge
AFFECTED="$(git diff-index --cached --name-only HEAD)"
#echo "AFFECTED $AFFECTED"
RT_AFFECTED=$(echo "${AFFECTED:?}" | grep ${RT:?} || true)
NO_FEATURES_TARGET_DIR="${SCRIPT_DIR:?}/../../rtchallenge/target/no-default-features"
if [ ! -z "$RT_AFFECTED" ]; then
echo "Files for the rt challenge were touched, running tests"
cd ${RT:?} && cargo build --examples && cargo test
cd ${RT:?} && \
cargo build --examples && \
cargo test && \
cargo build --target-dir=${NO_FEATURES_TARGET_DIR:?} --no-default-features --examples && \
cargo test --target-dir=${NO_FEATURES_TARGET_DIR:?} --no-default-features
fi
# Uncomment to debug presubmit.
#exit 1

66
rtchallenge/Cargo.lock generated
View File

@ -1,5 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "addr2line"
version = "0.15.2"
@ -306,6 +308,17 @@ dependencies = [
"backtrace",
]
[[package]]
name = "getrandom"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "gimli"
version = "0.24.0"
@ -508,6 +521,12 @@ dependencies = [
"miniz_oxide 0.3.7",
]
[[package]]
name = "ppv-lite86"
version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857"
[[package]]
name = "proc-macro-error"
version = "1.0.4"
@ -550,6 +569,46 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "rand"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
"rand_hc",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
dependencies = [
"getrandom",
]
[[package]]
name = "rand_hc"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7"
dependencies = [
"rand_core",
]
[[package]]
name = "rayon"
version = "1.5.1"
@ -606,6 +665,7 @@ dependencies = [
"enum-utils",
"num_cpus",
"png",
"rand",
"rayon",
"serde",
"serde_json",
@ -828,6 +888,12 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "wasi"
version = "0.10.2+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
[[package]]
name = "wasm-bindgen"
version = "0.2.74"

View File

@ -7,7 +7,9 @@ edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
disable_inverse_cache = []
default = [ "float-as-double" ]
disable-inverse-cache = []
float-as-double = []
[dependencies]
anyhow = "1.0.41"
@ -16,6 +18,7 @@ criterion = "0.3.4"
enum-utils = "0.1.2"
num_cpus = "1.13.0"
png = "0.16.8"
rand = "0.8.4"
rayon = "1.5.1"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.64"

View File

@ -3,6 +3,7 @@ use anyhow::{bail, Result};
use rtchallenge::{
canvas::Canvas,
tuples::{Color, Tuple},
Float,
};
#[derive(Debug)]
@ -47,8 +48,8 @@ fn main() -> Result<()> {
let bg = Color::new(0.2, 0.2, 0.2);
let mut c = Canvas::new(w, h, bg);
let mut i = 0;
let w = w as f32;
let h = h as f32;
let w = w as Float;
let h = h as Float;
while p.position.y > 0. {
p = tick(&e, &p);
println!("tick {}: proj {:?}", i, p);

View File

@ -1,11 +1,11 @@
use std::f32::consts::PI;
use anyhow::Result;
use rtchallenge::{
canvas::Canvas,
float::consts::PI,
matrices::Matrix4x4,
tuples::{Color, Tuple},
Float,
};
fn draw_dot(c: &mut Canvas, x: usize, y: usize) {
@ -30,13 +30,13 @@ fn main() -> Result<()> {
let rot_hour = Matrix4x4::rotation_z(-PI / 6.);
let mut p = t * p;
let w = w as f32;
let h = h as f32;
let w = w as Float;
let h = h as Float;
// 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::translation(0.5, -0.5, 0.);
Matrix4x4::scaling(w as Float, -h as Float, 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

@ -6,6 +6,7 @@ use rtchallenge::{
rays::Ray,
spheres::{intersect, Sphere},
tuples::{Color, Tuple},
Float,
};
fn main() -> Result<()> {
@ -16,7 +17,7 @@ fn main() -> Result<()> {
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 pixel_size = wall_size / w as Float;
let half = wall_size / 2.;
let color = Color::new(1., 0., 0.);
let mut shape = Sphere::default();
@ -25,9 +26,9 @@ fn main() -> Result<()> {
);
for y in 0..h {
let world_y = half - pixel_size * y as f32;
let world_y = half - pixel_size * y as Float;
for x in 0..w {
let world_x = -half + pixel_size * x as f32;
let world_x = -half + pixel_size * x as Float;
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);

View File

@ -7,7 +7,7 @@ use rtchallenge::{
rays::Ray,
spheres::{intersect, Sphere},
tuples::{Color, Tuple},
WHITE,
Float, WHITE,
};
fn main() -> Result<()> {
@ -18,7 +18,7 @@ fn main() -> Result<()> {
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 pixel_size = wall_size / w as Float;
let half = wall_size / 2.;
let mut shape = Sphere::default();
shape.material = Material {
@ -31,11 +31,11 @@ fn main() -> Result<()> {
let light_position = Tuple::point(-10., 10., -10.);
let light_color = WHITE;
let light = PointLight::new(light_position, light_color);
let in_shadow = false;
for y in 0..h {
let world_y = half - pixel_size * y as f32;
let world_y = half - pixel_size * y as Float;
for x in 0..w {
let world_x = -half + pixel_size * x as f32;
let world_x = -half + pixel_size * x as Float;
let position = Tuple::point(world_x, world_y, wall_z);
let direction = (position - ray_origin).normalize();
let r = Ray::new(ray_origin, direction);
@ -44,7 +44,7 @@ fn main() -> Result<()> {
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);
let color = lighting(&hit.object.material, &light, point, eye, normal, in_shadow);
c.set(x, y, color);
}
}

View File

@ -1,10 +1,11 @@
use std::{f32::consts::PI, time::Instant};
use std::time::Instant;
use anyhow::Result;
use structopt::StructOpt;
use rtchallenge::{
camera::{Camera, RenderStrategy},
float::consts::PI,
lights::PointLight,
materials::Material,
matrices::Matrix4x4,

View File

@ -0,0 +1,113 @@
use std::time::Instant;
use anyhow::Result;
use structopt::StructOpt;
use rtchallenge::{
camera::{Camera, RenderStrategy},
float::consts::PI,
lights::PointLight,
materials::Material,
matrices::Matrix4x4,
spheres::Sphere,
transformations::view_transform,
tuples::{Color, Tuple},
world::World,
WHITE,
};
/// End of chapter 8 challenge.
#[derive(StructOpt, Debug)]
#[structopt(name = "eoc8")]
struct Opt {
#[structopt(long, default_value = "rayon")]
render_strategy: RenderStrategy,
/// Number of samples per pixel. 0 renders from the center of the pixel, 1 or more samples N
/// times randomly across the pixel.
#[structopt(short, long, default_value = "0")]
samples: usize,
}
fn main() -> Result<()> {
let start = Instant::now();
let opt = Opt::from_args();
let width = 2560;
let height = 1440;
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 / 4.);
let from = Tuple::point(0., 1.5, -5.);
let to = Tuple::point(0., 1., 0.);
let up = Tuple::point(0., 1., 0.);
camera.set_transform(view_transform(from, to, up));
camera.render_strategy = opt.render_strategy;
camera.samples_per_pixel = opt.samples;
let mut floor = Sphere::default();
floor.set_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.set_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.set_transform(
Matrix4x4::translation(0., 0., 5.)
* Matrix4x4::rotation_y(PI / 4.)
* Matrix4x4::rotation_x(PI / 2.)
* Matrix4x4::scaling(10., 0.00001, 10.),
);
right_wall.material = floor.material.clone();
let mut middle = Sphere::default();
middle.set_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.set_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.set_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/eoc8.png";
println!("saving output to {}", path);
image.write_to_file(path)?;
println!("Render time {:.3} seconds", start.elapsed().as_secs_f32());
Ok(())
}

View File

@ -7,11 +7,19 @@ use std::{
thread,
};
use rand::Rng;
use rayon::iter::{IntoParallelIterator, ParallelIterator};
use serde::Deserialize;
use structopt::StructOpt;
use crate::{canvas::Canvas, matrices::Matrix4x4, rays::Ray, tuples::Tuple, world::World, BLACK};
use crate::{
canvas::Canvas,
matrices::Matrix4x4,
rays::Ray,
tuples::{Color, Tuple},
world::World,
Float, BLACK,
};
#[derive(Copy, Clone, StructOpt, Debug, Deserialize)]
#[serde(rename_all = "kebab-case")]
@ -31,13 +39,15 @@ impl FromStr for RenderStrategy {
pub struct Camera {
hsize: usize,
vsize: usize,
field_of_view: f32,
field_of_view: Float,
transform: Matrix4x4,
inverse_transform: Matrix4x4,
pixel_size: f32,
half_width: f32,
half_height: f32,
pixel_size: Float,
half_width: Float,
half_height: Float,
pub render_strategy: RenderStrategy,
/// 0 renders from the center of the pixel, 1 or higher is random sampling of the pixel.
pub samples_per_pixel: usize,
}
enum Request {
@ -54,9 +64,7 @@ impl Camera {
///
/// # Examples
/// ```
/// use std::f32::consts::PI;
///
/// use rtchallenge::{camera::Camera, matrices::Matrix4x4};
/// use rtchallenge::{camera::Camera, float::consts::PI, matrices::Matrix4x4, EPSILON};
///
/// let hsize = 160;
/// let vsize = 120;
@ -68,21 +76,21 @@ impl Camera {
///
/// // Pixel size for a horizontal canvas.
/// let c = Camera::new(200, 150, PI / 2.);
/// assert_eq!(c.pixel_size(), 0.01);
/// assert!((c.pixel_size() - 0.010).abs() < EPSILON);
///
/// // Pixel size for a horizontal canvas.
/// let c = Camera::new(150, 200, PI / 2.);
/// assert_eq!(c.pixel_size(), 0.01);
/// assert!((c.pixel_size() - 0.010).abs() < EPSILON);
/// ```
pub fn new(hsize: usize, vsize: usize, field_of_view: f32) -> Camera {
pub fn new(hsize: usize, vsize: usize, field_of_view: Float) -> Camera {
let half_view = (field_of_view / 2.).tan();
let aspect = hsize as f32 / vsize as f32;
let aspect = hsize as Float / vsize as Float;
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;
let pixel_size = 2. * half_width / hsize as Float;
Camera {
hsize,
vsize,
@ -92,7 +100,8 @@ impl Camera {
pixel_size,
half_height,
half_width,
render_strategy: RenderStrategy::WorkerPool,
render_strategy: RenderStrategy::Rayon,
samples_per_pixel: 0,
}
}
pub fn hsize(&self) -> usize {
@ -101,7 +110,7 @@ impl Camera {
pub fn vsize(&self) -> usize {
self.vsize
}
pub fn field_of_view(&self) -> f32 {
pub fn field_of_view(&self) -> Float {
self.field_of_view
}
pub fn transform(&self) -> Matrix4x4 {
@ -111,18 +120,43 @@ impl Camera {
self.transform = t;
self.inverse_transform = t.inverse();
}
pub fn pixel_size(&self) -> f32 {
pub fn pixel_size(&self) -> Float {
self.pixel_size
}
pub fn supersample_rays_for_pixel(&self, px: usize, py: usize, samples: usize) -> Vec<Ray> {
let mut rng = rand::thread_rng();
(0..samples)
.map(|_| {
// The offset from the edge of the canvas to the pixel's corner.
let xoffset = (px as Float + rng.gen::<Float>()) * self.pixel_size;
let yoffset = (py as Float + rng.gen::<Float>()) * 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 canvas is at z>=-1).
let pixel = self.inverse_transform * Tuple::point(world_x, world_y, -1.);
let origin = self.inverse_transform * Tuple::point(0., 0., 0.);
let direction = (pixel - origin).normalize();
Ray::new(origin, direction)
})
.collect()
}
/// 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};
/// use rtchallenge::{
/// camera::Camera, float::consts::PI, matrices::Matrix4x4, tuples::Tuple, Float,
/// };
///
/// // Constructing a ray through the center of the canvas.
/// let c = Camera::new(201, 101, PI / 2.);
@ -143,14 +177,14 @@ impl Camera {
/// assert_eq!(r.origin, Tuple::point(0., 2., -5.));
/// assert_eq!(
/// r.direction,
/// Tuple::vector(2_f32.sqrt() / 2., 0., -2_f32.sqrt() / 2.)
/// Tuple::vector((2. as Float).sqrt() / 2., 0., -(2. as Float).sqrt() / 2.)
/// );
/// ```
#[cfg(not(feature = "disable_inverse_cache"))]
#[cfg(not(feature = "disable-inverse-cache"))]
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;
let xoffset = (px as Float + 0.5) * self.pixel_size;
let yoffset = (py as Float + 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.)
@ -166,11 +200,11 @@ impl Camera {
Ray::new(origin, direction)
}
#[cfg(feature = "disable_inverse_cache")]
#[cfg(feature = "disable-inverse-cache")]
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;
let xoffset = (px as Float + 0.5) * self.pixel_size;
let yoffset = (py as Float + 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.)
@ -190,10 +224,9 @@ impl Camera {
/// Use camera to render an image of the given world.
/// # Examples
/// ```
/// use std::f32::consts::PI;
///
/// use rtchallenge::{
/// camera::Camera,
/// float::consts::PI,
/// transformations::view_transform,
/// tuples::{Color, Tuple},
/// world::World,
@ -243,15 +276,14 @@ impl Camera {
// Create a worker thread for each CPU core and pin the thread to the core.
let mut handles = core_ids
.into_iter()
.enumerate()
.map(|(i, id)| {
.map(|id| {
let w = Arc::clone(&world);
let c = Arc::clone(&camera);
let pixel_req_rx = pixel_req_rx.clone();
let pixel_resp_tx = pixel_resp_tx.clone();
thread::spawn(move || {
core_affinity::set_for_current(id);
render_worker(i, &c, &w, pixel_req_rx, &pixel_resp_tx);
render_worker_task(&c, &w, pixel_req_rx, &pixel_resp_tx);
})
})
.collect::<Vec<_>>();
@ -288,18 +320,17 @@ impl Camera {
}
/// This renderer use rayon to split each row into a seperate thread. It
/// seems to have really bad performance (only ~6x speedup over serial), and
/// the flame graph looks a mess. A strength over
/// `render_parallel_one_tread_per_core` is that it doesn't require `Camera`
/// and `World` to be cloneable.
/// seems to have more consistent performance than worker pool, equally fast
/// as WP at WP's fastest. The downside is the flame graph looks a mess. A
/// strength over `render_parallel_one_tread_per_core` is that it doesn't
/// require `Camera` and `World` to be cloneable.
fn render_parallel_rayon(&self, w: &World) -> Canvas {
let image_mu = Mutex::new(Canvas::new(self.hsize, self.vsize, BLACK));
(0..self.vsize).into_par_iter().for_each(|y| {
let mut row_image = Canvas::new(self.hsize, 1, BLACK);
for x in 0..self.hsize {
let ray = self.ray_for_pixel(x, y);
let color = w.color_at(&ray);
let color = self.sample(w, x, y);
row_image.set(x, 0, color);
}
// TODO(wathiede): create a row based setter for memcpying the row as a whole.
@ -318,17 +349,28 @@ impl Camera {
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);
let color = self.sample(w, x, y);
image.set(x, y, color);
}
}
image
}
}
fn render_worker(
tid: usize,
fn sample(&self, w: &World, x: usize, y: usize) -> Color {
if self.samples_per_pixel > 0 {
let color = self
.supersample_rays_for_pixel(x, y, self.samples_per_pixel)
.iter()
.map(|ray| w.color_at(&ray))
.fold(BLACK, |acc, c| acc + c);
color / self.samples_per_pixel as Float
} else {
let ray = self.ray_for_pixel(x, y);
w.color_at(&ray)
}
}
}
fn render_worker_task(
c: &Camera,
w: &World,
input_chan: Arc<Mutex<Receiver<Request>>>,
@ -337,16 +379,18 @@ fn render_worker(
loop {
let job = { input_chan.lock().unwrap().recv() };
match job {
Err(err) => {
eprintln!("Shutting down render_worker {}: {}", tid, err);
Err(_) => {
// From the docs:
// "The recv operation can only fail if the sending half of a
// channel (or sync_channel) is disconnected, implying that no
// further messages will ever be received."
return;
}
Ok(req) => match req {
Request::Line { width, y } => {
let mut pixels = Canvas::new(width, 1, BLACK);
for x in 0..width {
let ray = c.ray_for_pixel(x, y);
let color = w.color_at(&ray);
let color = c.sample(w, x, y);
pixels.set(x, 0, color);
}
output_chan

View File

@ -4,11 +4,12 @@ use crate::{
rays::Ray,
spheres::Sphere,
tuples::{dot, Tuple},
Float, EPSILON,
};
#[derive(Debug, Clone, PartialEq)]
pub struct Intersection<'i> {
pub t: f32,
pub t: Float,
pub object: &'i Sphere,
}
@ -25,7 +26,7 @@ impl<'i> Intersection<'i> {
/// assert_eq!(i.t, 3.5);
/// assert_eq!(i.object, &s);
/// ```
pub fn new(t: f32, object: &Sphere) -> Intersection {
pub fn new(t: Float, object: &Sphere) -> Intersection {
Intersection { t, object }
}
}
@ -134,10 +135,12 @@ impl<'i> Index<usize> for Intersections<'i> {
}
}
#[derive(Debug)]
pub struct PrecomputedData<'i> {
pub t: f32,
pub t: Float,
pub object: &'i Sphere,
pub point: Tuple,
pub over_point: Tuple,
pub eyev: Tuple,
pub normalv: Tuple,
pub inside: bool,
@ -150,8 +153,10 @@ pub struct PrecomputedData<'i> {
/// use rtchallenge::{
/// intersections::{prepare_computations, Intersection, Intersections},
/// rays::Ray,
/// matrices::Matrix4x4,
/// spheres::{intersect, Sphere},
/// tuples::Tuple,
/// EPSILON
/// };
///
/// // Precomputing the state of an intersection.
@ -182,6 +187,15 @@ pub struct PrecomputedData<'i> {
/// 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.));
///
/// // The hit should offset the point.
/// let r = Ray::new(Tuple::point(0., 0., -5.), Tuple::vector(0., 0., 1.));
/// let mut shape = Sphere::default();
/// shape .set_transform(Matrix4x4::translation(0.,0.,1.));
/// let i = Intersection::new(5., &shape);
/// let comps = prepare_computations(&i, &r);
/// assert!(comps.over_point.z< -EPSILON/2.);
/// assert!(comps.point.z>comps.over_point.z);
/// ```
pub fn prepare_computations<'i>(i: &'i Intersection, r: &Ray) -> PrecomputedData<'i> {
let point = r.position(i.t);
@ -192,10 +206,13 @@ pub fn prepare_computations<'i>(i: &'i Intersection, r: &Ray) -> PrecomputedData
} else {
(false, normalv)
};
let over_point = point + normalv * EPSILON;
PrecomputedData {
t: i.t,
object: i.object,
point,
over_point,
normalv,
inside,
eyev,

View File

@ -11,7 +11,29 @@ pub mod tuples;
pub mod world;
/// Value considered close enough for PartialEq implementations.
pub const EPSILON: f32 = 0.00001;
pub const EPSILON: Float = 0.00001;
pub const BLACK: tuples::Color = tuples::Color::new(0., 0., 0.);
pub const WHITE: tuples::Color = tuples::Color::new(1., 1., 1.);
#[cfg(feature = "float-as-double")]
/// submodule to defined types, constants and methods when `Float` is defined as a `f64` using the
/// "float-as-double" cargo feature.
pub mod float {
pub use std::f64::*;
/// Alias of the `f64` type, to be used through out the codebase anywhere a default sized
/// `Float` is necessary.
pub type Float = f64;
}
#[cfg(not(feature = "float-as-double"))]
/// submodule to defined types, constants and methods when `Float` is defined as a `f32` when not using the
/// "float-as-double" cargo feature.
pub mod float {
pub use std::f32::*;
/// Alias of the `f32` type, to be used through out the codebase anywhere a default sized
/// `Float` is necessary.
pub type Float = f32;
}
pub use float::Float;

View File

@ -2,15 +2,15 @@ use crate::{
lights::PointLight,
tuples::Color,
tuples::{dot, reflect, Tuple},
BLACK, WHITE,
Float, BLACK, WHITE,
};
#[derive(Debug, PartialEq, Clone)]
pub struct Material {
pub color: Color,
pub ambient: f32,
pub diffuse: f32,
pub specular: f32,
pub shininess: f32,
pub ambient: Float,
pub diffuse: Float,
pub specular: Float,
pub shininess: Float,
}
impl Default for Material {
@ -51,9 +51,10 @@ impl Default for Material {
/// lights::PointLight,
/// materials::{lighting, Material},
/// tuples::{Color, Tuple},
/// WHITE,
/// Float, WHITE,
/// };
///
/// let in_shadow = false;
/// let m = Material::default();
/// let position = Tuple::point(0., 0., 0.);
///
@ -61,35 +62,43 @@ impl Default for Material {
/// 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);
/// let result = lighting(&m, &light, position, eyev, normalv, in_shadow);
/// 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 eyev = Tuple::vector(0., (2. as Float).sqrt() / 2., -(2. as Float).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);
/// let result = lighting(&m, &light, position, eyev, normalv, in_shadow);
/// 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);
/// let result = lighting(&m, &light, position, eyev, normalv, in_shadow);
/// 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 eyev = Tuple::vector(0., -(2.0 as Float).sqrt() / 2., -(2.0 as Float).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));
/// let result = lighting(&m, &light, position, eyev, normalv, in_shadow);
/// assert_eq!(result, Color::new(1.63639, 1.63639, 1.63639));
///
/// // 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);
/// let result = lighting(&m, &light, position, eyev, normalv, in_shadow);
/// assert_eq!(result, Color::new(0.1, 0.1, 0.1));
///
/// // Lighting with the surface in shadow.
/// let in_shadow = true;
/// 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, in_shadow);
/// assert_eq!(result, Color::new(0.1, 0.1, 0.1));
/// ```
pub fn lighting(
@ -98,6 +107,7 @@ pub fn lighting(
point: Tuple,
eyev: Tuple,
normalv: Tuple,
in_shadow: bool,
) -> Color {
// Combine the surface color with the light's color.
let effective_color = material.color * light.intensity;
@ -128,5 +138,9 @@ pub fn lighting(
};
(diffuse, specular)
};
ambient + diffuse + specular
if in_shadow {
ambient
} else {
ambient + diffuse + specular
}
}

View File

@ -1,15 +1,15 @@
use std::fmt;
use std::ops::{Index, IndexMut, Mul, Sub};
use crate::{tuples::Tuple, EPSILON};
use crate::{tuples::Tuple, Float, EPSILON};
#[derive(Debug)]
pub struct Matrix2x2 {
m: [[f32; 2]; 2],
m: [[Float; 2]; 2],
}
impl Matrix2x2 {
/// Create a `Matrix2x2` with each of the given rows.
pub fn new(r0: [f32; 2], r1: [f32; 2]) -> Matrix2x2 {
pub fn new(r0: [Float; 2], r1: [Float; 2]) -> Matrix2x2 {
Matrix2x2 { m: [r0, r1] }
}
@ -24,13 +24,13 @@ impl Matrix2x2 {
///
/// assert_eq!(a.determinant(), 17.);
/// ```
pub fn determinant(&self) -> f32 {
pub fn determinant(&self) -> Float {
let m = self;
m[(0, 0)] * m[(1, 1)] - m[(0, 1)] * m[(1, 0)]
}
}
impl Index<(usize, usize)> for Matrix2x2 {
type Output = f32;
type Output = Float;
fn index(&self, (row, col): (usize, usize)) -> &Self::Output {
&self.m[row][col]
}
@ -53,11 +53,11 @@ impl PartialEq for Matrix2x2 {
#[derive(Debug)]
pub struct Matrix3x3 {
m: [[f32; 3]; 3],
m: [[Float; 3]; 3],
}
impl Matrix3x3 {
/// Create a `Matrix3x2` with each of the given rows.
pub fn new(r0: [f32; 3], r1: [f32; 3], r2: [f32; 3]) -> Matrix3x3 {
pub fn new(r0: [Float; 3], r1: [Float; 3], r2: [Float; 3]) -> Matrix3x3 {
Matrix3x3 { m: [r0, r1, r2] }
}
/// submatrix extracts a 2x2 matrix ignoring the 0-based `row` and `col` given.
@ -101,7 +101,7 @@ impl Matrix3x3 {
/// assert_eq!(b.determinant(), 25.0);
/// assert_eq!(b.determinant(), a.minor(1, 0));
/// ```
pub fn minor(&self, row: usize, col: usize) -> f32 {
pub fn minor(&self, row: usize, col: usize) -> Float {
self.submatrix(row, col).determinant()
}
@ -117,7 +117,7 @@ impl Matrix3x3 {
/// assert_eq!(a.minor(1, 0), 25.);
/// assert_eq!(a.cofactor(1, 0), -25.);
/// ```
pub fn cofactor(&self, row: usize, col: usize) -> f32 {
pub fn cofactor(&self, row: usize, col: usize) -> Float {
let negate = if (row + col) % 2 == 0 { 1. } else { -1. };
self.submatrix(row, col).determinant() * negate
}
@ -134,12 +134,12 @@ impl Matrix3x3 {
/// assert_eq!(a.cofactor(0, 2), -46.);
/// assert_eq!(a.determinant(), -196.);
/// ```
pub fn determinant(&self) -> f32 {
pub fn determinant(&self) -> Float {
(0..3).map(|i| self.cofactor(0, i) * self[(0, i)]).sum()
}
}
impl Index<(usize, usize)> for Matrix3x3 {
type Output = f32;
type Output = Float;
fn index(&self, (row, col): (usize, usize)) -> &Self::Output {
&self.m[row][col]
}
@ -166,9 +166,7 @@ impl PartialEq for Matrix3x3 {
///
/// # Examples
/// ```
/// use std::f32::consts::PI;
///
/// use rtchallenge::{matrices::Matrix4x4, tuples::Tuple};
/// use rtchallenge::{float::consts::PI, matrices::Matrix4x4, tuples::Tuple};
///
/// // Individual transformations are applied in sequence.
/// let p = Tuple::point(1., 0., 1.);
@ -195,11 +193,11 @@ impl PartialEq for Matrix3x3 {
/// ```
#[derive(Copy, Clone, Default)]
pub struct Matrix4x4 {
m: [[f32; 4]; 4],
m: [[Float; 4]; 4],
}
impl From<[f32; 16]> for Matrix4x4 {
fn from(t: [f32; 16]) -> Self {
impl From<[Float; 16]> for Matrix4x4 {
fn from(t: [Float; 16]) -> Self {
Matrix4x4 {
m: [
[t[0], t[1], t[2], t[3]],
@ -238,7 +236,7 @@ impl Matrix4x4 {
}
/// Create a `Matrix4x4` with each of the given rows.
pub fn new(r0: [f32; 4], r1: [f32; 4], r2: [f32; 4], r3: [f32; 4]) -> Matrix4x4 {
pub fn new(r0: [Float; 4], r1: [Float; 4], r2: [Float; 4], r3: [Float; 4]) -> Matrix4x4 {
Matrix4x4 {
m: [r0, r1, r2, r3],
}
@ -261,7 +259,7 @@ impl Matrix4x4 {
/// let v = Tuple::vector(-3., 4., 5.);
/// assert_eq!(transform * v, v);
/// ```
pub fn translation(x: f32, y: f32, z: f32) -> Matrix4x4 {
pub fn translation(x: Float, y: Float, z: Float) -> Matrix4x4 {
Matrix4x4::new(
[1., 0., 0., x],
[0., 1., 0., y],
@ -295,7 +293,7 @@ impl Matrix4x4 {
/// let p = Tuple::point(2., 3., 4.);
/// assert_eq!(transform * p, Tuple::point(-2., 3., 4.));
/// ```
pub fn scaling(x: f32, y: f32, z: f32) -> Matrix4x4 {
pub fn scaling(x: Float, y: Float, z: Float) -> Matrix4x4 {
Matrix4x4::new(
[x, 0., 0., 0.],
[0., y, 0., 0.],
@ -309,9 +307,7 @@ impl Matrix4x4 {
/// # Examples
///
/// ```
/// use std::f32::consts::PI;
///
/// use rtchallenge::{matrices::Matrix4x4, tuples::Tuple};
/// use rtchallenge::{float::consts::PI, matrices::Matrix4x4, tuples::Tuple, Float};
///
/// // A scaling matrix applied to a point.
/// let p = Tuple::point(0., 1., 0.);
@ -320,11 +316,11 @@ impl Matrix4x4 {
///
/// assert_eq!(
/// half_quarter * p,
/// Tuple::point(0., 2_f32.sqrt() / 2., 2_f32.sqrt() / 2.)
/// Tuple::point(0., (2.0 as Float).sqrt() / 2., (2.0 as Float).sqrt() / 2.)
/// );
/// assert_eq!(full_quarter * p, Tuple::point(0., 0., 1.),);
/// ```
pub fn rotation_x(radians: f32) -> Matrix4x4 {
pub fn rotation_x(radians: Float) -> Matrix4x4 {
let r = radians;
Matrix4x4::new(
[1., 0., 0., 0.],
@ -339,9 +335,7 @@ impl Matrix4x4 {
/// # Examples
///
/// ```
/// use std::f32::consts::PI;
///
/// use rtchallenge::{matrices::Matrix4x4, tuples::Tuple};
/// use rtchallenge::{float::consts::PI, matrices::Matrix4x4, tuples::Tuple, Float};
///
/// // A scaling matrix applied to a point.
/// let p = Tuple::point(0., 0., 1.);
@ -350,11 +344,11 @@ impl Matrix4x4 {
///
/// assert_eq!(
/// half_quarter * p,
/// Tuple::point(2_f32.sqrt() / 2., 0., 2_f32.sqrt() / 2.)
/// Tuple::point((2.0 as Float).sqrt() / 2., 0., (2.0 as Float).sqrt() / 2.)
/// );
/// assert_eq!(full_quarter * p, Tuple::point(1., 0., 0.,),);
/// ```
pub fn rotation_y(radians: f32) -> Matrix4x4 {
pub fn rotation_y(radians: Float) -> Matrix4x4 {
let r = radians;
Matrix4x4::new(
[r.cos(), 0., r.sin(), 0.],
@ -369,9 +363,7 @@ impl Matrix4x4 {
/// # Examples
///
/// ```
/// use std::f32::consts::PI;
///
/// use rtchallenge::{matrices::Matrix4x4, tuples::Tuple};
/// use rtchallenge::{float::consts::PI, matrices::Matrix4x4, tuples::Tuple, Float};
///
/// // A scaling matrix applied to a point.
/// let p = Tuple::point(0., 1., 0.);
@ -380,11 +372,11 @@ impl Matrix4x4 {
///
/// assert_eq!(
/// half_quarter * p,
/// Tuple::point(-2_f32.sqrt() / 2., 2_f32.sqrt() / 2., 0.)
/// Tuple::point(-(2.0 as Float).sqrt() / 2., (2.0 as Float).sqrt() / 2., 0.)
/// );
/// assert_eq!(full_quarter * p, Tuple::point(-1., 0., 0.,),);
/// ```
pub fn rotation_z(radians: f32) -> Matrix4x4 {
pub fn rotation_z(radians: Float) -> Matrix4x4 {
let r = radians;
Matrix4x4::new(
[r.cos(), -r.sin(), 0., 0.],
@ -462,7 +454,7 @@ impl Matrix4x4 {
/// let p = Tuple::point(2.,3.,4.);
/// assert_eq!(transform * p, Tuple::point(2.,3.,7.));
pub fn shearing(xy: f32, xz: f32, yx: f32, yz: f32, zx: f32, zy: f32) -> Matrix4x4 {
pub fn shearing(xy: Float, xz: Float, yx: Float, yz: Float, zx: Float, zy: Float) -> Matrix4x4 {
Matrix4x4::new(
[1., xy, xz, 0.],
[yx, 1., yz, 0.],
@ -481,7 +473,7 @@ impl Matrix4x4 {
/// use rtchallenge::matrices::Matrix4x4;
///
/// let i = Matrix4x4::identity();
/// assert_eq!(i.inverse_old() * i, i);
/// assert_eq!(i.inverse_rtiow() * i, i);
///
/// let m = Matrix4x4::new(
/// [2., 0., 0., 0.],
@ -489,10 +481,10 @@ impl Matrix4x4 {
/// [0., 0., 4., 0.],
/// [0., 0., 0., 1.],
/// );
/// assert_eq!(m.inverse_old() * m, i);
/// assert_eq!(m * m.inverse_old(), i);
/// assert_eq!(m.inverse_rtiow() * m, i);
/// assert_eq!(m * m.inverse_rtiow(), i);
/// ```
pub fn inverse_old(&self) -> Matrix4x4 {
pub fn inverse_rtiow(&self) -> Matrix4x4 {
// TODO(wathiede): how come the C++ version doesn't need to deal with non-invertable
// matrix.
let mut indxc: [usize; 4] = Default::default();
@ -503,7 +495,7 @@ impl Matrix4x4 {
for i in 0..4 {
let mut irow: usize = 0;
let mut icol: usize = 0;
let mut big: f32 = 0.;
let mut big: Float = 0.;
// Choose pivot
for j in 0..4 {
if ipiv[j] != 1 {
@ -538,7 +530,7 @@ impl Matrix4x4 {
}
// Set $m[icol][icol]$ to one by scaling row _icol_ appropriately
let pivinv: f32 = minv[icol][icol].recip();
let pivinv: Float = minv[icol][icol].recip();
minv[icol][icol] = 1.;
for j in 0..4 {
minv[icol][j] *= pivinv;
@ -606,11 +598,11 @@ impl Matrix4x4 {
}
/// Compute minor of a 4x4 matrix.
pub fn minor(&self, row: usize, col: usize) -> f32 {
pub fn minor(&self, row: usize, col: usize) -> Float {
self.submatrix(row, col).determinant()
}
/// Compute cofactor of a 4x4 matrix.
pub fn cofactor(&self, row: usize, col: usize) -> f32 {
pub fn cofactor(&self, row: usize, col: usize) -> Float {
let negate = if (row + col) % 2 == 0 { 1. } else { -1. };
self.submatrix(row, col).determinant() * negate
}
@ -632,7 +624,7 @@ impl Matrix4x4 {
/// assert_eq!(a.cofactor(0, 3), 51.);
/// assert_eq!(a.determinant(), -4071.);
/// ```
pub fn determinant(&self) -> f32 {
pub fn determinant(&self) -> Float {
(0..4).map(|i| self.cofactor(0, i) * self[(0, i)]).sum()
}
@ -743,6 +735,9 @@ impl Matrix4x4 {
/// assert_eq!(c * b.inverse(), a);
/// ```
pub fn inverse(&self) -> Matrix4x4 {
self.inverse_rtc()
}
pub fn inverse_rtc(&self) -> Matrix4x4 {
let m = self;
if !m.invertable() {
panic!("Matrix4x4::inverse called on matrix with determinant() == 0");
@ -870,7 +865,7 @@ impl PartialEq for Matrix4x4 {
}
impl Index<(usize, usize)> for Matrix4x4 {
type Output = f32;
type Output = Float;
fn index(&self, (row, col): (usize, usize)) -> &Self::Output {
&self.m[row][col]
}

View File

@ -1,4 +1,4 @@
use crate::{matrices::Matrix4x4, tuples::Tuple};
use crate::{matrices::Matrix4x4, tuples::Tuple, Float};
/// Rays have an origin and a direction. This datatype is the 'ray' in 'raytracer'.
pub struct Ray {
@ -38,7 +38,7 @@ impl Ray {
/// 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 {
pub fn position(&self, t: Float) -> Tuple {
self.origin + self.direction * t
}

View File

@ -54,7 +54,7 @@ impl Sphere {
///
/// # Examples
/// ```
/// use rtchallenge::{matrices::Matrix4x4, spheres::Sphere, tuples::Tuple};
/// use rtchallenge::{matrices::Matrix4x4, spheres::Sphere, tuples::Tuple,Float};
///
/// // Normal on X-axis
/// let s = Sphere::default();
@ -74,20 +74,20 @@ impl Sphere {
/// // Normal on a sphere at a nonaxial point.
/// let s = Sphere::default();
/// let n = s.normal_at(Tuple::point(
/// 3_f32.sqrt() / 3.,
/// 3_f32.sqrt() / 3.,
/// 3_f32.sqrt() / 3.,
/// (3. as Float).sqrt() / 3.,
/// (3. as Float).sqrt() / 3.,
/// (3. as Float).sqrt() / 3.,
/// ));
/// assert_eq!(
/// n,
/// Tuple::vector(3_f32.sqrt() / 3., 3_f32.sqrt() / 3., 3_f32.sqrt() / 3.,)
/// Tuple::vector((3. as Float).sqrt() / 3., (3. as Float).sqrt() / 3., (3. as Float).sqrt() / 3.,)
/// );
/// // Normals returned are normalized.
/// let s = Sphere::default();
/// let n = s.normal_at(Tuple::point(
/// 3_f32.sqrt() / 3.,
/// 3_f32.sqrt() / 3.,
/// 3_f32.sqrt() / 3.,
/// (3. as Float).sqrt() / 3.,
/// (3. as Float).sqrt() / 3.,
/// (3. as Float).sqrt() / 3.,
/// ));
/// assert_eq!(n, n.normalize());
///
@ -98,14 +98,14 @@ impl Sphere {
/// assert_eq!(n, Tuple::vector(0., 0.70711, -0.70711));
/// // Compute the normal on a transformed sphere.
/// use std::f32::consts::PI;
/// use rtchallenge::float::consts::PI;
///
/// let mut s = Sphere::default();
/// s.set_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.));
/// let n = s.normal_at(Tuple::point(0., (2. as Float).sqrt()/2., -(2. as Float).sqrt()/2.));
/// assert_eq!(n, Tuple::vector(0., 0.97014, -0.24254));
/// ```
#[cfg(not(feature = "disable_inverse_cache"))]
#[cfg(not(feature = "disable-inverse-cache"))]
pub fn normal_at(&self, world_point: Tuple) -> Tuple {
let object_point = self.inverse_transform * world_point;
let object_normal = object_point - Tuple::point(0., 0., 0.);
@ -113,7 +113,7 @@ impl Sphere {
world_normal.w = 0.;
world_normal.normalize()
}
#[cfg(feature = "disable_inverse_cache")]
#[cfg(feature = "disable-inverse-cache")]
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.);
@ -193,6 +193,10 @@ impl Sphere {
/// assert_eq!(xs.len(), 0);
/// ```
pub fn intersect<'s>(sphere: &'s Sphere, ray: &Ray) -> Intersections<'s> {
intersect_rtc(sphere, ray)
}
fn intersect_rtc<'s>(sphere: &'s Sphere, ray: &Ray) -> Intersections<'s> {
let ray = ray.transform(sphere.inverse_transform);
let sphere_to_ray = ray.origin - Tuple::point(0., 0., 0.);
let a = dot(ray.direction, ray.direction);
@ -200,11 +204,25 @@ pub fn intersect<'s>(sphere: &'s Sphere, ray: &Ray) -> Intersections<'s> {
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),
])
return Intersections::default();
}
Intersections::new(vec![
Intersection::new((-b - discriminant.sqrt()) / (2. * a), &sphere),
Intersection::new((-b + discriminant.sqrt()) / (2. * a), &sphere),
])
}
fn intersect_rtiow<'s>(sphere: &'s Sphere, ray: &Ray) -> Intersections<'s> {
let ray = ray.transform(sphere.inverse_transform);
let oc = ray.origin - Tuple::point(0., 0., 0.);
let a = dot(ray.direction, ray.direction);
let b = dot(oc, ray.direction);
let c = dot(oc, oc) - 1.;
let discriminant = b * b - a * c;
if discriminant < 0. {
return Intersections::default();
}
Intersections::new(vec![
Intersection::new((-b - discriminant.sqrt()) / a, &sphere),
Intersection::new((-b + discriminant.sqrt()) / a, &sphere),
])
}

View File

@ -1,25 +1,25 @@
use std::ops::{Add, Div, Mul, Neg, Sub};
use crate::EPSILON;
use crate::{Float, EPSILON};
#[derive(Debug, Copy, Clone)]
pub struct Tuple {
pub x: f32,
pub y: f32,
pub z: f32,
pub w: f32,
pub x: Float,
pub y: Float,
pub z: Float,
pub w: Float,
}
impl Tuple {
pub fn point(x: f32, y: f32, z: f32) -> Tuple {
pub fn point(x: Float, y: Float, z: Float) -> Tuple {
Tuple::new(x, y, z, 1.0)
}
pub fn vector(x: f32, y: f32, z: f32) -> Tuple {
pub fn vector(x: Float, y: Float, z: Float) -> Tuple {
Tuple::new(x, y, z, 0.0)
}
pub fn new(x: f32, y: f32, z: f32, w: f32) -> Tuple {
pub fn new(x: Float, y: Float, z: Float, w: Float) -> Tuple {
Tuple { x, y, z, w }
}
@ -31,7 +31,7 @@ impl Tuple {
self.w == 0.0
}
pub fn magnitude(&self) -> f32 {
pub fn magnitude(&self) -> Float {
(self.x * self.x + self.y * self.y + self.z * self.z + self.w * self.w).sqrt()
}
@ -50,7 +50,10 @@ impl Tuple {
///
/// # Examples
/// ```
/// use rtchallenge::tuples::{reflect, Tuple};
/// use rtchallenge::{
/// tuples::{reflect, Tuple},
/// Float,
/// };
///
/// // Reflecting a vector approaching at 45°
/// let v = Tuple::vector(1., -1., 0.);
@ -60,7 +63,7 @@ impl Tuple {
///
/// // 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 n = Tuple::vector((2. as Float).sqrt() / 2., (2. as Float).sqrt() / 2., 0.);
/// let r = reflect(v, n);
/// assert_eq!(r, Tuple::vector(1., 0., 0.));
/// ```
@ -80,9 +83,9 @@ impl Add for Tuple {
}
}
impl Div<f32> for Tuple {
impl Div<Float> for Tuple {
type Output = Self;
fn div(self, rhs: f32) -> Self::Output {
fn div(self, rhs: Float) -> Self::Output {
Self::Output {
x: self.x / rhs,
y: self.y / rhs,
@ -92,9 +95,9 @@ impl Div<f32> for Tuple {
}
}
impl Mul<f32> for Tuple {
impl Mul<Float> for Tuple {
type Output = Self;
fn mul(self, rhs: f32) -> Self::Output {
fn mul(self, rhs: Float) -> Self::Output {
Self::Output {
x: self.x * rhs,
y: self.y * rhs,
@ -104,7 +107,7 @@ impl Mul<f32> for Tuple {
}
}
impl Mul<Tuple> for f32 {
impl Mul<Tuple> for Float {
type Output = Tuple;
fn mul(self, rhs: Tuple) -> Self::Output {
Self::Output {
@ -147,7 +150,7 @@ impl PartialEq for Tuple {
&& ((self.w - rhs.w).abs() < EPSILON)
}
}
pub fn dot(a: Tuple, b: Tuple) -> f32 {
pub fn dot(a: Tuple, b: Tuple) -> Float {
a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w
}
pub fn cross(a: Tuple, b: Tuple) -> Tuple {
@ -160,12 +163,12 @@ pub fn cross(a: Tuple, b: Tuple) -> Tuple {
#[derive(Copy, Clone, Debug)]
pub struct Color {
pub red: f32,
pub green: f32,
pub blue: f32,
pub red: Float,
pub green: Float,
pub blue: Float,
}
impl Color {
pub const fn new(red: f32, green: f32, blue: f32) -> Color {
pub const fn new(red: Float, green: Float, blue: Float) -> Color {
Color { red, green, blue }
}
}
@ -187,9 +190,9 @@ impl Add for Color {
}
}
impl Div<f32> for Color {
impl Div<Float> for Color {
type Output = Self;
fn div(self, rhs: f32) -> Self::Output {
fn div(self, rhs: Float) -> Self::Output {
Self::Output {
red: self.red / rhs,
green: self.green / rhs,
@ -198,9 +201,9 @@ impl Div<f32> for Color {
}
}
impl Mul<f32> for Color {
impl Mul<Float> for Color {
type Output = Self;
fn mul(self, rhs: f32) -> Self::Output {
fn mul(self, rhs: Float) -> Self::Output {
Self::Output {
red: self.red * rhs,
green: self.green * rhs,
@ -209,7 +212,7 @@ impl Mul<f32> for Color {
}
}
impl Mul<Color> for f32 {
impl Mul<Color> for Float {
type Output = Color;
fn mul(self, rhs: Color) -> Self::Output {
Self::Output {
@ -253,7 +256,7 @@ impl Sub for Color {
#[cfg(test)]
mod tests {
use super::{cross, dot, Color, Tuple, EPSILON};
use super::{cross, dot, Color, Float, Tuple, EPSILON};
#[test]
fn is_point() {
// A tuple with w = 1 is a point
@ -341,8 +344,11 @@ mod tests {
assert_eq!(1., Tuple::vector(1., 0., 0.).magnitude());
assert_eq!(1., Tuple::vector(0., 1., 0.).magnitude());
assert_eq!(1., Tuple::vector(0., 0., 1.).magnitude());
assert_eq!(14_f32.sqrt(), Tuple::vector(1., 2., 3.).magnitude());
assert_eq!(14_f32.sqrt(), Tuple::vector(-1., -2., -3.).magnitude());
assert_eq!((14. as Float).sqrt(), Tuple::vector(1., 2., 3.).magnitude());
assert_eq!(
(14. as Float).sqrt(),
Tuple::vector(-1., -2., -3.).magnitude()
);
}
#[test]
fn vector_normalize() {
@ -351,7 +357,11 @@ mod tests {
Tuple::vector(4., 0., 0.).normalize()
);
assert_eq!(
Tuple::vector(1. / 14_f32.sqrt(), 2. / 14_f32.sqrt(), 3. / 14_f32.sqrt()),
Tuple::vector(
1. / (14. as Float).sqrt(),
2. / (14. as Float).sqrt(),
3. / (14. as Float).sqrt()
),
Tuple::vector(1., 2., 3.).normalize()
);
}

View File

@ -91,7 +91,9 @@ impl World {
/// use rtchallenge::{
/// intersections::{prepare_computations, Intersection},
/// lights::PointLight,
/// matrices::Matrix4x4,
/// rays::Ray,
/// spheres::Sphere,
/// tuples::{Color, Tuple},
/// world::World,
/// WHITE,
@ -115,16 +117,31 @@ impl World {
/// let comps = prepare_computations(&i, &r);
/// let c = w.shade_hit(&comps);
/// assert_eq!(c, Color::new(0.90498, 0.90498, 0.90498));
///
/// // Shading with an intersection in shadow.
/// let mut w = World::default();
/// w.light = Some(PointLight::new(Tuple::point(0., 0., -10.), WHITE));
/// let s1 = Sphere::default();
/// let mut s2 = Sphere::default();
/// s2.set_transform(Matrix4x4::translation(0., 0., 10.));
/// w.objects = vec![s1, s2.clone()];
/// let r = Ray::new(Tuple::point(0., 0., 5.), Tuple::vector(0., 0., 1.));
/// let i = Intersection::new(4., &s2);
/// let comps = prepare_computations(&i, &r);
/// let c = w.shade_hit(&comps);
/// assert_eq!(c, Color::new(0.1, 0.1, 0.1));
/// ```
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.
let shadowed = self.is_shadowed(comps.over_point);
lighting(
&comps.object.material,
&self.light.as_ref().expect("World has no lights"),
comps.point,
comps.over_point,
comps.eyev,
comps.normalv,
shadowed,
)
}
/// Compute color for given ray fired at the world.
@ -175,4 +192,48 @@ impl World {
None => BLACK,
}
}
/// Determine if point in world is in a shadow.
///
/// # Examples
/// ```
/// use rtchallenge::{
/// tuples::{ Tuple},
/// world::World,
/// };
///
/// let w = World::test_world();
///
/// // There is no shadow when nothing is collinear with point and light.
/// let p = Tuple::point(0.,10.,0.);
/// assert_eq!(w.is_shadowed(p), false);
///
/// // There shadow when an object is between the point and the light.
/// let p = Tuple::point(10.,-10.,10.);
/// assert_eq!(w.is_shadowed(p), true);
///
/// // There is no shadow when an object is behind the light.
/// let p = Tuple::point(-20.,20.,-20.);
/// assert_eq!(w.is_shadowed(p), false);
///
/// // There is no shadow when an object is behind the point.
/// let p = Tuple::point(-2.,2.,-2.);
/// assert_eq!(w.is_shadowed(p), false);
pub fn is_shadowed(&self, point: Tuple) -> bool {
// TODO(wathiede): how to make this multi light aware?
let light = self
.light
.as_ref()
.expect("cannot compute is_shadowed in world with no light");
let v = light.position - point;
let distance = v.magnitude();
let direction = v.normalize();
let r = Ray::new(point, direction);
let intersections = self.intersect(&r);
if let Some(h) = intersections.hit() {
return h.t < distance;
}
false
}
}