Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: iridescent materials with thin film interference #221

Merged
merged 3 commits into from
Dec 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading