From 5add4cfdb18903f57c144c0bed31b62c8788e247 Mon Sep 17 00:00:00 2001 From: Nathan Vegdahl Date: Thu, 27 Dec 2018 16:09:03 -0800 Subject: [PATCH] Implementing Color as a simple enum rather than as a trait. There's really no reason for it to be a Trait, and this will simplify a lot of things down the road. --- src/color.rs | 133 ++++++++++++++++++++++++++++---- src/light/distant_disk_light.rs | 16 ++-- src/light/rectangle_light.rs | 18 ++--- src/light/sphere_light.rs | 18 ++--- src/parse/psy.rs | 4 +- src/parse/psy_light.rs | 8 +- src/parse/psy_surface_shader.rs | 10 +-- src/scene/world.rs | 4 +- src/shading/mod.rs | 13 ++-- src/tracer.rs | 6 +- 10 files changed, 164 insertions(+), 66 deletions(-) diff --git a/src/color.rs b/src/color.rs index 3e1254d..912ff65 100644 --- a/src/color.rs +++ b/src/color.rs @@ -16,10 +16,6 @@ pub fn map_0_1_to_wavelength(n: f32) -> f32 { n * WL_RANGE + WL_MIN } -pub trait Color { - fn to_spectral_sample(&self, hero_wavelength: f32) -> SpectralSample; -} - #[inline(always)] fn nth_wavelength(hero_wavelength: f32, n: usize) -> f32 { let wl = hero_wavelength + (WL_RANGE_Q * n as f32); @@ -43,6 +39,126 @@ fn wavelengths(hero_wavelength: f32) -> Float4 { //---------------------------------------------------------------- +#[derive(Debug, Copy, Clone)] +pub enum Color { + XYZ(f32, f32, f32), + Blackbody { + temperature: f32, // In kelvin + factor: f32, // Brightness multiplier + }, +} + +impl Color { + #[inline(always)] + pub fn new_xyz(xyz: (f32, f32, f32)) -> Self { + Color::XYZ(xyz.0, xyz.1, xyz.2) + } + + #[inline(always)] + pub fn new_blackbody(temp: f32, fac: f32) -> Self { + Color::Blackbody { + temperature: temp, + factor: fac, + } + } + + pub fn to_spectral_sample(self, hero_wavelength: f32) -> SpectralSample { + let wls = wavelengths(hero_wavelength); + match self { + Color::XYZ(x, y, z) => SpectralSample { + e: xyz_to_spectrum_4((x, y, z), wls), + hero_wavelength: hero_wavelength, + }, + Color::Blackbody { + temperature, + factor, + } => { + SpectralSample::from_parts( + // TODO: make this SIMD + Float4::new( + plancks_law(temperature, wls.get_0()) * factor, + plancks_law(temperature, wls.get_1()) * factor, + plancks_law(temperature, wls.get_2()) * factor, + plancks_law(temperature, wls.get_3()) * factor, + ), + hero_wavelength, + ) + } + } + } + + /// Calculates an approximate total spectral energy of the color. + /// + /// Note: this really is very _approximate_. + pub fn approximate_energy(self) -> f32 { + // TODO: better approximation for Blackbody. + match self { + Color::XYZ(_, y, _) => y, + Color::Blackbody { factor, .. } => factor, + } + } +} + +impl Lerp for Color { + /// Note that this isn't a proper lerp in spectral space. However, + /// for our purposes that should be fine: all we care about is that + /// the interpolation is smooth and "reasonable". + /// + /// If at some point it turns out this causes artifacts, then we + /// also have bigger problems: texture filtering in the shading + /// pipeline will have the same issues, which will be even harder + /// to address. However, I strongly suspect this will not be an issue. + /// (Famous last words!) + fn lerp(self, other: Self, alpha: f32) -> Self { + let inv_alpha = 1.0 - alpha; + match (self, other) { + (Color::XYZ(x1, y1, z1), Color::XYZ(x2, y2, z2)) => Color::XYZ( + (x1 * inv_alpha) + (x2 * alpha), + (y1 * inv_alpha) + (y2 * alpha), + (z1 * inv_alpha) + (z2 * alpha), + ), + ( + Color::Blackbody { + temperature: tmp1, + factor: fac1, + }, + Color::Blackbody { + temperature: tmp2, + factor: fac2, + }, + ) => Color::Blackbody { + temperature: (tmp1 * inv_alpha) + (tmp2 * alpha), + factor: (fac1 * inv_alpha) + (fac2 * alpha), + }, + _ => panic!("Cannot lerp colors with different representations."), + } + } +} + +fn plancks_law(temperature: f32, wavelength: f32) -> f32 { + const C: f32 = 299_792_458.0; // Speed of light + const H: f32 = 6.62607015e-34; // Planck constant + const KB: f32 = 1.38064852e-23; // Boltzmann constant + + // // As written at https://en.wikipedia.org/wiki/Planck's_law, here for + // // reference and clarity: + // let a = (2.0 * H * C * C) / (wavelength * wavelength * wavelength * wavelength * wavelength); + // let b = 1.0 / (((H * C) / (wavelength * KB * temperature)).exp() - 1.0); + // a * b + + // Optimized version of the commented code above: + const TMP1: f32 = (2.0f64 * H as f64 * C as f64 * C as f64) as f32; + const TMP2: f32 = (H as f64 * C as f64 / KB as f64) as f32; + let wl5 = { + let wl2 = wavelength * wavelength; + wl2 * wl2 * wavelength + }; + let tmp3 = wl5 * (fast_exp(TMP2 / (wavelength * temperature)) - 1.0); + TMP1 / tmp3 +} + +//---------------------------------------------------------------- + #[derive(Copy, Clone, Debug)] pub struct SpectralSample { pub e: Float4, @@ -197,15 +313,6 @@ impl XYZ { } } -impl Color for XYZ { - fn to_spectral_sample(&self, hero_wavelength: f32) -> SpectralSample { - SpectralSample { - e: xyz_to_spectrum_4((self.x, self.y, self.z), wavelengths(hero_wavelength)), - hero_wavelength: hero_wavelength, - } - } -} - impl Lerp for XYZ { fn lerp(self, other: XYZ, alpha: f32) -> XYZ { (self * (1.0 - alpha)) + (other * alpha) diff --git a/src/light/distant_disk_light.rs b/src/light/distant_disk_light.rs index bb89f5d..6867124 100644 --- a/src/light/distant_disk_light.rs +++ b/src/light/distant_disk_light.rs @@ -3,7 +3,7 @@ use std::f64::consts::PI as PI_64; use mem_arena::MemArena; use crate::{ - color::{Color, SpectralSample, XYZ}, + color::{Color, SpectralSample}, lerp::lerp_slice, math::{coordinate_system_from_vector, Vector}, sampling::{uniform_sample_cone, uniform_sample_cone_pdf}, @@ -17,7 +17,7 @@ use super::WorldLightSource; pub struct DistantDiskLight<'a> { radii: &'a [f32], directions: &'a [Vector], - colors: &'a [XYZ], + colors: &'a [Color], } impl<'a> DistantDiskLight<'a> { @@ -25,7 +25,7 @@ impl<'a> DistantDiskLight<'a> { arena: &'a MemArena, radii: &[f32], directions: &[Vector], - colors: &[XYZ], + colors: &[Color], ) -> DistantDiskLight<'a> { DistantDiskLight { radii: arena.copy_slice(&radii), @@ -78,7 +78,7 @@ impl<'a> WorldLightSource for DistantDiskLight<'a> { let sample = uniform_sample_cone(u, v, cos_theta_max).normalized(); // Calculate the final values and return everything. - let spectral_sample = (col * solid_angle_inv as f32).to_spectral_sample(wavelength); + let spectral_sample = col.to_spectral_sample(wavelength) * solid_angle_inv as f32; let shadow_vec = (x * sample.x()) + (y * sample.y()) + (z * sample.z()); let pdf = uniform_sample_cone_pdf(cos_theta_max); (spectral_sample, shadow_vec, pdf as f32) @@ -89,11 +89,9 @@ impl<'a> WorldLightSource for DistantDiskLight<'a> { } fn approximate_energy(&self) -> f32 { - let color: XYZ = self - .colors + self.colors .iter() - .fold(XYZ::new(0.0, 0.0, 0.0), |a, &b| a + b) - / self.colors.len() as f32; - color.y + .fold(0.0, |a, &b| a + b.approximate_energy()) + / self.colors.len() as f32 } } diff --git a/src/light/rectangle_light.rs b/src/light/rectangle_light.rs index edcb65c..d9818bd 100644 --- a/src/light/rectangle_light.rs +++ b/src/light/rectangle_light.rs @@ -3,7 +3,7 @@ use mem_arena::MemArena; use crate::{ bbox::BBox, boundable::Boundable, - color::{Color, SpectralSample, XYZ}, + color::{Color, SpectralSample}, lerp::lerp_slice, math::{cross, dot, Matrix4x4, Normal, Point, Vector}, ray::{AccelRay, Ray}, @@ -23,7 +23,7 @@ const SIMPLE_SAMPLING_THRESHOLD: f32 = 0.01; #[derive(Copy, Clone, Debug)] pub struct RectangleLight<'a> { dimensions: &'a [(f32, f32)], - colors: &'a [XYZ], + colors: &'a [Color], bounds_: &'a [BBox], } @@ -31,7 +31,7 @@ impl<'a> RectangleLight<'a> { pub fn new<'b>( arena: &'b MemArena, dimensions: &[(f32, f32)], - colors: &[XYZ], + colors: &[Color], ) -> RectangleLight<'b> { let bbs: Vec<_> = dimensions .iter() @@ -188,7 +188,7 @@ impl<'a> SurfaceLight for RectangleLight<'a> { .into_point(); let shadow_vec = sample_point - arr; let spectral_sample = - (col * surface_area_inv as f32 * 0.5).to_spectral_sample(wavelength); + (col).to_spectral_sample(wavelength) * surface_area_inv as f32 * 0.5; let pdf = (sample_point - arr).length2() / dot(shadow_vec.normalized(), normal.into_vector().normalized()).abs() / (surface_area_1 + surface_area_2); @@ -232,7 +232,7 @@ impl<'a> SurfaceLight for RectangleLight<'a> { // Calculate pdf and light energy let pdf = 1.0 / (area_1 + area_2); // PDF of the ray direction being sampled let spectral_sample = - (col * surface_area_inv as f32 * 0.5).to_spectral_sample(wavelength); + col.to_spectral_sample(wavelength) * surface_area_inv as f32 * 0.5; ( spectral_sample, @@ -247,12 +247,10 @@ impl<'a> SurfaceLight for RectangleLight<'a> { } fn approximate_energy(&self) -> f32 { - let color: XYZ = self - .colors + self.colors .iter() - .fold(XYZ::new(0.0, 0.0, 0.0), |a, &b| a + b) - / self.colors.len() as f32; - color.y + .fold(0.0, |a, &b| a + b.approximate_energy()) + / self.colors.len() as f32 } } diff --git a/src/light/sphere_light.rs b/src/light/sphere_light.rs index 008d689..9132ec8 100644 --- a/src/light/sphere_light.rs +++ b/src/light/sphere_light.rs @@ -5,7 +5,7 @@ use mem_arena::MemArena; use crate::{ bbox::BBox, boundable::Boundable, - color::{Color, SpectralSample, XYZ}, + color::{Color, SpectralSample}, lerp::lerp_slice, math::{coordinate_system_from_vector, dot, Matrix4x4, Normal, Point, Vector}, ray::{AccelRay, Ray}, @@ -26,12 +26,12 @@ const SAMPLE_POINT_FUDGE: f32 = 0.001; #[derive(Copy, Clone, Debug)] pub struct SphereLight<'a> { radii: &'a [f32], - colors: &'a [XYZ], + colors: &'a [Color], bounds_: &'a [BBox], } impl<'a> SphereLight<'a> { - pub fn new<'b>(arena: &'b MemArena, radii: &[f32], colors: &[XYZ]) -> SphereLight<'b> { + pub fn new<'b>(arena: &'b MemArena, radii: &[f32], colors: &[Color]) -> SphereLight<'b> { let bbs: Vec<_> = radii .iter() .map(|r| BBox { @@ -164,7 +164,7 @@ impl<'a> SurfaceLight for SphereLight<'a> { ) }; let pdf = uniform_sample_cone_pdf(cos_theta_max); - let spectral_sample = (col * surface_area_inv as f32).to_spectral_sample(wavelength); + let spectral_sample = col.to_spectral_sample(wavelength) * surface_area_inv as f32; return ( spectral_sample, (sample_point, normal, sample_point_err), @@ -182,7 +182,7 @@ impl<'a> SurfaceLight for SphereLight<'a> { ) }; let pdf = 1.0 / (4.0 * PI_64); - let spectral_sample = (col * surface_area_inv as f32).to_spectral_sample(wavelength); + let spectral_sample = col.to_spectral_sample(wavelength) * surface_area_inv as f32; return ( spectral_sample, (sample_point, normal, sample_point_err), @@ -196,12 +196,10 @@ impl<'a> SurfaceLight for SphereLight<'a> { } fn approximate_energy(&self) -> f32 { - let color: XYZ = self - .colors + self.colors .iter() - .fold(XYZ::new(0.0, 0.0, 0.0), |a, &b| a + b) - / self.colors.len() as f32; - color.y + .fold(0.0, |a, &b| a + b.approximate_energy()) + / self.colors.len() as f32 } } diff --git a/src/parse/psy.rs b/src/parse/psy.rs index 77d7913..343278c 100644 --- a/src/parse/psy.rs +++ b/src/parse/psy.rs @@ -8,7 +8,7 @@ use mem_arena::MemArena; use crate::{ camera::Camera, - color::{rec709_e_to_xyz, XYZ}, + color::{rec709_e_to_xyz, Color}, light::WorldLightSource, math::Matrix4x4, renderer::Renderer, @@ -504,7 +504,7 @@ fn parse_world<'a>(arena: &'a MemArena, tree: &'a DataTree) -> Result, { // TODO: proper color space management, not just assuming // rec.709. - background_color = XYZ::from_tuple(rec709_e_to_xyz(color)); + background_color = Color::new_xyz(rec709_e_to_xyz(color)); } else { return Err(PsyParseError::IncorrectLeafData( byte_offset, diff --git a/src/parse/psy_light.rs b/src/parse/psy_light.rs index 288cbd2..ea968ca 100644 --- a/src/parse/psy_light.rs +++ b/src/parse/psy_light.rs @@ -7,7 +7,7 @@ use nom::{call, closure, tuple, tuple_parser, IResult}; use mem_arena::MemArena; use crate::{ - color::{rec709_e_to_xyz, XYZ}, + color::{rec709_e_to_xyz, Color}, light::{DistantDiskLight, RectangleLight, SphereLight}, math::Vector, }; @@ -68,7 +68,7 @@ pub fn parse_distant_disk_light<'a>( // TODO: handle color space conversions properly. // Probably will need a special color type with its // own parser...? - colors.push(XYZ::from_tuple(rec709_e_to_xyz(color))); + colors.push(Color::new_xyz(rec709_e_to_xyz(color))); } else { // Found color, but its contents is not in the right format return Err(PsyParseError::UnknownError(byte_offset)); @@ -122,7 +122,7 @@ pub fn parse_sphere_light<'a>( // TODO: handle color space conversions properly. // Probably will need a special color type with its // own parser...? - colors.push(XYZ::from_tuple(rec709_e_to_xyz(color))); + colors.push(Color::new_xyz(rec709_e_to_xyz(color))); } else { // Found color, but its contents is not in the right format return Err(PsyParseError::UnknownError(byte_offset)); @@ -178,7 +178,7 @@ pub fn parse_rectangle_light<'a>( // TODO: handle color space conversions properly. // Probably will need a special color type with its // own parser...? - colors.push(XYZ::from_tuple(rec709_e_to_xyz(color))); + colors.push(Color::new_xyz(rec709_e_to_xyz(color))); } else { // Found color, but its contents is not in the right format return Err(PsyParseError::UnknownError(byte_offset)); diff --git a/src/parse/psy_surface_shader.rs b/src/parse/psy_surface_shader.rs index 4aed1c7..bf176f9 100644 --- a/src/parse/psy_surface_shader.rs +++ b/src/parse/psy_surface_shader.rs @@ -7,7 +7,7 @@ use nom::{call, closure, tuple, tuple_parser, IResult}; use mem_arena::MemArena; use crate::{ - color::{rec709_e_to_xyz, XYZ}, + color::{rec709_e_to_xyz, Color}, shading::{SimpleSurfaceShader, SurfaceShader}, }; @@ -44,7 +44,7 @@ pub fn parse_surface_shader<'a>( // TODO: handle color space conversions properly. // Probably will need a special color type with its // own parser...? - XYZ::from_tuple(rec709_e_to_xyz(color)) + Color::new_xyz(rec709_e_to_xyz(color)) } else { // Found color, but its contents is not in the right format return Err(PsyParseError::UnknownError(byte_offset)); @@ -68,7 +68,7 @@ pub fn parse_surface_shader<'a>( // TODO: handle color space conversions properly. // Probably will need a special color type with its // own parser...? - XYZ::from_tuple(rec709_e_to_xyz(color)) + Color::new_xyz(rec709_e_to_xyz(color)) } else { // Found color, but its contents is not in the right format return Err(PsyParseError::UnknownError(byte_offset)); @@ -93,7 +93,7 @@ pub fn parse_surface_shader<'a>( // TODO: handle color space conversions properly. // Probably will need a special color type with its // own parser...? - XYZ::from_tuple(rec709_e_to_xyz(color)) + Color::new_xyz(rec709_e_to_xyz(color)) } else { // Found color, but its contents is not in the right format return Err(PsyParseError::UnknownError(byte_offset)); @@ -172,7 +172,7 @@ pub fn parse_surface_shader<'a>( // TODO: handle color space conversions properly. // Probably will need a special color type with its // own parser...? - XYZ::from_tuple(rec709_e_to_xyz(color)) + Color::new_xyz(rec709_e_to_xyz(color)) } else { // Found color, but its contents is not in the right format return Err(PsyParseError::UnknownError(byte_offset)); diff --git a/src/scene/world.rs b/src/scene/world.rs index baf6615..1c1e930 100644 --- a/src/scene/world.rs +++ b/src/scene/world.rs @@ -1,7 +1,7 @@ -use crate::{color::XYZ, light::WorldLightSource}; +use crate::{color::Color, light::WorldLightSource}; #[derive(Debug)] pub struct World<'a> { - pub background_color: XYZ, + pub background_color: Color, pub lights: &'a [&'a WorldLightSource], } diff --git a/src/shading/mod.rs b/src/shading/mod.rs index 0c16bf4..a94046e 100644 --- a/src/shading/mod.rs +++ b/src/shading/mod.rs @@ -2,10 +2,7 @@ pub mod surface_closure; use std::fmt::Debug; -use crate::{ - color::{Color, XYZ}, - surface::SurfaceIntersectionData, -}; +use crate::{color::Color, surface::SurfaceIntersectionData}; use self::surface_closure::{ EmitClosure, GGXClosure, GTRClosure, LambertClosure, SurfaceClosureUnion, @@ -37,19 +34,19 @@ pub trait SurfaceShader: Debug + Sync { #[derive(Debug, Copy, Clone)] pub enum SimpleSurfaceShader { Emit { - color: XYZ, + color: Color, }, Lambert { - color: XYZ, + color: Color, }, GTR { - color: XYZ, + color: Color, roughness: f32, tail_shape: f32, fresnel: f32, }, GGX { - color: XYZ, + color: Color, roughness: f32, fresnel: f32, }, diff --git a/src/tracer.rs b/src/tracer.rs index c3392be..4105dfc 100644 --- a/src/tracer.rs +++ b/src/tracer.rs @@ -2,7 +2,7 @@ use std::iter; use crate::{ algorithm::partition, - color::{rec709_to_xyz, XYZ}, + color::{rec709_to_xyz, Color}, lerp::lerp_slice, ray::{AccelRay, Ray}, scene::{Assembly, InstanceType, Object}, @@ -182,7 +182,7 @@ impl<'a> TracerInner<'a> { match *obj { Object::Surface(surface) => { let unassigned_shader = SimpleSurfaceShader::Emit { - color: XYZ::from_tuple(rec709_to_xyz((1.0, 0.0, 1.0))), + color: Color::new_xyz(rec709_to_xyz((1.0, 0.0, 1.0))), }; let shader = surface_shader.unwrap_or(&unassigned_shader); @@ -198,7 +198,7 @@ impl<'a> TracerInner<'a> { Object::SurfaceLight(surface) => { // Lights don't use shaders let bogus_shader = SimpleSurfaceShader::Emit { - color: XYZ::from_tuple(rec709_to_xyz((1.0, 0.0, 1.0))), + color: Color::new_xyz(rec709_to_xyz((1.0, 0.0, 1.0))), }; surface.intersect_rays(