Skip to content

Commit

Permalink
Merge pull request #221 from Walther/thin-film
Browse files Browse the repository at this point in the history
feat: iridescent materials with thin film interference
  • Loading branch information
Walther authored Dec 22, 2024
2 parents d51c0b6 + 943b534 commit 4706503
Show file tree
Hide file tree
Showing 4 changed files with 113 additions and 11 deletions.
45 changes: 42 additions & 3 deletions clovers/src/materials.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pub mod gltf;
pub mod isotropic;
pub mod lambertian;
pub mod metal;
pub mod thin_film;

pub use cone_light::*;
pub use dielectric::*;
Expand All @@ -25,6 +26,7 @@ pub use lambertian::*;
pub use metal::*;
use palette::{white_point::E, Xyz};
use rand::prelude::SmallRng;
pub use thin_film::*;

/// Initialization structure for a `Material`. Either contains a `Material` by itself, or a String `name` to be found in a shared material list.
#[derive(Debug, Clone)]
Expand Down Expand Up @@ -54,6 +56,43 @@ pub struct SharedMaterial {
pub material: Material,
}

/// The main material struct for the renderer.
///
/// This is a wrapper type. It contains the common properties shared by all materials, and an `inner` field with properties and method implementations specific to each material [`Kind`].
#[cfg_attr(feature = "serde-derive", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug, Default)]
pub struct Material {
/// The inner material with properties and method implementations specific to each material [`Kind`].
#[cfg_attr(feature = "serde-derive", serde(flatten))]
pub kind: Kind,
/// Optional thin film interference layer on top of the material
#[cfg_attr(feature = "serde-derive", serde(default))]
pub thin_film: Option<ThinFilm>,
}

impl MaterialTrait for Material {
fn scatter(
&self,
ray: &Ray,
hit_record: &HitRecord,
rng: &mut SmallRng,
) -> Option<ScatterRecord> {
let mut scatter_record = self.kind.scatter(ray, hit_record, rng)?;
if let Some(f) = &self.thin_film {
scatter_record.attenuation *= f.interference(ray, hit_record);
};
Some(scatter_record)
}

fn scattering_pdf(&self, hit_record: &HitRecord, scattered: &Ray) -> Option<Float> {
self.kind.scattering_pdf(hit_record, scattered)
}

fn emit(&self, ray: &Ray, hit_record: &HitRecord) -> Xyz<E> {
self.kind.emit(ray, hit_record)
}
}

#[enum_dispatch]
/// Trait for materials. Requires three function implementations: `scatter`, `scattering_pdf`, and `emit`.
pub trait MaterialTrait: Debug {
Expand All @@ -80,8 +119,8 @@ pub trait MaterialTrait: Debug {
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde-derive", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde-derive", serde(tag = "kind"))]
/// A material enum. TODO: for ideal clean abstraction, this should be a trait. However, that comes with some additional considerations, including e.g. performance.
pub enum Material {
/// An enum for the material kind
pub enum Kind {
/// Dielectric material
Dielectric(Dielectric),
/// Dispersive material
Expand All @@ -98,7 +137,7 @@ pub enum Material {
Isotropic(Isotropic),
}

impl Default for Material {
impl Default for Kind {
fn default() -> Self {
Self::Lambertian(Lambertian::default())
}
Expand Down
59 changes: 59 additions & 0 deletions clovers/src/materials/thin_film.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
//! An iridescence feature based on thin-film interference.
use core::f32::consts::PI;

use crate::{ray::Ray, Float, HitRecord};

#[derive(Clone, Debug)]
/// An iridescence feature based on thin-film interference.
#[cfg_attr(feature = "serde-derive", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde-derive", serde(default))]
pub struct ThinFilm {
/// Refractive index of the material.
pub refractive_index: Float,
/// Thickness of the film in nanometers.
pub thickness: Float,
}

impl ThinFilm {
/// Creates a new instance of [`ThinFilm`] with the specified `refractive_index` and `thickness` in nanometers.
#[must_use]
pub fn new(refractive_index: Float, thickness: Float) -> Self {
Self {
refractive_index,
thickness,
}
}

/// Calculates the strength of the interference. This should be used as a multiplier to the material's albedo. Range: `0..2` inclusive, with area 1, preserving energy conversation across spectrum.
#[must_use]
#[allow(clippy::cast_precision_loss)]
pub fn interference(&self, ray: &Ray, hit_record: &HitRecord) -> Float {
// Assume ray coming from air
// TODO: other material interfaces?
let n1 = 1.0;
let n2 = self.refractive_index;
let n_ratio = n1 / n2;

// https://en.wikipedia.org/wiki/Snell%27s_law#Vector_form
let cos_theta_1: Float = -ray.direction.dot(&hit_record.normal);
let sin_theta_1: Float = (1.0 - cos_theta_1 * cos_theta_1).sqrt();
let sin_theta_2: Float = n_ratio * sin_theta_1;
let cos_theta_2: Float = (1.0 - (sin_theta_2 * sin_theta_2)).sqrt();

// https://en.wikipedia.org/wiki/Thin-film_interference
let optical_path_difference = 2.0 * self.refractive_index * self.thickness * cos_theta_2;
let m = optical_path_difference / (ray.wavelength as Float);
// range 0 to 2, area 1
1.0 + (m * 2.0 * PI).cos()
}
}

impl Default for ThinFilm {
fn default() -> Self {
Self {
thickness: 500.0,
refractive_index: 1.5,
}
}
}
10 changes: 5 additions & 5 deletions clovers/src/objects/constant_medium.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
//! `ConstantMedium` object. This should probably be a [Material] at some point, but this will do for now. This is essentially a fog with a known size, shape and density.
//! `ConstantMedium` object. This should probably be a Material at some point, but this will do for now. This is essentially a fog with a known size, shape and density.
use crate::{
aabb::AABB,
hitable::{Hitable, HitableTrait},
materials::{isotropic::Isotropic, Material},
materials::{isotropic::Isotropic, Kind},
random::random_unit_vector,
ray::Ray,
textures::Texture,
Expand Down Expand Up @@ -41,10 +41,10 @@ fn default_density() -> Float {
}

#[derive(Debug, Clone)]
/// `ConstantMedium` object. This should probably be a [Material] at some point, but this will do for now. This is essentially a fog with a known size, shape and density.
/// `ConstantMedium` object. This should probably be a Material at some point, but this will do for now. This is essentially a fog with a known size, shape and density.
pub struct ConstantMedium<'scene> {
boundary: Box<Hitable<'scene>>,
phase_function: Material,
phase_function: Kind,
neg_inv_density: Float,
}

Expand All @@ -54,7 +54,7 @@ impl<'scene> ConstantMedium<'scene> {
pub fn new(boundary: Box<Hitable<'scene>>, density: Float, texture: Texture) -> Self {
ConstantMedium {
boundary,
phase_function: Material::Isotropic(Isotropic::new(texture)),
phase_function: Kind::Isotropic(Isotropic::new(texture)),
neg_inv_density: -1.0 / density,
}
}
Expand Down
10 changes: 7 additions & 3 deletions scenes/dragon.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"scale": 2500,
"center": [-45, -135, -100],
"rotation": [0, 200, 0],
"material": "white lambertian"
"material": "iridescent"
},
{
"kind": "Quad",
Expand Down Expand Up @@ -55,11 +55,15 @@
}
},
{
"name": "white lambertian",
"name": "iridescent",
"kind": "Lambertian",
"albedo": {
"kind": "SolidColor",
"color": [0.8, 0.8, 0.8]
"color": [0.6, 0.6, 0.6]
},
"thin_film": {
"refraction_index": 1.5,
"thickness": 256.0
}
},
{
Expand Down

0 comments on commit 4706503

Please sign in to comment.