diff --git a/clovers-cli/src/main.rs b/clovers-cli/src/main.rs index 4923b397..6c94dfc5 100644 --- a/clovers-cli/src/main.rs +++ b/clovers-cli/src/main.rs @@ -46,7 +46,7 @@ struct Opts { #[clap(short = 'd', long, default_value = "100")] max_depth: u32, /// Gamma correction value - #[clap(short, long, default_value = "2.0")] + #[clap(short, long, default_value = "2.2")] gamma: Float, /// Suppress most of the text output #[clap(short, long)] diff --git a/clovers/src/bvhnode.rs b/clovers/src/bvhnode.rs index 79d9018b..4f7ace23 100644 --- a/clovers/src/bvhnode.rs +++ b/clovers/src/bvhnode.rs @@ -6,9 +6,9 @@ use rand::{rngs::SmallRng, Rng}; use crate::{ aabb::AABB, + colors::Wavelength, hitable::{Empty, HitRecord, Hitable, HitableTrait}, ray::Ray, - spectral::Wavelength, Box, Float, Vec, Vec3, }; diff --git a/clovers/src/camera.rs b/clovers/src/camera.rs index 300ac7c0..3d4bf789 100644 --- a/clovers/src/camera.rs +++ b/clovers/src/camera.rs @@ -2,7 +2,7 @@ #![allow(clippy::too_many_arguments)] // TODO: Camera::new() has a lot of arguments. -use crate::spectral::random_wavelength; +use crate::colors::random_wavelength; use crate::{random::random_in_unit_disk, ray::Ray, Float, Vec3, PI}; use rand::rngs::SmallRng; use rand::Rng; diff --git a/clovers/src/color.rs b/clovers/src/color.rs index 7531a48a..e6cbc02d 100644 --- a/clovers/src/color.rs +++ b/clovers/src/color.rs @@ -2,6 +2,7 @@ // TODO: more flexible colors? +use crate::colors::sRGB; use crate::{Float, Vec3}; use core::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign}; use rand::rngs::SmallRng; @@ -200,3 +201,11 @@ impl Div for Color { Color::new(self.r / rhs, self.g / rhs, self.b / rhs) } } + +impl From for Color { + fn from(value: sRGB) -> Self { + // TODO: verify correctness / possibly remove the simplistic `Color` type + let sRGB { r, g, b } = value; + Color { r, g, b } + } +} diff --git a/clovers/src/colorize.rs b/clovers/src/colorize.rs index c5954584..9730b787 100644 --- a/clovers/src/colorize.rs +++ b/clovers/src/colorize.rs @@ -2,6 +2,7 @@ use crate::{ color::Color, + colors::{sRGB, sRGB_Linear, XYZ_Normalized, XYZ_Tristimulus}, hitable::HitableTrait, materials::MaterialType, pdf::{HitablePDF, MixturePDF, PDFTrait, PDF}, @@ -29,7 +30,12 @@ pub fn colorize(ray: &Ray, scene: &Scene, depth: u32, max_depth: u32, rng: &mut }; // Spectral rendering: compute a tint based on the current ray's wavelength - let tint: Color = ray.wavelength.into(); + // TODO: all color handling in XYZ space? + let tint: XYZ_Tristimulus = ray.wavelength.into(); + let tint: XYZ_Normalized = tint.into(); + let tint: sRGB_Linear = tint.into(); + let tint: sRGB = tint.into(); + let tint: Color = tint.into(); // Get the emitted color from the surface that we just hit let mut emitted: Color = hit_record.material.emit( diff --git a/clovers/src/colors.rs b/clovers/src/colors.rs new file mode 100644 index 00000000..ad7dcc0a --- /dev/null +++ b/clovers/src/colors.rs @@ -0,0 +1,9 @@ +//! Color utilities. + +pub mod photon; +pub mod rgb; +pub mod xyz; + +pub use photon::*; +pub use rgb::*; +pub use xyz::*; diff --git a/clovers/src/colors/photon.rs b/clovers/src/colors/photon.rs new file mode 100644 index 00000000..b5bc6e0f --- /dev/null +++ b/clovers/src/colors/photon.rs @@ -0,0 +1,38 @@ +//! The fundamental building blocks of spectral rendering. + +use core::{array::from_fn, ops::Range}; +use rand::rngs::SmallRng; +use rand_distr::uniform::SampleRange; + +/// A fundamental light particle. +#[derive(Copy, Clone, Debug)] +#[cfg_attr(feature = "serde-derive", derive(serde::Serialize, serde::Deserialize))] +pub struct Photon { + /// Wavelength of the photon + pub wavelength: Wavelength, + // TODO: spin for polarization + // pub spin: bool, +} + +/// Wavelength in nanometers +pub type Wavelength = usize; + +const MIN_WAVELENGTH: Wavelength = 380; +const MAX_WAVELENGTH: Wavelength = 780; +const SPECTRUM: Range = MIN_WAVELENGTH..MAX_WAVELENGTH; +const SPECTRUM_SIZE: usize = MAX_WAVELENGTH - MIN_WAVELENGTH; +const WAVE_SAMPLE_COUNT: usize = 4; + +/// Return a random wavelength, sampled uniformly from the visible spectrum. +pub fn random_wavelength(rng: &mut SmallRng) -> Wavelength { + SPECTRUM.sample_single(rng) +} + +/// Given a hero wavelength, create additional equidistant wavelengths in the visible spectrum. Returns an array of wavelengths, with the original hero wavelength as the first one. +#[must_use] +pub fn rotate_wavelength(hero: Wavelength) -> [Wavelength; WAVE_SAMPLE_COUNT] { + from_fn(|j| { + (hero - MIN_WAVELENGTH + ((1 + j) / WAVE_SAMPLE_COUNT) * SPECTRUM_SIZE) + % (SPECTRUM_SIZE + MIN_WAVELENGTH) + }) +} diff --git a/clovers/src/colors/rgb.rs b/clovers/src/colors/rgb.rs new file mode 100644 index 00000000..c0fde3a2 --- /dev/null +++ b/clovers/src/colors/rgb.rs @@ -0,0 +1,70 @@ +//! RGB colorspace utilities. + +use crate::Float; + +use super::XYZ_Normalized; + +/// Linear `sRGB` color based on three [Floats](crate::Float) values. +#[derive(Copy, Clone, Debug)] +#[cfg_attr(feature = "serde-derive", derive(serde::Serialize, serde::Deserialize))] +#[allow(non_camel_case_types)] +pub struct sRGB_Linear { + /// The red component of the color, as a [Float] + pub r: Float, + /// The green component of the color, as a [Float] + pub g: Float, + /// The blue component of the color, as a [Float] + pub b: Float, +} + +/// Conversion from XYZ (D65) to linear `sRGB` values +impl From for sRGB_Linear { + fn from(value: XYZ_Normalized) -> Self { + let XYZ_Normalized { x, y, z } = value; + let r = 3.240_625_5 * x - 1.537_208 * y - 0.498_628_6 * z; + let g = -0.968_930_7 * x + 1.875_756_1 * y + 0.041_517_5 * z; + let b = 0.055_710_1 * x - 0.204_021_1 * y + 1.056_995_9 * z; + + let r = r.clamp(0.0, 1.0); + let g = g.clamp(0.0, 1.0); + let b = b.clamp(0.0, 1.0); + + sRGB_Linear { r, g, b } + } +} + +/// Color component transfer function. +/// Note: Produces `sRGB` digital values with a range 0 to 1, which must then be multiplied by 2^(bit depth) – 1 and quantized. +/// +#[must_use] +pub fn color_component_transfer(c: Float) -> Float { + if c.abs() < 0.003_130_8 { + 12.92 * c + } else { + 1.055 * c.powf(1.0 / 2.4) - 0.055 + } +} + +/// Gamma-corrected `sRGB` color based on three [Floats](crate::Float) values. +#[derive(Copy, Clone, Debug)] +#[cfg_attr(feature = "serde-derive", derive(serde::Serialize, serde::Deserialize))] +#[allow(non_camel_case_types)] +pub struct sRGB { + /// The red component of the color, as a [Float] + pub r: Float, + /// The green component of the color, as a [Float] + pub g: Float, + /// The blue component of the color, as a [Float] + pub b: Float, +} + +impl From for sRGB { + fn from(value: sRGB_Linear) -> Self { + let sRGB_Linear { r, g, b } = value; + sRGB { + r: color_component_transfer(r), + g: color_component_transfer(g), + b: color_component_transfer(b), + } + } +} diff --git a/clovers/src/colors/xyz.rs b/clovers/src/colors/xyz.rs new file mode 100644 index 00000000..358bcf46 --- /dev/null +++ b/clovers/src/colors/xyz.rs @@ -0,0 +1,78 @@ +//! CIE 1931 XYZ colorspace utilities. + +use crate::Float; + +use super::Wavelength; + +/// CIE 1931 XYZ Tristimulus color based on three [Floats](crate::Float) values. +#[derive(Copy, Clone, Debug)] +#[cfg_attr(feature = "serde-derive", derive(serde::Serialize, serde::Deserialize))] +#[allow(non_camel_case_types)] +pub struct XYZ_Tristimulus { + /// The x component of the color, as a [Float] + pub x: Float, + /// The y component of the color, as a [Float] + pub y: Float, + /// The z component of the color, as a [Float] + pub z: Float, +} + +/// Helper function adapted from +fn gaussian(x: Float, alpha: Float, mu: Float, sigma1: Float, sigma2: Float) -> Float { + let t = (x - mu) / (if x < mu { sigma1 } else { sigma2 }); + alpha * (-(t * t) / 2.0).exp() +} + +/// Helper function adapted from +impl From for XYZ_Tristimulus { + // TODO: precision loss + #[allow(clippy::cast_precision_loss)] + fn from(lambda: Wavelength) -> Self { + // With the wavelength λ measured in nanometers, we then approximate the 1931 color matching functions: + let l: Float = lambda as Float; + let x = 0.0 // for readability of next lines + + gaussian(l, 1.056, 599.8, 37.9, 31.0) + + gaussian(l, 0.362, 442.0, 16.0, 26.7) + + gaussian(l, -0.065, 501.1, 20.4, 26.2); + let y = gaussian(l, 0.821, 568.8, 46.9, 40.5) + gaussian(l, 0.286, 530.9, 16.3, 31.1); + let z = gaussian(l, 1.217, 437.0, 11.8, 36.0) + gaussian(l, 0.681, 459.0, 26.0, 13.8); + + XYZ_Tristimulus { x, y, z } + } +} + +/// CIE 1931 XYZ Normalized color based on three [Floats](crate::Float) values. +#[derive(Copy, Clone, Debug)] +#[cfg_attr(feature = "serde-derive", derive(serde::Serialize, serde::Deserialize))] +#[allow(non_camel_case_types)] +pub struct XYZ_Normalized { + /// The x component of the color, as a [Float] + pub x: Float, + /// The y component of the color, as a [Float] + pub y: Float, + /// The z component of the color, as a [Float] + pub z: Float, +} + +/// Tristimulus value normalization. +impl From for XYZ_Normalized { + fn from(value: XYZ_Tristimulus) -> Self { + let XYZ_Tristimulus { x, y, z } = value; + // TODO: why does this normalization make the image bad? It should be needed? + + // let x_n = (76.04 * (x - 0.1901)) / (80.0 * (76.04 - 0.1901)); + // let y_n = (y - 0.2) / (80.0 - 0.2); + // let z_n = (87.12 * (z - 0.2178)) / (80.0 * (87.12 - 0.2178)); + + // FIXME: normalization not done, but image looks more correct? + let x_n = x; + let y_n = y; + let z_n = z; + + XYZ_Normalized { + x: x_n, + y: y_n, + z: z_n, + } + } +} diff --git a/clovers/src/hitable.rs b/clovers/src/hitable.rs index 6e5fda9a..97f3d14f 100644 --- a/clovers/src/hitable.rs +++ b/clovers/src/hitable.rs @@ -10,12 +10,12 @@ use crate::objects::{GLTFTriangle, GLTF}; use crate::{ aabb::AABB, bvhnode::BVHNode, + colors::Wavelength, materials::MaterialTrait, objects::{ Boxy, ConstantMedium, FlipFace, MovingSphere, Quad, RotateY, Sphere, Translate, Triangle, }, ray::Ray, - spectral::Wavelength, Float, Vec3, }; diff --git a/clovers/src/lib.rs b/clovers/src/lib.rs index e69bac22..5a549cf4 100644 --- a/clovers/src/lib.rs +++ b/clovers/src/lib.rs @@ -79,6 +79,7 @@ pub mod bvhnode; pub mod camera; pub mod color; pub mod colorize; +pub mod colors; pub mod hitable; pub mod interval; pub mod materials; @@ -89,7 +90,6 @@ pub mod pdf; pub mod random; pub mod ray; pub mod scenes; -pub mod spectral; pub mod textures; /// Rendering options struct diff --git a/clovers/src/materials/dispersive.rs b/clovers/src/materials/dispersive.rs index 8d65c640..c6f5e372 100644 --- a/clovers/src/materials/dispersive.rs +++ b/clovers/src/materials/dispersive.rs @@ -17,10 +17,10 @@ use rand::{rngs::SmallRng, Rng}; use crate::{ color::Color, + colors::Wavelength, hitable::HitRecord, pdf::{ZeroPDF, PDF}, ray::Ray, - spectral::Wavelength, Float, Vec3, }; diff --git a/clovers/src/objects/boxy.rs b/clovers/src/objects/boxy.rs index 9654c55f..cd694c99 100644 --- a/clovers/src/objects/boxy.rs +++ b/clovers/src/objects/boxy.rs @@ -3,10 +3,10 @@ use super::Quad; use crate::{ aabb::AABB, + colors::Wavelength, hitable::{HitRecord, Hitable, HitableTrait}, materials::{Material, MaterialInit}, ray::Ray, - spectral::Wavelength, Box, Float, Vec3, }; use rand::{rngs::SmallRng, Rng}; diff --git a/clovers/src/objects/constant_medium.rs b/clovers/src/objects/constant_medium.rs index e530431f..71ca266e 100644 --- a/clovers/src/objects/constant_medium.rs +++ b/clovers/src/objects/constant_medium.rs @@ -2,11 +2,11 @@ use crate::{ aabb::AABB, + colors::Wavelength, hitable::{HitRecord, Hitable, HitableTrait}, materials::{isotropic::Isotropic, Material}, random::random_unit_vector, ray::Ray, - spectral::Wavelength, textures::Texture, Box, Float, Vec3, EPSILON_CONSTANT_MEDIUM, }; diff --git a/clovers/src/objects/flip_face.rs b/clovers/src/objects/flip_face.rs index 7421fa05..13219c10 100644 --- a/clovers/src/objects/flip_face.rs +++ b/clovers/src/objects/flip_face.rs @@ -2,9 +2,9 @@ use crate::{ aabb::AABB, + colors::Wavelength, hitable::{HitRecord, Hitable, HitableTrait}, ray::Ray, - spectral::Wavelength, Box, Float, Vec3, }; use rand::rngs::SmallRng; diff --git a/clovers/src/objects/gltf.rs b/clovers/src/objects/gltf.rs index 26289f11..2e8f3cb2 100644 --- a/clovers/src/objects/gltf.rs +++ b/clovers/src/objects/gltf.rs @@ -12,11 +12,11 @@ use tracing::debug; use crate::{ aabb::AABB, bvhnode::BVHNode, + colors::Wavelength, hitable::{get_orientation, HitRecord, Hitable, HitableTrait}, interval::Interval, materials::gltf::GLTFMaterial, ray::Ray, - spectral::Wavelength, Float, Vec3, EPSILON_RECT_THICKNESS, EPSILON_SHADOW_ACNE, }; diff --git a/clovers/src/objects/moving_sphere.rs b/clovers/src/objects/moving_sphere.rs index 9a4b4770..a3dd8e8c 100644 --- a/clovers/src/objects/moving_sphere.rs +++ b/clovers/src/objects/moving_sphere.rs @@ -2,11 +2,11 @@ use crate::{ aabb::AABB, + colors::Wavelength, hitable::{HitRecord, HitableTrait}, materials::{Material, MaterialInit}, random::random_in_unit_sphere, ray::Ray, - spectral::Wavelength, Float, Vec3, PI, }; use rand::rngs::SmallRng; diff --git a/clovers/src/objects/quad.rs b/clovers/src/objects/quad.rs index dc7104a0..f51f680c 100644 --- a/clovers/src/objects/quad.rs +++ b/clovers/src/objects/quad.rs @@ -1,9 +1,9 @@ //! A quadrilateral object. // TODO: better docs +use crate::colors::Wavelength; 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, diff --git a/clovers/src/objects/rotate.rs b/clovers/src/objects/rotate.rs index df30007d..591fd3d1 100644 --- a/clovers/src/objects/rotate.rs +++ b/clovers/src/objects/rotate.rs @@ -2,9 +2,9 @@ use crate::{ aabb::AABB, + colors::Wavelength, hitable::{HitRecord, Hitable, HitableTrait}, ray::Ray, - spectral::Wavelength, Box, Float, Vec3, }; use rand::rngs::SmallRng; diff --git a/clovers/src/objects/sphere.rs b/clovers/src/objects/sphere.rs index 62bd4d35..6842d0f3 100644 --- a/clovers/src/objects/sphere.rs +++ b/clovers/src/objects/sphere.rs @@ -2,11 +2,11 @@ use crate::{ aabb::AABB, + colors::Wavelength, hitable::{HitRecord, HitableTrait}, materials::{Material, MaterialInit}, onb::ONB, ray::Ray, - spectral::Wavelength, Float, Vec3, EPSILON_SHADOW_ACNE, PI, }; use rand::{rngs::SmallRng, Rng}; diff --git a/clovers/src/objects/stl.rs b/clovers/src/objects/stl.rs index fe8907dc..e10a8275 100644 --- a/clovers/src/objects/stl.rs +++ b/clovers/src/objects/stl.rs @@ -9,11 +9,11 @@ use std::fs::OpenOptions; use crate::{ aabb::AABB, bvhnode::BVHNode, + colors::Wavelength, hitable::{HitRecord, Hitable, HitableTrait}, materials::{Material, MaterialInit, SharedMaterial}, objects::Triangle, ray::Ray, - spectral::Wavelength, Float, Vec3, }; diff --git a/clovers/src/objects/translate.rs b/clovers/src/objects/translate.rs index 5b0eae10..9ccc5fbd 100644 --- a/clovers/src/objects/translate.rs +++ b/clovers/src/objects/translate.rs @@ -2,9 +2,9 @@ use crate::{ aabb::AABB, + colors::Wavelength, hitable::{HitRecord, Hitable, HitableTrait}, ray::Ray, - spectral::Wavelength, Box, Float, Vec3, }; use rand::rngs::SmallRng; diff --git a/clovers/src/objects/triangle.rs b/clovers/src/objects/triangle.rs index 35d289d7..4f3c8f84 100644 --- a/clovers/src/objects/triangle.rs +++ b/clovers/src/objects/triangle.rs @@ -1,10 +1,10 @@ //! A triangle object. Almost exact copy of [Quad](crate::objects::Quad), with an adjusted `hit_ab` method. // TODO: better docs +use crate::colors::Wavelength; 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::HitRecord, materials::Material, ray::Ray, Float, Vec3, diff --git a/clovers/src/pdf.rs b/clovers/src/pdf.rs index 0ce5a280..0e5c6b37 100644 --- a/clovers/src/pdf.rs +++ b/clovers/src/pdf.rs @@ -3,10 +3,10 @@ #![allow(missing_docs)] // TODO: Lots of undocumented things for now use crate::{ + colors::Wavelength, hitable::{Hitable, HitableTrait}, onb::ONB, random::{random_cosine_direction, random_in_unit_sphere}, - spectral::Wavelength, Box, Float, Vec3, PI, }; use enum_dispatch::enum_dispatch; diff --git a/clovers/src/ray.rs b/clovers/src/ray.rs index fd526b71..43cc6500 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::{spectral::Wavelength, Float, Vec3}; +use crate::{colors::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)] diff --git a/clovers/src/spectral.rs b/clovers/src/spectral.rs deleted file mode 100644 index 80c833c9..00000000 --- a/clovers/src/spectral.rs +++ /dev/null @@ -1,72 +0,0 @@ -//! Spectral rendering functionalities for the renderer - -use core::{array::from_fn, ops::Range}; -use rand::rngs::SmallRng; -use rand_distr::uniform::SampleRange; - -use crate::{color::Color, Float}; - -/// Wavelength in nanometers -pub type Wavelength = usize; - -const MIN_WAVELENGTH: Wavelength = 380; -const MAX_WAVELENGTH: Wavelength = 780; -const SPECTRUM: Range = MIN_WAVELENGTH..MAX_WAVELENGTH; -const SPECTRUM_SIZE: usize = MAX_WAVELENGTH - MIN_WAVELENGTH; -const WAVE_SAMPLE_COUNT: usize = 4; - -/// Return a random wavelength, sampled uniformly from the visible spectrum. -pub fn random_wavelength(rng: &mut SmallRng) -> Wavelength { - SPECTRUM.sample_single(rng) -} - -/// Given a hero wavelength, create additional equidistant wavelengths in the visible spectrum. Returns an array of wavelengths, with the original hero wavelength as the first one. -#[must_use] -pub fn rotate_wavelength(hero: Wavelength) -> [Wavelength; WAVE_SAMPLE_COUNT] { - from_fn(|j| { - (hero - MIN_WAVELENGTH + ((1 + j) / WAVE_SAMPLE_COUNT) * SPECTRUM_SIZE) - % (SPECTRUM_SIZE + MIN_WAVELENGTH) - }) -} - -/// Helper function adapted from -fn gaussian(x: Float, alpha: Float, mu: Float, sigma1: Float, sigma2: Float) -> Float { - let t = (x - mu) / (if x < mu { sigma1 } else { sigma2 }); - alpha * (-(t * t) / 2.0).exp() -} - -/// Color component transfer function. -/// Note: Produces `sRGB` digital values with a range 0 to 1, which must then be multiplied by 2^(bit depth) – 1 and quantized. -/// -#[must_use] -pub fn color_component_transfer(c: Float) -> Float { - if c.abs() < 0.003_130_8 { - 12.92 * c - } else { - 1.055 * c.powf(1.0 / 2.4) - 0.055 - } -} - -/// Helper function adapted from -impl From for Color { - // TODO: precision loss - #[allow(clippy::cast_precision_loss)] - fn from(lambda: Wavelength) -> Self { - // With the wavelength λ measured in nanometers, we then approximate the 1931 color matching functions: - let l: Float = lambda as Float; - let x = 0.0 // for readability of next lines - + gaussian(l, 1.056, 599.8, 37.9, 31.0) - + gaussian(l, 0.362, 442.0, 16.0, 26.7) - + gaussian(l, -0.065, 501.1, 20.4, 26.2); - let y = gaussian(l, 0.821, 568.8, 46.9, 40.5) + gaussian(l, 0.286, 530.9, 16.3, 31.1); - let z = gaussian(l, 1.217, 437.0, 11.8, 36.0) + gaussian(l, 0.681, 459.0, 26.0, 13.8); - // Convert from XYZ to sRGB - // https://color.org/chardata/rgb/sRGB.pdf - // TODO: more correct color management! - let r = 3.240_625_5 * x - 1.537_208 * y - 0.498_628_6 * z; - let g = -0.968_930_7 * x + 1.875_756_1 * y + 0.041_517_5 * z; - let b = 0.055_710_1 * x - 0.204_021_1 * y + 1.056_995_9 * z; - - Color { r, g, b } - } -}