From bbe64b1604e658264c9bdf38d8d24c4ea14c7142 Mon Sep 17 00:00:00 2001 From: Walther Date: Sat, 23 Sep 2023 15:47:35 +0300 Subject: [PATCH] feature: spectral rendering and dispersive materials --- README.md | 11 ++ clovers/src/bvhnode.rs | 18 ++- clovers/src/camera.rs | 14 +- clovers/src/colorize.rs | 49 ++++--- clovers/src/hitable.rs | 19 ++- clovers/src/materials.rs | 12 +- clovers/src/materials/dielectric.rs | 31 +++-- clovers/src/materials/dispersive.rs | 128 ++++++++++++++++++ clovers/src/materials/gltf.rs | 7 +- clovers/src/materials/metal.rs | 11 +- clovers/src/objects/boxy.rs | 12 +- clovers/src/objects/constant_medium.rs | 13 +- clovers/src/objects/flip_face.rs | 12 +- clovers/src/objects/gltf.rs | 35 +++-- clovers/src/objects/moving_sphere.rs | 10 +- clovers/src/objects/quad.rs | 23 +++- clovers/src/objects/rotate.rs | 17 ++- clovers/src/objects/sphere.rs | 23 +++- clovers/src/objects/stl.rs | 13 +- clovers/src/objects/translate.rs | 19 ++- clovers/src/objects/triangle.rs | 152 +++++++++++++--------- clovers/src/pdf.rs | 59 +++++++-- clovers/src/ray.rs | 14 +- scenes/dispersive.json | 173 +++++++++++++++++++++++++ 24 files changed, 705 insertions(+), 170 deletions(-) create mode 100644 clovers/src/materials/dispersive.rs create mode 100644 scenes/dispersive.json diff --git a/README.md b/README.md index e05e4b16..923a23be 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/clovers/src/bvhnode.rs b/clovers/src/bvhnode.rs index 708bfcdb..79d9018b 100644 --- a/clovers/src/bvhnode.rs +++ b/clovers/src/bvhnode.rs @@ -8,6 +8,7 @@ use crate::{ aabb::AABB, hitable::{Empty, HitRecord, Hitable, HitableTrait}, ray::Ray, + spectral::Wavelength, Box, Float, Vec, Vec3, }; @@ -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 } } diff --git a/clovers/src/camera.rs b/clovers/src/camera.rs index 7670a65e..300ac7c0 100644 --- a/clovers/src/camera.rs +++ b/clovers/src/camera.rs @@ -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; @@ -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, + } } } diff --git a/clovers/src/colorize.rs b/clovers/src/colorize.rs index 6916c8f0..c5954584 100644 --- a/clovers/src/colorize.rs +++ b/clovers/src/colorize.rs @@ -28,8 +28,11 @@ 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, @@ -37,26 +40,33 @@ pub fn colorize(ray: &Ray, scene: &Scene, depth: u32, max_depth: u32, rng: &mut 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 @@ -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 @@ -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 diff --git a/clovers/src/hitable.rs b/clovers/src/hitable.rs index 221d702e..6e5fda9a 100644 --- a/clovers/src/hitable.rs +++ b/clovers/src/hitable.rs @@ -15,6 +15,7 @@ use crate::{ Boxy, ConstantMedium, FlipFace, MovingSphere, Quad, RotateY, Sphere, Translate, Triangle, }, ray::Ray, + spectral::Wavelength, Float, Vec3, }; @@ -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 } @@ -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; diff --git a/clovers/src/materials.rs b/clovers/src/materials.rs index 7e504571..7c35d412 100644 --- a/clovers/src/materials.rs +++ b/clovers/src/materials.rs @@ -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; @@ -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::*; @@ -66,7 +68,7 @@ pub trait MaterialTrait: Debug { rng: &mut SmallRng, ) -> Option; - /// 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, @@ -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) } } @@ -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 @@ -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 } diff --git a/clovers/src/materials/dielectric.rs b/clovers/src/materials/dielectric.rs index 477c2bc1..04ca0454 100644 --- a/clovers/src/materials/dielectric.rs +++ b/clovers/src/materials/dielectric.rs @@ -40,10 +40,8 @@ impl MaterialTrait for Dielectric { hit_record: &HitRecord, rng: &mut SmallRng, ) -> Option { - 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 @@ -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::() < 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 }) } diff --git a/clovers/src/materials/dispersive.rs b/clovers/src/materials/dispersive.rs new file mode 100644 index 00000000..8d65c640 --- /dev/null +++ b/clovers/src/materials/dispersive.rs @@ -0,0 +1,128 @@ +//! Dispersive material. +//! Based on [Cauchy's equation](https://en.wikipedia.org/wiki/Cauchy%27s_equation) + +/* +Material A B (μm2) +Fused silica 1.4580 0.00354 +Borosilicate glass BK7 1.5046 0.00420 +Hard crown glass K5 1.5220 0.00459 +Barium crown glass BaK4 1.5690 0.00531 +Barium flint glass BaF10 1.6700 0.00743 +Dense flint glass SF10 1.7280 0.01342 +*/ + +// TODO: consider other options, e.g. Sellmeier https://en.wikipedia.org/wiki/Sellmeier_equation + +use rand::{rngs::SmallRng, Rng}; + +use crate::{ + color::Color, + hitable::HitRecord, + pdf::{ZeroPDF, PDF}, + ray::Ray, + spectral::Wavelength, + Float, Vec3, +}; + +use super::{reflect, refract, schlick, MaterialTrait, MaterialType, ScatterRecord}; + +#[derive(Clone, Debug)] +#[cfg_attr(feature = "serde-derive", derive(serde::Serialize, serde::Deserialize))] +/// A dispersive glass material. +pub struct Dispersive { + /// Cauchy coefficient A of the material + #[cfg_attr(feature = "serde-derive", serde(default = "default_a"))] + pub cauchy_a: Float, + /// Cauchy coefficient B of the material + #[cfg_attr(feature = "serde-derive", serde(default = "default_b"))] + pub cauchy_b: Float, +} + +fn default_a() -> Float { + 1.5046 +} + +fn default_b() -> Float { + 0.00420 +} + +// TODO: less precision loss? +#[allow(clippy::cast_precision_loss)] +impl Dispersive { + /// Creates a new [Dispersive] material with the given refractive index and Abbe number. + #[must_use] + pub fn new(cauchy_a: Float, cauchy_b: Float) -> Self { + Dispersive { cauchy_a, cauchy_b } + } + + /// Calculates the refractive index of the material for the given wavelength + #[must_use] + pub fn refractive_index(&self, wavelength: Wavelength) -> Float { + let wave_micros = wavelength as Float / 1000.0; + self.cauchy_a + (self.cauchy_b / (wave_micros * wave_micros)) + } +} + +impl Default for Dispersive { + fn default() -> Self { + Dispersive::new(default_a(), default_b()) + } +} + +impl MaterialTrait for Dispersive { + fn scatter( + &self, + ray: &Ray, + hit_record: &HitRecord, + rng: &mut SmallRng, + ) -> Option { + // Calculate refractive index based on the wavelength of the incoming material + // TODO: colored dispersive glass? + let attenuation = Color::new(1.0, 1.0, 1.0); + let refractive_index = self.refractive_index(ray.wavelength); + let refraction_ratio: Float = if hit_record.front_face { + 1.0 / refractive_index + } else { + refractive_index + }; + + // Copied from Dielectric, is this correct? + 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(); + 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, refraction_ratio); + if rng.gen::() < reflect_probability { + reflect(unit_direction, hit_record.normal) + } else { + // 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, + pdf_ptr: PDF::ZeroPDF(ZeroPDF::new()), //TODO: ugly hack due to nullptr in original tutorial + }) + // End copied + } + + fn scattering_pdf( + &self, + _hit_record: &HitRecord, + _scattered: &Ray, + _rng: &mut SmallRng, + ) -> Option { + None // TODO: should a dispersive material scatter? how much? + } +} diff --git a/clovers/src/materials/gltf.rs b/clovers/src/materials/gltf.rs index 4229114b..b42949ff 100644 --- a/clovers/src/materials/gltf.rs +++ b/clovers/src/materials/gltf.rs @@ -100,7 +100,12 @@ impl<'scene> MaterialTrait for GLTFMaterial<'scene> { let direction = reflected + roughness * random_in_unit_sphere(rng); Some(ScatterRecord { - specular_ray: Some(Ray::new(hit_record.position, direction, ray.time)), + specular_ray: Some(Ray { + origin: hit_record.position, + direction, + time: ray.time, + wavelength: ray.wavelength, + }), attenuation, material_type: MaterialType::Specular, pdf_ptr: PDF::ZeroPDF(ZeroPDF::new()), diff --git a/clovers/src/materials/metal.rs b/clovers/src/materials/metal.rs index 721c8c1d..98db31cf 100644 --- a/clovers/src/materials/metal.rs +++ b/clovers/src/materials/metal.rs @@ -32,11 +32,12 @@ impl MaterialTrait for Metal { ) -> Option { let reflected: Vec3 = reflect(ray.direction.normalize(), hit_record.normal); Some(ScatterRecord { - specular_ray: Some(Ray::new( - hit_record.position, - reflected + self.fuzz * random_in_unit_sphere(rng), - ray.time, - )), + specular_ray: Some(Ray { + origin: hit_record.position, + direction: reflected + self.fuzz * random_in_unit_sphere(rng), + time: ray.time, + wavelength: ray.wavelength, + }), attenuation: self .albedo .color(hit_record.u, hit_record.v, hit_record.position), diff --git a/clovers/src/objects/boxy.rs b/clovers/src/objects/boxy.rs index 2d67bd0e..9654c55f 100644 --- a/clovers/src/objects/boxy.rs +++ b/clovers/src/objects/boxy.rs @@ -6,6 +6,7 @@ use crate::{ hitable::{HitRecord, Hitable, HitableTrait}, materials::{Material, MaterialInit}, ray::Ray, + spectral::Wavelength, Box, Float, Vec3, }; use rand::{rngs::SmallRng, Rng}; @@ -108,11 +109,18 @@ impl<'scene> HitableTrait for Boxy<'scene> { /// Returns a probability density function value? // TODO: understand & explain #[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 { let mut sum = 0.0; self.sides.iter().for_each(|object| { - sum += object.pdf_value(origin, vector, time, rng) / 6.0; + sum += object.pdf_value(origin, vector, wavelength, time, rng) / 6.0; }); sum diff --git a/clovers/src/objects/constant_medium.rs b/clovers/src/objects/constant_medium.rs index 699b1dd7..e530431f 100644 --- a/clovers/src/objects/constant_medium.rs +++ b/clovers/src/objects/constant_medium.rs @@ -6,6 +6,7 @@ use crate::{ materials::{isotropic::Isotropic, Material}, random::random_unit_vector, ray::Ray, + spectral::Wavelength, textures::Texture, Box, Float, Vec3, EPSILON_CONSTANT_MEDIUM, }; @@ -135,8 +136,16 @@ impl<'scene> HitableTrait for ConstantMedium<'scene> { /// Returns a probability density function value based on the boundary object #[must_use] - fn pdf_value(&self, origin: Vec3, vector: Vec3, time: Float, rng: &mut SmallRng) -> Float { - self.boundary.pdf_value(origin, vector, time, rng) + fn pdf_value( + &self, + origin: Vec3, + vector: Vec3, + wavelength: Wavelength, + time: Float, + rng: &mut SmallRng, + ) -> Float { + self.boundary + .pdf_value(origin, vector, wavelength, time, rng) } /// Returns a random point on the surface of the boundary of the fog diff --git a/clovers/src/objects/flip_face.rs b/clovers/src/objects/flip_face.rs index 3d6e53be..7421fa05 100644 --- a/clovers/src/objects/flip_face.rs +++ b/clovers/src/objects/flip_face.rs @@ -4,6 +4,7 @@ use crate::{ aabb::AABB, hitable::{HitRecord, Hitable, HitableTrait}, ray::Ray, + spectral::Wavelength, Box, Float, Vec3, }; use rand::rngs::SmallRng; @@ -62,8 +63,15 @@ impl<'scene> HitableTrait for FlipFace<'scene> { /// Returns a probability density function value based on the inner object #[must_use] - fn pdf_value(&self, origin: Vec3, vector: Vec3, time: Float, rng: &mut SmallRng) -> Float { - self.object.pdf_value(origin, vector, time, rng) + fn pdf_value( + &self, + origin: Vec3, + vector: Vec3, + wavelength: Wavelength, + time: Float, + rng: &mut SmallRng, + ) -> Float { + self.object.pdf_value(origin, vector, wavelength, time, rng) } /// Returns a random point on the surface of the inner object diff --git a/clovers/src/objects/gltf.rs b/clovers/src/objects/gltf.rs index 59738ea5..26289f11 100644 --- a/clovers/src/objects/gltf.rs +++ b/clovers/src/objects/gltf.rs @@ -16,6 +16,7 @@ use crate::{ interval::Interval, materials::gltf::GLTFMaterial, ray::Ray, + spectral::Wavelength, Float, Vec3, EPSILON_RECT_THICKNESS, EPSILON_SHADOW_ACNE, }; @@ -94,8 +95,16 @@ impl<'scene> HitableTrait for GLTF<'scene> { /// Returns a probability density function value based on the object #[must_use] - fn pdf_value(&self, origin: Vec3, vector: Vec3, time: Float, rng: &mut SmallRng) -> Float { - self.bvhnode.pdf_value(origin, vector, time, rng) + fn pdf_value( + &self, + origin: Vec3, + vector: Vec3, + wavelength: Wavelength, + time: Float, + rng: &mut SmallRng, + ) -> Float { + self.bvhnode + .pdf_value(origin, vector, wavelength, time, rng) } /// Returns a random point on the ssurface of the object @@ -324,14 +333,22 @@ impl<'scene> HitableTrait for GLTFTriangle<'scene> { Some(&self.aabb) } - 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 { + let ray = Ray { + origin, + direction: vector, + time, + wavelength, + }; // TODO: this is from quad and not updated! - match self.hit( - &Ray::new(origin, vector, time), - EPSILON_SHADOW_ACNE, - Float::INFINITY, - rng, - ) { + match self.hit(&ray, EPSILON_SHADOW_ACNE, Float::INFINITY, rng) { Some(hit_record) => { let distance_squared = hit_record.distance * hit_record.distance * vector.norm_squared(); diff --git a/clovers/src/objects/moving_sphere.rs b/clovers/src/objects/moving_sphere.rs index a74cbfb2..9a4b4770 100644 --- a/clovers/src/objects/moving_sphere.rs +++ b/clovers/src/objects/moving_sphere.rs @@ -6,6 +6,7 @@ use crate::{ materials::{Material, MaterialInit}, random::random_in_unit_sphere, ray::Ray, + spectral::Wavelength, Float, Vec3, PI, }; use rand::rngs::SmallRng; @@ -160,7 +161,14 @@ impl<'scene> HitableTrait for MovingSphere<'scene> { Some(&self.aabb) } - 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 { // TODO: fix 0.0 } diff --git a/clovers/src/objects/quad.rs b/clovers/src/objects/quad.rs index 0205eba5..dc7104a0 100644 --- a/clovers/src/objects/quad.rs +++ b/clovers/src/objects/quad.rs @@ -3,6 +3,7 @@ use crate::hitable::HitableTrait; use crate::materials::MaterialInit; +use crate::spectral::Wavelength; use crate::EPSILON_SHADOW_ACNE; use crate::{ aabb::AABB, hitable::get_orientation, hitable::HitRecord, materials::Material, ray::Ray, Float, @@ -134,13 +135,21 @@ impl<'scene> HitableTrait for Quad<'scene> { /// Returns a probability density function value? // TODO: understand & explain #[must_use] - fn pdf_value(&self, origin: Vec3, vector: Vec3, time: Float, rng: &mut SmallRng) -> Float { - match self.hit( - &Ray::new(origin, vector, time), - EPSILON_SHADOW_ACNE, - Float::INFINITY, - rng, - ) { + fn pdf_value( + &self, + origin: Vec3, + vector: Vec3, + wavelength: Wavelength, + time: Float, + rng: &mut SmallRng, + ) -> Float { + let ray = Ray { + origin, + direction: vector, + time, + wavelength, + }; + match self.hit(&ray, EPSILON_SHADOW_ACNE, Float::INFINITY, rng) { Some(hit_record) => { let distance_squared = hit_record.distance * hit_record.distance * vector.norm_squared(); diff --git a/clovers/src/objects/rotate.rs b/clovers/src/objects/rotate.rs index 9981a8ec..df30007d 100644 --- a/clovers/src/objects/rotate.rs +++ b/clovers/src/objects/rotate.rs @@ -4,6 +4,7 @@ use crate::{ aabb::AABB, hitable::{HitRecord, Hitable, HitableTrait}, ray::Ray, + spectral::Wavelength, Box, Float, Vec3, }; use rand::rngs::SmallRng; @@ -113,7 +114,12 @@ impl<'scene> HitableTrait for RotateY<'scene> { direction[0] = self.cos_theta * ray.direction[0] - self.sin_theta * ray.direction[2]; direction[2] = self.sin_theta * ray.direction[0] + self.cos_theta * ray.direction[2]; - let rotated_r: Ray = Ray::new(origin, direction, ray.time); + let rotated_r: Ray = Ray { + origin, + direction, + time: ray.time, + wavelength: ray.wavelength, + }; let Some(hit_record) = self.object.hit(&rotated_r, distance_min, distance_max, rng) else { // Did not hit rotated object, early return None @@ -153,7 +159,14 @@ impl<'scene> HitableTrait for RotateY<'scene> { self.aabb.as_ref() } - 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 { // TODO: fix 0.0 } diff --git a/clovers/src/objects/sphere.rs b/clovers/src/objects/sphere.rs index f07ebde6..62bd4d35 100644 --- a/clovers/src/objects/sphere.rs +++ b/clovers/src/objects/sphere.rs @@ -6,6 +6,7 @@ use crate::{ materials::{Material, MaterialInit}, onb::ONB, ray::Ray, + spectral::Wavelength, Float, Vec3, EPSILON_SHADOW_ACNE, PI, }; use rand::{rngs::SmallRng, Rng}; @@ -126,13 +127,21 @@ impl<'scene> HitableTrait for Sphere<'scene> { /// Returns the probability density function for the sphere? TODO: what does this do again and how #[must_use] - fn pdf_value(&self, origin: Vec3, vector: Vec3, time: Float, rng: &mut SmallRng) -> Float { - match self.hit( - &Ray::new(origin, vector, time), - EPSILON_SHADOW_ACNE, - Float::INFINITY, - rng, - ) { + fn pdf_value( + &self, + origin: Vec3, + vector: Vec3, + wavelength: Wavelength, + time: Float, + rng: &mut SmallRng, + ) -> Float { + let ray = Ray { + origin, + direction: vector, + time, + wavelength, + }; + match self.hit(&ray, EPSILON_SHADOW_ACNE, Float::INFINITY, rng) { None => 0.0, Some(_hit_record) => { let cos_theta_max = (1.0 diff --git a/clovers/src/objects/stl.rs b/clovers/src/objects/stl.rs index a35d8cae..fe8907dc 100644 --- a/clovers/src/objects/stl.rs +++ b/clovers/src/objects/stl.rs @@ -13,6 +13,7 @@ use crate::{ materials::{Material, MaterialInit, SharedMaterial}, objects::Triangle, ray::Ray, + spectral::Wavelength, Float, Vec3, }; @@ -48,8 +49,16 @@ impl<'scene> HitableTrait for STL<'scene> { /// Returns a probability density function value based on the object #[must_use] - fn pdf_value(&self, origin: Vec3, vector: Vec3, time: Float, rng: &mut SmallRng) -> Float { - self.bvhnode.pdf_value(origin, vector, time, rng) + fn pdf_value( + &self, + origin: Vec3, + vector: Vec3, + wavelength: Wavelength, + time: Float, + rng: &mut SmallRng, + ) -> Float { + self.bvhnode + .pdf_value(origin, vector, wavelength, time, rng) } /// Returns a random point on the ssurface of the object diff --git a/clovers/src/objects/translate.rs b/clovers/src/objects/translate.rs index b29354db..5b0eae10 100644 --- a/clovers/src/objects/translate.rs +++ b/clovers/src/objects/translate.rs @@ -4,6 +4,7 @@ use crate::{ aabb::AABB, hitable::{HitRecord, Hitable, HitableTrait}, ray::Ray, + spectral::Wavelength, Box, Float, Vec3, }; use rand::rngs::SmallRng; @@ -52,7 +53,12 @@ impl<'scene> HitableTrait for Translate<'scene> { distance_max: Float, rng: &mut SmallRng, ) -> Option { - let moved_ray: Ray = Ray::new(ray.origin - self.offset, ray.direction, ray.time); + let moved_ray = Ray { + origin: ray.origin - self.offset, + direction: ray.direction, + time: ray.time, + wavelength: ray.wavelength, + }; match self.object.hit(&moved_ray, distance_min, distance_max, rng) { // Didn't hit anything, return None @@ -74,10 +80,17 @@ impl<'scene> HitableTrait for Translate<'scene> { /// Returns a probability density function value based on the inner object #[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 { // TODO: is this correct? self.object - .pdf_value(origin + self.offset, vector, time, rng) + .pdf_value(origin + self.offset, vector, wavelength, time, rng) } /// Returns a random point on the surface of the moved object diff --git a/clovers/src/objects/triangle.rs b/clovers/src/objects/triangle.rs index c3b48809..35d289d7 100644 --- a/clovers/src/objects/triangle.rs +++ b/clovers/src/objects/triangle.rs @@ -4,10 +4,11 @@ use crate::hitable::HitableTrait; use crate::interval::Interval; use crate::materials::MaterialInit; +use crate::spectral::Wavelength; use crate::EPSILON_SHADOW_ACNE; use crate::{ - aabb::AABB, hitable::get_orientation, hitable::HitRecord, materials::Material, ray::Ray, Float, - Vec3, EPSILON_RECT_THICKNESS, + aabb::AABB, hitable::HitRecord, materials::Material, ray::Ray, Float, Vec3, + EPSILON_RECT_THICKNESS, }; use rand::rngs::SmallRng; use rand::Rng; @@ -146,18 +147,18 @@ impl<'scene> HitableTrait for Triangle<'scene> { } // Ray hits the 2D shape; set the rest of the hit record and return - - let (front_face, normal) = get_orientation(ray, self.normal); - - Some(HitRecord { + let mut record = HitRecord { distance: t, position: intersection, - normal, u: alpha, v: beta, material: self.material, - front_face, - }) + normal: self.normal, + front_face: false, + }; + record.set_face_normal(ray, self.normal); + + Some(record) } /// Returns the bounding box of the triangle @@ -170,14 +171,22 @@ impl<'scene> HitableTrait for Triangle<'scene> { /// Returns a probability density function value? // TODO: understand & explain #[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 { + let ray = Ray { + origin, + direction: vector, + time, + wavelength, + }; // TODO: this is from quad and not updated! - match self.hit( - &Ray::new(origin, vector, time), - EPSILON_SHADOW_ACNE, - Float::INFINITY, - rng, - ) { + match self.hit(&ray, EPSILON_SHADOW_ACNE, Float::INFINITY, rng) { Some(hit_record) => { let distance_squared = hit_record.distance * hit_record.distance * vector.norm_squared(); @@ -222,30 +231,30 @@ mod tests { use super::*; + const TIME_0: Float = 0.0; + const TIME_1: Float = 1.0; + const RAY: Ray = Ray { + origin: Vec3::new(0.0, 0.0, -1.0), + direction: Vec3::new(0.0, 0.0, 1.0), + time: TIME_0, + wavelength: 600, + }; + #[test] fn xy_unit_triangle() { - let time_0 = 0.0; - let time_1 = 1.0; + let mut rng = SmallRng::from_entropy(); let material = Box::default(); // Unit triangle at origin - let xy_unit_triangle = Triangle::new( + let triangle = Triangle::new( Vec3::new(0.0, 0.0, 0.0), Vec3::new(1.0, 0.0, 0.0), Vec3::new(0.0, 1.0, 0.0), &material, ); - let ray = Ray::new( - Vec3::new(0.0, 0.0, -1.0).normalize(), - Vec3::new(0.0, 0.0, 1.0).normalize(), - time_0, - ); - - let mut rng = SmallRng::from_entropy(); - - let aabb = xy_unit_triangle - .bounding_box(time_0, time_1) + let aabb = triangle + .bounding_box(TIME_0, TIME_1) .expect("No AABB for the triangle"); let expected_aabb = AABB::new( @@ -256,42 +265,34 @@ mod tests { assert_eq!(aabb, &expected_aabb); - let boxhit = aabb.hit(&ray, time_0, time_1); + let boxhit = aabb.hit(&RAY, TIME_0, TIME_1); assert!(boxhit); - let hit_record = xy_unit_triangle - .hit(&ray, Float::NEG_INFINITY, Float::INFINITY, &mut rng) + let hit_record = triangle + .hit(&RAY, Float::NEG_INFINITY, Float::INFINITY, &mut rng) .expect("No hit record for triangle and ray"); assert!(hit_record.distance - 1.0 <= Float::EPSILON); assert_eq!(hit_record.position, Vec3::new(0.0, 0.0, 0.0)); assert_eq!(hit_record.normal, Vec3::new(0.0, 0.0, -1.0)); + assert!(!hit_record.front_face); } #[test] fn yx_unit_triangle() { - let time_0 = 0.0; - let time_1 = 1.0; + let mut rng = SmallRng::from_entropy(); let material = Box::default(); // Unit triangle at origin, u and v coords swapped - let xy_unit_triangle = Triangle::new( + let triangle = Triangle::new( Vec3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 1.0, 0.0), Vec3::new(1.0, 0.0, 0.0), &material, ); - let ray = Ray::new( - Vec3::new(0.0, 0.0, -1.0).normalize(), - Vec3::new(0.0, 0.0, 1.0).normalize(), - time_0, - ); - - let mut rng = SmallRng::from_entropy(); - - let aabb = xy_unit_triangle - .bounding_box(time_0, time_1) + let aabb = triangle + .bounding_box(TIME_0, TIME_1) .expect("No AABB for the triangle"); let expected_aabb = AABB::new( @@ -302,42 +303,72 @@ mod tests { assert_eq!(aabb, &expected_aabb); - let boxhit = aabb.hit(&ray, time_0, time_1); + let boxhit = aabb.hit(&RAY, TIME_0, TIME_1); assert!(boxhit); - let hit_record = xy_unit_triangle - .hit(&ray, Float::NEG_INFINITY, Float::INFINITY, &mut rng) + let hit_record = triangle + .hit(&RAY, Float::NEG_INFINITY, Float::INFINITY, &mut rng) .expect("No hit record for triangle and ray"); assert!(hit_record.distance - 1.0 <= Float::EPSILON); assert_eq!(hit_record.position, Vec3::new(0.0, 0.0, 0.0)); assert_eq!(hit_record.normal, Vec3::new(0.0, 0.0, -1.0)); + assert!(hit_record.front_face); } #[test] fn neg_xy_unit_triangle() { - let time_0 = 0.0; - let time_1 = 1.0; + let mut rng = SmallRng::from_entropy(); let material: Box = Box::default(); // Unit triangle at origin, u and v coords swapped - let neg_xy_unit_triangle = Triangle::new( + let triangle = Triangle::new( Vec3::new(0.0, 0.0, 0.0), Vec3::new(-1.0, 0.0, 0.0), Vec3::new(0.0, -1.0, 0.0), &material, ); - let ray = Ray::new( - Vec3::new(0.0, 0.0, -1.0).normalize(), - Vec3::new(0.0, 0.0, 1.0).normalize(), - time_0, + let aabb = triangle + .bounding_box(TIME_0, TIME_1) + .expect("No AABB for the triangle"); + + let expected_aabb = AABB::new( + Interval::new(-1.0, 0.0), + Interval::new(-1.0, 0.0), + Interval::new(0.0, 0.0).expand(EPSILON_RECT_THICKNESS), ); + assert_eq!(aabb, &expected_aabb); + + let boxhit = aabb.hit(&RAY, TIME_0, TIME_1); + assert!(boxhit); + + let hit_record = triangle + .hit(&RAY, Float::NEG_INFINITY, Float::INFINITY, &mut rng) + .expect("No hit record for triangle and ray"); + + assert!(hit_record.distance - 1.0 <= Float::EPSILON); + assert_eq!(hit_record.position, Vec3::new(0.0, 0.0, 0.0)); + assert_eq!(hit_record.normal, Vec3::new(0.0, 0.0, -1.0)); + assert!(!hit_record.front_face); + } + + #[test] + fn neg_yx_unit_triangle() { let mut rng = SmallRng::from_entropy(); + let material: Box = Box::default(); + + // Unit triangle at origin, u and v coords swapped + let triangle = Triangle::new( + Vec3::new(0.0, 0.0, 0.0), + Vec3::new(0.0, -1.0, 0.0), + Vec3::new(-1.0, 0.0, 0.0), + &material, + ); - let aabb = neg_xy_unit_triangle - .bounding_box(time_0, time_1) + let aabb = triangle + .bounding_box(TIME_0, TIME_1) .expect("No AABB for the triangle"); let expected_aabb = AABB::new( @@ -348,15 +379,16 @@ mod tests { assert_eq!(aabb, &expected_aabb); - let boxhit = aabb.hit(&ray, time_0, time_1); + let boxhit = aabb.hit(&RAY, TIME_0, TIME_1); assert!(boxhit); - let hit_record = neg_xy_unit_triangle - .hit(&ray, Float::NEG_INFINITY, Float::INFINITY, &mut rng) + let hit_record = triangle + .hit(&RAY, Float::NEG_INFINITY, Float::INFINITY, &mut rng) .expect("No hit record for triangle and ray"); assert!(hit_record.distance - 1.0 <= Float::EPSILON); assert_eq!(hit_record.position, Vec3::new(0.0, 0.0, 0.0)); assert_eq!(hit_record.normal, Vec3::new(0.0, 0.0, -1.0)); + assert!(hit_record.front_face); } } diff --git a/clovers/src/pdf.rs b/clovers/src/pdf.rs index 7e44bdac..0ce5a280 100644 --- a/clovers/src/pdf.rs +++ b/clovers/src/pdf.rs @@ -6,6 +6,7 @@ use crate::{ hitable::{Hitable, HitableTrait}, onb::ONB, random::{random_cosine_direction, random_in_unit_sphere}, + spectral::Wavelength, Box, Float, Vec3, PI, }; use enum_dispatch::enum_dispatch; @@ -25,7 +26,13 @@ pub enum PDF<'scene> { #[enum_dispatch] pub(crate) trait PDFTrait { #[must_use] - fn value(&self, direction: Vec3, time: Float, rng: &mut SmallRng) -> Float; + fn value( + &self, + direction: Vec3, + wavelength: Wavelength, + time: Float, + rng: &mut SmallRng, + ) -> Float; #[must_use] fn generate(&self, rng: &mut SmallRng) -> Vec3; @@ -47,7 +54,13 @@ impl CosinePDF { impl PDFTrait for CosinePDF { #[must_use] - fn value(&self, direction: Vec3, _time: Float, _rng: &mut SmallRng) -> Float { + fn value( + &self, + direction: Vec3, + _wavelength: Wavelength, + _time: Float, + _rng: &mut SmallRng, + ) -> Float { let cosine = direction.normalize().dot(&self.uvw.w); if cosine <= 0.0 { 0.0 @@ -77,8 +90,15 @@ impl<'scene> HitablePDF<'scene> { impl<'scene> PDFTrait for HitablePDF<'scene> { #[must_use] - fn value(&self, direction: Vec3, time: Float, rng: &mut SmallRng) -> Float { - self.hitable.pdf_value(self.origin, direction, time, rng) + fn value( + &self, + direction: Vec3, + wavelength: Wavelength, + time: Float, + rng: &mut SmallRng, + ) -> Float { + self.hitable + .pdf_value(self.origin, direction, wavelength, time, rng) } #[must_use] @@ -106,8 +126,15 @@ impl<'scene> MixturePDF<'scene> { impl<'scene> PDFTrait for MixturePDF<'scene> { #[must_use] - fn value(&self, direction: Vec3, time: Float, rng: &mut SmallRng) -> Float { - 0.5 * self.pdf1.value(direction, time, rng) + 0.5 * self.pdf2.value(direction, time, rng) + fn value( + &self, + direction: Vec3, + wavelength: Wavelength, + time: Float, + rng: &mut SmallRng, + ) -> Float { + 0.5 * self.pdf1.value(direction, wavelength, time, rng) + + 0.5 * self.pdf2.value(direction, wavelength, time, rng) } #[must_use] @@ -132,7 +159,13 @@ impl SpherePDF { impl PDFTrait for SpherePDF { #[must_use] - fn value(&self, _direction: Vec3, _time: Float, _rng: &mut SmallRng) -> Float { + fn value( + &self, + _direction: Vec3, + _wavelength: Wavelength, + _time: Float, + _rng: &mut SmallRng, + ) -> Float { 1.0 / (4.0 * PI) } @@ -155,13 +188,19 @@ impl ZeroPDF { impl PDFTrait for ZeroPDF { #[must_use] - fn value(&self, _direction: Vec3, _time: Float, _rng: &mut SmallRng) -> Float { + fn value( + &self, + _direction: Vec3, + _wavelength: Wavelength, + _time: Float, + _rng: &mut SmallRng, + ) -> Float { 0.0 } #[must_use] - fn generate(&self, _rng: &mut SmallRng) -> Vec3 { - Vec3::new(1.0, 0.0, 0.0) + fn generate(&self, rng: &mut SmallRng) -> Vec3 { + random_in_unit_sphere(rng) } } diff --git a/clovers/src/ray.rs b/clovers/src/ray.rs index d7130586..fd526b71 100644 --- a/clovers/src/ray.rs +++ b/clovers/src/ray.rs @@ -1,6 +1,6 @@ //! The very core of the ray tracing rendering itself: the [Ray](crate::ray::Ray) -use crate::{Float, Vec3}; +use crate::{spectral::Wavelength, Float, Vec3}; /// A Ray has an origin and a direction, as well as an instant in time it exists in. Motion blur is achieved by creating multiple rays with slightly different times. #[derive(Clone, Debug, PartialEq)] @@ -11,19 +11,11 @@ pub struct Ray { pub direction: Vec3, /// The time instant at which the ray exists. pub time: Float, + /// Wavelength of the ray + pub wavelength: Wavelength, } impl Ray { - /// Creates a single Ray. A Ray has an origin and a direction, as well as an instant in time it exists in. Motion blur is achieved by creating multiple rays with slightly different times. - #[must_use] - pub fn new(origin: Vec3, direction: Vec3, time: Float) -> Ray { - Ray { - origin, - direction, - time, - } - } - /// Evaluates the position (coordinate) at which the ray is at the given parameter, considering the origin and direction. Considering a default unit speed of 1 per unit time, this function can be given either a time or a distance. #[must_use] pub fn evaluate(&self, parameter: Float) -> Vec3 { diff --git a/scenes/dispersive.json b/scenes/dispersive.json new file mode 100644 index 00000000..c25bb51c --- /dev/null +++ b/scenes/dispersive.json @@ -0,0 +1,173 @@ +{ + "time_0": 0, + "time_1": 1, + "camera": { + "look_from": [-150, 278, -800], + "look_at": [450, 275, 555], + "up": [0, 1, 0], + "vertical_fov": 40, + "aperture": 0, + "focus_distance": 10 + }, + "background_color": [0, 0, 0], + "objects": [ + { + "kind": "Quad", + "q": [555, 0, 0], + "u": [0, 0, 555], + "v": [0, 555, 0], + "material": "grey wall", + "comment": "wall, left" + }, + { + "kind": "Quad", + "q": [0, 0, 0], + "u": [555, 0, 0], + "v": [0, 0, 555], + "material": "grey wall", + "comment": "floor" + }, + { + "kind": "Quad", + "q": [0, 555, 0], + "u": [555, 0, 0], + "v": [0, 0, 555], + "material": "grey wall", + "comment": "ceiling" + }, + { + "kind": "Quad", + "q": [277, 554, 120], + "u": [2, 0, 0], + "v": [0, 0, 200], + "material": "strong lamp", + "comment": "narrow ceiling light" + }, + { + "kind": "Quad", + "q": [279, 554, 120], + "u": [0, -40, 0], + "v": [0, 0, 200], + "material": "grey wall", + "comment": "light guide, left" + }, + { + "kind": "Quad", + "q": [277, 554, 120], + "u": [0, -40, 0], + "v": [0, 0, 200], + "material": "grey wall", + "comment": "light guide, right" + }, + { + "kind": "Quad", + "q": [0, 0, 555], + "u": [555, 0, 0], + "v": [0, 555, 0], + "material": "grey wall", + "comment": "back wall" + }, + { + "kind": "STL", + "center": [300, 350, 250], + "scale": 20, + "rotation": [0, 0, -30], + "path": "stl/prism.stl", + "material": "Dense flint glass SF10", + "comment": "triangular prism" + } + ], + "priority_objects": [ + { + "kind": "Quad", + "q": [277, 554, 120], + "u": [2, 0, 0], + "v": [0, 0, 200], + "material": "strong lamp", + "comment": "narrow ceiling light" + }, + { + "kind": "STL", + "center": [300, 350, 250], + "scale": 20, + "rotation": [0, 0, -30], + "path": "stl/prism.stl", + "material": "Dense flint glass SF10", + "comment": "triangular prism" + } + ], + "materials": [ + { + "name": "dielectric glass", + "kind": "Dielectric", + "refractive_index": 1.5, + "color": [1, 1, 1] + }, + { + "name": "Fused silica", + "kind": "Dispersive", + "cauchy_a": 1.458, + "cauchy_b": 0.00354 + }, + { + "name": "Borosilicate glass BK7", + "kind": "Dispersive", + "cauchy_a": 1.5046, + "cauchy_b": 0.0042 + }, + { + "name": "Hard crown glass K5", + "kind": "Dispersive", + "cauchy_a": 1.522, + "cauchy_b": 0.00459 + }, + { + "name": "Barium crown glass BaK4", + "kind": "Dispersive", + "cauchy_a": 1.569, + "cauchy_b": 0.00531 + }, + { + "name": "Barium flint glass BaF10", + "kind": "Dispersive", + "cauchy_a": 1.67, + "cauchy_b": 0.00743 + }, + { + "name": "Dense flint glass SF10", + "kind": "Dispersive", + "cauchy_a": 1.728, + "cauchy_b": 0.01342 + }, + { + "name": "Super dispersive glass", + "kind": "Dispersive", + "cauchy_a": 1.8, + "cauchy_b": 0.5 + }, + { + "name": "weak lamp", + "kind": "DiffuseLight", + "emit": { + "kind": "SolidColor", + "color": [10, 10, 10] + } + }, + { + "name": "strong lamp", + "kind": "DiffuseLight", + "emit": { + "kind": "SolidColor", + "color": [1000, 1000, 1000] + } + }, + { + "name": "grey wall", + "kind": "Lambertian", + "albedo": { + "kind": "SolidColor", + "color": [0.73, 0.73, 0.73] + } + } + ] +}