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
Changes from 1 commit
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
Next Next commit
feat: add support for iridescent materials with thin film interference
Walther committed Dec 21, 2024
commit 3ab280edd568585f68df9b368000934f42a0d76a
45 changes: 42 additions & 3 deletions clovers/src/materials.rs
Original file line number Diff line number Diff line change
@@ -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<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 {
@@ -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())
}
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,
}
}
}
6 changes: 3 additions & 3 deletions clovers/src/objects/constant_medium.rs
Original file line number Diff line number Diff line change
@@ -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<Hitable<'scene>>,
phase_function: Material,
phase_function: Kind,
neg_inv_density: Float,
}

@@ -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,
}
}
51 changes: 51 additions & 0 deletions scenes/iridescent.json
Original file line number Diff line number Diff line change
@@ -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
}
}
]
}