Compare commits

...

47 commits

Author SHA1 Message Date
Bruno BELANYI 965fda7395 examples: add background color to example scene 2020-03-25 00:09:49 +01:00
Bruno BELANYI 08063b5aa3 library: render: scene: add background color 2020-03-25 00:09:49 +01:00
Bruno BELANYI c2fd001430 library: render: object: remove index field 2020-03-25 00:09:49 +01:00
Bruno BELANYI 2e224b280d Merge branch 'add-beevee' into 'master'
Add beevee

See merge request EPITA_bruno.belanyi/image/ing2/pathtracer!4
2020-03-24 21:04:52 +00:00
Bruno BELANYI 6df2d857d4 library: use beevee's Intersected trait 2020-03-24 21:55:51 +01:00
Bruno BELANYI 21c7aea1c0 beevee: bvh: use Intersected trait for objects
This avoids having black squares around the objects because we computed
that it was the closest but the ray didn't actually hit it...
2020-03-24 21:55:51 +01:00
Bruno BELANYI 48bb3550cb library: use beevee instead of rust-bvh
I also used this opportunity to use `nalgebra::Unit` as much as
possible.
2020-03-24 21:55:48 +01:00
Bruno BELANYI d8a4a2eaad beevee: bvh: add intersection test to BVH 2020-03-24 21:47:14 +01:00
Bruno BELANYI 45fca6f3ed beevee: bvh: remove reference to objects in BVH 2020-03-24 21:47:14 +01:00
Bruno BELANYI 69779d7dd4 beevee: aabb: add sqdist_to_point method to AABB 2020-03-24 21:47:14 +01:00
Bruno BELANYI 6d39682feb beevee: ray: make Ray's fields public 2020-03-24 21:47:14 +01:00
Bruno BELANYI 566a9b198d beevee: bvh: add build implementation 2020-03-24 21:47:14 +01:00
Bruno BELANYI 8b3cbccb75 beevee: add psqselect dependency 2020-03-24 21:47:14 +01:00
Bruno BELANYI a405af8f88 beevee: aabb: bounded: add centroid method 2020-03-24 21:47:14 +01:00
Bruno BELANYI eddad707eb beevee: ray: add Ray implementation 2020-03-24 21:47:14 +01:00
Bruno BELANYI 0ce49bb3a3 beevee: aabb: add Bounded trait implementation 2020-03-24 21:47:14 +01:00
Bruno BELANYI 58ee42d21d beevee: aabb: add AABB implementation 2020-03-24 21:47:14 +01:00
Bruno BELANYI ca28281b67 beevee: add Axis enum implementation 2020-03-24 21:47:14 +01:00
Bruno BELANYI 9c56134c67 beevee: add library to workspace
This is designed to be the new BVH for the raytracer.
2020-03-24 21:47:14 +01:00
Bruno BELANYI 01d2c2d973 project: move to Cargo workspace 2020-03-24 21:47:09 +01:00
Bruno BELANYI b5835b2726 Merge branch 'fix-refraction' into 'master'
Fix refraction

See merge request EPITA_bruno.belanyi/image/ing2/pathtracer!2
2020-03-24 20:39:48 +00:00
Bruno BELANYI 8e09f45f69 library: render: move helpers to utils module
Those helper functions are used by `Scene` to render the scene, but
they have no use being in the same file. Instead make it a crate-public
module of `render`.
2020-03-23 16:51:06 +01:00
Bruno BELANYI c0c332e3fa examples: triangles: fix ball diffraction index
The ball was intended to be a glass ball, so an index of 1.5
2020-03-23 16:49:55 +01:00
Bruno BELANYI 70c0e0cdf3 library: render: scene: handle refraction cleanly
The refraction information was incorrect in the previous implementation.
We never updated the medium information when going out of a transparent
material. It is now handled using a small structure holding both the old
index, and new_index.

We can now handle any number of successive medium changes, with the
associated change of indices correctly.

This implementation assumes that any ray going into an object is going
to come out in the same medium that it was originally travelling in
before entering said object. This is an acceptable approximation to me.
2020-03-23 16:49:55 +01:00
Bruno BELANYI 6af36d814f library: render: scene: cleanup refracted calculation 2020-03-23 16:49:55 +01:00
Bruno BELANYI 16066a3c7d library: render: scene: normalize vectors 2020-03-23 16:49:55 +01:00
Bruno BELANYI fccf3caef1 library: render: scene: clean-up ReflTrans handling 2020-03-23 16:49:55 +01:00
Bruno BELANYI b29e6d1613 library: render: scene: fix reflection handling
The reflection will be calculated even if it ends up not contributing to
the final color of an object. This allows for a more systematic use of
coefficients without applying them twice, like it was done for the
refraction transparency handling...
2020-03-23 16:49:55 +01:00
Bruno BELANYI 61db9c0cd4 Merge branch 'doc' into 'master'
Document public APIs

See merge request EPITA_bruno.belanyi/image/ing2/pathtracer!1
2020-03-23 12:06:13 +00:00
Bruno BELANYI ef8a418479 library: texture: do not expose sub-modules 2020-03-23 12:44:17 +01:00
Bruno BELANYI db4b8b05ba library: shape: do not expose sub-modules 2020-03-23 12:44:17 +01:00
Bruno BELANYI bef61b43b8 library: material: do not expose sub-modules 2020-03-23 12:44:17 +01:00
Bruno BELANYI f8102ddb3b library: light: do not expose sub-modules 2020-03-23 12:44:17 +01:00
Antoine Martin 0d59f49ea7 library: document root module 2020-03-23 12:44:17 +01:00
Antoine Martin 64125dbc43 library: document render module 2020-03-23 12:44:17 +01:00
Antoine Martin a9fd726a0d library: document shape module 2020-03-23 12:44:17 +01:00
Antoine Martin ce1b8eeaaa library: document texture module 2020-03-23 12:43:22 +01:00
Antoine Martin 94ab40413a library: document serialize module 2020-03-23 12:42:36 +01:00
Antoine Martin 614217d33e library: document material module 2020-03-23 12:42:36 +01:00
Antoine Martin 2aed964101 library: document light module 2020-03-23 12:41:39 +01:00
Antoine Martin 04deae1d88 library: document core module 2020-03-22 00:14:25 +01:00
Antoine Martin 2c6e0b42d2 library: core: document light_properties module 2020-03-22 00:14:25 +01:00
Antoine Martin b71f0aee5a library: core: document film module 2020-03-22 00:14:25 +01:00
Antoine Martin 07dbad95b5 library: core: document color module 2020-03-22 00:14:25 +01:00
Antoine Martin db84708392 library: core: document camera module 2020-03-22 00:14:25 +01:00
Antoine Martin ffd40148a7 library: add missing_docs warning 2020-03-20 17:53:08 +01:00
Antoine Martin db3ba1b312 library: render: scene: use iterators for cast_ray 2020-03-20 00:47:25 +01:00
45 changed files with 2165 additions and 244 deletions

View file

@ -1,38 +1,6 @@
[package]
name = "pathtracer"
version = "0.1.0"
authors = [
"Bruno BELANYI <brunobelanyi@gmail.com>",
"Antoine Martin <antoine97.martin@gmail.com>"
[workspace]
members = [
"beevee",
"pathtracer",
]
edition = "2018"
description = "A pathtracer written in Rust"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
name = "pathtracer"
path = "src/lib.rs"
[[bin]]
name = "pathtracer"
path = "src/main.rs"
[dependencies]
bvh = "0.3.2"
derive_more = "0.99.3"
enum_dispatch = "0.2.1"
image = "0.23.0"
indicatif = "0.14.0"
rand = "0.7"
rayon = "1.3.0"
serde_yaml = "0.8"
structopt = "0.3"
[dependencies.nalgebra]
version = "0.20.0"
features = ["serde-serialize"]
[dependencies.serde]
version = "1.0"
features = ["derive"]

11
beevee/Cargo.toml Normal file
View file

@ -0,0 +1,11 @@
[package]
name = "beevee"
version = "0.1.0"
authors = ["Bruno BELANYI <brunobelanyi@gmail.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
nalgebra = "0.20"
pdqselect = "0.1.0"

View file

@ -0,0 +1,63 @@
use super::AABB;
use crate::Point;
/// A trait for objects that can be bounded using an [`AABB`]
///
/// [`AABB`]: struct.AABB.html
pub trait Bounded {
/// Return the [`AABB`] surrounding self.
///
/// [`AABB`]: struct.AABB.html
fn aabb(&self) -> AABB;
/// Return the centroid of self.
fn centroid(&self) -> Point;
}
/// Implementation of [`Bounded`] for [`AABB`]
///
/// [`Bounded`]: trait.Bounded.html
/// [`AABB`]: struct.Point.html
///
/// # Examples
/// ```
/// use beevee::Point;
/// use beevee::aabb::{AABB, Bounded};
///
/// let low = Point::new(0., 0., 0.);
/// let high = Point::new(1., 2., 3.);
/// let aabb = AABB::with_bounds(low, high);
/// assert_eq!(aabb, aabb.aabb());
/// ```
impl Bounded for AABB {
fn aabb(&self) -> AABB {
*self
}
fn centroid(&self) -> Point {
self.centroid()
}
}
/// Implementation of [`Bounded`] for [`Point`]
///
/// [`Bounded`]: trait.Bounded.html
/// [`Point`]: ../type.Point.html
///
/// # Examples
/// ```
/// use beevee::Point;
/// use beevee::aabb::Bounded;
///
/// let point = Point::new(1., 2., 3.);
/// let aabb = point.aabb();
/// assert!(aabb.contains(&point));
/// ```
impl Bounded for Point {
fn aabb(&self) -> AABB {
AABB::with_bounds(*self, *self)
}
fn centroid(&self) -> Point {
*self
}
}

View file

@ -0,0 +1,444 @@
//! An Axis-Alighned Bounding Box.
use crate::{Axis, Point, Vector};
use std::fmt::{Display, Formatter, Result};
/// An Axis-Aligned Bounding Box.
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct AABB {
/// The corner with the lowest (x, y, z) coordinates.
pub low: Point,
/// The corner with the highest (x, y, z) coordinates.
pub high: Point,
}
impl AABB {
/// Create a new empty [`AABB`]
///
/// [`AABB`]: struct.AABB.html
///
/// # Examples
///
/// ```
/// use beevee::Point;
/// use beevee::aabb::AABB;
///
/// let aabb = AABB::empty();
///
/// // Here for teh origin, but also true for any other point
/// let point = Point::origin();
///
/// assert!(!aabb.contains(&point));
/// ```
#[must_use]
pub fn empty() -> Self {
let lowest = std::f32::NEG_INFINITY;
let highest = std::f32::INFINITY;
AABB {
low: Point::new(highest, highest, highest),
high: Point::new(lowest, lowest, lowest),
}
}
/// Create a new empty [`AABB`]
///
/// [`AABB`]: struct.AABB.html
///
/// # Examples
///
/// ```
/// # use beevee::Point;
/// # use beevee::aabb::AABB;
/// #
/// let low = Point::new(0., 0., 0.);
/// let high = Point::new(1., 1., 1.);
/// let aabb = AABB::with_bounds(low, high);
///
/// assert_eq!(aabb, AABB::with_bounds(low, high));
/// ```
#[must_use]
pub fn with_bounds(low: Point, high: Point) -> Self {
debug_assert!(low.x <= high.x);
debug_assert!(low.y <= high.y);
debug_assert!(low.z <= high.z);
AABB { low, high }
}
/// Return a new bounding box containing both `self` and the new [`Point`]
///
/// [`Point`]: ../type.Point.html
///
/// # Examples
///
/// ```
/// # use beevee::Point;
/// # use beevee::aabb::AABB;
/// #
/// let aabb = AABB::empty();
/// let new_aabb = aabb.grow(&Point::origin());
///
/// assert_eq!(new_aabb, AABB::with_bounds(Point::origin(), Point::origin()));
/// ```
#[must_use]
pub fn grow(&self, point: &Point) -> Self {
let mut ans = *self;
ans.grow_mut(point);
ans
}
/// Grow the bounding box to accomodate a new [`Point`].
///
/// [`Point`]: ../type.Point.html
///
/// # Examples
///
/// ```
/// # use beevee::Point;
/// # use beevee::aabb::AABB;
/// #
/// let mut aabb = AABB::empty();
/// aabb.grow_mut(&Point::origin());
///
/// assert_eq!(aabb, AABB::with_bounds(Point::origin(), Point::origin()));
/// ```
pub fn grow_mut(&mut self, point: &Point) -> &mut Self {
// Update lowest bound
self.low.x = self.low.x.min(point.x);
self.low.y = self.low.y.min(point.y);
self.low.z = self.low.z.min(point.z);
// Update higher bound
self.high.x = self.high.x.max(point.x);
self.high.y = self.high.y.max(point.y);
self.high.z = self.high.z.max(point.z);
// Return self for method chaining
self
}
/// Return true if the bounding box contains the [`Point`], false otherwise
///
/// [`Point`]: ../type.Point.html
///
/// # Examples
/// ```
/// # use beevee::Point;
/// # use beevee::aabb::AABB;
/// #
/// let low = Point::new(0., 0., 0.);
/// let high = Point::new(1., 1., 1.);
/// let aabb = AABB::with_bounds(low, high);
///
/// // It contains the whole box from low to high
/// assert!(aabb.contains(&low));
/// assert!(aabb.contains(&high));
/// assert!(aabb.contains(&Point::new(0.5, 0.5, 0.5)));
///
/// // And doesn't contain anything else
/// assert!(!aabb.contains(&Point::new(-1., -1., -1.)));
/// assert!(!aabb.contains(&Point::new(1.1, 0., 0.)));
/// assert!(!aabb.contains(&Point::new(2., -2., 0.)));
/// ```
pub fn contains(&self, point: &Point) -> bool {
(self.low.x..=self.high.x).contains(&point.x)
&& (self.low.y..=self.high.y).contains(&point.y)
&& (self.low.z..=self.high.z).contains(&point.z)
}
/// Return a new `AABB` which encloses `self` and the other [`AABB`].
///
/// [`AABB`]: struct.AABB.html
///
/// # Examples
/// ```
/// # use beevee::Point;
/// # use beevee::aabb::AABB;
/// #
/// let low = Point::new(0., 0., 0.);
/// let high = Point::new(1., 1., 1.);
/// let aabb = AABB::empty();
/// let other = AABB::with_bounds(low, high);
///
/// // Grow the AABB to enclose the other one.
/// let union = aabb.union(&other);
///
/// // The result is now the union of an empty bounding box and the other one.
/// assert_eq!(union, other);
/// ```
pub fn union(&self, other: &Self) -> Self {
// Clone the first bounding box.
let mut ans = *self;
// Update the new bounding box.
ans.union_mut(other);
// Return the new bounding box.
ans
}
/// Grow `self` to enclose the other [`AABB`].
///
/// [`AABB`]: struct.AABB.html
///
/// # Examples
/// ```
/// # use beevee::Point;
/// # use beevee::aabb::AABB;
/// #
/// let low = Point::new(0., 0., 0.);
/// let high = Point::new(1., 1., 1.);
/// let mut aabb = AABB::empty();
/// let other = AABB::with_bounds(low, high);
///
/// // Grow the AABB to enclose the other one.
/// aabb.union_mut(&other);
///
/// // The bounding box has grown to become equal to other one.
/// assert_eq!(aabb, other);
/// ```
pub fn union_mut(&mut self, other: &Self) -> &mut Self {
self.grow_mut(&other.low);
self.grow_mut(&other.high);
self
}
/// Return a vector correspondin to the diagonal from `low` to `high` for the [`AABB`].
///
/// [`AABB`]: struct.AABB.html
///
/// # Examples
/// ```
/// # use beevee::Point;
/// # use beevee::aabb::AABB;
/// #
/// let low = Point::new(0., 0., 0.);
/// let high = Point::new(1., 1., 1.);
/// let aabb = AABB::with_bounds(low, high);
///
/// assert_eq!(aabb.diagonal(), high - low);
/// ```
pub fn diagonal(&self) -> Vector {
self.high - self.low
}
/// Return the center of the [`AABB`].
///
/// [`AABB`]: struct.AABB.html
///
/// # Examples
/// ```
/// # use beevee::Point;
/// # use beevee::aabb::AABB;
/// #
/// let low = Point::new(0., 0., 0.);
/// let high = Point::new(1., 1., 1.);
/// let aabb = AABB::with_bounds(low, high);
///
/// assert_eq!(aabb.centroid(), low + (high - low) / 2.);
/// ```
pub fn centroid(&self) -> Point {
self.low + self.diagonal() / 2.
}
/// Return true if the [`AABB`] is empty, false otherwise.
/// [`AABB`]: struct.AABB.html
///
/// # Examples
/// ```
/// # use beevee::Point;
/// # use beevee::aabb::AABB;
/// #
/// let low = Point::new(0., 0., 0.);
/// let high = Point::new(1., 1., 1.);
/// let not_empty = AABB::with_bounds(low, high);
/// let empty = AABB::empty();
///
/// assert!(!not_empty.is_empty());
/// assert!(empty.is_empty());
/// ```
pub fn is_empty(&self) -> bool {
self.low.x > self.high.x || self.low.y > self.high.y || self.low.z > self.high.z
}
/// Return the total surface area of the [`AABB`].
///
/// [`AABB`]: struct.AABB.html
///
/// # Examples
/// ```
/// # use beevee::Point;
/// # use beevee::aabb::AABB;
/// #
/// let low = Point::new(0., 0., 0.);
/// let high = Point::new(1., 1., 1.);
/// let aabb = AABB::with_bounds(low, high);
///
/// assert!((aabb.surface() - 6.).abs() < std::f32::EPSILON);
/// ```
pub fn surface(&self) -> f32 {
let diagonal = self.diagonal();
2. * (diagonal.x * diagonal.y + diagonal.x * diagonal.z + diagonal.y * diagonal.z)
}
/// Return the total volume of the [`AABB`].
///
/// [`AABB`]: struct.AABB.html
///
/// # Examples
/// ```
/// # use beevee::Point;
/// # use beevee::aabb::AABB;
/// #
/// let low = Point::new(0., 0., 0.);
/// let high = Point::new(1., 1., 1.);
/// let aabb = AABB::with_bounds(low, high);
///
/// assert!((aabb.volume() - 1.).abs() < std::f32::EPSILON);
/// ```
pub fn volume(&self) -> f32 {
let diagonal = self.diagonal();
diagonal.x * diagonal.y * diagonal.z
}
/// Return the axis along which the [`AABB`] is the largest.
///
/// [`AABB`]: struct.AABB.html
///
/// # Examples
/// ```
/// # use beevee::Point;
/// # use beevee::aabb::AABB;
/// use beevee::Axis;
///
/// let low = Point::new(0., 0., 0.);
/// let high = Point::new(3., 2., 1.);
/// let aabb = AABB::with_bounds(low, high);
///
/// assert_eq!(aabb.largest_axis(), Axis::X);
/// ```
///
/// ```
/// # use beevee::Point;
/// # use beevee::aabb::AABB;
/// use beevee::Axis;
///
/// let low = Point::new(0., 0., 0.);
/// let high = Point::new(1., 1., 1.);
/// let aabb = AABB::with_bounds(low, high);
///
/// // Prefers the X axis in case of a three-way tie, then the Y axis in a tie with Z
/// assert_eq!(aabb.largest_axis(), Axis::X);
/// ```
pub fn largest_axis(&self) -> Axis {
let diagonal = self.diagonal();
if diagonal.x >= diagonal.y && diagonal.x >= diagonal.z {
Axis::X
} else if diagonal.y >= diagonal.z {
Axis::Y
} else {
Axis::Z
}
}
/// Return the shortest distance from an [`AABB`] to a [`Point`].
///
/// [`AABB`]: struct.AABB.html
/// [`AABB`]: ../type.Point.html
///
/// # Examples
/// ```
/// # use beevee::Point;
/// # use beevee::aabb::AABB;
/// #
/// let low = Point::new(0., 0., 0.);
/// let high = Point::new(1., 1., 1.);
/// let aabb = AABB::with_bounds(low, high);
///
/// assert!((aabb.distance_to_point(Point::new(-1., 0., 0.)) - 1.).abs() < std::f32::EPSILON);
/// ```
///
/// ```
/// # use beevee::Point;
/// # use beevee::aabb::AABB;
/// #
/// let low = Point::new(0., 0., 0.);
/// let high = Point::new(1., 1., 1.);
/// let aabb = AABB::with_bounds(low, high);
///
/// // Returns 0. when the point is contained by the AABB
/// assert!(aabb.distance_to_point(Point::new(0.5, 0.5, 0.5)).abs() < std::f32::EPSILON);
/// ```
pub fn distance_to_point(&self, point: Point) -> f32 {
f32::sqrt(self.sqdist_to_point(point))
}
/// Return the square of the shortest distance from an [`AABB`] to a [`Point`].
///
/// [`AABB`]: struct.AABB.html
/// [`AABB`]: ../type.Point.html
///
/// # Examples
/// ```
/// # use beevee::Point;
/// # use beevee::aabb::AABB;
/// #
/// let low = Point::new(0., 0., 0.);
/// let high = Point::new(1., 1., 1.);
/// let aabb = AABB::with_bounds(low, high);
///
/// assert!((aabb.sqdist_to_point(Point::new(-1., 0., 0.)) - 1.).abs() < std::f32::EPSILON);
/// ```
///
/// ```
/// # use beevee::Point;
/// # use beevee::aabb::AABB;
/// #
/// let low = Point::new(0., 0., 0.);
/// let high = Point::new(1., 1., 1.);
/// let aabb = AABB::with_bounds(low, high);
///
/// // Returns 0. when the point is contained by the AABB
/// assert!(aabb.sqdist_to_point(Point::new(0.5, 0.5, 0.5)).abs() < std::f32::EPSILON);
/// ```
pub fn sqdist_to_point(&self, point: Point) -> f32 {
let dx = (self.low.x - point.x).max(0.).max(point.x - self.high.x);
let dy = (self.low.y - point.y).max(0.).max(point.y - self.high.y);
let dz = (self.low.z - point.z).max(0.).max(point.z - self.high.z);
dx * dx + dy * dy + dz * dz
}
}
/// Display implementation for [`AABB`].
///
/// [`AABB`]: struct.AABB.html
///
/// # Examples
/// ```
/// # use beevee::Point;
/// # use beevee::aabb::AABB;
/// let low = Point::new(0., 0., 0.);
/// let high = Point::new(1., 1., 1.);
/// let aabb = AABB::with_bounds(low, high);
///
/// assert_eq!(format!("{}", aabb), "low: {0, 0, 0}, high: {1, 1, 1}");
/// ```
impl Display for AABB {
fn fmt(&self, f: &mut Formatter) -> Result {
write!(f, "low: {}, high: {}", self.low, self.high)
}
}
/// Return an empty [`AABB`].
///
/// [`AABB`]: struct.AABB.html
///
/// # Examples
///
/// ```
/// # use beevee::aabb::AABB;
/// let default = <AABB as Default>::default();
/// let empty = AABB::empty();
///
/// assert_eq!(default, empty);
/// ```
impl Default for AABB {
fn default() -> Self {
AABB::empty()
}
}

7
beevee/src/aabb/mod.rs Normal file
View file

@ -0,0 +1,7 @@
//! The module relating to Axis-Aligned Bounding Boxes.
mod bounded;
pub use bounded::*;
mod bounding_box;
pub use bounding_box::*;

170
beevee/src/axis.rs Normal file
View file

@ -0,0 +1,170 @@
use crate::{Point, Vector};
use std::fmt::{Display, Formatter, Result};
use std::ops::{Index, IndexMut};
/// An enum for indexing different spatial structures.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Axis {
/// The X axis.
X = 0,
/// The Y axis.
Y = 1,
/// The Z axis.
Z = 2,
}
/// Display implementation for [`Axis`].
///
/// [`Axis`]: enum.Axis.html
///
/// # Examples
/// ```
/// # use beevee::Axis;
/// assert_eq!(format!("{}", Axis::X), "x");
/// assert_eq!(format!("{}", Axis::Y), "y");
/// assert_eq!(format!("{}", Axis::Z), "z");
/// ```
impl Display for Axis {
fn fmt(&self, f: &mut Formatter) -> Result {
write!(
f,
"{}",
match *self {
Axis::X => "x",
Axis::Y => "y",
Axis::Z => "z",
}
)
}
}
/// Slice indexing implementation by [`Axis`]
///
/// [`Axis`]: enum.Axis.html
///
/// # Examples
/// ```
/// # use beevee::Axis;
/// #
/// let slice = &[0., 1., 2.];
/// assert_eq!(slice[Axis::X], 0.);
/// ```
impl<T> Index<Axis> for [T] {
type Output = T;
fn index(&self, axis: Axis) -> &Self::Output {
&self[axis as usize]
}
}
/// Mutable slice indexing implementation by [`Axis`]
///
/// [`Axis`]: enum.Axis.html
///
/// # Examples
/// ```
/// # use beevee::Axis;
/// #
/// let slice = &mut [0., 1., 2.];
/// slice[Axis::X] = 3.;
/// assert_eq!(slice, &[3., 1., 2.]);
/// ```
impl<T> IndexMut<Axis> for [T] {
fn index_mut(&mut self, axis: Axis) -> &mut T {
&mut self[axis as usize]
}
}
/// [`Point`] indexing implementation by [`Axis`]
///
/// [`Axis`]: enum.Axis.html
/// [`Point`]: type.Point.html
///
/// # Examples
/// ```
/// # use beevee::{Axis, Point};
/// #
/// let point = Point::new(0., 1., 2.);
/// assert_eq!(point[Axis::X], 0.);
/// ```
impl Index<Axis> for Point {
type Output = f32;
fn index(&self, axis: Axis) -> &Self::Output {
match axis {
Axis::X => &self.x,
Axis::Y => &self.y,
Axis::Z => &self.z,
}
}
}
/// Mutable [`Point`] indexing implementation by [`Axis`]
///
/// [`Axis`]: enum.Axis.html
/// [`Point`]: type.Point.html
///
/// # Examples
/// ```
/// # use beevee::{Axis, Point};
/// #
/// let mut point = Point::new(0., 1., 2.);
/// point[Axis::X] = 3.;
/// assert_eq!(point, Point::new(3., 1., 2.));
/// ```
impl IndexMut<Axis> for Point {
fn index_mut(&mut self, axis: Axis) -> &mut f32 {
match axis {
Axis::X => &mut self.x,
Axis::Y => &mut self.y,
Axis::Z => &mut self.z,
}
}
}
/// [`Vector`] indexing implementation by [`Axis`]
///
/// [`Axis`]: enum.Axis.html
/// [`Vector`]: type.Vector.html
///
/// # Examples
/// ```
/// # use beevee::{Axis, Vector};
/// #
/// let point = Vector::new(0., 1., 2.);
/// assert_eq!(point[Axis::X], 0.);
/// ```
impl Index<Axis> for Vector {
type Output = f32;
fn index(&self, axis: Axis) -> &Self::Output {
match axis {
Axis::X => &self.x,
Axis::Y => &self.y,
Axis::Z => &self.z,
}
}
}
/// Mutable [`Vector`] indexing implementation by [`Axis`]
///
/// [`Axis`]: enum.Axis.html
/// [`Vector`]: type.Vector.html
///
/// # Examples
/// ```
/// # use beevee::{Axis, Vector};
/// #
/// let mut point = Vector::new(0., 1., 2.);
/// point[Axis::X] = 3.;
/// assert_eq!(point, Vector::new(3., 1., 2.));
/// ```
impl IndexMut<Axis> for Vector {
fn index_mut(&mut self, axis: Axis) -> &mut f32 {
match axis {
Axis::X => &mut self.x,
Axis::Y => &mut self.y,
Axis::Z => &mut self.z,
}
}
}

View file

@ -0,0 +1,11 @@
use crate::aabb::Bounded;
use crate::ray::Ray;
/// The trait for any object to be used in the [`BVH`].
///
/// [`BVH`]: struct.BVH.html
pub trait Intersected: Bounded {
/// Return None if there is no intersection, or the distance along the ray to the closest
/// intersection
fn intersect(&self, ray: &Ray) -> Option<f32>;
}

7
beevee/src/bvh/mod.rs Normal file
View file

@ -0,0 +1,7 @@
//! The Boudning Volume Hiearchy
mod intersected;
pub use intersected::*;
mod tree;
pub use tree::*;

495
beevee/src/bvh/tree.rs Normal file
View file

@ -0,0 +1,495 @@
use super::Intersected;
use crate::aabb::AABB;
use crate::ray::Ray;
use crate::Axis;
/// An enum representing either an internal or a leaf node of the [`BVH`]
///
/// [`BVH`]: struct.BVH.html
#[derive(Clone, Debug, PartialEq)]
enum NodeEnum {
Internal { left: Box<Node>, right: Box<Node> },
Leaf,
}
/// A node representing either an internal or a leaf node of the [`BVH`]
///
#[derive(Clone, Debug, PartialEq)]
struct Node {
bounds: AABB,
begin: usize,
end: usize,
kind: NodeEnum,
}
/// The BVH containing all the objects of type O.
/// This type must implement [`Intersected`].
///
/// [`Intersected`]: trait.Intersected.html
#[derive(Clone, Debug, PartialEq)]
pub struct BVH {
tree: Node,
}
impl BVH {
/// Build a [`BVH`] for the given slice of objects.
/// Each leaf node will be built in a way to try and contain less than 32 objects.
///
/// # Examples
/// ```
/// use beevee::{Point, Vector};
/// use beevee::aabb::{AABB, Bounded};
/// use beevee::bvh::{BVH, Intersected};
/// use beevee::ray::Ray;
///
/// #[derive(Clone, Debug, PartialEq)]
/// struct Sphere {
/// center: Point,
/// radius: f32,
/// }
///
/// impl Bounded for Sphere {
/// fn aabb(&self) -> AABB {
/// let delt = Vector::new(self.radius, self.radius, self.radius);
/// AABB::with_bounds(self.center - delt, self.center + delt)
/// }
/// fn centroid(&self) -> Point {
/// self.center
/// }
/// }
///
/// impl Intersected for Sphere {
/// fn intersect(&self, ray: &Ray) -> Option<f32> {
/// use std::mem;
///
/// let delt = self.center - ray.origin;
/// let tca = ray.direction.dot(&delt);
/// let d2 = delt.norm_squared() - tca * tca;
/// let r_2 = self.radius * self.radius;
///
/// if d2 > r_2 {
/// return None;
/// }
///
/// let thc = (r_2 - d2).sqrt();
/// let mut t_0 = tca - thc;
/// let mut t_1 = tca + thc;
///
/// if t_0 > t_1 {
/// mem::swap(&mut t_0, &mut t_1)
/// }
/// if t_0 < 0. {
/// t_0 = t_1
/// }
/// if t_0 < 0. {
/// None
/// } else {
/// Some(t_0)
/// }
/// }
/// }
///
/// let spheres: &mut [Sphere] = &mut [Sphere{ center: Point::origin(), radius: 2.5 }];
/// let bvh = BVH::build(spheres);
/// ```
pub fn build<O: Intersected>(objects: &mut [O]) -> Self {
Self::with_max_capacity(objects, 32)
}
/// Build a [`BVH`] for the given slice of objects, with an indicated max capacity per
/// leaf-node. The max capacity is not respected when the SAH heuristic indicate that it would
/// be better to iterate over all objects instead of splitting.
///
/// # Examples
/// ```
/// use beevee::{Point, Vector};
/// use beevee::aabb::{AABB, Bounded};
/// use beevee::bvh::{BVH, Intersected};
/// use beevee::ray::Ray;
///
/// #[derive(Clone, Debug, PartialEq)]
/// struct Sphere {
/// center: Point,
/// radius: f32,
/// }
///
/// impl Bounded for Sphere {
/// fn aabb(&self) -> AABB {
/// let delt = Vector::new(self.radius, self.radius, self.radius);
/// AABB::with_bounds(self.center - delt, self.center + delt)
/// }
/// fn centroid(&self) -> Point {
/// self.center
/// }
/// }
///
/// impl Intersected for Sphere {
/// fn intersect(&self, ray: &Ray) -> Option<f32> {
/// use std::mem;
///
/// let delt = self.center - ray.origin;
/// let tca = ray.direction.dot(&delt);
/// let d2 = delt.norm_squared() - tca * tca;
/// let r_2 = self.radius * self.radius;
///
/// if d2 > r_2 {
/// return None;
/// }
///
/// let thc = (r_2 - d2).sqrt();
/// let mut t_0 = tca - thc;
/// let mut t_1 = tca + thc;
///
/// if t_0 > t_1 {
/// mem::swap(&mut t_0, &mut t_1)
/// }
/// if t_0 < 0. {
/// t_0 = t_1
/// }
/// if t_0 < 0. {
/// None
/// } else {
/// Some(t_0)
/// }
/// }
/// }
///
/// let spheres: &mut [Sphere] = &mut [Sphere{ center: Point::origin(), radius: 2.5 }];
/// let bvh = BVH::with_max_capacity(spheres, 32);
/// ```
pub fn with_max_capacity<O: Intersected>(objects: &mut [O], max_cap: usize) -> Self {
let tree = build_node(objects, 0, objects.len(), max_cap);
Self { tree }
}
/// Return the true if the [`BVH`] has been built soundly:
/// * Each child node is contained inside the parent's bounding box.
/// * Each object in a leaf node is inside the node's bounding box.
/// * There is no missing object indices.
///
/// # Examples
/// ```
/// # use beevee::{Point, Vector};
/// # use beevee::aabb::{AABB, Bounded};
/// # use beevee::bvh::{BVH, Intersected};
/// # use beevee::ray::Ray;
/// #
/// # #[derive(Clone, Debug, PartialEq)]
/// # struct Sphere {
/// # center: Point,
/// # radius: f32,
/// # }
/// #
/// # impl Bounded for Sphere {
/// # fn aabb(&self) -> AABB {
/// # let delt = Vector::new(self.radius, self.radius, self.radius);
/// # AABB::with_bounds(self.center - delt, self.center + delt)
/// # }
/// # fn centroid(&self) -> Point {
/// # self.center
/// # }
/// # }
/// #
/// # impl Intersected for Sphere {
/// # fn intersect(&self, ray: &Ray) -> Option<f32> {
/// # use std::mem;
/// #
/// # let delt = self.center - ray.origin;
/// # let tca = ray.direction.dot(&delt);
/// # let d2 = delt.norm_squared() - tca * tca;
/// # let r_2 = self.radius * self.radius;
/// #
/// # if d2 > r_2 {
/// # return None;
/// # }
/// #
/// # let thc = (r_2 - d2).sqrt();
/// # let mut t_0 = tca - thc;
/// # let mut t_1 = tca + thc;
/// #
/// # if t_0 > t_1 {
/// # mem::swap(&mut t_0, &mut t_1)
/// # }
/// # if t_0 < 0. {
/// # t_0 = t_1
/// # }
/// # if t_0 < 0. {
/// # None
/// # } else {
/// # Some(t_0)
/// # }
/// # }
/// # }
/// #
/// // Using the same sphere definition than build
/// let spheres: &mut [Sphere] = &mut [Sphere{ center: Point::origin(), radius: 2.5 }];
/// let bvh = BVH::with_max_capacity(spheres, 32);
/// assert!(bvh.is_sound(spheres));
/// ```
pub fn is_sound<O: Intersected>(&self, objects: &[O]) -> bool {
fn check_node<O: Intersected>(objects: &[O], node: &Node) -> bool {
if node.begin > node.end {
return false;
}
match node.kind {
NodeEnum::Leaf => objects[node.begin..node.end]
.iter()
.all(|o| node.bounds.union(&o.aabb()) == node.bounds),
NodeEnum::Internal {
ref left,
ref right,
} => {
check_node(objects, left.as_ref())
&& check_node(objects, right.as_ref())
&& node.bounds.union(&left.bounds) == node.bounds
&& node.bounds.union(&right.bounds) == node.bounds
}
}
};
check_node(objects, &self.tree)
}
/// Iterate recursively over the [`BVH`] to find an intersection point with the given [`Ray`].
/// This algorithm tries to only iterate over Nodes that are abolutely necessary, and skip
/// visiting nodes that are too far away.
///
/// [`BVH`]: struct.BVH.html
/// [`Ray`]: ../ray/struct.Ray.html
/// # Examples
/// ```
/// # use beevee::{Point, Vector};
/// # use beevee::aabb::{AABB, Bounded};
/// # use beevee::bvh::{BVH, Intersected};
/// # use beevee::ray::Ray;
/// #
/// # #[derive(Clone, Debug, PartialEq)]
/// # struct Sphere {
/// # center: Point,
/// # radius: f32,
/// # }
/// #
/// # impl Bounded for Sphere {
/// # fn aabb(&self) -> AABB {
/// # let delt = Vector::new(self.radius, self.radius, self.radius);
/// # AABB::with_bounds(self.center - delt, self.center + delt)
/// # }
/// # fn centroid(&self) -> Point {
/// # self.center
/// # }
/// # }
/// #
/// # impl Intersected for Sphere {
/// # fn intersect(&self, ray: &Ray) -> Option<f32> {
/// # use std::mem;
/// #
/// # let delt = self.center - ray.origin;
/// # let tca = ray.direction.dot(&delt);
/// # let d2 = delt.norm_squared() - tca * tca;
/// # let r_2 = self.radius * self.radius;
/// #
/// # if d2 > r_2 {
/// # return None;
/// # }
/// #
/// # let thc = (r_2 - d2).sqrt();
/// # let mut t_0 = tca - thc;
/// # let mut t_1 = tca + thc;
/// #
/// # if t_0 > t_1 {
/// # mem::swap(&mut t_0, &mut t_1)
/// # }
/// # if t_0 < 0. {
/// # t_0 = t_1
/// # }
/// # if t_0 < 0. {
/// # None
/// # } else {
/// # Some(t_0)
/// # }
/// # }
/// # }
/// #
/// // Using the same sphere definition than build
/// let spheres: &mut [Sphere] = &mut [Sphere{ center: Point::origin(), radius: 0.5 }];
/// let bvh = BVH::with_max_capacity(spheres, 32);
///
/// // This ray is directly looking at the spheres
/// let ray = Ray::new(Point::new(-1., 0., 0.), Vector::x_axis());
/// let res = bvh.walk(&ray, spheres);
///
/// assert!(res.is_some());
/// let (dist, obj) = res.unwrap();
/// assert_eq!(dist, 0.5);
/// assert_eq!(obj, &spheres[0]);
/// ```
pub fn walk<'o, O: Intersected>(&self, ray: &Ray, objects: &'o [O]) -> Option<(f32, &'o O)> {
walk_rec_helper(ray, objects, &self.tree, std::f32::INFINITY)
}
}
fn walk_rec_helper<'o, O: Intersected>(
ray: &Ray,
objects: &'o [O],
node: &Node,
min: f32,
) -> Option<(f32, &'o O)> {
use std::cmp::Ordering;
match &node.kind {
// Return the smallest intersection distance on leaf nodes
NodeEnum::Leaf => objects[node.begin..node.end]
.iter()
// This turns the Option<f32> of an intersection into an Option<(f32, &O)>
.filter_map(|o| o.intersect(ray).map(|d| (d, o)))
// Discard values that are too far away
.filter(|(dist, _)| dist < &min)
// Only keep the minimum value, if there is one
.min_by(|(lhs, _), (rhs, _)| lhs.partial_cmp(rhs).unwrap_or(Ordering::Equal)),
// Recursively find the best node otherwise
NodeEnum::Internal { left, right } => {
let left_dist = left.bounds.distance_to_point(ray.origin);
let right_dist = right.bounds.distance_to_point(ray.origin);
// Pick the short and far nodes
let (near, far, short_dist, far_dist) = if left_dist < right_dist {
(left, right, left_dist, right_dist)
} else {
(right, left, right_dist, left_dist)
};
// Don't recurse if we know we cannot possibly find a short-enough intersection
if short_dist > min {
return None;
}
// Recurse to the nearest Node first
let nearest_res = walk_rec_helper(ray, objects, near.as_ref(), min);
// Return immediately if there is no point going to the right at all
if far_dist > min {
return nearest_res;
}
match nearest_res {
// Short-circuit if we know it is shorter than any point in the far node
Some((t, obj)) if t <= far_dist => Some((t, obj)),
// We have short_dist <= far_dist <= min in this scenario
// With the eventual val.0 in the [short_dist, min) window
val => {
// Compute the new minimal distance encountered
let min = val.map_or(min, |(t, _)| min.min(t));
// Recursing with this new minimum can only return None or a better intersecion
walk_rec_helper(ray, objects, far.as_ref(), min).or(val)
}
}
}
}
}
fn bounds_from_slice<O: Intersected>(objects: &[O]) -> AABB {
objects
.iter()
.map(|o| o.aabb())
.fold(AABB::empty(), |acc, other| acc.union(&other))
}
fn build_node<O: Intersected>(objects: &mut [O], begin: usize, end: usize, max_cap: usize) -> Node {
let aabb = bounds_from_slice(objects);
// Don't split nodes under capacity
if objects.len() <= max_cap {
return Node {
bounds: aabb,
begin,
end,
kind: NodeEnum::Leaf,
};
}
// Calculate the SAH heuristic for this slice
let (split, axis, cost) = compute_sah(&mut objects[begin..end], aabb.surface(), max_cap);
// Only split if the heuristic shows that it is worth it
if cost >= objects.len() as f32 {
return Node {
bounds: aabb,
begin,
end,
kind: NodeEnum::Leaf,
};
}
// Avoid degenerate cases, and recenter the split inside [begin, end)
let split = if split == 0 || split >= (end - begin - 1) {
begin + (end - begin) / 2
} else {
begin + split
};
// Project along chosen axis
pdqselect::select_by(objects, split, |lhs, rhs| {
lhs.centroid()[axis]
.partial_cmp(&rhs.centroid()[axis])
.expect("Can't use Nans in the SAH computation")
});
// Construct children recurivsely on [begin, split) and [split, end)
let left = Box::new(build_node(objects, begin, split, max_cap));
let right = Box::new(build_node(objects, split, end, max_cap));
// Build the node recursivelly
Node {
bounds: aabb,
begin,
end,
kind: NodeEnum::Internal { left, right },
}
}
/// Returns the index at which to split for SAH, the Axis along which to split, and the calculated
/// cost.
fn compute_sah<O: Intersected>(
objects: &mut [O],
surface: f32,
max_cap: usize,
) -> (usize, Axis, f32) {
// FIXME(Bruno): too imperative to my taste...
let mut mid = objects.len() / 2;
let mut dim = Axis::X; // Arbitrary split
let mut min = std::f32::INFINITY;
// Pre-allocate the vectors
let mut left_surfaces = Vec::<f32>::with_capacity(objects.len() - 1);
let mut right_surfaces = Vec::<f32>::with_capacity(objects.len() - 1);
// For each axis compute the cost
for &axis in [Axis::X, Axis::Y, Axis::Z].iter() {
left_surfaces.clear();
right_surfaces.clear();
// Sort in order along the axis
objects.sort_by(|lhs, rhs| {
lhs.centroid()[axis]
.partial_cmp(&rhs.centroid()[axis])
.expect("Can't use NaNs in the SAH computation")
});
// Compute the surface for each possible split
{
let mut left_box = AABB::empty();
let mut right_box = AABB::empty();
for i in 0..(objects.len() - 1) {
left_box.union_mut(&objects[i].aabb());
left_surfaces.push(left_box.surface());
right_box.union_mut(&objects[objects.len() - 1 - i].aabb());
right_surfaces.push(right_box.surface());
}
}
// Calculate the cost
for left_count in 1..objects.len() {
let right_count = objects.len() - left_count;
let cost = 1. / max_cap as f32
+ (left_count as f32 * left_surfaces[left_count - 1]
+ right_count as f32 * right_surfaces[right_count])
/ surface;
if cost < min {
min = cost;
dim = axis;
mid = left_count
}
}
}
(mid, dim, min)
}

28
beevee/src/lib.rs Normal file
View file

@ -0,0 +1,28 @@
#![warn(missing_docs)]
//! A Bounding Volume Hierarchy crate for use with ray-tracing.
/// The point to describe the [`AABB`]'s corners.
///
/// [`AABB`]: aabb/struct.AABB.html
pub type Point = nalgebra::Point3<f32>;
/// The Vector to describe the [`Ray`]'s direction.
///
/// [`Ray`]: ray/struct.Ray.html
pub type Vector = nalgebra::Vector3<f32>;
/// The module relating to Axis-Aligned Bouding Boxes.
pub mod aabb;
mod axis;
pub use axis::*;
/// The module relating to Bouding Volume Hiearchy
pub mod bvh;
/// Module defining a [`Ray`] structure to intersect with the [`BVH`]
///
/// [`BVH`]: ../bvh/struct.BVH.html
/// [`Ray`]: ray/struct.Ray.html
pub mod ray;

152
beevee/src/ray.rs Normal file
View file

@ -0,0 +1,152 @@
use crate::aabb::AABB;
use crate::{Point, Vector};
use nalgebra::Unit;
use std::fmt::{Display, Formatter, Result};
/// The [`Ray`] to intersect with the [`BVH`].
///
/// [`BVH`]: ../bvh/struct.BVH.html
/// [`Ray`]: struct.Ray.html
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct Ray {
/// The point of origin of the ray.
pub origin: Point,
/// A unit vector representing the direction of the ray.
pub direction: Unit<Vector>,
/// The inverse of each coefficient of the ray's direction.
pub inv_direction: Vector,
}
impl Ray {
/// Create a new [`Ray`] with the given origin and direction
///
/// [`Ray`]: struct.Ray.html
///
/// # Examples
/// ```
/// use beevee::{Point, Vector};
/// use beevee::ray::Ray;
///
/// let ray = Ray::new(Point::origin(), Vector::x_axis());
/// ```
#[must_use]
pub fn new(origin: Point, direction: Unit<Vector>) -> Self {
let inv_direction = Vector::new(1. / direction.x, 1. / direction.y, 1. / direction.z);
Ray {
origin,
direction,
inv_direction,
}
}
/// Return the distance to intersect with an [`AABB`], or [`None`] if there's no intersection.
///
/// [`AABB`]: ../aabb/struct.AABB.html
/// [`Ray`]: struct.Ray.html
///
/// # Examples
/// ```
/// use beevee::{Point, Vector};
/// use beevee::aabb::AABB;
/// use beevee::ray::Ray;
///
/// let aabb = AABB::with_bounds(Point::new(1., -1., -1.), Point::new(3., 1., 1.));
/// let ray = Ray::new(Point::origin(), Vector::x_axis());
///
/// assert_eq!(ray.aabb_intersection(&aabb), Some(1.));
/// ```
///
/// ```
/// use beevee::{Point, Vector};
/// use beevee::aabb::AABB;
/// use beevee::ray::Ray;
///
/// let aabb = AABB::with_bounds(Point::new(-1., -1., -1.), Point::new(1., 1., 1.));
/// let ray = Ray::new(Point::origin(), Vector::x_axis());
///
/// // Also works from inside the AABB.
/// assert_eq!(ray.aabb_intersection(&aabb), Some(1.));
/// ```
///
/// ```
/// use beevee::{Point, Vector};
/// use beevee::aabb::AABB;
/// use beevee::ray::Ray;
///
/// let aabb = AABB::with_bounds(Point::new(1., -1., -1.), Point::new(3., 1., 1.));
/// let ray = Ray::new(Point::origin(), Vector::y_axis());
///
/// assert_eq!(ray.aabb_intersection(&aabb), None);
/// ```
pub fn aabb_intersection(&self, aabb: &AABB) -> Option<f32> {
use crate::Axis;
let min_max = |axis: Axis| {
let a = (aabb.high[axis] - self.origin[axis]) * self.inv_direction[axis];
let b = (aabb.low[axis] - self.origin[axis]) * self.inv_direction[axis];
if self.direction[axis] < 0. {
(a, b)
} else {
(b, a)
}
};
let (mut t_min, mut t_max) = min_max(Axis::X);
let (y_min, y_max) = min_max(Axis::Y);
if y_min > t_max || y_max < t_min {
return None;
}
if y_min > t_min {
t_min = y_min;
}
if y_max < t_max {
t_max = y_max;
}
let (z_min, z_max) = min_max(Axis::Z);
if z_min > t_max || z_max < t_min {
return None;
}
if z_min > t_min {
t_min = z_min;
}
if z_max < t_max {
t_max = z_max;
}
if t_max < 0. {
return None;
}
if t_min < 0. {
Some(t_max)
} else {
Some(t_min)
}
}
}
/// Display implementation for [`Ray`].
///
/// [`Ray`]: struct.Ray.html
///
/// # Examples
/// ```
/// # use beevee::{Point, Vector};
/// # use beevee::ray::Ray;
/// let ray = Ray::new(Point::origin(), Vector::x_axis());
///
/// assert_eq!(format!("{}", ray), "origin: {0, 0, 0}, direction: {1, 0, 0}")
/// ```
impl Display for Ray {
fn fmt(&self, f: &mut Formatter) -> Result {
write!(
f,
"origin: {}, direction: {{{}, {}, {}}}",
self.origin, self.direction.x, self.direction.y, self.direction.z,
)
}
}

38
pathtracer/Cargo.toml Normal file
View file

@ -0,0 +1,38 @@
[package]
name = "pathtracer"
version = "0.1.0"
authors = [
"Bruno BELANYI <brunobelanyi@gmail.com>",
"Antoine Martin <antoine97.martin@gmail.com>"
]
edition = "2018"
description = "A pathtracer written in Rust"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
name = "pathtracer"
path = "src/lib.rs"
[[bin]]
name = "pathtracer"
path = "src/main.rs"
[dependencies]
beevee = { path = "../beevee" }
derive_more = "0.99.3"
enum_dispatch = "0.2.1"
image = "0.23.0"
indicatif = "0.14.0"
rand = "0.7"
rayon = "1.3.0"
serde_yaml = "0.8"
structopt = "0.3"
[dependencies.nalgebra]
version = "0.20.0"
features = ["serde-serialize"]
[dependencies.serde]
version = "1.0"
features = ["derive"]

View file

@ -1,5 +1,6 @@
aliasing_limit: 10
reflection_limit: 5
background: {r: 0.5, g: 0.5, b: 0.5}
camera:
origin: [-1.0, 0.0, 0.0]

View file

@ -35,7 +35,7 @@ objects:
g: 1.0
b: 1.0
transparency: 1.0
index: 1.9
index: 1.5
texture:
type: uniform
color:

View file

@ -1,3 +1,5 @@
//! Camera related logic
use super::film::Film;
use crate::{Point, Vector};
use serde::{Deserialize, Deserializer};
@ -12,6 +14,24 @@ pub struct Camera {
}
impl Camera {
/// Creates a new `Camera`.
///
/// # Examples
///
/// ```
/// # use pathtracer::core::Camera;
/// use pathtracer::{Point, Vector};
///
/// let cam = Camera::new(
/// Point::new(-1., 0., 0.),
/// Vector::new(1., 0., 0.),
/// Vector::new(0., 1., 0.),
/// 2. * f32::atan(1.), /* 90° in radian */
/// 1.,
/// 1080,
/// 1080,
/// );
/// ```
pub fn new(
origin: Point,
forward: Vector,
@ -28,15 +48,73 @@ impl Camera {
Camera { origin, film }
}
/// Get the `Camera`'s [`Film`].
///
/// [`Film`]: ../film/struct.Film.html
///
/// # Examples
///
/// ```
/// # use pathtracer::core::{Camera, Film};
/// #
/// let cam = Camera::default();
/// let film: &Film = cam.film();
/// ```
pub fn film(&self) -> &Film {
&self.film
}
/// Get the `Camera`'s `Point` of origin.
///
/// # Examples
///
/// ```
/// # use pathtracer::core::Camera;
/// # use pathtracer::Point;
/// #
/// let cam = Camera::default();
/// let origin: &Point = cam.origin();
/// ```
pub fn origin(&self) -> &Point {
&self.origin
}
}
impl Default for Camera {
/// Returns a `Camera` with a 1080x1080 `Film`
///
/// # Examples
///
/// ```
/// # use pathtracer::core::Camera;
/// use pathtracer::{Point, Vector};
///
/// let default = Camera::default();
/// let new = Camera::new(
/// Point::new(0., 0., 0.),
/// Vector::new(1., 0., 0.),
/// Vector::new(0., 1., 0.),
/// 2. * f32::atan(1.), /* 90° in radian */
/// 1.,
/// 1080,
/// 1080,
/// );
///
/// assert_eq!(default, new);
/// ```
fn default() -> Self {
Self::new(
Point::origin(),
Vector::new(1., 0., 0.),
Vector::new(0., 1., 0.),
2. * f32::atan(1.), /* 90° in radian */
1.,
1080,
1080,
)
}
}
#[derive(Debug, Deserialize)]
struct SerializedCamera {
origin: Point,

View file

@ -1,3 +1,5 @@
//! Color definition and operations
use derive_more::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Sub, SubAssign, Sum};
use serde::Deserialize;
use std::ops::{Div, DivAssign, Mul, MulAssign};
@ -19,12 +21,34 @@ use std::ops::{Div, DivAssign, Mul, MulAssign};
)]
/// A structure to represent operations in the linear RGB colorspace.
pub struct LinearColor {
/// The color's red component
pub r: f32,
/// The color's green component
pub g: f32,
/// The color's blue component
pub b: f32,
}
impl LinearColor {
/// Creates the color black.
///
/// All 3 components are set to 0.
///
/// # Examples
///
/// ```
/// # use pathtracer::core::LinearColor;
/// #
/// let black = LinearColor::black();
/// assert_eq!(
/// black,
/// LinearColor {
/// r: 0.,
/// g: 0.,
/// b: 0.
/// }
/// );
/// ```
pub fn black() -> Self {
LinearColor {
r: 0.,
@ -33,11 +57,30 @@ impl LinearColor {
}
}
/// Creates a new `Color`.
///
/// # Examples
///
/// ```
/// # use pathtracer::core::LinearColor;
/// #
/// let color = LinearColor::new(1.0, 0.0, 0.0); // bright red!
/// ```
pub fn new(r: f32, g: f32, b: f32) -> Self {
LinearColor { r, g, b }
}
#[must_use]
/// Clamps the color's RGB components between 0.0 and 1.0.
///
/// # Examples
///
/// ```
/// # use pathtracer::core::LinearColor;
/// #
/// let color = LinearColor::new(1.5, -1.0, 0.5);
/// assert_eq!(color.clamp(), LinearColor::new(1.0, 0.0, 0.5))
/// ```
pub fn clamp(self) -> Self {
fn clamp(v: f32) -> f32 {
if v > 1. {
@ -109,19 +152,6 @@ impl From<LinearColor> for image::Rgb<u8> {
mod test {
use super::*;
#[test]
fn black_is_black() {
let black = LinearColor::black();
assert_eq!(
black,
LinearColor {
r: 0.,
g: 0.,
b: 0.
}
)
}
#[test]
fn default_is_black() {
assert_eq!(<LinearColor as Default>::default(), LinearColor::black())
@ -304,12 +334,6 @@ mod test {
);
}
#[test]
fn clamp_works() {
let color = LinearColor::new(1.5, -1., 0.5);
assert_eq!(color.clamp(), LinearColor::new(1., 0., 0.5))
}
#[test]
fn deserialization_works() {
let yaml = "{r: 1.0, g: 0.5, b: 0.2}";

View file

@ -1,3 +1,5 @@
//! Camera film logic
use crate::{Point, Vector};
/// Represent an abstract camera film, to know where each pixel is in space.
@ -11,6 +13,23 @@ pub struct Film {
}
impl Film {
/// Creates a new `Film`.
///
/// # Examples
///
/// ```
/// # use pathtracer::core::Film;
/// # use pathtracer::{Point, Vector};
/// #
/// let film = Film::new(
/// 1080,
/// 1080,
/// 10.0,
/// Point::origin(),
/// Vector::new(0.0, 1.0, 0.0),
/// Vector::new(1.0, 0.0, 0.0)
/// );
/// ```
pub fn new(x: u32, y: u32, screen_size: f32, center: Point, up: Vector, right: Vector) -> Self {
let (x_size, y_size) = if x > y {
(screen_size, screen_size * y as f32 / x as f32)
@ -26,30 +45,103 @@ impl Film {
}
}
/// Get the `Film`'s width.
///
/// # Examples
///
/// ```
/// # use pathtracer::core::Film;
/// #
/// let film = Film::default();
/// let width: u32 = film.width();
/// ```
pub fn width(&self) -> u32 {
self.x
}
/// Get the `Film`'s height.
///
/// # Examples
///
/// ```
/// # use pathtracer::core::Film;
/// #
/// let film = Film::default();
/// let height: u32 = film.height();
/// ```
pub fn height(&self) -> u32 {
self.y
}
/// Get a ratio of the pixel's position on the screen.
///
/// # Examples
///
/// ```
/// # use pathtracer::core::Film;
/// #
/// let film = Film::default(); // 1080x1080 film, width of 1.0
/// let (x, y) = film.pixel_ratio(108.0, 972.0);
/// assert_eq!(x, 0.1);
/// assert_eq!(y, 0.9);
/// ```
pub fn pixel_ratio(&self, x: f32, y: f32) -> (f32, f32) {
(x / self.x as f32, y / self.y as f32)
}
/// Get a pixel's absolute position from a relative screen ratio.
///
/// # Examples
///
/// ```
/// # use pathtracer::core::Film;
/// use pathtracer::Point;
///
/// let film = Film::default(); // 1080x1080 film, width of 1.0
/// let (x, y) = film.pixel_ratio(108.0, 1080.0);
/// let pos: Point = film.pixel_at_ratio(x, y);
/// assert_eq!(pos, Point::new(-0.4, -0.5, 0.0));
/// ```
pub fn pixel_at_ratio(&self, x: f32, y: f32) -> Point {
let delt_x = x - 0.5;
let delt_y = 0.5 - y;
self.center + self.ratio_right * delt_x + self.ratio_up * delt_y
}
/// Get a pixel's absolute position from screen coordinates.
///
/// # Examples
///
/// ```
/// # use pathtracer::core::Film;
/// use pathtracer::Point;
///
/// let film = Film::default(); // 1080x1080 film, width of 1.0
/// let pos: Point = film.pixel_at_coord(108, 1080);
/// assert_eq!(pos, Point::new(-0.4, -0.5, 0.0));
/// ```
pub fn pixel_at_coord(&self, x: u32, y: u32) -> Point {
let (x, y) = self.pixel_ratio(x as f32, y as f32);
self.pixel_at_ratio(x, y)
}
}
impl Default for Film {
/// Creates a simple 1080x1080 `Film`.
///
/// The screen size is 1.0, and the screen is centered at the origin.
fn default() -> Self {
Film::new(
1080,
1080,
1.0,
Point::origin(),
Vector::new(0.0, 1.0, 0.0),
Vector::new(1.0, 0.0, 0.0),
)
}
}
#[cfg(test)]
mod test {
use super::*;

View file

@ -1,9 +1,13 @@
//! Light property coefficients (diffuse, specular, transparency, reflectivity...)
use super::color::LinearColor;
use serde::Deserialize;
#[derive(Debug, PartialEq, Clone, Deserialize)]
#[serde(untagged)]
/// This enum stores the reflectivity or transparency information.
pub enum ReflTransEnum {
/// Transparence properties.
Transparency {
/// The transparency coefficient.
#[serde(rename = "transparency")]
@ -11,6 +15,7 @@ pub enum ReflTransEnum {
/// The diffraction index.
index: f32,
},
/// Reflectivity properties.
Reflectivity {
/// The reflectivity coefficient.
#[serde(rename = "reflectivity")]
@ -31,6 +36,20 @@ pub struct LightProperties {
}
impl LightProperties {
/// Creates a new `LightProperties` struct.
///
/// # Examples
///
/// ```
/// # use pathtracer::core::light_properties::{LightProperties, ReflTransEnum};
/// # use pathtracer::core::color::LinearColor;
/// #
/// let lp = LightProperties::new(
/// LinearColor::new(0.25, 0.5, 1.),
/// LinearColor::new(0.75, 0.375, 0.125),
/// Some(ReflTransEnum::Reflectivity { coef: 0.5 }),
/// );
/// ```
pub fn new(
diffuse: LinearColor,
specular: LinearColor,

View file

@ -1,3 +1,5 @@
//! Core pathtracing pipeline elements
pub mod camera;
pub use camera::*;

17
pathtracer/src/lib.rs Normal file
View file

@ -0,0 +1,17 @@
#![warn(missing_docs)]
//! A pathtracing crate
/// 3D points and vectors
pub use beevee::{Point, Vector};
/// A 2D point coordinate
pub type Point2D = nalgebra::Point2<f32>;
pub mod core;
pub mod light;
pub mod material;
pub mod render;
pub mod serialize;
pub mod shape;
pub mod texture;

View file

@ -10,6 +10,16 @@ pub struct AmbientLight {
}
impl AmbientLight {
/// Creates a new `AmbientLight`.
///
/// # Examples
///
/// ```
/// # use pathtracer::light::AmbientLight;
/// # use pathtracer::core::color::LinearColor;
/// #
/// let amb_light = AmbientLight::new(LinearColor::new(1.0, 0.0, 1.0));
/// ```
pub fn new(color: LinearColor) -> Self {
AmbientLight { color }
}

View file

@ -1,22 +1,34 @@
use super::{Light, SpatialLight};
use crate::core::LinearColor;
use crate::{Point, Vector};
use nalgebra::Unit;
use serde::Deserialize;
/// Represent a light emanating from a far away source, with parallel rays on all points.
#[derive(Debug, PartialEq, Deserialize)]
pub struct DirectionalLight {
#[serde(deserialize_with = "crate::serialize::vector_normalizer")]
direction: Vector,
direction: Unit<Vector>,
color: LinearColor,
}
impl DirectionalLight {
pub fn new(direction: Vector, color: LinearColor) -> Self {
DirectionalLight {
direction: direction.normalize(),
color,
}
/// Creates a new `DirectionalLight`.
///
/// # Examples
///
/// ```
/// # use pathtracer::light::DirectionalLight;
/// # use pathtracer::core::color::LinearColor;
/// # use pathtracer::Vector;
/// #
/// let dir_light = DirectionalLight::new(
/// Vector::x_axis(),
/// LinearColor::new(1.0, 0.0, 1.0),
/// );
/// ```
pub fn new(direction: Unit<Vector>, color: LinearColor) -> Self {
DirectionalLight { direction, color }
}
}
@ -27,8 +39,8 @@ impl Light for DirectionalLight {
}
impl SpatialLight for DirectionalLight {
fn to_source(&self, _: &Point) -> (Vector, f32) {
(self.direction * -1., std::f32::INFINITY)
fn to_source(&self, _: &Point) -> (Unit<Vector>, f32) {
(-self.direction, std::f32::INFINITY)
}
}
@ -38,7 +50,7 @@ mod test {
#[test]
fn new_works() {
let direction = Vector::new(1., 0., 0.);
let direction = Vector::x_axis();
let color = LinearColor::new(1., 1., 1.);
let light = DirectionalLight::new(direction, color.clone());
let res = DirectionalLight { direction, color };
@ -46,7 +58,7 @@ mod test {
}
fn simple_light() -> impl SpatialLight {
let direction = Vector::new(1., 0., 0.);
let direction = Vector::x_axis();
let color = LinearColor::new(1., 1., 1.);
DirectionalLight::new(direction, color)
}
@ -62,7 +74,10 @@ mod test {
fn to_source_is_correct() {
let light = simple_light();
let ans = light.to_source(&Point::new(1., 0., 0.));
let expected = (Vector::new(-1., 0., 0.), std::f32::INFINITY);
let expected = (
Unit::new_normalize(Vector::new(-1., 0., 0.)),
std::f32::INFINITY,
);
assert_eq!(ans, expected)
}
@ -72,7 +87,7 @@ mod test {
let light: DirectionalLight = serde_yaml::from_str(yaml).unwrap();
assert_eq!(
light,
DirectionalLight::new(Vector::new(1., 0., 0.), LinearColor::new(1., 0.5, 0.2))
DirectionalLight::new(Vector::x_axis(), LinearColor::new(1., 0.5, 0.2))
)
}
}

View file

@ -1,5 +1,8 @@
//! Various light implementations
use super::core::LinearColor;
use super::{Point, Vector};
use nalgebra::Unit;
/// Represent a light in the scene being rendered.
pub trait Light: std::fmt::Debug {
@ -10,17 +13,17 @@ pub trait Light: std::fmt::Debug {
/// Represent a light which has an abstract position in the scene being rendered.
pub trait SpatialLight: Light {
/// Get a unit vector from the origin to the position of the light, and its distance
fn to_source(&self, origin: &Point) -> (Vector, f32);
fn to_source(&self, origin: &Point) -> (Unit<Vector>, f32);
}
pub mod ambient_light;
mod ambient_light;
pub use ambient_light::*;
pub mod directional_light;
mod directional_light;
pub use directional_light::*;
pub mod point_light;
mod point_light;
pub use point_light::*;
pub mod spot_light;
mod spot_light;
pub use spot_light::*;

View file

@ -1,6 +1,7 @@
use super::{Light, SpatialLight};
use crate::core::LinearColor;
use crate::{Point, Vector};
use nalgebra::Unit;
use serde::Deserialize;
/// Represent a light emanating from a point in space, following the square distance law.
@ -11,6 +12,20 @@ pub struct PointLight {
}
impl PointLight {
/// Creates a new `PointLight`.
///
/// # Examples
///
/// ```
/// # use pathtracer::light::PointLight;
/// # use pathtracer::core::color::LinearColor;
/// # use pathtracer::Point;
/// #
/// let dir_light = PointLight::new(
/// Point::origin(),
/// LinearColor::new(1.0, 0.0, 1.0),
/// );
/// ```
pub fn new(position: Point, color: LinearColor) -> Self {
PointLight { position, color }
}
@ -24,10 +39,10 @@ impl Light for PointLight {
}
impl SpatialLight for PointLight {
fn to_source(&self, point: &Point) -> (Vector, f32) {
fn to_source(&self, point: &Point) -> (Unit<Vector>, f32) {
let delt = self.position - point;
let dist = delt.norm();
(delt.normalize(), dist)
(Unit::new_normalize(delt), dist)
}
}
@ -61,7 +76,7 @@ mod test {
fn to_source_is_correct() {
let light = simple_light();
let ans = light.to_source(&Point::new(1., 0., 0.));
let expected = (Vector::new(-1., 0., 0.), 1.);
let expected = (Unit::new_normalize(Vector::new(-1., 0., 0.)), 1.);
assert_eq!(ans, expected);
}

View file

@ -1,14 +1,16 @@
use super::{Light, SpatialLight};
use crate::core::LinearColor;
use crate::{Point, Vector};
use nalgebra::Unit;
use serde::{Deserialize, Deserializer};
/// Represent a light emanating from a directed light-source, outputting rays in a cone.
///
/// The illumination cone cannot have an FOV over 180°.
#[derive(Debug, PartialEq)]
pub struct SpotLight {
position: Point,
direction: Vector,
direction: Unit<Vector>,
cosine_value: f32,
color: LinearColor,
}
@ -17,13 +19,13 @@ impl SpotLight {
/// Construct a SpotLight with the given FOV in radian.
pub fn radians_new(
position: Point,
direction: Vector,
direction: Unit<Vector>,
fov_rad: f32,
color: LinearColor,
) -> Self {
SpotLight {
position,
direction: direction.normalize(),
direction,
cosine_value: (fov_rad / 2.).cos(),
color,
}
@ -32,7 +34,7 @@ impl SpotLight {
/// Construct a SpotLight with the given FOV in degrees.
pub fn degrees_new(
position: Point,
direction: Vector,
direction: Unit<Vector>,
fov_deg: f32,
color: LinearColor,
) -> Self {
@ -58,10 +60,10 @@ impl Light for SpotLight {
}
impl SpatialLight for SpotLight {
fn to_source(&self, point: &Point) -> (Vector, f32) {
fn to_source(&self, point: &Point) -> (Unit<Vector>, f32) {
let delt = self.position - point;
let dist = delt.norm();
(delt.normalize(), dist)
(Unit::new_normalize(delt), dist)
}
}
@ -69,7 +71,7 @@ impl SpatialLight for SpotLight {
struct SerializedSpotLight {
position: Point,
#[serde(deserialize_with = "crate::serialize::vector_normalizer")]
direction: Vector,
direction: Unit<Vector>,
fov: f32,
color: LinearColor,
}
@ -98,7 +100,7 @@ mod test {
fn radian_new_works() {
let light = SpotLight::radians_new(
Point::origin(),
Vector::new(1., 0., 0.),
Vector::x_axis(),
std::f32::consts::PI / 2.,
LinearColor::new(1., 1., 1.),
);
@ -108,7 +110,7 @@ mod test {
light,
SpotLight {
position: Point::origin(),
direction: Vector::new(1., 0., 0.),
direction: Vector::x_axis(),
cosine_value: calculated_cosine_value,
color: LinearColor::new(1., 1., 1.),
}
@ -121,7 +123,7 @@ mod test {
fn degrees_new_works() {
let light = SpotLight::degrees_new(
Point::origin(),
Vector::new(1., 0., 0.),
Vector::x_axis(),
60.,
LinearColor::new(1., 1., 1.),
);
@ -130,7 +132,7 @@ mod test {
light,
SpotLight {
position: Point::origin(),
direction: Vector::new(1., 0., 0.),
direction: Vector::x_axis(),
cosine_value: calculated_cosine_value,
color: LinearColor::new(1., 1., 1.),
}
@ -142,7 +144,7 @@ mod test {
fn simple_light() -> impl SpatialLight {
SpotLight::degrees_new(
Point::origin(),
Vector::new(1., 0., 0.),
Vector::x_axis(),
90.,
LinearColor::new(1., 1., 1.),
)
@ -180,7 +182,7 @@ mod test {
fn to_source_is_correct() {
let light = simple_light();
let ans = light.to_source(&Point::new(1., 0., 0.));
let expected = (Vector::new(-1., 0., 0.), 1.);
let expected = (Unit::new_normalize(Vector::new(-1., 0., 0.)), 1.);
assert_eq!(ans, expected);
}
@ -197,7 +199,7 @@ mod test {
light,
SpotLight::degrees_new(
Point::origin(),
Vector::new(1., 0., 0.),
Vector::x_axis(),
90.,
LinearColor::new(1., 0.5, 0.2)
)

View file

@ -1,3 +1,5 @@
//! Various material implementations
use super::core::LightProperties;
use super::Point2D;
use serde::Deserialize;
@ -5,6 +7,7 @@ use serde::Deserialize;
/// All the existing `Material` implementation.
#[serde(tag = "type")]
#[serde(rename_all = "lowercase")]
#[allow(missing_docs)]
#[enum_dispatch::enum_dispatch]
#[derive(Debug, PartialEq, Deserialize)]
pub enum MaterialEnum {
@ -19,5 +22,5 @@ pub trait Material: std::fmt::Debug {
fn properties(&self, point: Point2D) -> LightProperties;
}
pub mod uniform;
mod uniform;
pub use uniform::*;

View file

@ -11,6 +11,22 @@ pub struct UniformMaterial {
}
impl UniformMaterial {
/// Creates a new `UniformMaterial`.
///
/// # Examples
///
/// ```
/// # use pathtracer::material::UniformMaterial;
/// # use pathtracer::core::{LightProperties, LinearColor};
/// #
/// let uni_mat = UniformMaterial::new(
/// LightProperties::new(
/// LinearColor::new(1.0, 0.0, 0.0), // diffuse component
/// LinearColor::new(0.0, 0.0, 0.0), // specular component
/// None,
/// ),
/// );
/// ```
pub fn new(properties: LightProperties) -> Self {
UniformMaterial { properties }
}

View file

@ -1,8 +1,11 @@
//! Utility module to compute overall illumination
use crate::light::*;
use serde::Deserialize;
use std::iter::Iterator;
#[derive(Debug, PartialEq, Deserialize)]
/// A struct centralizing the light computation logic.
pub struct LightAggregate {
#[serde(default)]
ambients: Vec<AmbientLight>,
@ -15,10 +18,39 @@ pub struct LightAggregate {
}
impl LightAggregate {
/// Creates a new empty `LightAggregate`.
///
/// # Examples
///
/// ```
/// # use pathtracer::render::LightAggregate;
/// #
/// let la = LightAggregate::empty();
/// assert_eq!(la.ambient_lights_iter().count(), 0);
/// assert_eq!(la.spatial_lights_iter().count(), 0);
/// ```
pub fn empty() -> Self {
LightAggregate::new(vec![], vec![], vec![], vec![])
}
/// Creates a new `LightAggregate` from `Vec`s of [`Light`]s.
///
/// [`Light`]: ../../light/trait.Light.html
///
/// # Examples
///
/// ```
/// # use pathtracer::render::LightAggregate;
/// #
/// let la = LightAggregate::new(
/// Vec::new(),
/// Vec::new(),
/// Vec::new(),
/// Vec::new(),
/// );
/// assert_eq!(la.ambient_lights_iter().count(), 0);
/// assert_eq!(la.spatial_lights_iter().count(), 0);
/// ```
pub fn new(
ambients: Vec<AmbientLight>,
directionals: Vec<DirectionalLight>,
@ -33,10 +65,21 @@ impl LightAggregate {
}
}
/// Returns an iterator over the aggregate's [`AmbientLight`]s.
///
/// [`AmbientLight`]: ../../light/ambient_light/struct.AmbientLight.html
pub fn ambient_lights_iter(&self) -> impl Iterator<Item = &'_ dyn Light> {
self.ambients.iter().map(|l| l as &dyn Light)
}
/// Returns an iterator over the aggregate's [`SpatialLight`]s.
///
/// This simply merges iterators over [`DirectionalLight`], [`PointLight`] and [`SpotLight`].
///
/// [`SpatialLight`]: ../../light/trait.SpatialLight.html
/// [`DirectionalLight`]: ../../light/directional_light/struct.DirectionalLight.html
/// [`PointLight`]: ../../light/point_light/struct.PointLight.html
/// [`Spotight`]: ../../light/spot_light/struct.Spotight.html
pub fn spatial_lights_iter(&self) -> impl Iterator<Item = &'_ dyn SpatialLight> {
self.directionals
.iter()
@ -98,7 +141,7 @@ mod test {
let expected = LightAggregate::new(
vec![AmbientLight::new(LinearColor::new(1., 0.5, 0.2))],
vec![DirectionalLight::new(
Vector::new(1., 0., 0.),
Vector::x_axis(),
LinearColor::new(1., 0.5, 0.2),
)],
vec![PointLight::new(
@ -107,7 +150,7 @@ mod test {
)],
vec![SpotLight::degrees_new(
Point::origin(),
Vector::new(1., 0., 0.),
Vector::x_axis(),
90.,
LinearColor::new(1., 0.5, 0.2),
)],

View file

@ -1,3 +1,5 @@
//! Rendering logic
pub mod light_aggregate;
pub use light_aggregate::*;
@ -6,3 +8,5 @@ pub use object::*;
pub mod scene;
pub use scene::*;
pub(crate) mod utils;

View file

@ -1,27 +1,57 @@
//! Logic for the scene objects
use crate::material::MaterialEnum;
use crate::shape::{Shape, ShapeEnum};
use crate::texture::TextureEnum;
use bvh::aabb::{Bounded, AABB};
use bvh::bounding_hierarchy::BHShape;
use crate::Point;
use beevee::{
aabb::{Bounded, AABB},
bvh::Intersected,
ray::Ray,
};
use serde::Deserialize;
/// An object being rendered in the scene.
#[derive(Debug, PartialEq, Deserialize)]
pub struct Object {
/// The `Object`'s physical shape
pub shape: ShapeEnum,
/// The `Object`'s material
pub material: MaterialEnum,
/// The `Object`'s texture
pub texture: TextureEnum,
#[serde(skip_deserializing)]
index: usize,
}
impl Object {
/// Creates a new `Object`.
///
/// # Examples
///
/// ```
/// # use pathtracer::core::{LightProperties, LinearColor};
/// # use pathtracer::material::UniformMaterial;
/// # use pathtracer::render::Object;
/// # use pathtracer::shape::Sphere;
/// # use pathtracer::texture::UniformTexture;
/// # use pathtracer::Point;
/// #
/// let obj = Object::new(
/// Sphere::new(Point::origin(), 1.0).into(),
/// UniformMaterial::new(
/// LightProperties::new(
/// LinearColor::new(1.0, 0.0, 0.0), // diffuse component
/// LinearColor::new(0.0, 0.0, 0.0), // specular component
/// None,
/// ),
/// ).into(),
/// UniformTexture::new(LinearColor::new(0.5, 0.5, 0.5)).into(),
/// );
/// ```
pub fn new(shape: ShapeEnum, material: MaterialEnum, texture: TextureEnum) -> Self {
Object {
shape,
material,
texture,
index: 0,
}
}
}
@ -30,14 +60,15 @@ impl Bounded for Object {
fn aabb(&self) -> AABB {
self.shape.aabb()
}
}
impl BHShape for Object {
fn set_bh_node_index(&mut self, index: usize) {
self.index = index
}
fn bh_node_index(&self) -> usize {
self.index
fn centroid(&self) -> Point {
self.shape.centroid()
}
}
impl Intersected for Object {
fn intersect(&self, ray: &Ray) -> Option<f32> {
self.shape.intersect(ray)
}
}
@ -49,7 +80,6 @@ mod test {
use crate::material::UniformMaterial;
use crate::shape::Sphere;
use crate::texture::UniformTexture;
use crate::Point;
fn simple_object() -> Object {
let shape = Sphere::new(Point::new(5., 0., 0.), 1.);
@ -77,7 +107,6 @@ mod test {
shape: shape.into(),
material: material.into(),
texture: texture.into(),
index: 0,
}
)
}

View file

@ -1,4 +1,6 @@
use super::{light_aggregate::LightAggregate, object::Object};
//! Scene rendering logic
use super::{light_aggregate::LightAggregate, object::Object, utils::*};
use crate::{
core::{Camera, LightProperties, LinearColor, ReflTransEnum},
material::Material,
@ -6,8 +8,9 @@ use crate::{
texture::Texture,
{Point, Vector},
};
use bvh::{bvh::BVH, ray::Ray};
use beevee::{bvh::BVH, ray::Ray};
use image::RgbImage;
use nalgebra::Unit;
use rand::prelude::thread_rng;
use rand::Rng;
use serde::{Deserialize, Deserializer};
@ -18,26 +21,64 @@ pub struct Scene {
lights: LightAggregate,
objects: Vec<Object>,
bvh: BVH,
background: LinearColor,
aliasing_limit: u32,
reflection_limit: u32,
diffraction_index: f32,
}
impl Scene {
/// Creates a new `Scene`.
///
/// # Examples
///
/// ```
/// # use pathtracer::core::{Camera, LightProperties, LinearColor};
/// # use pathtracer::material::UniformMaterial;
/// # use pathtracer::render::{LightAggregate, Object, Scene};
/// # use pathtracer::shape::Sphere;
/// # use pathtracer::texture::UniformTexture;
/// # use pathtracer::Point;
/// #
/// let scene = Scene::new(
/// Camera::default(),
/// LightAggregate::empty(),
/// vec![
/// Object::new(
/// Sphere::new(Point::origin(), 1.0).into(),
/// UniformMaterial::new(
/// LightProperties::new(
/// LinearColor::new(1.0, 0.0, 0.0), // diffuse component
/// LinearColor::new(0.0, 0.0, 0.0), // specular component
/// None,
/// ),
/// ).into(),
/// UniformTexture::new(LinearColor::new(0.5, 0.5, 0.5)).into(),
/// ),
/// ],
/// LinearColor::black(), // Background color
/// 5, // aliasing limit
/// 3, // reflection recursion limit
/// 0.0, // diffraction index
/// );
/// ```
pub fn new(
camera: Camera,
lights: LightAggregate,
mut objects: Vec<Object>,
background: LinearColor,
aliasing_limit: u32,
reflection_limit: u32,
diffraction_index: f32,
) -> Self {
// NOTE(Antoine): fun fact: BVH::build stack overflows when given an empty slice :)
let bvh = BVH::build(&mut objects);
Scene {
camera,
lights,
objects,
bvh,
background,
aliasing_limit,
reflection_limit,
diffraction_index,
@ -82,17 +123,20 @@ impl Scene {
fn pixel(&self, x: f32, y: f32) -> LinearColor {
let (x, y) = self.camera.film().pixel_ratio(x, y);
let pixel = self.camera.film().pixel_at_ratio(x, y);
let direction = (pixel - self.camera.origin()).normalize();
self.cast_ray(Ray::new(pixel, direction))
.map_or_else(LinearColor::black, |(t, obj)| {
let direction = Unit::new_normalize(pixel - self.camera.origin());
let indices = RefractionInfo::with_index(self.diffraction_index);
self.cast_ray(Ray::new(pixel, direction)).map_or_else(
|| self.background.clone(),
|(t, obj)| {
self.color_at(
pixel + direction * t,
pixel + direction.as_ref() * t,
obj,
direction,
self.reflection_limit,
self.diffraction_index,
indices,
)
})
},
)
}
/// Get pixel color with anti-aliasing
@ -111,64 +155,46 @@ impl Scene {
}
fn cast_ray(&self, ray: Ray) -> Option<(f32, &Object)> {
// NOTE(Bruno): should be written using iterators
let mut shot_obj: Option<&Object> = None;
let mut t = std::f32::INFINITY;
// NOTE: we don't care about all objects... Only the closest one
for object in self.bvh.traverse(&ray, &self.objects).iter() {
match object.shape.intersect(&ray) {
Some(dist) if dist < t => {
t = dist;
shot_obj = Some(&object);
}
_ => {}
}
}
shot_obj.map(|obj| (t, obj))
self.bvh.walk(&ray, &self.objects)
}
fn color_at(
&self,
point: Point,
object: &Object,
incident_ray: Vector,
incident_ray: Unit<Vector>,
reflection_limit: u32,
diffraction_index: f32,
mut indices: RefractionInfo,
) -> LinearColor {
let texel = object.shape.project_texel(&point);
let properties = object.material.properties(texel);
let object_color = object.texture.texel_color(texel);
let normal = object.shape.normal(&point);
let reflected = reflected(incident_ray, normal);
let reflected_ray = reflected(incident_ray, normal);
let lighting = self.illuminate(point, object_color, &properties, normal, reflected);
match properties.refl_trans {
None => lighting,
Some(ReflTransEnum::Transparency { coef, index }) => {
// Calculate the refracted ray, if it was refracted
refracted(incident_ray, normal, diffraction_index, index).map_or_else(
let lighting = self.illuminate(point, object_color, &properties, normal, reflected_ray);
if properties.refl_trans.is_none() {
// Avoid calculating reflection when not needed
return lighting;
}
let reflected = self.reflection(point, reflected_ray, reflection_limit, indices.clone());
// We can unwrap safely thanks to the check for None before
match properties.refl_trans.unwrap() {
ReflTransEnum::Transparency { coef, index } => {
// Calculate the refracted ray, if it was refracted, and mutate indices accordingly
refracted(incident_ray, normal, &mut indices, index).map_or_else(
// Total reflection
|| self.reflection(point, 1., reflected, reflection_limit, diffraction_index),
|| reflected.clone(),
// Refraction (refracted ray, amount of *reflection*)
|(r, refl_t)| {
let refr_light = self.refraction(point, coef, r, reflection_limit, index)
* (1. - refl_t)
+ self.reflection(
point,
refl_t,
reflected,
reflection_limit,
diffraction_index,
) * refl_t;
let refracted = self.refraction(point, coef, r, reflection_limit, indices);
let refr_light = refracted * (1. - refl_t) + reflected.clone() * refl_t;
refr_light * coef + lighting * (1. - coef)
},
)
}
Some(ReflTransEnum::Reflectivity { coef }) => {
self.reflection(point, coef, reflected, reflection_limit, diffraction_index)
+ lighting * (1. - coef)
}
ReflTransEnum::Reflectivity { coef } => reflected * coef + lighting * (1. - coef),
}
}
@ -176,20 +202,20 @@ impl Scene {
&self,
point: Point,
transparency: f32,
refracted: Vector,
refracted: Unit<Vector>,
reflection_limit: u32,
new_index: f32,
indices: RefractionInfo,
) -> LinearColor {
if transparency > 1e-5 && reflection_limit > 0 {
let refraction_start = point + refracted * 0.001;
let refraction_start = point + refracted.as_ref() * 0.001;
if let Some((t, obj)) = self.cast_ray(Ray::new(refraction_start, refracted)) {
let resulting_position = refraction_start + refracted * t;
let resulting_position = refraction_start + refracted.as_ref() * t;
let refracted = self.color_at(
resulting_position,
obj,
refracted,
reflection_limit - 1,
new_index,
indices,
);
return refracted * transparency;
}
@ -200,23 +226,22 @@ impl Scene {
fn reflection(
&self,
point: Point,
reflectivity: f32,
reflected: Vector,
reflected: Unit<Vector>,
reflection_limit: u32,
diffraction_index: f32,
indices: RefractionInfo,
) -> LinearColor {
if reflectivity > 1e-5 && reflection_limit > 0 {
let reflection_start = point + reflected * 0.001;
if reflection_limit > 0 {
let reflection_start = point + reflected.as_ref() * 0.001;
if let Some((t, obj)) = self.cast_ray(Ray::new(reflection_start, reflected)) {
let resulting_position = reflection_start + reflected * t;
let resulting_position = reflection_start + reflected.as_ref() * t;
let color = self.color_at(
resulting_position,
obj,
reflected,
reflection_limit - 1,
diffraction_index,
indices,
);
return color * reflectivity;
return color;
}
};
LinearColor::black()
@ -227,8 +252,8 @@ impl Scene {
point: Point,
object_color: LinearColor,
properties: &LightProperties,
normal: Vector,
reflected: Vector,
normal: Unit<Vector>,
reflected: Unit<Vector>,
) -> LinearColor {
let ambient = self.illuminate_ambient(object_color.clone());
let spatial = self.illuminate_spatial(point, properties, normal, reflected);
@ -247,14 +272,14 @@ impl Scene {
&self,
point: Point,
properties: &LightProperties,
normal: Vector,
reflected: Vector,
normal: Unit<Vector>,
reflected: Unit<Vector>,
) -> LinearColor {
self.lights
.spatial_lights_iter()
.map(|light| {
let (direction, t) = light.to_source(&point);
let light_ray = Ray::new(point + 0.001 * direction, direction);
let light_ray = Ray::new(point + 0.001 * direction.as_ref(), direction);
match self.cast_ray(light_ray) {
// Take shadows into account
Some((obstacle_t, _)) if obstacle_t < t => return LinearColor::black(),
@ -270,31 +295,6 @@ impl Scene {
}
}
fn reflected(incident: Vector, normal: Vector) -> Vector {
let proj = incident.dot(&normal);
let delt = normal * (proj * 2.);
incident - delt
}
/// Returns None if the ray was totally reflected, Some(refracted_ray, reflected_amount) if not
fn refracted(incident: Vector, normal: Vector, n_1: f32, n_2: f32) -> Option<(Vector, f32)> {
let cos1 = incident.dot(&normal);
let normal = if cos1 < 0. { normal } else { -normal };
let eta = n_1 / n_2;
let k = 1. - eta * eta * (1. - cos1 * cos1);
if k < 0. {
return None;
}
let cos1 = cos1.abs();
let refracted = eta * incident + (eta * cos1 - f32::sqrt(k)) * normal;
let cos2 = -refracted.dot(&normal); // Take the negation because we're on the other side
let f_r = (n_2 * cos1 - n_1 * cos2) / (n_2 * cos1 + n_1 * cos2);
let f_t = (n_1 * cos2 - n_2 * cos1) / (n_1 * cos2 + n_2 * cos1);
let refl_t = (f_r * f_r + f_t * f_t) / 2.;
//Some((refracted, 0.))
Some((refracted, refl_t))
}
#[derive(Debug, PartialEq, Deserialize)]
struct SerializedScene {
camera: Camera,
@ -303,6 +303,8 @@ struct SerializedScene {
#[serde(default)]
objects: Vec<Object>,
#[serde(default)]
background: LinearColor,
#[serde(default)]
aliasing_limit: u32,
#[serde(default)]
reflection_limit: u32,
@ -316,6 +318,7 @@ impl From<SerializedScene> for Scene {
scene.camera,
scene.lights,
scene.objects,
scene.background,
scene.aliasing_limit,
scene.reflection_limit,
scene.starting_diffraction,
@ -343,4 +346,21 @@ mod test {
let _: Scene = serde_yaml::from_str(yaml).unwrap();
// FIXME: actually test the equality ?
}
#[test]
#[ignore] // stack overflow because of BVH :(
fn bvh_fails() {
use crate::core::Camera;
use crate::render::{LightAggregate, Scene};
let _scene = Scene::new(
Camera::default(),
LightAggregate::empty(),
Vec::new(), // Objects list
LinearColor::black(), // Background color
5, // aliasing limit
3, // reflection recursion limit
0.0, // diffraction index
);
}
}

View file

@ -0,0 +1,67 @@
use crate::Vector;
use nalgebra::Unit;
pub fn reflected(incident: Unit<Vector>, normal: Unit<Vector>) -> Unit<Vector> {
let proj = incident.dot(&normal);
let delt = normal.into_inner() * (proj * 2.);
Unit::new_normalize(incident.as_ref() - delt)
}
/// Returns None if the ray was totally reflected, Some(refracted_ray, reflected_amount) if not
pub fn refracted(
incident: Unit<Vector>,
normal: Unit<Vector>,
indices: &mut RefractionInfo,
new_index: f32,
) -> Option<(Unit<Vector>, f32)> {
let cos1 = incident.dot(&normal);
let normal = if cos1 < 0. {
// Entering object, change the medium
indices.enter_medium(new_index); // The old index is now in old_index
normal
} else {
// Exiting object, exit the medium
indices.exit_medium(); // We swapped the indices
-normal
};
let (n_1, n_2) = (indices.old_index, indices.new_index);
let eta = n_1 / n_2;
let k = 1. - eta * eta * (1. - cos1 * cos1);
if k < 0. {
return None;
}
let cos1 = cos1.abs();
let cos2 = k.sqrt();
let refracted = eta * incident.as_ref() + (eta * cos1 - cos2) * normal.as_ref();
let f_r = (n_2 * cos1 - n_1 * cos2) / (n_2 * cos1 + n_1 * cos2);
let f_t = (n_1 * cos2 - n_2 * cos1) / (n_1 * cos2 + n_2 * cos1);
let refl_t = (f_r * f_r + f_t * f_t) / 2.;
//Some((refracted, 0.))
Some((Unit::new_normalize(refracted), refl_t))
}
#[derive(Debug, PartialEq, Clone)]
pub struct RefractionInfo {
pub old_index: f32,
pub new_index: f32,
}
impl RefractionInfo {
pub fn with_index(index: f32) -> Self {
RefractionInfo {
old_index: index,
new_index: index,
}
}
pub fn enter_medium(&mut self, index: f32) {
*self = RefractionInfo {
old_index: self.new_index,
new_index: index,
}
}
pub fn exit_medium(&mut self) {
std::mem::swap(&mut self.old_index, &mut self.new_index)
}
}

View file

@ -0,0 +1,6 @@
//! Helper functions deserialize coefficients.
/// Returns the identity for a f32, i.e. 1.0.
pub fn default_identity() -> f32 {
1.
}

View file

@ -1,3 +1,5 @@
//! Helper functions to help scene (de)serialization
pub mod vector;
pub use vector::*;

View file

@ -0,0 +1,16 @@
//! Helper functions to deserialize `Vector` values.
use crate::Vector;
use nalgebra::Unit;
use serde::de::{Deserialize, Deserializer};
/// Deserialize a vector.
///
/// Needs a custom implementation to make sur the vector is normalized when deserialized.
pub fn vector_normalizer<'de, D>(deserializer: D) -> Result<Unit<Vector>, D::Error>
where
D: Deserializer<'de>,
{
let v: Vector = Deserialize::deserialize(deserializer)?;
Ok(Unit::new_normalize(v))
}

View file

@ -1,13 +1,18 @@
//! Various shape implementations
use super::{Point, Point2D, Vector};
use bvh::{
use beevee::{
aabb::{Bounded, AABB},
bvh::Intersected,
ray::Ray,
};
use nalgebra::Unit;
use serde::Deserialize;
/// All the existing `Shape` implementation.
#[serde(tag = "type")]
#[serde(rename_all = "lowercase")]
#[allow(missing_docs)]
#[enum_dispatch::enum_dispatch]
#[derive(Debug, PartialEq, Deserialize)]
pub enum ShapeEnum {
@ -21,21 +26,33 @@ pub trait Shape: std::fmt::Debug {
/// Return the distance at which the object intersects with the ray, or None if it does not.
fn intersect(&self, ray: &Ray) -> Option<f32>;
/// Return the unit vector corresponding to the normal at this point of the shape.
fn normal(&self, point: &Point) -> Vector;
fn normal(&self, point: &Point) -> Unit<Vector>;
/// Project the point from the shape's surface to its texel coordinates.
fn project_texel(&self, point: &Point) -> Point2D;
/// Enclose the `Shape` in an axi-aligned bounding-box.
fn aabb(&self) -> AABB;
/// Return the centroid of the shape.
fn centroid(&self) -> Point;
}
impl Bounded for dyn Shape {
fn aabb(&self) -> AABB {
self.aabb()
}
fn centroid(&self) -> Point {
self.centroid()
}
}
pub mod sphere;
impl Intersected for dyn Shape {
fn intersect(&self, ray: &Ray) -> Option<f32> {
self.intersect(ray)
}
}
mod sphere;
pub use sphere::*;
pub mod triangle;
mod triangle;
pub use triangle::*;

View file

@ -1,7 +1,8 @@
use super::Shape;
use crate::{Point, Point2D, Vector};
use bvh::aabb::AABB;
use bvh::ray::Ray;
use beevee::aabb::AABB;
use beevee::ray::Ray;
use nalgebra::Unit;
use serde::Deserialize;
/// Represent a sphere shape inside the scene.
@ -67,13 +68,13 @@ impl Shape for Sphere {
}
}
fn normal(&self, point: &Point) -> Vector {
fn normal(&self, point: &Point) -> Unit<Vector> {
let delt = if self.inverted {
self.center - point
} else {
point - self.center
};
delt.normalize()
Unit::new_normalize(delt)
}
fn project_texel(&self, point: &Point) -> Point2D {
@ -90,6 +91,10 @@ impl Shape for Sphere {
let max = self.center + delt;
AABB::with_bounds(min, max)
}
fn centroid(&self) -> Point {
self.center
}
}
#[cfg(test)]
@ -103,21 +108,30 @@ mod test {
#[test]
fn intersect_along_axis_works() {
let sphere = simple_sphere();
let ray = Ray::new(Point::new(-2., 0., 0.), Vector::new(1., 0., 0.));
let ray = Ray::new(
Point::new(-2., 0., 0.),
Unit::new_normalize(Vector::new(1., 0., 0.)),
);
assert_eq!(sphere.intersect(&ray), Some(1.))
}
#[test]
fn non_intersect_along_axis_works() {
let sphere = simple_sphere();
let ray = Ray::new(Point::new(-2., 0., 0.), Vector::new(-1., 0., 0.));
let ray = Ray::new(
Point::new(-2., 0., 0.),
Unit::new_normalize(Vector::new(-1., 0., 0.)),
);
assert_eq!(sphere.intersect(&ray), None)
}
#[test]
fn intersect_not_on_axis() {
let sphere = simple_sphere();
let ray = Ray::new(Point::new(1., 1., 1.), Vector::new(-1., -1., -1.));
let ray = Ray::new(
Point::new(1., 1., 1.),
Unit::new_normalize(Vector::new(-1., -1., -1.)),
);
assert_eq!(sphere.intersect(&ray), Some(f32::sqrt(3.) - 1.))
}
@ -126,7 +140,7 @@ mod test {
let sphere = simple_sphere();
assert_eq!(
sphere.normal(&Point::new(-1., 0., 0.)),
Vector::new(-1., 0., 0.)
Unit::new_normalize(Vector::new(-1., 0., 0.))
)
}
@ -135,7 +149,7 @@ mod test {
let sphere = Sphere::inverted_new(Point::origin(), 1.);
assert_eq!(
sphere.normal(&Point::new(-1., 0., 0.)),
Vector::new(1., 0., 0.)
Unit::new_normalize(Vector::new(1., 0., 0.))
)
}

View file

@ -1,7 +1,8 @@
use super::Shape;
use crate::{Point, Point2D, Vector};
use bvh::aabb::AABB;
use bvh::ray::Ray;
use beevee::aabb::AABB;
use beevee::ray::Ray;
use nalgebra::Unit;
use serde::{Deserialize, Deserializer};
/// Represent a triangle inside the scene.
@ -13,6 +14,22 @@ pub struct Triangle {
}
impl Triangle {
/// Creates a new `Triangle` from 3 [`Point`]s.
///
/// [`Point`]: ../../type.Point.html
///
/// # Examples
///
/// ```
/// # use pathtracer::shape::Triangle;
/// # use pathtracer::Point;
/// #
/// let t = Triangle::new(
/// Point::new(1.0, 0.0, 0.0),
/// Point::new(0.0, 1.0, 0.0),
/// Point::new(0.0, 0.0, 1.0),
/// );
/// ```
pub fn new(c0: Point, c1: Point, c2: Point) -> Self {
Triangle {
c0,
@ -72,8 +89,8 @@ impl Shape for Triangle {
}
}
fn normal(&self, _: &Point) -> Vector {
self.c0c1.cross(&self.c0c2).normalize()
fn normal(&self, _: &Point) -> Unit<Vector> {
Unit::new_normalize(self.c0c1.cross(&self.c0c2))
}
fn project_texel(&self, point: &Point) -> Point2D {
@ -86,6 +103,10 @@ impl Shape for Triangle {
.grow(&(self.c0 + self.c0c1))
.grow(&(self.c0 + self.c0c2))
}
fn centroid(&self) -> Point {
self.c0 + (self.c0c1 + self.c0c2) / 2.
}
}
#[derive(Debug, Deserialize)]
@ -116,6 +137,7 @@ impl<'de> Deserialize<'de> for Triangle {
#[cfg(test)]
mod test {
use super::*;
use nalgebra::Unit;
fn simple_triangle() -> Triangle {
Triangle::new(
@ -130,7 +152,7 @@ mod test {
let triangle = simple_triangle();
let ans = triangle.intersect(&Ray::new(
Point::new(-1., 0.5, 0.5),
Vector::new(1., 0., 0.),
Unit::new_normalize(Vector::new(1., 0., 0.)),
));
assert_eq!(ans, Some(1.0))
}
@ -140,7 +162,7 @@ mod test {
let triangle = simple_triangle();
let ans = triangle.intersect(&Ray::new(
Point::new(-1., 0.5, 0.),
Vector::new(1., 0., 0.5),
Unit::new_normalize(Vector::new(1., 0., 0.5)),
));
assert!(ans.is_some());
assert!((ans.unwrap() - f32::sqrt(1.0 + 0.25)).abs() < 1e-5)
@ -149,7 +171,10 @@ mod test {
#[test]
fn intersect_out_of_bounds_is_none() {
let triangle = simple_triangle();
let ans = triangle.intersect(&Ray::new(Point::new(-1., 0.5, 0.), Vector::new(1., 1., 1.)));
let ans = triangle.intersect(&Ray::new(
Point::new(-1., 0.5, 0.),
Unit::new_normalize(Vector::new(1., 1., 1.)),
));
assert_eq!(ans, None)
}
@ -157,7 +182,7 @@ mod test {
fn normal_works() {
let triangle = simple_triangle();
let normal = triangle.normal(&Point::origin());
assert_eq!(normal, Vector::new(-1., 0., 0.));
assert_eq!(normal, Unit::new_normalize(Vector::new(-1., 0., 0.)));
}
#[test]

View file

@ -1,3 +1,5 @@
//! Various texture implementations
use super::core::LinearColor;
use super::Point2D;
use serde::Deserialize;
@ -5,6 +7,7 @@ use serde::Deserialize;
/// All the existing `Texture` implementation.
#[serde(tag = "type")]
#[serde(rename_all = "lowercase")]
#[allow(missing_docs)]
#[enum_dispatch::enum_dispatch]
#[derive(Debug, PartialEq, Deserialize)]
pub enum TextureEnum {
@ -19,5 +22,5 @@ pub trait Texture: std::fmt::Debug {
fn texel_color(&self, point: Point2D) -> LinearColor;
}
pub mod uniform;
mod uniform;
pub use uniform::*;

View file

@ -10,6 +10,16 @@ pub struct UniformTexture {
}
impl UniformTexture {
/// Creates a new `UniformTexture`.
///
/// # Examples
///
/// ```
/// # use pathtracer::texture::UniformTexture;
/// # use pathtracer::core::LinearColor;
/// #
/// let uni_text = UniformTexture::new(LinearColor::new(0.5, 0.5, 0.5));
/// ```
pub fn new(color: LinearColor) -> Self {
UniformTexture { color }
}

View file

@ -1,13 +0,0 @@
use bvh::nalgebra::{Point2, Point3, Vector3};
pub type Point2D = Point2<f32>;
pub type Point = Point3<f32>;
pub type Vector = Vector3<f32>;
pub mod core;
pub mod light;
pub mod material;
pub mod render;
pub mod serialize;
pub mod shape;
pub mod texture;

View file

@ -1,3 +0,0 @@
pub fn default_identity() -> f32 {
1.
}

View file

@ -1,10 +0,0 @@
use crate::Vector;
use serde::de::{Deserialize, Deserializer};
pub fn vector_normalizer<'de, D>(deserializer: D) -> Result<Vector, D::Error>
where
D: Deserializer<'de>,
{
let v: Vector = Deserialize::deserialize(deserializer)?;
Ok(v.normalize())
}