project: move to Cargo workspace
This commit is contained in:
parent
b5835b2726
commit
01d2c2d973
32 changed files with 42 additions and 37 deletions
38
pathtracer/Cargo.toml
Normal file
38
pathtracer/Cargo.toml
Normal 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]
|
||||
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"]
|
||||
83
pathtracer/examples/colorful.yaml
Normal file
83
pathtracer/examples/colorful.yaml
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
# Optional field
|
||||
aliasing_limit: 10
|
||||
# Optional field
|
||||
reflection_limit: 5
|
||||
|
||||
camera:
|
||||
origin: [0.0, 0.0, 0.0]
|
||||
forward: [ 1.0, 0.0, 0.0]
|
||||
up: [0.0, 1.0, 0.0]
|
||||
fov: 90.0
|
||||
distance_to_image: 1.0
|
||||
x: 1920
|
||||
y: 1080
|
||||
|
||||
# Optional field, each key itself being optional
|
||||
lights:
|
||||
ambients:
|
||||
- color: {r: 0.05, g: 0.05, b: 0.05}
|
||||
directionals:
|
||||
- direction: [0.5, 0.5, 0.5]
|
||||
color: {r: 0.0, g: 0.5, b: 0.0}
|
||||
- direction: [0.5, 0.5, -0.5]
|
||||
color: {r: 0.0, g: 0.0, b: 0.5}
|
||||
- direction: [0.7, -0.5, 0.0]
|
||||
color: {r: 0.5, g: 0.0, b: 0.0}
|
||||
points:
|
||||
- position: [0.0, 0.0, 0.0]
|
||||
color: {r: 0.2, g: 0.2, b: 0.2}
|
||||
spots:
|
||||
- position: [0.0, 0.0, 0.0]
|
||||
direction: [1.0, 0.0, 0.0]
|
||||
fov: 5.0
|
||||
color: {r: 1.0, g: 1.0, b: 0.0}
|
||||
|
||||
objects:
|
||||
- shape:
|
||||
type: sphere
|
||||
center: [4.5, 0.0, 0.0]
|
||||
radius: 0.4
|
||||
material:
|
||||
type: uniform
|
||||
diffuse:
|
||||
r: 0.0
|
||||
g: 0.0
|
||||
b: 0.0
|
||||
specular:
|
||||
r: 1.0
|
||||
g: 1.0
|
||||
b: 1.0
|
||||
# Optional fields (go together)
|
||||
#transparency: 0.5
|
||||
#index: 1.5
|
||||
texture:
|
||||
type: uniform
|
||||
color:
|
||||
r: 1.0
|
||||
g: 1.0
|
||||
b: 1.0
|
||||
|
||||
- shape:
|
||||
type: sphere
|
||||
# Optional field
|
||||
# inverted: false
|
||||
center: [10.0, 0.0, 0.0]
|
||||
radius: 5.0
|
||||
material:
|
||||
type: uniform
|
||||
diffuse:
|
||||
r: 1.0
|
||||
g: 1.0
|
||||
b: 1.0
|
||||
specular:
|
||||
r: 1.0
|
||||
g: 1.0
|
||||
b: 1.0
|
||||
# Optional field
|
||||
#reflectivity: 0.0
|
||||
texture:
|
||||
type: uniform
|
||||
color:
|
||||
r: 1.0
|
||||
g: 1.0
|
||||
b: 1.0
|
||||
40
pathtracer/examples/scene.yaml
Normal file
40
pathtracer/examples/scene.yaml
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
aliasing_limit: 10
|
||||
reflection_limit: 5
|
||||
|
||||
camera:
|
||||
origin: [-1.0, 0.0, 0.0]
|
||||
forward: [ 1.0, 0.0, 0.0]
|
||||
up: [0.0, 1.0, 0.0]
|
||||
fov: 90.0
|
||||
distance_to_image: 1.0
|
||||
x: 1080
|
||||
y: 1080
|
||||
|
||||
lights:
|
||||
ambients:
|
||||
- color: {r: 1.0, g: 0.5, b: 0.2}
|
||||
directionals:
|
||||
- direction: [1.0, 0.0, 0.0]
|
||||
color: {r: 1.0, g: 0.5, b: 0.2}
|
||||
points:
|
||||
- position: [1.0, 1.0, 1.0]
|
||||
color: {r: 1.0, g: 0.5, b: 0.2}
|
||||
spots:
|
||||
- position: [0.0, 0.0, 0.0]
|
||||
direction: [1.0, 0.0, 0.0]
|
||||
fov: 90.0
|
||||
color: {r: 1.0, g: 0.5, b: 0.2}
|
||||
|
||||
objects:
|
||||
- shape:
|
||||
type: sphere
|
||||
inverted: false
|
||||
center: [5., 0.0, 0.0]
|
||||
radius: 1.0
|
||||
material:
|
||||
type: uniform
|
||||
diffuse: {r: 0.5, g: 0.5, b: 0.5}
|
||||
specular: {r: 1., g: 1., b: 1.}
|
||||
texture:
|
||||
type: uniform
|
||||
color: {r: 0.25, g: 0.5, b: 1.}
|
||||
91
pathtracer/examples/triangles.yaml
Normal file
91
pathtracer/examples/triangles.yaml
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
# Optional field
|
||||
reflection_limit: 5
|
||||
|
||||
camera:
|
||||
origin: [0.0, 0.0, 0.0]
|
||||
forward: [ 1.0, 0.0, 0.0]
|
||||
up: [0.0, 1.0, 0.0]
|
||||
fov: 90.0
|
||||
distance_to_image: 1.0
|
||||
x: 1080
|
||||
y: 1080
|
||||
|
||||
lights:
|
||||
directionals:
|
||||
- direction: [0.5, 0.5, 0.5]
|
||||
color: {r: 0.0, g: 0.5, b: 0.0}
|
||||
- direction: [0.5, 0.5, -0.5]
|
||||
color: {r: 0.0, g: 0.0, b: 0.5}
|
||||
- direction: [0.7, -0.5, 0.0]
|
||||
color: {r: 0.5, g: 0.0, b: 0.0}
|
||||
|
||||
objects:
|
||||
- shape:
|
||||
type: sphere
|
||||
center: [5.0, -0.2, 0.2]
|
||||
radius: 1.0
|
||||
material:
|
||||
type: uniform
|
||||
diffuse:
|
||||
r: 1.0
|
||||
g: 1.0
|
||||
b: 1.0
|
||||
specular:
|
||||
r: 1.0
|
||||
g: 1.0
|
||||
b: 1.0
|
||||
transparency: 1.0
|
||||
index: 1.5
|
||||
texture:
|
||||
type: uniform
|
||||
color:
|
||||
r: 1.0
|
||||
g: 1.0
|
||||
b: 1.0
|
||||
|
||||
|
||||
- shape:
|
||||
type: triangle
|
||||
corners:
|
||||
- [10., -10., -10.]
|
||||
- [10., 10., 10.]
|
||||
- [10., 10., -10.]
|
||||
material:
|
||||
type: uniform
|
||||
diffuse:
|
||||
r: 1.0
|
||||
g: 1.0
|
||||
b: 1.0
|
||||
specular:
|
||||
r: 1.0
|
||||
g: 1.0
|
||||
b: 1.0
|
||||
texture:
|
||||
type: uniform
|
||||
color:
|
||||
r: 1.0
|
||||
g: 1.0
|
||||
b: 0.0
|
||||
|
||||
- shape:
|
||||
type: triangle
|
||||
corners:
|
||||
- [10., -10., -10.]
|
||||
- [10., -10., 10.]
|
||||
- [10., 10., 10.]
|
||||
material:
|
||||
type: uniform
|
||||
diffuse:
|
||||
r: 1.0
|
||||
g: 1.0
|
||||
b: 1.0
|
||||
specular:
|
||||
r: 1.0
|
||||
g: 1.0
|
||||
b: 1.0
|
||||
texture:
|
||||
type: uniform
|
||||
color:
|
||||
r: 0.5
|
||||
g: 1.0
|
||||
b: 0.5
|
||||
211
pathtracer/src/core/camera.rs
Normal file
211
pathtracer/src/core/camera.rs
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
//! Camera related logic
|
||||
|
||||
use super::film::Film;
|
||||
use crate::{Point, Vector};
|
||||
use serde::{Deserialize, Deserializer};
|
||||
|
||||
/// Represent an abstract camera to observe the scene.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct Camera {
|
||||
/// Where the camera is set in the scene (i.e: its focal point).
|
||||
origin: Point,
|
||||
/// The film to represent each pixel in the scene.
|
||||
film: Film,
|
||||
}
|
||||
|
||||
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,
|
||||
up: Vector,
|
||||
fov: f32,
|
||||
dist_to_image: f32,
|
||||
x: u32,
|
||||
y: u32,
|
||||
) -> Self {
|
||||
let right = forward.cross(&up);
|
||||
let center = origin + forward.normalize() * dist_to_image;
|
||||
let screen_size = 2. * f32::tan(fov / 2.) * dist_to_image;
|
||||
let film = Film::new(x, y, screen_size, center, up, right);
|
||||
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,
|
||||
forward: Vector,
|
||||
up: Vector,
|
||||
fov: f32,
|
||||
distance_to_image: f32,
|
||||
x: u32,
|
||||
y: u32,
|
||||
}
|
||||
|
||||
impl From<SerializedCamera> for Camera {
|
||||
fn from(cam: SerializedCamera) -> Self {
|
||||
Camera::new(
|
||||
cam.origin,
|
||||
cam.forward,
|
||||
cam.up,
|
||||
std::f32::consts::PI * cam.fov / 180.,
|
||||
cam.distance_to_image,
|
||||
cam.x,
|
||||
cam.y,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Camera {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let cam: SerializedCamera = Deserialize::deserialize(deserializer)?;
|
||||
Ok(cam.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn new_works() {
|
||||
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,
|
||||
);
|
||||
assert_eq!(
|
||||
cam,
|
||||
Camera {
|
||||
origin: Point::new(-1., 0., 0.),
|
||||
film: Film::new(
|
||||
1080,
|
||||
1080,
|
||||
2.,
|
||||
Point::origin(),
|
||||
Vector::new(0., 1., 0.),
|
||||
Vector::new(0., 0., 1.),
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialization_works() {
|
||||
let yaml = r#"
|
||||
origin: [-1.0, 0.0, 0.0]
|
||||
forward: [ 1.0, 0.0, 0.0]
|
||||
up: [0.0, 1.0, 0.0]
|
||||
fov: 90.0
|
||||
distance_to_image: 1.0
|
||||
x: 1080
|
||||
y: 1080
|
||||
"#;
|
||||
let cam: Camera = serde_yaml::from_str(yaml).unwrap();
|
||||
assert_eq!(
|
||||
cam,
|
||||
Camera {
|
||||
origin: Point::new(-1., 0., 0.),
|
||||
film: Film::new(
|
||||
1080,
|
||||
1080,
|
||||
2.,
|
||||
Point::origin(),
|
||||
Vector::new(0., 1., 0.),
|
||||
Vector::new(0., 0., 1.),
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
350
pathtracer/src/core/color.rs
Normal file
350
pathtracer/src/core/color.rs
Normal file
|
|
@ -0,0 +1,350 @@
|
|||
//! 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};
|
||||
|
||||
#[derive(
|
||||
Debug,
|
||||
Clone,
|
||||
PartialEq,
|
||||
Add,
|
||||
AddAssign,
|
||||
Div,
|
||||
DivAssign,
|
||||
Mul,
|
||||
MulAssign,
|
||||
Sub,
|
||||
SubAssign,
|
||||
Sum,
|
||||
Deserialize,
|
||||
)]
|
||||
/// 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.,
|
||||
g: 0.,
|
||||
b: 0.,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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. {
|
||||
1.
|
||||
} else if v < 0. {
|
||||
0.
|
||||
} else {
|
||||
v
|
||||
}
|
||||
};
|
||||
LinearColor::new(clamp(self.r), clamp(self.g), clamp(self.b))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for LinearColor {
|
||||
fn default() -> Self {
|
||||
Self::black()
|
||||
}
|
||||
}
|
||||
|
||||
impl Mul for LinearColor {
|
||||
type Output = LinearColor;
|
||||
|
||||
fn mul(self, other: Self) -> Self::Output {
|
||||
LinearColor {
|
||||
r: self.r * other.r,
|
||||
g: self.g * other.g,
|
||||
b: self.b * other.b,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MulAssign for LinearColor {
|
||||
fn mul_assign(&mut self, other: Self) {
|
||||
*self = self.clone() * other
|
||||
}
|
||||
}
|
||||
|
||||
impl Div for LinearColor {
|
||||
type Output = LinearColor;
|
||||
|
||||
fn div(self, other: Self) -> Self::Output {
|
||||
LinearColor {
|
||||
r: self.r / other.r,
|
||||
g: self.g / other.g,
|
||||
b: self.b / other.b,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DivAssign for LinearColor {
|
||||
fn div_assign(&mut self, other: Self) {
|
||||
*self = self.clone() / other
|
||||
}
|
||||
}
|
||||
|
||||
impl From<LinearColor> for image::Rgb<u8> {
|
||||
fn from(mut color: LinearColor) -> Self {
|
||||
color = color.clamp();
|
||||
image::Rgb([
|
||||
(color.r * 255.) as u8,
|
||||
(color.g * 255.) as u8,
|
||||
(color.b * 255.) as u8,
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn default_is_black() {
|
||||
assert_eq!(<LinearColor as Default>::default(), LinearColor::black())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn red_is_red() {
|
||||
let red = LinearColor::new(1., 0., 0.);
|
||||
assert_eq!(
|
||||
red,
|
||||
LinearColor {
|
||||
r: 1.,
|
||||
g: 0.,
|
||||
b: 0.
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn green_is_green() {
|
||||
let green = LinearColor::new(0., 1., 0.);
|
||||
assert_eq!(
|
||||
green,
|
||||
LinearColor {
|
||||
r: 0.,
|
||||
g: 1.,
|
||||
b: 0.
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blue_is_blue() {
|
||||
let blue = LinearColor::new(0., 0., 1.);
|
||||
assert_eq!(
|
||||
blue,
|
||||
LinearColor {
|
||||
r: 0.,
|
||||
g: 0.,
|
||||
b: 1.
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mul_by_float_works() {
|
||||
let color = LinearColor::new(0.125, 0.25, 0.0625);
|
||||
assert_eq!(
|
||||
color * 4.,
|
||||
LinearColor {
|
||||
r: 0.5,
|
||||
g: 1.,
|
||||
b: 0.25,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn div_by_float_works() {
|
||||
let color = LinearColor::new(0.2, 0.4, 0.6);
|
||||
assert_eq!(
|
||||
color / 2.,
|
||||
LinearColor {
|
||||
r: 0.1,
|
||||
g: 0.2,
|
||||
b: 0.3,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mulassign_by_float_works() {
|
||||
let mut color = LinearColor::new(0.125, 0.25, 0.0625);
|
||||
color *= 4.;
|
||||
assert_eq!(
|
||||
color,
|
||||
LinearColor {
|
||||
r: 0.5,
|
||||
g: 1.,
|
||||
b: 0.25,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn divassign_by_float_works() {
|
||||
let mut color = LinearColor::new(0.2, 0.4, 0.6);
|
||||
color /= 2.;
|
||||
assert_eq!(
|
||||
color,
|
||||
LinearColor {
|
||||
r: 0.1,
|
||||
g: 0.2,
|
||||
b: 0.3,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mul_by_color_works() {
|
||||
let lhs = LinearColor::new(0.125, 0.25, 0.0625);
|
||||
let rhs = LinearColor::new(1.0, 0.5, 2.0);
|
||||
assert_eq!(lhs * rhs, LinearColor::new(0.125, 0.125, 0.125))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn div_by_color_works() {
|
||||
let lhs = LinearColor::new(1.0, 0.5, 0.25);
|
||||
let rhs = LinearColor::new(4.0, 2.0, 1.0);
|
||||
assert_eq!(lhs / rhs, LinearColor::new(0.25, 0.25, 0.25))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mulassign_by_color_works() {
|
||||
let mut lhs = LinearColor::new(0.125, 0.25, 0.0625);
|
||||
lhs *= LinearColor::new(1.0, 0.5, 2.0);
|
||||
assert_eq!(lhs, LinearColor::new(0.125, 0.125, 0.125))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn divassign_by_color_works() {
|
||||
let mut lhs = LinearColor::new(1.0, 0.5, 0.25);
|
||||
lhs /= LinearColor::new(4.0, 2.0, 1.0);
|
||||
assert_eq!(lhs, LinearColor::new(0.25, 0.25, 0.25))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_works() {
|
||||
let lhs = LinearColor::new(1., 0., 0.125);
|
||||
let rhs = LinearColor::new(0., 0.5, 0.25);
|
||||
assert_eq!(
|
||||
lhs + rhs,
|
||||
LinearColor {
|
||||
r: 1.,
|
||||
g: 0.5,
|
||||
b: 0.375,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sub_works() {
|
||||
let lhs = LinearColor::new(1., 0.5, 0.25);
|
||||
let rhs = LinearColor::new(0.5, 0.125, 0.25);
|
||||
assert_eq!(
|
||||
lhs - rhs,
|
||||
LinearColor {
|
||||
r: 0.5,
|
||||
g: 0.375,
|
||||
b: 0.,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn addassign_works() {
|
||||
let mut lhs = LinearColor::new(1., 0., 0.125);
|
||||
lhs += LinearColor::new(0., 0.5, 0.25);
|
||||
assert_eq!(
|
||||
lhs,
|
||||
LinearColor {
|
||||
r: 1.,
|
||||
g: 0.5,
|
||||
b: 0.375,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn subassign_works() {
|
||||
let mut lhs = LinearColor::new(1., 0.5, 0.25);
|
||||
lhs -= LinearColor::new(0.5, 0.125, 0.25);
|
||||
assert_eq!(
|
||||
lhs,
|
||||
LinearColor {
|
||||
r: 0.5,
|
||||
g: 0.375,
|
||||
b: 0.,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialization_works() {
|
||||
let yaml = "{r: 1.0, g: 0.5, b: 0.2}";
|
||||
let ans: LinearColor = serde_yaml::from_str(yaml).unwrap();
|
||||
assert_eq!(
|
||||
ans,
|
||||
LinearColor {
|
||||
r: 1.0,
|
||||
g: 0.5,
|
||||
b: 0.2
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
253
pathtracer/src/core/film.rs
Normal file
253
pathtracer/src/core/film.rs
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
//! Camera film logic
|
||||
|
||||
use crate::{Point, Vector};
|
||||
|
||||
/// Represent an abstract camera film, to know where each pixel is in space.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct Film {
|
||||
x: u32,
|
||||
y: u32,
|
||||
center: Point,
|
||||
ratio_up: Vector,
|
||||
ratio_right: Vector,
|
||||
}
|
||||
|
||||
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)
|
||||
} else {
|
||||
(screen_size * x as f32 / y as f32, screen_size)
|
||||
};
|
||||
Film {
|
||||
x,
|
||||
y,
|
||||
center,
|
||||
ratio_up: up.normalize() * y_size,
|
||||
ratio_right: right.normalize() * x_size,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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::*;
|
||||
|
||||
#[test]
|
||||
fn simple_new_works() {
|
||||
let film = Film::new(
|
||||
1080,
|
||||
1080,
|
||||
1.,
|
||||
Point::origin(),
|
||||
Vector::new(0., 1., 0.),
|
||||
Vector::new(0., 0., 1.),
|
||||
);
|
||||
assert_eq!(
|
||||
film,
|
||||
Film {
|
||||
x: 1080,
|
||||
y: 1080,
|
||||
center: Point::origin(),
|
||||
ratio_up: Vector::new(0., 1., 0.),
|
||||
ratio_right: Vector::new(0., 0., 1.),
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_with_smaller_x_works() {
|
||||
let film = Film::new(
|
||||
1080,
|
||||
1440,
|
||||
1.,
|
||||
Point::origin(),
|
||||
Vector::new(0., 1., 0.),
|
||||
Vector::new(0., 0., 1.),
|
||||
);
|
||||
assert_eq!(
|
||||
film,
|
||||
Film {
|
||||
x: 1080,
|
||||
y: 1440,
|
||||
center: Point::origin(),
|
||||
ratio_up: Vector::new(0., 1., 0.),
|
||||
ratio_right: Vector::new(0., 0., 0.75),
|
||||
}
|
||||
)
|
||||
}
|
||||
#[test]
|
||||
fn new_with_smaller_y_works() {
|
||||
let film = Film::new(
|
||||
1080,
|
||||
540,
|
||||
1.,
|
||||
Point::origin(),
|
||||
Vector::new(0., 1., 0.),
|
||||
Vector::new(0., 0., 1.),
|
||||
);
|
||||
assert_eq!(
|
||||
film,
|
||||
Film {
|
||||
x: 1080,
|
||||
y: 540,
|
||||
center: Point::origin(),
|
||||
ratio_up: Vector::new(0., 0.5, 0.),
|
||||
ratio_right: Vector::new(0., 0., 1.),
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fn simple_film() -> Film {
|
||||
Film::new(
|
||||
1080,
|
||||
1080,
|
||||
1.,
|
||||
Point::origin(),
|
||||
Vector::new(0., 1., 0.),
|
||||
Vector::new(0., 0., 1.),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pixel_ratio_works() {
|
||||
let film = simple_film();
|
||||
assert_eq!(film.pixel_ratio(0., 0.), (0., 0.));
|
||||
assert_eq!(film.pixel_ratio(1080., 1080.), (1., 1.));
|
||||
assert_eq!(film.pixel_ratio(1080., 540.), (1., 0.5));
|
||||
assert_eq!(film.pixel_ratio(540., 1080.), (0.5, 1.));
|
||||
assert_eq!(film.pixel_ratio(1080., 810.), (1., 0.75));
|
||||
assert_eq!(film.pixel_ratio(810., 1080.), (0.75, 1.))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pixel_at_ratio_works() {
|
||||
let film = simple_film();
|
||||
assert_eq!(film.pixel_at_ratio(0., 0.), Point::new(0., 0.5, -0.5));
|
||||
assert_eq!(film.pixel_at_ratio(1., 1.), Point::new(0., -0.5, 0.5));
|
||||
assert_eq!(film.pixel_at_ratio(1., 0.5), Point::new(0., 0., 0.5));
|
||||
assert_eq!(film.pixel_at_ratio(0.5, 1.), Point::new(0., -0.5, 0.));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pixel_at_coord_works() {
|
||||
let film = simple_film();
|
||||
assert_eq!(film.pixel_at_coord(0, 0), Point::new(0., 0.5, -0.5));
|
||||
assert_eq!(film.pixel_at_coord(1080, 1080), Point::new(0., -0.5, 0.5));
|
||||
assert_eq!(film.pixel_at_coord(1080, 540), Point::new(0., 0., 0.5));
|
||||
assert_eq!(film.pixel_at_coord(540, 1080), Point::new(0., -0.5, 0.));
|
||||
}
|
||||
}
|
||||
143
pathtracer/src/core/light_properties.rs
Normal file
143
pathtracer/src/core/light_properties.rs
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
//! 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")]
|
||||
coef: f32,
|
||||
/// The diffraction index.
|
||||
index: f32,
|
||||
},
|
||||
/// Reflectivity properties.
|
||||
Reflectivity {
|
||||
/// The reflectivity coefficient.
|
||||
#[serde(rename = "reflectivity")]
|
||||
coef: f32,
|
||||
},
|
||||
}
|
||||
|
||||
/// A structure holding all the physical proprerties relating to light at a point.
|
||||
#[derive(Debug, PartialEq, Clone, Deserialize)]
|
||||
pub struct LightProperties {
|
||||
/// The diffuse component.
|
||||
pub diffuse: LinearColor,
|
||||
/// The specular component.
|
||||
pub specular: LinearColor,
|
||||
/// The transparency or reflectivity properties.
|
||||
#[serde(flatten)]
|
||||
pub refl_trans: Option<ReflTransEnum>,
|
||||
}
|
||||
|
||||
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,
|
||||
refl_trans: Option<ReflTransEnum>,
|
||||
) -> Self {
|
||||
LightProperties {
|
||||
diffuse,
|
||||
specular,
|
||||
refl_trans,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn new_works() {
|
||||
let diffuse = LinearColor::new(0.25, 0.5, 1.);
|
||||
let specular = LinearColor::new(0.75, 0.375, 0.125);
|
||||
let refl_trans = Some(ReflTransEnum::Reflectivity { coef: 0.5 });
|
||||
let properties =
|
||||
LightProperties::new(diffuse.clone(), specular.clone(), refl_trans.clone());
|
||||
assert_eq!(
|
||||
properties,
|
||||
LightProperties {
|
||||
diffuse,
|
||||
specular,
|
||||
refl_trans,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialization_without_refl_trans_works() {
|
||||
let yaml = r#"
|
||||
diffuse: {r: 1.0, g: 0.5, b: 0.25}
|
||||
specular: {r: 0.25, g: 0.125, b: 0.75}
|
||||
"#;
|
||||
let properties: LightProperties = serde_yaml::from_str(yaml).unwrap();
|
||||
assert_eq!(
|
||||
properties,
|
||||
LightProperties::new(
|
||||
LinearColor::new(1., 0.5, 0.25),
|
||||
LinearColor::new(0.25, 0.125, 0.75),
|
||||
None
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialization_with_reflection_works() {
|
||||
let yaml = r#"
|
||||
diffuse: {r: 1.0, g: 0.5, b: 0.25}
|
||||
specular: {r: 0.25, g: 0.125, b: 0.75}
|
||||
transparency: 0.5
|
||||
index: 1.5
|
||||
"#;
|
||||
let properties: LightProperties = serde_yaml::from_str(yaml).unwrap();
|
||||
assert_eq!(
|
||||
properties,
|
||||
LightProperties::new(
|
||||
LinearColor::new(1., 0.5, 0.25),
|
||||
LinearColor::new(0.25, 0.125, 0.75),
|
||||
Some(ReflTransEnum::Transparency {
|
||||
coef: 0.5,
|
||||
index: 1.5
|
||||
})
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialization_with_transparency_works() {
|
||||
let yaml = r#"
|
||||
diffuse: {r: 1.0, g: 0.5, b: 0.25}
|
||||
specular: {r: 0.25, g: 0.125, b: 0.75}
|
||||
reflectivity: 0.25
|
||||
"#;
|
||||
let properties: LightProperties = serde_yaml::from_str(yaml).unwrap();
|
||||
assert_eq!(
|
||||
properties,
|
||||
LightProperties::new(
|
||||
LinearColor::new(1., 0.5, 0.25),
|
||||
LinearColor::new(0.25, 0.125, 0.75),
|
||||
Some(ReflTransEnum::Reflectivity { coef: 0.25 })
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
13
pathtracer/src/core/mod.rs
Normal file
13
pathtracer/src/core/mod.rs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
//! Core pathtracing pipeline elements
|
||||
|
||||
pub mod camera;
|
||||
pub use camera::*;
|
||||
|
||||
pub mod color;
|
||||
pub use color::*;
|
||||
|
||||
pub mod film;
|
||||
pub use film::*;
|
||||
|
||||
pub mod light_properties;
|
||||
pub use light_properties::*;
|
||||
20
pathtracer/src/lib.rs
Normal file
20
pathtracer/src/lib.rs
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
#![warn(missing_docs)]
|
||||
|
||||
//! A pathtracing crate
|
||||
|
||||
use bvh::nalgebra::{Point2, Point3, Vector3};
|
||||
|
||||
/// A 2D point coordinate
|
||||
pub type Point2D = Point2<f32>;
|
||||
/// A 3D point coordinate
|
||||
pub type Point = Point3<f32>;
|
||||
/// A 3D vector
|
||||
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;
|
||||
59
pathtracer/src/light/ambient_light.rs
Normal file
59
pathtracer/src/light/ambient_light.rs
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
use super::Light;
|
||||
use crate::core::LinearColor;
|
||||
use crate::Point;
|
||||
use serde::Deserialize;
|
||||
|
||||
/// Represent an ambient lighting which is equal in all points of the scene.
|
||||
#[derive(Debug, PartialEq, Deserialize)]
|
||||
pub struct AmbientLight {
|
||||
color: LinearColor,
|
||||
}
|
||||
|
||||
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 }
|
||||
}
|
||||
}
|
||||
|
||||
impl Light for AmbientLight {
|
||||
fn illumination(&self, _: &Point) -> LinearColor {
|
||||
self.color.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn new_works() {
|
||||
let color = LinearColor::new(1., 1., 1.);
|
||||
let light = AmbientLight::new(color.clone());
|
||||
let res = AmbientLight { color };
|
||||
assert_eq!(light, res)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn illumination_is_correct() {
|
||||
let light = AmbientLight::new(LinearColor::new(1., 1., 1.));
|
||||
let lum = light.illumination(&Point::new(1., 1., 1.));
|
||||
assert_eq!(lum, LinearColor::new(1., 1., 1.))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialization_works() {
|
||||
let yaml = "color: {r: 1.0, g: 0.5, b: 0.2}";
|
||||
let light: AmbientLight = serde_yaml::from_str(yaml).unwrap();
|
||||
assert_eq!(light, AmbientLight::new(LinearColor::new(1., 0.5, 0.2)))
|
||||
}
|
||||
}
|
||||
92
pathtracer/src/light/directional_light.rs
Normal file
92
pathtracer/src/light/directional_light.rs
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
use super::{Light, SpatialLight};
|
||||
use crate::core::LinearColor;
|
||||
use crate::{Point, Vector};
|
||||
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,
|
||||
color: LinearColor,
|
||||
}
|
||||
|
||||
impl DirectionalLight {
|
||||
/// Creates a new `DirectionalLight`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use pathtracer::light::DirectionalLight;
|
||||
/// # use pathtracer::core::color::LinearColor;
|
||||
/// # use pathtracer::Vector;
|
||||
/// #
|
||||
/// let dir_light = DirectionalLight::new(
|
||||
/// Vector::new(1.0, 0.0, 0.0),
|
||||
/// LinearColor::new(1.0, 0.0, 1.0),
|
||||
/// );
|
||||
/// ```
|
||||
pub fn new(direction: Vector, color: LinearColor) -> Self {
|
||||
DirectionalLight {
|
||||
direction: direction.normalize(),
|
||||
color,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Light for DirectionalLight {
|
||||
fn illumination(&self, _: &Point) -> LinearColor {
|
||||
self.color.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl SpatialLight for DirectionalLight {
|
||||
fn to_source(&self, _: &Point) -> (Vector, f32) {
|
||||
(self.direction * -1., std::f32::INFINITY)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn new_works() {
|
||||
let direction = Vector::new(1., 0., 0.);
|
||||
let color = LinearColor::new(1., 1., 1.);
|
||||
let light = DirectionalLight::new(direction, color.clone());
|
||||
let res = DirectionalLight { direction, color };
|
||||
assert_eq!(light, res)
|
||||
}
|
||||
|
||||
fn simple_light() -> impl SpatialLight {
|
||||
let direction = Vector::new(1., 0., 0.);
|
||||
let color = LinearColor::new(1., 1., 1.);
|
||||
DirectionalLight::new(direction, color)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn illumination_is_correct() {
|
||||
let light = simple_light();
|
||||
let lum = light.illumination(&Point::new(1., 1., 1.));
|
||||
assert_eq!(lum, LinearColor::new(1., 1., 1.))
|
||||
}
|
||||
|
||||
#[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);
|
||||
assert_eq!(ans, expected)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialization_works() {
|
||||
let yaml = "{direction: [1.0, 0.0, 0.0], color: {r: 1.0, g: 0.5, b: 0.2}}";
|
||||
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))
|
||||
)
|
||||
}
|
||||
}
|
||||
28
pathtracer/src/light/mod.rs
Normal file
28
pathtracer/src/light/mod.rs
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
//! Various light implementations
|
||||
|
||||
use super::core::LinearColor;
|
||||
use super::{Point, Vector};
|
||||
|
||||
/// Represent a light in the scene being rendered.
|
||||
pub trait Light: std::fmt::Debug {
|
||||
/// Get the illumination of that light on that point.
|
||||
fn illumination(&self, point: &Point) -> LinearColor;
|
||||
}
|
||||
|
||||
/// 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);
|
||||
}
|
||||
|
||||
mod ambient_light;
|
||||
pub use ambient_light::*;
|
||||
|
||||
mod directional_light;
|
||||
pub use directional_light::*;
|
||||
|
||||
mod point_light;
|
||||
pub use point_light::*;
|
||||
|
||||
mod spot_light;
|
||||
pub use spot_light::*;
|
||||
91
pathtracer/src/light/point_light.rs
Normal file
91
pathtracer/src/light/point_light.rs
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
use super::{Light, SpatialLight};
|
||||
use crate::core::LinearColor;
|
||||
use crate::{Point, Vector};
|
||||
use serde::Deserialize;
|
||||
|
||||
/// Represent a light emanating from a point in space, following the square distance law.
|
||||
#[derive(Debug, PartialEq, Deserialize)]
|
||||
pub struct PointLight {
|
||||
position: Point,
|
||||
color: LinearColor,
|
||||
}
|
||||
|
||||
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 }
|
||||
}
|
||||
}
|
||||
|
||||
impl Light for PointLight {
|
||||
fn illumination(&self, point: &Point) -> LinearColor {
|
||||
let dist = (self.position - point).norm();
|
||||
self.color.clone() / dist
|
||||
}
|
||||
}
|
||||
|
||||
impl SpatialLight for PointLight {
|
||||
fn to_source(&self, point: &Point) -> (Vector, f32) {
|
||||
let delt = self.position - point;
|
||||
let dist = delt.norm();
|
||||
(delt.normalize(), dist)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn new_works() {
|
||||
let position = Point::origin();
|
||||
let color = LinearColor::black();
|
||||
let light = PointLight::new(position, color.clone());
|
||||
let res = PointLight { position, color };
|
||||
assert_eq!(light, res)
|
||||
}
|
||||
|
||||
fn simple_light() -> impl SpatialLight {
|
||||
let position = Point::origin();
|
||||
let color = LinearColor::new(1., 1., 1.);
|
||||
PointLight::new(position, color)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn illumination_is_correct() {
|
||||
let light = simple_light();
|
||||
let lum = light.illumination(&Point::new(1., 0., 0.));
|
||||
assert_eq!(lum, LinearColor::new(1., 1., 1.))
|
||||
}
|
||||
|
||||
#[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.);
|
||||
assert_eq!(ans, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialization_works() {
|
||||
let yaml = "{position: [1.0, 1.0, 1.0], color: {r: 1.0, g: 0.5, b: 0.2}}";
|
||||
let light: PointLight = serde_yaml::from_str(yaml).unwrap();
|
||||
assert_eq!(
|
||||
light,
|
||||
PointLight::new(Point::new(1., 1., 1.), LinearColor::new(1., 0.5, 0.2))
|
||||
)
|
||||
}
|
||||
}
|
||||
207
pathtracer/src/light/spot_light.rs
Normal file
207
pathtracer/src/light/spot_light.rs
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
use super::{Light, SpatialLight};
|
||||
use crate::core::LinearColor;
|
||||
use crate::{Point, Vector};
|
||||
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,
|
||||
cosine_value: f32,
|
||||
color: LinearColor,
|
||||
}
|
||||
|
||||
impl SpotLight {
|
||||
/// Construct a SpotLight with the given FOV in radian.
|
||||
pub fn radians_new(
|
||||
position: Point,
|
||||
direction: Vector,
|
||||
fov_rad: f32,
|
||||
color: LinearColor,
|
||||
) -> Self {
|
||||
SpotLight {
|
||||
position,
|
||||
direction: direction.normalize(),
|
||||
cosine_value: (fov_rad / 2.).cos(),
|
||||
color,
|
||||
}
|
||||
}
|
||||
|
||||
/// Construct a SpotLight with the given FOV in degrees.
|
||||
pub fn degrees_new(
|
||||
position: Point,
|
||||
direction: Vector,
|
||||
fov_deg: f32,
|
||||
color: LinearColor,
|
||||
) -> Self {
|
||||
SpotLight::radians_new(
|
||||
position,
|
||||
direction,
|
||||
std::f32::consts::PI * fov_deg / 180.,
|
||||
color,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Light for SpotLight {
|
||||
fn illumination(&self, point: &Point) -> LinearColor {
|
||||
let delt = point - self.position;
|
||||
let cos = self.direction.dot(&delt.normalize());
|
||||
if cos >= self.cosine_value {
|
||||
self.color.clone() / delt.norm_squared()
|
||||
} else {
|
||||
LinearColor::black()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SpatialLight for SpotLight {
|
||||
fn to_source(&self, point: &Point) -> (Vector, f32) {
|
||||
let delt = self.position - point;
|
||||
let dist = delt.norm();
|
||||
(delt.normalize(), dist)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct SerializedSpotLight {
|
||||
position: Point,
|
||||
#[serde(deserialize_with = "crate::serialize::vector_normalizer")]
|
||||
direction: Vector,
|
||||
fov: f32,
|
||||
color: LinearColor,
|
||||
}
|
||||
|
||||
impl From<SerializedSpotLight> for SpotLight {
|
||||
fn from(light: SerializedSpotLight) -> Self {
|
||||
SpotLight::degrees_new(light.position, light.direction, light.fov, light.color)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for SpotLight {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let cam: SerializedSpotLight = Deserialize::deserialize(deserializer)?;
|
||||
Ok(cam.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn radian_new_works() {
|
||||
let light = SpotLight::radians_new(
|
||||
Point::origin(),
|
||||
Vector::new(1., 0., 0.),
|
||||
std::f32::consts::PI / 2.,
|
||||
LinearColor::new(1., 1., 1.),
|
||||
);
|
||||
// The FOV is 90°, therefore the angle to the direction is 45° [= PI / 4]
|
||||
let calculated_cosine_value = (std::f32::consts::PI / 4.).cos();
|
||||
assert_eq!(
|
||||
light,
|
||||
SpotLight {
|
||||
position: Point::origin(),
|
||||
direction: Vector::new(1., 0., 0.),
|
||||
cosine_value: calculated_cosine_value,
|
||||
color: LinearColor::new(1., 1., 1.),
|
||||
}
|
||||
);
|
||||
// Checking this way because of rounding issues...
|
||||
assert!((calculated_cosine_value - f32::sqrt(2.) / 2.).abs() < 1e-5)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn degrees_new_works() {
|
||||
let light = SpotLight::degrees_new(
|
||||
Point::origin(),
|
||||
Vector::new(1., 0., 0.),
|
||||
60.,
|
||||
LinearColor::new(1., 1., 1.),
|
||||
);
|
||||
let calculated_cosine_value = (std::f32::consts::PI * 60. / 360.).cos();
|
||||
assert_eq!(
|
||||
light,
|
||||
SpotLight {
|
||||
position: Point::origin(),
|
||||
direction: Vector::new(1., 0., 0.),
|
||||
cosine_value: calculated_cosine_value,
|
||||
color: LinearColor::new(1., 1., 1.),
|
||||
}
|
||||
);
|
||||
// Checking this way because of rounding issues...
|
||||
assert!((calculated_cosine_value - f32::sqrt(3.) / 2.).abs() < 1e-5)
|
||||
}
|
||||
|
||||
fn simple_light() -> impl SpatialLight {
|
||||
SpotLight::degrees_new(
|
||||
Point::origin(),
|
||||
Vector::new(1., 0., 0.),
|
||||
90.,
|
||||
LinearColor::new(1., 1., 1.),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn illumination_in_axis_works() {
|
||||
let light = simple_light();
|
||||
let lum = light.illumination(&Point::new(1., 0., 0.));
|
||||
assert_eq!(lum, LinearColor::new(1., 1., 1.))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn illumination_on_limit_works_1() {
|
||||
let light = simple_light();
|
||||
let lum = light.illumination(&Point::new(1., 1., 0.));
|
||||
assert_eq!(lum, LinearColor::new(0.5, 0.5, 0.5))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn illumination_on_limit_works_2() {
|
||||
let light = simple_light();
|
||||
let lum = light.illumination(&Point::new(1., 0., 1.));
|
||||
assert_eq!(lum, LinearColor::new(0.5, 0.5, 0.5))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn illumination_out_of_ray_works() {
|
||||
let light = simple_light();
|
||||
let lum = light.illumination(&Point::new(1., 1., 1.));
|
||||
assert_eq!(lum, LinearColor::new(0., 0., 0.))
|
||||
}
|
||||
|
||||
#[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.);
|
||||
assert_eq!(ans, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialization_works() {
|
||||
let yaml = r#"
|
||||
position: [0.0, 0.0, 0.0]
|
||||
direction: [1.0, 0.0, 0.0]
|
||||
fov: 90.0
|
||||
color: {r: 1.0, g: 0.5, b: 0.2}
|
||||
"#;
|
||||
let light: SpotLight = serde_yaml::from_str(yaml).unwrap();
|
||||
assert_eq!(
|
||||
light,
|
||||
SpotLight::degrees_new(
|
||||
Point::origin(),
|
||||
Vector::new(1., 0., 0.),
|
||||
90.,
|
||||
LinearColor::new(1., 0.5, 0.2)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
24
pathtracer/src/main.rs
Normal file
24
pathtracer/src/main.rs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
use pathtracer::render::Scene;
|
||||
use std::path::PathBuf;
|
||||
use structopt::StructOpt;
|
||||
|
||||
#[derive(StructOpt, Debug)]
|
||||
struct Options {
|
||||
/// Input description for the scene to be rendered.
|
||||
#[structopt(short, long, parse(from_os_str), default_value = "scene.yaml")]
|
||||
input: PathBuf,
|
||||
/// Output image for the rendered scene.
|
||||
#[structopt(short, long, parse(from_os_str), default_value = "scene.png")]
|
||||
output: PathBuf,
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let options = Options::from_args();
|
||||
let f = std::fs::File::open(options.input)?;
|
||||
|
||||
let scene: Scene = serde_yaml::from_reader(f)?;
|
||||
let image = scene.render();
|
||||
|
||||
image.save(options.output)?;
|
||||
Ok(())
|
||||
}
|
||||
26
pathtracer/src/material/mod.rs
Normal file
26
pathtracer/src/material/mod.rs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
//! Various material implementations
|
||||
|
||||
use super::core::LightProperties;
|
||||
use super::Point2D;
|
||||
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 {
|
||||
#[serde(rename = "uniform")]
|
||||
UniformMaterial,
|
||||
}
|
||||
|
||||
/// Represent the physical light properties of an object in the scene;
|
||||
#[enum_dispatch::enum_dispatch(MaterialEnum)]
|
||||
pub trait Material: std::fmt::Debug {
|
||||
/// Get the physical properties at a point.
|
||||
fn properties(&self, point: Point2D) -> LightProperties;
|
||||
}
|
||||
|
||||
mod uniform;
|
||||
pub use uniform::*;
|
||||
86
pathtracer/src/material/uniform.rs
Normal file
86
pathtracer/src/material/uniform.rs
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
use super::Material;
|
||||
use crate::core::LightProperties;
|
||||
use crate::Point2D;
|
||||
use serde::Deserialize;
|
||||
|
||||
/// A material with the same characteristics on all points.
|
||||
#[derive(Clone, Debug, PartialEq, Deserialize)]
|
||||
pub struct UniformMaterial {
|
||||
#[serde(flatten)]
|
||||
properties: LightProperties,
|
||||
}
|
||||
|
||||
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 }
|
||||
}
|
||||
}
|
||||
|
||||
impl Material for UniformMaterial {
|
||||
fn properties(&self, _: Point2D) -> LightProperties {
|
||||
self.properties.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::core::color::LinearColor;
|
||||
use crate::core::ReflTransEnum;
|
||||
|
||||
#[test]
|
||||
fn new_works() {
|
||||
let properties = LightProperties {
|
||||
diffuse: LinearColor::new(0., 0.5, 0.),
|
||||
specular: LinearColor::new(1., 1., 1.),
|
||||
refl_trans: None,
|
||||
};
|
||||
let mat = UniformMaterial::new(properties.clone());
|
||||
assert_eq!(mat, UniformMaterial { properties })
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn properties_works() {
|
||||
let properties = LightProperties::new(
|
||||
LinearColor::new(0., 0.5, 0.),
|
||||
LinearColor::new(1., 1., 1.),
|
||||
None,
|
||||
);
|
||||
let mat = UniformMaterial::new(properties.clone());
|
||||
assert_eq!(mat.properties(Point2D::origin()), properties)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialization_works() {
|
||||
let yaml = r#"
|
||||
diffuse: {r: 1.0, g: 0.5, b: 0.25}
|
||||
specular: {r: 0.25, g: 0.125, b: 0.75}
|
||||
reflectivity: 0.25
|
||||
"#;
|
||||
let material: UniformMaterial = serde_yaml::from_str(yaml).unwrap();
|
||||
assert_eq!(
|
||||
material,
|
||||
UniformMaterial::new(LightProperties::new(
|
||||
LinearColor::new(1., 0.5, 0.25),
|
||||
LinearColor::new(0.25, 0.125, 0.75),
|
||||
Some(ReflTransEnum::Reflectivity { coef: 0.25 })
|
||||
))
|
||||
)
|
||||
}
|
||||
}
|
||||
161
pathtracer/src/render/light_aggregate.rs
Normal file
161
pathtracer/src/render/light_aggregate.rs
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
//! 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>,
|
||||
#[serde(default)]
|
||||
directionals: Vec<DirectionalLight>,
|
||||
#[serde(default)]
|
||||
points: Vec<PointLight>,
|
||||
#[serde(default)]
|
||||
spots: Vec<SpotLight>,
|
||||
}
|
||||
|
||||
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>,
|
||||
points: Vec<PointLight>,
|
||||
spots: Vec<SpotLight>,
|
||||
) -> Self {
|
||||
LightAggregate {
|
||||
ambients,
|
||||
directionals,
|
||||
points,
|
||||
spots,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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()
|
||||
.map(|l| l as &dyn SpatialLight)
|
||||
.chain(self.points.iter().map(|l| l as &dyn SpatialLight))
|
||||
.chain(self.spots.iter().map(|l| l as &dyn SpatialLight))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for LightAggregate {
|
||||
fn default() -> Self {
|
||||
LightAggregate::empty()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn empty_works() {
|
||||
let lights = LightAggregate::empty();
|
||||
assert_eq!(
|
||||
lights,
|
||||
LightAggregate {
|
||||
ambients: vec![],
|
||||
directionals: vec![],
|
||||
points: vec![],
|
||||
spots: vec![],
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_is_empty() {
|
||||
let lights = <LightAggregate as Default>::default();
|
||||
assert_eq!(lights, LightAggregate::empty())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialization_works() {
|
||||
use crate::{core::LinearColor, Point, Vector};
|
||||
|
||||
let yaml = r#"
|
||||
ambients:
|
||||
- color: {r: 1.0, g: 0.5, b: 0.2}
|
||||
directionals:
|
||||
- direction: [1.0, 0.0, 0.0]
|
||||
color: {r: 1.0, g: 0.5, b: 0.2}
|
||||
points:
|
||||
- position: [1.0, 1.0, 1.0]
|
||||
color: {r: 1.0, g: 0.5, b: 0.2}
|
||||
spots:
|
||||
- position: [0.0, 0.0, 0.0]
|
||||
direction: [1.0, 0.0, 0.0]
|
||||
fov: 90.0
|
||||
color: {r: 1.0, g: 0.5, b: 0.2}
|
||||
"#;
|
||||
let expected = LightAggregate::new(
|
||||
vec![AmbientLight::new(LinearColor::new(1., 0.5, 0.2))],
|
||||
vec![DirectionalLight::new(
|
||||
Vector::new(1., 0., 0.),
|
||||
LinearColor::new(1., 0.5, 0.2),
|
||||
)],
|
||||
vec![PointLight::new(
|
||||
Point::new(1., 1., 1.),
|
||||
LinearColor::new(1., 0.5, 0.2),
|
||||
)],
|
||||
vec![SpotLight::degrees_new(
|
||||
Point::origin(),
|
||||
Vector::new(1., 0., 0.),
|
||||
90.,
|
||||
LinearColor::new(1., 0.5, 0.2),
|
||||
)],
|
||||
);
|
||||
let lights: LightAggregate = serde_yaml::from_str(yaml).unwrap();
|
||||
assert_eq!(lights, expected)
|
||||
}
|
||||
}
|
||||
12
pathtracer/src/render/mod.rs
Normal file
12
pathtracer/src/render/mod.rs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
//! Rendering logic
|
||||
|
||||
pub mod light_aggregate;
|
||||
pub use light_aggregate::*;
|
||||
|
||||
pub mod object;
|
||||
pub use object::*;
|
||||
|
||||
pub mod scene;
|
||||
pub use scene::*;
|
||||
|
||||
pub(crate) mod utils;
|
||||
135
pathtracer/src/render/object.rs
Normal file
135
pathtracer/src/render/object.rs
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
//! 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 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 inside the `BVH`
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::core::color::LinearColor;
|
||||
use crate::core::LightProperties;
|
||||
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.);
|
||||
let material = UniformMaterial::new(LightProperties::new(
|
||||
LinearColor::new(0.5, 0.5, 0.5),
|
||||
LinearColor::new(1., 1., 1.),
|
||||
None,
|
||||
));
|
||||
let texture = UniformTexture::new(LinearColor::new(0.25, 0.5, 1.));
|
||||
Object::new(shape.into(), material.into(), texture.into())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_works() {
|
||||
let shape = Sphere::new(Point::new(5., 0., 0.), 1.);
|
||||
let material = UniformMaterial::new(LightProperties::new(
|
||||
LinearColor::new(0.5, 0.5, 0.5),
|
||||
LinearColor::new(1., 1., 1.),
|
||||
None,
|
||||
));
|
||||
let texture = UniformTexture::new(LinearColor::new(0.25, 0.5, 1.));
|
||||
assert_eq!(
|
||||
simple_object(),
|
||||
Object {
|
||||
shape: shape.into(),
|
||||
material: material.into(),
|
||||
texture: texture.into(),
|
||||
index: 0,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialization_works() {
|
||||
let yaml = r#"
|
||||
shape:
|
||||
type: sphere
|
||||
inverted: false
|
||||
center: [5., 0.0, 0.0]
|
||||
radius: 1.0
|
||||
material:
|
||||
type: uniform
|
||||
diffuse: {r: 0.5, g: 0.5, b: 0.5}
|
||||
specular: {r: 1., g: 1., b: 1.}
|
||||
texture:
|
||||
type: uniform
|
||||
color: {r: 0.25, g: 0.5, b: 1.}
|
||||
"#;
|
||||
let object: Object = serde_yaml::from_str(yaml).unwrap();
|
||||
let expected = simple_object();
|
||||
assert_eq!(object, expected)
|
||||
}
|
||||
}
|
||||
363
pathtracer/src/render/scene.rs
Normal file
363
pathtracer/src/render/scene.rs
Normal file
|
|
@ -0,0 +1,363 @@
|
|||
//! Scene rendering logic
|
||||
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use super::{light_aggregate::LightAggregate, object::Object, utils::*};
|
||||
use crate::{
|
||||
core::{Camera, LightProperties, LinearColor, ReflTransEnum},
|
||||
material::Material,
|
||||
shape::Shape,
|
||||
texture::Texture,
|
||||
{Point, Vector},
|
||||
};
|
||||
use bvh::{bvh::BVH, ray::Ray};
|
||||
use image::RgbImage;
|
||||
use rand::prelude::thread_rng;
|
||||
use rand::Rng;
|
||||
use serde::{Deserialize, Deserializer};
|
||||
|
||||
/// Represent the scene being rendered.
|
||||
pub struct Scene {
|
||||
camera: Camera,
|
||||
lights: LightAggregate,
|
||||
objects: Vec<Object>,
|
||||
bvh: BVH,
|
||||
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(),
|
||||
/// ),
|
||||
/// ],
|
||||
/// 5, // aliasing limit
|
||||
/// 3, // reflection recursion limit
|
||||
/// 0.0, // diffraction index
|
||||
/// );
|
||||
/// ```
|
||||
pub fn new(
|
||||
camera: Camera,
|
||||
lights: LightAggregate,
|
||||
mut objects: Vec<Object>,
|
||||
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,
|
||||
aliasing_limit,
|
||||
reflection_limit,
|
||||
diffraction_index,
|
||||
}
|
||||
}
|
||||
|
||||
/// Render the scene into an image.
|
||||
pub fn render(&self) -> RgbImage {
|
||||
let mut image = RgbImage::new(self.camera.film().width(), self.camera.film().height());
|
||||
|
||||
let total = (image.width() * image.height()) as u64;
|
||||
let pb = indicatif::ProgressBar::new(total);
|
||||
pb.set_draw_delta(total / 10000);
|
||||
pb.set_style(indicatif::ProgressStyle::default_bar().template(
|
||||
"{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {percent:>3}%: {pos}/{len} pixels (ETA: {eta})",
|
||||
));
|
||||
|
||||
let pixel_func = if self.aliasing_limit > 0 {
|
||||
Self::anti_alias_pixel
|
||||
} else {
|
||||
Self::pixel
|
||||
};
|
||||
|
||||
rayon::scope(|s| {
|
||||
// FIXME(Bruno): it would go even faster to cut the image in blocks of rows, leading to
|
||||
// better cache-line behaviour...
|
||||
for (_, row) in image.enumerate_rows_mut() {
|
||||
s.spawn(|_| {
|
||||
for (x, y, pixel) in row {
|
||||
*pixel = pixel_func(&self, x as f32, y as f32).into();
|
||||
pb.inc(1);
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
pb.finish();
|
||||
image
|
||||
}
|
||||
|
||||
/// Get pixel color for (x, y) a pixel **coordinate**
|
||||
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();
|
||||
let indices = RefractionInfo::with_index(self.diffraction_index);
|
||||
self.cast_ray(Ray::new(pixel, direction))
|
||||
.map_or_else(LinearColor::black, |(t, obj)| {
|
||||
self.color_at(
|
||||
pixel + direction * t,
|
||||
obj,
|
||||
direction,
|
||||
self.reflection_limit,
|
||||
indices,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Get pixel color with anti-aliasing
|
||||
fn anti_alias_pixel(&self, x: f32, y: f32) -> LinearColor {
|
||||
let range = 0..self.aliasing_limit;
|
||||
let mut rng = thread_rng();
|
||||
let acc: LinearColor = range
|
||||
.map(|_| {
|
||||
let random_x: f32 = rng.gen();
|
||||
let random_y: f32 = rng.gen();
|
||||
self.pixel(x + random_x, y + random_y)
|
||||
})
|
||||
.map(LinearColor::clamp)
|
||||
.sum();
|
||||
acc / self.aliasing_limit as f32
|
||||
}
|
||||
|
||||
fn cast_ray(&self, ray: Ray) -> Option<(f32, &Object)> {
|
||||
self.bvh
|
||||
.traverse(&ray, &self.objects)
|
||||
.iter()
|
||||
.filter_map(|obj| obj.shape.intersect(&ray).map(|distance| (distance, *obj)))
|
||||
.min_by(|(dist_a, _), (dist_b, _)| {
|
||||
dist_a.partial_cmp(dist_b).unwrap_or(Ordering::Equal)
|
||||
})
|
||||
}
|
||||
|
||||
fn color_at(
|
||||
&self,
|
||||
point: Point,
|
||||
object: &Object,
|
||||
incident_ray: Vector,
|
||||
reflection_limit: u32,
|
||||
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_ray = reflected(incident_ray, normal);
|
||||
|
||||
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
|
||||
|| reflected.clone(),
|
||||
// Refraction (refracted ray, amount of *reflection*)
|
||||
|(r, 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)
|
||||
},
|
||||
)
|
||||
}
|
||||
ReflTransEnum::Reflectivity { coef } => reflected * coef + lighting * (1. - coef),
|
||||
}
|
||||
}
|
||||
|
||||
fn refraction(
|
||||
&self,
|
||||
point: Point,
|
||||
transparency: f32,
|
||||
refracted: Vector,
|
||||
reflection_limit: u32,
|
||||
indices: RefractionInfo,
|
||||
) -> LinearColor {
|
||||
if transparency > 1e-5 && reflection_limit > 0 {
|
||||
let refraction_start = point + refracted * 0.001;
|
||||
if let Some((t, obj)) = self.cast_ray(Ray::new(refraction_start, refracted)) {
|
||||
let resulting_position = refraction_start + refracted * t;
|
||||
let refracted = self.color_at(
|
||||
resulting_position,
|
||||
obj,
|
||||
refracted,
|
||||
reflection_limit - 1,
|
||||
indices,
|
||||
);
|
||||
return refracted * transparency;
|
||||
}
|
||||
}
|
||||
LinearColor::black()
|
||||
}
|
||||
|
||||
fn reflection(
|
||||
&self,
|
||||
point: Point,
|
||||
reflected: Vector,
|
||||
reflection_limit: u32,
|
||||
indices: RefractionInfo,
|
||||
) -> LinearColor {
|
||||
if reflection_limit > 0 {
|
||||
let reflection_start = point + reflected * 0.001;
|
||||
if let Some((t, obj)) = self.cast_ray(Ray::new(reflection_start, reflected)) {
|
||||
let resulting_position = reflection_start + reflected * t;
|
||||
let color = self.color_at(
|
||||
resulting_position,
|
||||
obj,
|
||||
reflected,
|
||||
reflection_limit - 1,
|
||||
indices,
|
||||
);
|
||||
return color;
|
||||
}
|
||||
};
|
||||
LinearColor::black()
|
||||
}
|
||||
|
||||
fn illuminate(
|
||||
&self,
|
||||
point: Point,
|
||||
object_color: LinearColor,
|
||||
properties: &LightProperties,
|
||||
normal: Vector,
|
||||
reflected: Vector,
|
||||
) -> LinearColor {
|
||||
let ambient = self.illuminate_ambient(object_color.clone());
|
||||
let spatial = self.illuminate_spatial(point, properties, normal, reflected);
|
||||
ambient + object_color * spatial
|
||||
}
|
||||
|
||||
fn illuminate_ambient(&self, color: LinearColor) -> LinearColor {
|
||||
self.lights
|
||||
.ambient_lights_iter()
|
||||
.map(|light| color.clone() * light.illumination(&Point::origin()))
|
||||
.map(LinearColor::clamp)
|
||||
.sum()
|
||||
}
|
||||
|
||||
fn illuminate_spatial(
|
||||
&self,
|
||||
point: Point,
|
||||
properties: &LightProperties,
|
||||
normal: Vector,
|
||||
reflected: 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);
|
||||
match self.cast_ray(light_ray) {
|
||||
// Take shadows into account
|
||||
Some((obstacle_t, _)) if obstacle_t < t => return LinearColor::black(),
|
||||
_ => {}
|
||||
}
|
||||
let lum = light.illumination(&point);
|
||||
let diffused = properties.diffuse.clone() * normal.dot(&direction);
|
||||
let specular = properties.specular.clone() * reflected.dot(&direction);
|
||||
lum * (diffused + specular)
|
||||
})
|
||||
.map(LinearColor::clamp)
|
||||
.sum()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Deserialize)]
|
||||
struct SerializedScene {
|
||||
camera: Camera,
|
||||
#[serde(default)]
|
||||
lights: LightAggregate,
|
||||
#[serde(default)]
|
||||
objects: Vec<Object>,
|
||||
#[serde(default)]
|
||||
aliasing_limit: u32,
|
||||
#[serde(default)]
|
||||
reflection_limit: u32,
|
||||
#[serde(default = "crate::serialize::default_identity")]
|
||||
starting_diffraction: f32,
|
||||
}
|
||||
|
||||
impl From<SerializedScene> for Scene {
|
||||
fn from(scene: SerializedScene) -> Self {
|
||||
Scene::new(
|
||||
scene.camera,
|
||||
scene.lights,
|
||||
scene.objects,
|
||||
scene.aliasing_limit,
|
||||
scene.reflection_limit,
|
||||
scene.starting_diffraction,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Scene {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let cam: SerializedScene = Deserialize::deserialize(deserializer)?;
|
||||
Ok(cam.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn deserialization_works() {
|
||||
let yaml = std::include_str!("../../examples/scene.yaml");
|
||||
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
|
||||
5, // aliasing limit
|
||||
3, // reflection recursion limit
|
||||
0.0, // diffraction index
|
||||
);
|
||||
}
|
||||
}
|
||||
67
pathtracer/src/render/utils.rs
Normal file
67
pathtracer/src/render/utils.rs
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
use crate::Vector;
|
||||
|
||||
pub fn reflected(incident: Vector, normal: Vector) -> Vector {
|
||||
let proj = incident.dot(&normal);
|
||||
let delt = normal * (proj * 2.);
|
||||
(incident - delt).normalize()
|
||||
}
|
||||
|
||||
/// Returns None if the ray was totally reflected, Some(refracted_ray, reflected_amount) if not
|
||||
/// Adds an element to the top of indices that should be removed
|
||||
pub fn refracted(
|
||||
incident: Vector,
|
||||
normal: Vector,
|
||||
indices: &mut RefractionInfo,
|
||||
new_index: f32,
|
||||
) -> Option<(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 + (eta * cos1 - cos2) * normal;
|
||||
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.normalize(), 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)
|
||||
}
|
||||
}
|
||||
6
pathtracer/src/serialize/coefficient.rs
Normal file
6
pathtracer/src/serialize/coefficient.rs
Normal 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.
|
||||
}
|
||||
7
pathtracer/src/serialize/mod.rs
Normal file
7
pathtracer/src/serialize/mod.rs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
//! Helper functions to help scene (de)serialization
|
||||
|
||||
pub mod vector;
|
||||
pub use vector::*;
|
||||
|
||||
pub mod coefficient;
|
||||
pub use coefficient::*;
|
||||
15
pathtracer/src/serialize/vector.rs
Normal file
15
pathtracer/src/serialize/vector.rs
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
//! Helper functions to deserialize `Vector` values.
|
||||
|
||||
use crate::Vector;
|
||||
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<Vector, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let v: Vector = Deserialize::deserialize(deserializer)?;
|
||||
Ok(v.normalize())
|
||||
}
|
||||
44
pathtracer/src/shape/mod.rs
Normal file
44
pathtracer/src/shape/mod.rs
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
//! Various shape implementations
|
||||
|
||||
use super::{Point, Point2D, Vector};
|
||||
use bvh::{
|
||||
aabb::{Bounded, AABB},
|
||||
ray::Ray,
|
||||
};
|
||||
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 {
|
||||
Sphere,
|
||||
Triangle,
|
||||
}
|
||||
|
||||
/// Represent an abstract shape inside the scene.
|
||||
#[enum_dispatch::enum_dispatch(ShapeEnum)]
|
||||
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;
|
||||
/// 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;
|
||||
}
|
||||
|
||||
impl Bounded for dyn Shape {
|
||||
fn aabb(&self) -> AABB {
|
||||
self.aabb()
|
||||
}
|
||||
}
|
||||
|
||||
mod sphere;
|
||||
pub use sphere::*;
|
||||
|
||||
mod triangle;
|
||||
pub use triangle::*;
|
||||
176
pathtracer/src/shape/sphere.rs
Normal file
176
pathtracer/src/shape/sphere.rs
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
use super::Shape;
|
||||
use crate::{Point, Point2D, Vector};
|
||||
use bvh::aabb::AABB;
|
||||
use bvh::ray::Ray;
|
||||
use serde::Deserialize;
|
||||
|
||||
/// Represent a sphere shape inside the scene.
|
||||
#[derive(Clone, Debug, PartialEq, Deserialize)]
|
||||
pub struct Sphere {
|
||||
/// The sphere is inverted if it is expected to be seen from the inside.
|
||||
#[serde(default)]
|
||||
inverted: bool,
|
||||
/// The center of the sphere in space.
|
||||
center: Point,
|
||||
/// The radius of the sphere being rendered.
|
||||
radius: f32,
|
||||
}
|
||||
|
||||
impl Sphere {
|
||||
/// Return a sphere which should be rendered as seen from the outside.
|
||||
pub fn new(center: Point, radius: f32) -> Self {
|
||||
Sphere {
|
||||
center,
|
||||
radius,
|
||||
inverted: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a sphere which should be rendered as seen from the inside.
|
||||
pub fn inverted_new(center: Point, radius: f32) -> Self {
|
||||
Sphere {
|
||||
center,
|
||||
radius,
|
||||
inverted: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Shape 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)
|
||||
}
|
||||
}
|
||||
|
||||
fn normal(&self, point: &Point) -> Vector {
|
||||
let delt = if self.inverted {
|
||||
self.center - point
|
||||
} else {
|
||||
point - self.center
|
||||
};
|
||||
delt.normalize()
|
||||
}
|
||||
|
||||
fn project_texel(&self, point: &Point) -> Point2D {
|
||||
// Project the sphere on the XY-plane
|
||||
Point2D::new(
|
||||
0.5 + (point.x - self.center.x) / (2. * self.radius),
|
||||
0.5 + (point.y - self.center.y) / (2. * self.radius),
|
||||
)
|
||||
}
|
||||
|
||||
fn aabb(&self) -> AABB {
|
||||
let delt = Vector::new(self.radius, self.radius, self.radius);
|
||||
let min = self.center - delt;
|
||||
let max = self.center + delt;
|
||||
AABB::with_bounds(min, max)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
fn simple_sphere() -> Sphere {
|
||||
Sphere::new(Point::origin(), 1.)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intersect_along_axis_works() {
|
||||
let sphere = simple_sphere();
|
||||
let ray = Ray::new(Point::new(-2., 0., 0.), 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.));
|
||||
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.));
|
||||
assert_eq!(sphere.intersect(&ray), Some(f32::sqrt(3.) - 1.))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normal_works() {
|
||||
let sphere = simple_sphere();
|
||||
assert_eq!(
|
||||
sphere.normal(&Point::new(-1., 0., 0.)),
|
||||
Vector::new(-1., 0., 0.)
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inverted_normal_works() {
|
||||
let sphere = Sphere::inverted_new(Point::origin(), 1.);
|
||||
assert_eq!(
|
||||
sphere.normal(&Point::new(-1., 0., 0.)),
|
||||
Vector::new(1., 0., 0.)
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn projection_works_1() {
|
||||
let sphere = simple_sphere();
|
||||
let projection = sphere.project_texel(&Point::new(-1., -1., 1.));
|
||||
assert!(projection.x.abs() < 1e-5);
|
||||
assert!(projection.y.abs() < 1e-5)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn projection_works_2() {
|
||||
let sphere = simple_sphere();
|
||||
let projection = sphere.project_texel(&Point::new(1., -1., 1.));
|
||||
assert!((projection.x - 1.).abs() < 1e-5);
|
||||
assert!(projection.y.abs() < 1e-5)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn projection_works_3() {
|
||||
let sphere = simple_sphere();
|
||||
let projection = sphere.project_texel(&Point::new(1., 0., 1.));
|
||||
assert!((projection.x - 1.).abs() < 1e-5);
|
||||
assert!((projection.y - 0.5).abs() < 1e-5)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialization_works() {
|
||||
let yaml = r#"
|
||||
inverted: false
|
||||
center: [0.5, 1.0, 2.0]
|
||||
radius: 2.5
|
||||
"#;
|
||||
let sphere: Sphere = serde_yaml::from_str(yaml).unwrap();
|
||||
assert_eq!(sphere, Sphere::new(Point::new(0.5, 1.0, 2.0), 2.5))
|
||||
}
|
||||
}
|
||||
242
pathtracer/src/shape/triangle.rs
Normal file
242
pathtracer/src/shape/triangle.rs
Normal file
|
|
@ -0,0 +1,242 @@
|
|||
use super::Shape;
|
||||
use crate::{Point, Point2D, Vector};
|
||||
use bvh::aabb::AABB;
|
||||
use bvh::ray::Ray;
|
||||
use serde::{Deserialize, Deserializer};
|
||||
|
||||
/// Represent a triangle inside the scene.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Triangle {
|
||||
c0: Point,
|
||||
c0c1: Vector,
|
||||
c0c2: Vector,
|
||||
}
|
||||
|
||||
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,
|
||||
c0c1: c1 - c0,
|
||||
c0c2: c2 - c0,
|
||||
}
|
||||
}
|
||||
|
||||
fn barycentric(&self, point: &Point) -> Point2D {
|
||||
let c0_pos = point - self.c0;
|
||||
// P - A = u * (B - A) + v * (C - A)
|
||||
// (C - A) = v0 is c0c2
|
||||
// (B - A) = v1 is c0c1
|
||||
// (P - A) = v2 is c0_pos
|
||||
let dot00 = self.c0c2.dot(&self.c0c2);
|
||||
let dot01 = self.c0c2.dot(&self.c0c1);
|
||||
let dot02 = self.c0c2.dot(&c0_pos);
|
||||
let dot11 = self.c0c1.dot(&self.c0c1);
|
||||
let dot12 = self.c0c1.dot(&c0_pos);
|
||||
|
||||
let inv_denom = 1. / (dot00 * dot11 - dot01 * dot01);
|
||||
let u = (dot00 * dot12 - dot01 * dot02) * inv_denom;
|
||||
let v = (dot11 * dot02 - dot01 * dot12) * inv_denom;
|
||||
Point2D::new(u, v)
|
||||
}
|
||||
}
|
||||
|
||||
impl Shape for Triangle {
|
||||
fn intersect(&self, ray: &Ray) -> Option<f32> {
|
||||
let pvec = ray.direction.cross(&self.c0c2);
|
||||
let det = self.c0c1.dot(&pvec);
|
||||
|
||||
if det.abs() < 1e-5 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let to_ray = ray.origin - self.c0;
|
||||
let inv_det = 1. / det;
|
||||
let u = to_ray.dot(&pvec) * inv_det;
|
||||
|
||||
if u < 0. || u > 1. {
|
||||
return None;
|
||||
}
|
||||
|
||||
let qvec = to_ray.cross(&self.c0c1);
|
||||
let v = ray.direction.dot(&qvec) * inv_det;
|
||||
|
||||
if v < 0. || u + v > 1. {
|
||||
return None;
|
||||
}
|
||||
|
||||
let t = self.c0c2.dot(&qvec) * inv_det;
|
||||
if t < 0. {
|
||||
None
|
||||
} else {
|
||||
Some(t)
|
||||
}
|
||||
}
|
||||
|
||||
fn normal(&self, _: &Point) -> Vector {
|
||||
self.c0c1.cross(&self.c0c2).normalize()
|
||||
}
|
||||
|
||||
fn project_texel(&self, point: &Point) -> Point2D {
|
||||
self.barycentric(point)
|
||||
}
|
||||
|
||||
fn aabb(&self) -> AABB {
|
||||
AABB::empty()
|
||||
.grow(&self.c0)
|
||||
.grow(&(self.c0 + self.c0c1))
|
||||
.grow(&(self.c0 + self.c0c2))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct SerializedTriangle {
|
||||
corners: [Point; 3],
|
||||
}
|
||||
|
||||
impl From<SerializedTriangle> for Triangle {
|
||||
fn from(triangle: SerializedTriangle) -> Self {
|
||||
Triangle::new(
|
||||
triangle.corners[0],
|
||||
triangle.corners[1],
|
||||
triangle.corners[2],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Triangle {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let cam: SerializedTriangle = Deserialize::deserialize(deserializer)?;
|
||||
Ok(cam.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
fn simple_triangle() -> Triangle {
|
||||
Triangle::new(
|
||||
Point::origin(),
|
||||
Point::new(0., 1., 1.),
|
||||
Point::new(0., 1., 0.),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intersect_along_normal_works() {
|
||||
let triangle = simple_triangle();
|
||||
let ans = triangle.intersect(&Ray::new(
|
||||
Point::new(-1., 0.5, 0.5),
|
||||
Vector::new(1., 0., 0.),
|
||||
));
|
||||
assert_eq!(ans, Some(1.0))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intersect_at_angle_works() {
|
||||
let triangle = simple_triangle();
|
||||
let ans = triangle.intersect(&Ray::new(
|
||||
Point::new(-1., 0.5, 0.),
|
||||
Vector::new(1., 0., 0.5),
|
||||
));
|
||||
assert!(ans.is_some());
|
||||
assert!((ans.unwrap() - f32::sqrt(1.0 + 0.25)).abs() < 1e-5)
|
||||
}
|
||||
|
||||
#[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.)));
|
||||
assert_eq!(ans, None)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normal_works() {
|
||||
let triangle = simple_triangle();
|
||||
let normal = triangle.normal(&Point::origin());
|
||||
assert_eq!(normal, Vector::new(-1., 0., 0.));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn project_texel_works_1() {
|
||||
let triangle = simple_triangle();
|
||||
let ans = triangle.project_texel(&Point::origin());
|
||||
assert!((ans - Point2D::origin()).magnitude() < 1e-5)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn project_texel_works_2() {
|
||||
let triangle = simple_triangle();
|
||||
let ans = triangle.project_texel(&Point::new(0., 1., 1.));
|
||||
assert!((ans - Point2D::new(1., 0.)).norm() < 1e-5)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn project_texel_works_3() {
|
||||
let triangle = simple_triangle();
|
||||
let ans = triangle.project_texel(&Point::new(0., 1., 0.));
|
||||
assert!((ans - Point2D::new(0., 1.)).norm() < 1e-5)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn project_texel_works_4() {
|
||||
let triangle = Triangle::new(
|
||||
Point::new(0., f32::sqrt(3.) / 2., 0.),
|
||||
Point::new(-0.5, 0., 0.),
|
||||
Point::new(0.5, 0., 0.),
|
||||
);
|
||||
// The centroid is at a third of the length of the height of the triangle
|
||||
let ans = triangle.project_texel(&Point::new(0., f32::sqrt(3.) / 6., 0.));
|
||||
assert!((ans - Point2D::new(1. / 3., 1. / 3.)).norm() < 1e-5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn project_texel_works_5() {
|
||||
let triangle = Triangle::new(
|
||||
Point::new(0., f32::sqrt(3.) / 2., 0.),
|
||||
Point::new(-0.5, 0., 0.),
|
||||
Point::new(0.5, 0., 0.),
|
||||
);
|
||||
// The centroid is at a third of the length of the height of the triangle
|
||||
let ans = triangle.project_texel(&Point::origin());
|
||||
assert!((ans - Point2D::new(0.5, 0.5)).norm() < 1e-5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialization_works() {
|
||||
let yaml = r#"
|
||||
corners:
|
||||
- [0.0, 0.0, 0.0]
|
||||
- [0.0, 1.0, 1.0]
|
||||
- [0.0, 1.0, 0.0]
|
||||
"#;
|
||||
let triangle: Triangle = serde_yaml::from_str(yaml).unwrap();
|
||||
assert_eq!(
|
||||
triangle,
|
||||
Triangle::new(
|
||||
Point::origin(),
|
||||
Point::new(0., 1., 1.),
|
||||
Point::new(0., 1., 0.)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
26
pathtracer/src/texture/mod.rs
Normal file
26
pathtracer/src/texture/mod.rs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
//! Various texture implementations
|
||||
|
||||
use super::core::LinearColor;
|
||||
use super::Point2D;
|
||||
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 {
|
||||
#[serde(rename = "uniform")]
|
||||
UniformTexture,
|
||||
}
|
||||
|
||||
/// Represent an object's texture.
|
||||
#[enum_dispatch::enum_dispatch(TextureEnum)]
|
||||
pub trait Texture: std::fmt::Debug {
|
||||
/// Get the color at a given texel coordinate
|
||||
fn texel_color(&self, point: Point2D) -> LinearColor;
|
||||
}
|
||||
|
||||
mod uniform;
|
||||
pub use uniform::*;
|
||||
69
pathtracer/src/texture/uniform.rs
Normal file
69
pathtracer/src/texture/uniform.rs
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
use super::Texture;
|
||||
use crate::core::LinearColor;
|
||||
use crate::Point2D;
|
||||
use serde::Deserialize;
|
||||
|
||||
/// A texture with the same color on all points.
|
||||
#[derive(Clone, Debug, PartialEq, Deserialize)]
|
||||
pub struct UniformTexture {
|
||||
color: LinearColor,
|
||||
}
|
||||
|
||||
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 }
|
||||
}
|
||||
}
|
||||
|
||||
impl Texture for UniformTexture {
|
||||
fn texel_color(&self, _: Point2D) -> LinearColor {
|
||||
self.color.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn new_works() {
|
||||
let color = LinearColor::new(0.2, 0.4, 0.6);
|
||||
let texture = UniformTexture::new(color.clone());
|
||||
assert_eq!(texture, UniformTexture { color })
|
||||
}
|
||||
|
||||
fn simple_texture() -> UniformTexture {
|
||||
UniformTexture::new(LinearColor::new(0.25, 0.5, 1.))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn texel_color_works() {
|
||||
let texture = simple_texture();
|
||||
assert_eq!(
|
||||
texture.texel_color(Point2D::origin()),
|
||||
LinearColor::new(0.25, 0.5, 1.)
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialization_works() {
|
||||
let yaml = r#"
|
||||
color: {r: 1.0, g: 0.5, b: 0.25}
|
||||
"#;
|
||||
let texture: UniformTexture = serde_yaml::from_str(yaml).unwrap();
|
||||
assert_eq!(
|
||||
texture,
|
||||
UniformTexture::new(LinearColor::new(1., 0.5, 0.25))
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue