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.
This commit is contained in:
Nathan Vegdahl 2018-12-27 16:09:03 -08:00
parent b598ce499c
commit 5add4cfdb1
10 changed files with 164 additions and 66 deletions

View File

@ -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)

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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<World<'a>,
{
// 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,

View File

@ -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));

View File

@ -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));

View File

@ -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],
}

View File

@ -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,
},

View File

@ -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(