From 3ab280edd568585f68df9b368000934f42a0d76a Mon Sep 17 00:00:00 2001 From: Walther Date: Sun, 22 Dec 2024 00:56:58 +0200 Subject: [PATCH 1/3] feat: add support for iridescent materials with thin film interference --- clovers/src/materials.rs | 45 ++++++++++++++++++-- clovers/src/materials/thin_film.rs | 59 ++++++++++++++++++++++++++ clovers/src/objects/constant_medium.rs | 6 +-- scenes/iridescent.json | 51 ++++++++++++++++++++++ 4 files changed, 155 insertions(+), 6 deletions(-) create mode 100644 clovers/src/materials/thin_film.rs create mode 100644 scenes/iridescent.json diff --git a/clovers/src/materials.rs b/clovers/src/materials.rs index 22124330..9698272e 100644 --- a/clovers/src/materials.rs +++ b/clovers/src/materials.rs @@ -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::*; @@ -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)] @@ -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, +} + +impl MaterialTrait for Material { + fn scatter( + &self, + ray: &Ray, + hit_record: &HitRecord, + rng: &mut SmallRng, + ) -> Option { + 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 { + self.kind.scattering_pdf(hit_record, scattered) + } + + fn emit(&self, ray: &Ray, hit_record: &HitRecord) -> Xyz { + self.kind.emit(ray, hit_record) + } +} + #[enum_dispatch] /// Trait for materials. Requires three function implementations: `scatter`, `scattering_pdf`, and `emit`. pub trait MaterialTrait: Debug { @@ -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 @@ -98,7 +137,7 @@ pub enum Material { Isotropic(Isotropic), } -impl Default for Material { +impl Default for Kind { fn default() -> Self { Self::Lambertian(Lambertian::default()) } diff --git a/clovers/src/materials/thin_film.rs b/clovers/src/materials/thin_film.rs new file mode 100644 index 00000000..4131ea42 --- /dev/null +++ b/clovers/src/materials/thin_film.rs @@ -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, + } + } +} diff --git a/clovers/src/objects/constant_medium.rs b/clovers/src/objects/constant_medium.rs index 7d3ed8d4..3de93047 100644 --- a/clovers/src/objects/constant_medium.rs +++ b/clovers/src/objects/constant_medium.rs @@ -3,7 +3,7 @@ use crate::{ aabb::AABB, hitable::{Hitable, HitableTrait}, - materials::{isotropic::Isotropic, Material}, + materials::{isotropic::Isotropic, Kind}, random::random_unit_vector, ray::Ray, textures::Texture, @@ -44,7 +44,7 @@ fn default_density() -> Float { /// `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>, - phase_function: Material, + phase_function: Kind, neg_inv_density: Float, } @@ -54,7 +54,7 @@ impl<'scene> ConstantMedium<'scene> { pub fn new(boundary: Box>, 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, } } diff --git a/scenes/iridescent.json b/scenes/iridescent.json new file mode 100644 index 00000000..e4713928 --- /dev/null +++ b/scenes/iridescent.json @@ -0,0 +1,51 @@ +{ + "time_0": 0, + "time_1": 1, + "camera": { + "look_from": [30, 20, 10], + "look_at": [0, -1, 0], + "up": [-0.5, 1, -0.5], + "vertical_fov": 40, + "aperture": 0, + "focus_distance": 10 + }, + "background_color": [0, 0, 0], + "objects": [ + { + "kind": "Quad", + "priority": true, + "q": [-100, 80, -100], + "u": [200, 0, 0], + "v": [0, 0, 100], + "material": "lamp" + }, + { + "kind": "Boxy", + "corner_0": [-7, -7, -7], + "corner_1": [7, 7, 7], + "material": "iridescent" + } + ], + "materials": [ + { + "name": "lamp", + "kind": "DiffuseLight", + "emit": { + "kind": "SolidColor", + "color": [2.5, 2.5, 2.5] + } + }, + { + "name": "iridescent", + "kind": "Lambertian", + "albedo": { + "kind": "SolidColor", + "color": [0.8, 0.8, 0.8] + }, + "thin_film": { + "refractive_index": 1.5, + "thickness": 600.0 + } + } + ] +} From e32ae2c6b9b8a8878c96456142eb77c958adab80 Mon Sep 17 00:00:00 2001 From: Walther Date: Sun, 22 Dec 2024 02:03:21 +0200 Subject: [PATCH 2/3] chore: remove iridescent scene, use material in dragon instead --- scenes/dragon.json | 10 ++++++--- scenes/iridescent.json | 51 ------------------------------------------ 2 files changed, 7 insertions(+), 54 deletions(-) delete mode 100644 scenes/iridescent.json diff --git a/scenes/dragon.json b/scenes/dragon.json index 3cf0a4b8..bf1376de 100644 --- a/scenes/dragon.json +++ b/scenes/dragon.json @@ -18,7 +18,7 @@ "scale": 2500, "center": [-45, -135, -100], "rotation": [0, 200, 0], - "material": "white lambertian" + "material": "iridescent" }, { "kind": "Quad", @@ -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 } }, { diff --git a/scenes/iridescent.json b/scenes/iridescent.json deleted file mode 100644 index e4713928..00000000 --- a/scenes/iridescent.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "time_0": 0, - "time_1": 1, - "camera": { - "look_from": [30, 20, 10], - "look_at": [0, -1, 0], - "up": [-0.5, 1, -0.5], - "vertical_fov": 40, - "aperture": 0, - "focus_distance": 10 - }, - "background_color": [0, 0, 0], - "objects": [ - { - "kind": "Quad", - "priority": true, - "q": [-100, 80, -100], - "u": [200, 0, 0], - "v": [0, 0, 100], - "material": "lamp" - }, - { - "kind": "Boxy", - "corner_0": [-7, -7, -7], - "corner_1": [7, 7, 7], - "material": "iridescent" - } - ], - "materials": [ - { - "name": "lamp", - "kind": "DiffuseLight", - "emit": { - "kind": "SolidColor", - "color": [2.5, 2.5, 2.5] - } - }, - { - "name": "iridescent", - "kind": "Lambertian", - "albedo": { - "kind": "SolidColor", - "color": [0.8, 0.8, 0.8] - }, - "thin_film": { - "refractive_index": 1.5, - "thickness": 600.0 - } - } - ] -} From 943b534c2a1d7d6ed0861fb8e7b100e470b633fe Mon Sep 17 00:00:00 2001 From: Walther Date: Sun, 22 Dec 2024 02:09:08 +0200 Subject: [PATCH 3/3] chore: fix cargo doc lint warn --- clovers/src/objects/constant_medium.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/clovers/src/objects/constant_medium.rs b/clovers/src/objects/constant_medium.rs index 3de93047..fa3da9ff 100644 --- a/clovers/src/objects/constant_medium.rs +++ b/clovers/src/objects/constant_medium.rs @@ -1,4 +1,4 @@ -//! `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, @@ -41,7 +41,7 @@ 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>, phase_function: Kind,