Skip to content

Commit

Permalink
feature: spectral rendering and dispersive materials
Browse files Browse the repository at this point in the history
  • Loading branch information
Walther committed Sep 23, 2023
1 parent e12ff12 commit bbe64b1
Show file tree
Hide file tree
Showing 24 changed files with 705 additions and 170 deletions.
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,14 @@ This repository has some example model files for demonstrating triangle-based ob
- Stanford Dragon model `dragon.stl` (stl converted version) CC Attribution [Thingiverse](https://www.thingiverse.com/thing:27666)
- Rubber Duck model `duck.stl` CC0 1.0 Universal Public Domain [Thingiverse](https://www.thingiverse.com/thing:139894)
- Triangular Prism model `prism.stl` Public Domain [Wikipedia](https://commons.wikimedia.org/wiki/File:Triangular_prism.stl)

## Useful references

Making this renderer would not have been possible without the availability of an abundance of great literature. Here are listed some of the sources that have been useful:

- [Ray Tracing in One Weekend](https://raytracing.github.io/books/RayTracingInOneWeekend.html)
- [Ray Tracing: The Next Week](https://raytracing.github.io/books/RayTracingTheNextWeek.html)
- [Ray Tracing: The Rest of Your Life](https://raytracing.github.io/books/RayTracingTheRestOfYourLife.html)
- [Physically Meaningful Rendering using Tristimulus Colours](https://doi.org/10.1111/cgf.12676)
- [Hero Wavelength Spectral Sampling](https://doi.org/10.1111/cgf.12419)
- [How to interpret the sRGB color space](https://color.org/chardata/rgb/sRGB.pdf)
18 changes: 13 additions & 5 deletions clovers/src/bvhnode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use crate::{
aabb::AABB,
hitable::{Empty, HitRecord, Hitable, HitableTrait},
ray::Ray,
spectral::Wavelength,
Box, Float, Vec, Vec3,
};

Expand Down Expand Up @@ -184,13 +185,20 @@ impl<'scene> HitableTrait for BVHNode<'scene> {

/// Returns a probability density function value based on the children
#[must_use]
fn pdf_value(&self, origin: Vec3, vector: Vec3, time: f32, rng: &mut SmallRng) -> f32 {
fn pdf_value(
&self,
origin: Vec3,
vector: Vec3,
wavelength: Wavelength,
time: Float,
rng: &mut SmallRng,
) -> Float {
match (&*self.left, &*self.right) {
(_, Hitable::Empty(_)) => self.left.pdf_value(origin, vector, time, rng),
(Hitable::Empty(_), _) => self.right.pdf_value(origin, vector, time, rng),
(_, Hitable::Empty(_)) => self.left.pdf_value(origin, vector, wavelength, time, rng),
(Hitable::Empty(_), _) => self.right.pdf_value(origin, vector, wavelength, time, rng),
(_, _) => {
(self.left.pdf_value(origin, vector, time, rng)
+ self.right.pdf_value(origin, vector, time, rng))
(self.left.pdf_value(origin, vector, wavelength, time, rng)
+ self.right.pdf_value(origin, vector, wavelength, time, rng))
/ 2.0
}
}
Expand Down
14 changes: 10 additions & 4 deletions clovers/src/camera.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#![allow(clippy::too_many_arguments)] // TODO: Camera::new() has a lot of arguments.

use crate::spectral::random_wavelength;
use crate::{random::random_in_unit_disk, ray::Ray, Float, Vec3, PI};
use rand::rngs::SmallRng;
use rand::Rng;
Expand Down Expand Up @@ -106,10 +107,15 @@ impl Camera {
let offset: Vec3 = self.u * rd.x + self.v * rd.y;
// Randomized time used for motion blur
let time: Float = rng.gen_range(self.time_0..self.time_1);
Ray::new(
self.origin + offset,
self.lower_left_corner + s * self.horizontal + t * self.vertical - self.origin - offset,
// Random wavelength for spectral rendering
let wavelength = random_wavelength(rng);
Ray {
origin: self.origin + offset,
direction: self.lower_left_corner + s * self.horizontal + t * self.vertical
- self.origin
- offset,
time,
)
wavelength,
}
}
}
49 changes: 32 additions & 17 deletions clovers/src/colorize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,35 +28,45 @@ pub fn colorize(ray: &Ray, scene: &Scene, depth: u32, max_depth: u32, rng: &mut
return scene.background_color;
};

// Spectral rendering: compute a tint based on the current ray's wavelength
let tint: Color = ray.wavelength.into();

// Get the emitted color from the surface that we just hit
let emitted: Color = hit_record.material.emit(
let mut emitted: Color = hit_record.material.emit(
ray,
&hit_record,
hit_record.u,
hit_record.v,
hit_record.position,
);

// Tint the emitted light based on the wavelength. Emissive values do not need to be clamped, as they do not need to be energy-conserving.
emitted = emitted * tint;

// Do we scatter?
let Some(scatter_record) = hit_record.material.scatter(ray, &hit_record, rng) else {
// No scatter, early return the emitted color only
return emitted;
};
// We have scattered, and received an attenuation from the material.
// Tint based on the wavelength. Reflective values need to be clamped to ensure the reflections are energy-conserving.
// TODO: verify correctness!
let attenuation = scatter_record.attenuation * tint;
let attenuation = attenuation.clamp();

// We have scattered, check material type and recurse accordingly
// Check the material type and recurse accordingly:
match scatter_record.material_type {
MaterialType::Specular => {
// If we hit a specular material, generate a specular ray, and multiply it with the value of the scatter_record.
// Note that the `emitted` value from earlier is not used, as the scatter_record.attenuation has an appropriately adjusted color
scatter_record.attenuation
* colorize(
// a scatter_record from a specular material should always have this ray
&scatter_record.specular_ray.unwrap(),
scene,
depth + 1,
max_depth,
rng,
)
// If we hit a specular material, generate a specular ray, and multiply it with the attenuation
let specular = colorize(
// a scatter_record from a specular material should always have this ray
&scatter_record.specular_ray.unwrap(),
scene,
depth + 1,
max_depth,
rng,
);
specular * attenuation
}
MaterialType::Diffuse => {
// Use a probability density function to figure out where to scatter a new ray
Expand All @@ -66,8 +76,13 @@ pub fn colorize(ray: &Ray, scene: &Scene, depth: u32, max_depth: u32, rng: &mut
hit_record.position,
));
let mixture_pdf = MixturePDF::new(light_ptr, scatter_record.pdf_ptr);
let scatter_ray = Ray::new(hit_record.position, mixture_pdf.generate(rng), ray.time);
let pdf_val = mixture_pdf.value(scatter_ray.direction, ray.time, rng);
let scatter_ray = Ray {
origin: hit_record.position,
direction: mixture_pdf.generate(rng),
time: ray.time,
wavelength: ray.wavelength,
};
let pdf_val = mixture_pdf.value(scatter_ray.direction, ray.wavelength, ray.time, rng);
if pdf_val <= 0.0 {
// scattering impossible, prevent division by zero below
// for more ctx, see https://github.com/RayTracing/raytracing.github.io/issues/979#issuecomment-1034517236
Expand All @@ -86,8 +101,8 @@ pub fn colorize(ray: &Ray, scene: &Scene, depth: u32, max_depth: u32, rng: &mut

// Recurse for the scattering ray
let recurse = colorize(&scatter_ray, scene, depth + 1, max_depth, rng);
// Weight it according to the PDF
let scattered = scatter_record.attenuation * scattering_pdf * recurse / pdf_val;
// Tint and weight it according to the PDF
let scattered = attenuation * scattering_pdf * recurse / pdf_val;
// Ensure positive color
let scattered = scattered.non_negative();
// Blend it all together
Expand Down
19 changes: 17 additions & 2 deletions clovers/src/hitable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use crate::{
Boxy, ConstantMedium, FlipFace, MovingSphere, Quad, RotateY, Sphere, Translate, Triangle,
},
ray::Ray,
spectral::Wavelength,
Float, Vec3,
};

Expand Down Expand Up @@ -94,7 +95,14 @@ impl HitableTrait for Empty {
None
}

fn pdf_value(&self, _origin: Vec3, _vector: Vec3, _time: Float, _rng: &mut SmallRng) -> Float {
fn pdf_value(
&self,
_origin: Vec3,
_vector: Vec3,
_wavelength: Wavelength,
_time: Float,
_rng: &mut SmallRng,
) -> Float {
0.0
}

Expand All @@ -118,7 +126,14 @@ pub(crate) trait HitableTrait {
fn bounding_box(&self, t0: Float, t1: Float) -> Option<&AABB>;

#[must_use]
fn pdf_value(&self, origin: Vec3, vector: Vec3, time: Float, rng: &mut SmallRng) -> Float;
fn pdf_value(
&self,
origin: Vec3,
vector: Vec3,
wavelength: Wavelength,
time: Float,
rng: &mut SmallRng,
) -> Float;

#[must_use]
fn random(&self, origin: Vec3, rng: &mut SmallRng) -> Vec3;
Expand Down
12 changes: 8 additions & 4 deletions clovers/src/materials.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use core::fmt::Debug;
use crate::{color::Color, hitable::HitRecord, pdf::PDF, ray::Ray, Float, Vec3};
pub mod dielectric;
pub mod diffuse_light;
pub mod dispersive;
#[cfg(feature = "gl_tf")]
pub mod gltf;
pub mod isotropic;
Expand All @@ -13,6 +14,7 @@ pub mod metal;

pub use dielectric::*;
pub use diffuse_light::*;
pub use dispersive::*;
use enum_dispatch::enum_dispatch;
pub use isotropic::*;
pub use lambertian::*;
Expand Down Expand Up @@ -66,7 +68,7 @@ pub trait MaterialTrait: Debug {
rng: &mut SmallRng,
) -> Option<Float>;

/// Returns the emissivity of the material at the given position.
/// Returns the emissivity of the material at the given position. Defaults to black as most materials don't emit - override when needed.
fn emit(
&self,
_ray: &Ray,
Expand All @@ -75,7 +77,6 @@ pub trait MaterialTrait: Debug {
_v: Float,
_position: Vec3,
) -> Color {
// Most materials don't emit, override when needed
Color::new(0.0, 0.0, 0.0)
}
}
Expand All @@ -88,6 +89,8 @@ pub trait MaterialTrait: Debug {
pub enum Material {
/// Dielectric material
Dielectric(Dielectric),
/// Dispersive material
Dispersive(Dispersive),
/// Lambertian material
Lambertian(Lambertian),
/// DiffuseLight material
Expand Down Expand Up @@ -135,9 +138,10 @@ fn reflect(vector: Vec3, normal: Vec3) -> Vec3 {
}

#[must_use]
fn refract(uv: Vec3, normal: Vec3, etai_over_etat: Float) -> Vec3 {
fn refract(uv: Vec3, normal: Vec3, refraction_ratio: Float) -> Vec3 {
let cos_theta: Float = -uv.dot(&normal);
let r_out_parallel: Vec3 = etai_over_etat * (uv + cos_theta * normal);
let cos_theta = cos_theta.min(1.0); // Clamp
let r_out_parallel: Vec3 = refraction_ratio * (uv + cos_theta * normal);
let r_out_perp: Vec3 = -(1.0 - r_out_parallel.norm_squared()).sqrt() * normal;
r_out_parallel + r_out_perp
}
Expand Down
31 changes: 17 additions & 14 deletions clovers/src/materials/dielectric.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,8 @@ impl MaterialTrait for Dielectric {
hit_record: &HitRecord,
rng: &mut SmallRng,
) -> Option<ScatterRecord> {
let albedo = self.color;
let specular_ray: Ray;

let etai_over_etat: Float = if hit_record.front_face {
let attenuation = self.color;
let refraction_ratio: Float = if hit_record.front_face {
1.0 / self.refractive_index
} else {
self.refractive_index
Expand All @@ -52,23 +50,28 @@ impl MaterialTrait for Dielectric {
let unit_direction: Vec3 = ray.direction.normalize();
let cos_theta: Float = (-unit_direction.dot(&hit_record.normal)).min(1.0);
let sin_theta: Float = (1.0 - cos_theta * cos_theta).sqrt();
if etai_over_etat * sin_theta > 1.0 {
let reflected: Vec3 = reflect(unit_direction, hit_record.normal);
specular_ray = Ray::new(hit_record.position, reflected, ray.time);
let specular_direction: Vec3 = if refraction_ratio * sin_theta > 1.0 {
reflect(unit_direction, hit_record.normal)
} else {
let reflect_probability: Float = schlick(cos_theta, etai_over_etat);
let reflect_probability: Float = schlick(cos_theta, refraction_ratio);
if rng.gen::<Float>() < reflect_probability {
let reflected: Vec3 = reflect(unit_direction, hit_record.normal);
specular_ray = Ray::new(hit_record.position, reflected, ray.time);
reflect(unit_direction, hit_record.normal)
} else {
let refracted: Vec3 = refract(unit_direction, hit_record.normal, etai_over_etat);
specular_ray = Ray::new(hit_record.position, refracted, ray.time);
// Refracted
refract(unit_direction, hit_record.normal, refraction_ratio)
}
}
};
let specular_ray = Ray {
origin: hit_record.position,
direction: specular_direction,
time: ray.time,
wavelength: ray.wavelength,
};

Some(ScatterRecord {
material_type: MaterialType::Specular,
specular_ray: Some(specular_ray),
attenuation: albedo,
attenuation,
pdf_ptr: PDF::ZeroPDF(ZeroPDF::new()), //TODO: ugly hack due to nullptr in original tutorial
})
}
Expand Down
Loading

0 comments on commit bbe64b1

Please sign in to comment.