rustic-zen 0.3.0

Photon-Garden raytracer for creating artistic renderings
Documentation
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

//! Color variables from wavelengths

mod cdf;
mod wavelength;
use rand_distr::num_traits::{real::Real, Float, FromPrimitive, ToPrimitive};

use self::cdf::{BLACKBODY_CDF_DATA, BLACKBODY_CDF_TEMP};
pub(crate) use self::wavelength::{FIRST_WAVELENGTH, LAST_WAVELENGTH, WAVELENGTH_TO_RGB};

pub fn wavelength_to_colour<P>(nm: f64) -> (P, P, P)
where
    P: Copy + Real,
{
    // Special Case: monochromatic white.
    if nm == 0.0 {
        // We don't know why this is 8K yet.
        return (
            P::from(8192).unwrap(),
            P::from(8192).unwrap(),
            P::from(8192).unwrap(),
        );
    }

    if !nm.is_normal() {
        panic!("nm (= {:?}) is not normal", nm);
    }

    // Case: Light outside of visible spectrum
    if nm < FIRST_WAVELENGTH || nm > LAST_WAVELENGTH {
        return (P::zero(), P::zero(), P::zero());
    }

    let fp_index: f32 = nm as f32 - FIRST_WAVELENGTH as f32;
    let index: usize = fp_index.floor() as usize;
    let frac = P::from(fp_index.fract()).unwrap();
    let inv = P::from(1.0).unwrap() - frac;

    let c1: (i16, i16, i16) = WAVELENGTH_TO_RGB[index];
    let c2: (i16, i16, i16) = WAVELENGTH_TO_RGB[index + 1];

    //           <------------LERP Algorithm------------>
    let r: P = inv * P::from(c1.0).unwrap() + frac * P::from(c2.0).unwrap();
    let g: P = inv * P::from(c1.1).unwrap() + frac * P::from(c2.1).unwrap();
    let b: P = inv * P::from(c1.2).unwrap() + frac * P::from(c2.2).unwrap();

    return (r, g, b);
}

pub fn blackbody_wavelength<T>(temp: T, noise: T) -> T
where
    T: Float + ToPrimitive + FromPrimitive + From<f32>,
{
    let index: usize = (1..BLACKBODY_CDF_DATA.len())
        .find(|x| BLACKBODY_CDF_DATA[*x] >= noise.to_f64().unwrap())
        .expect("Blackbody Index out of range");

    let lower: T = (BLACKBODY_CDF_DATA[index - 1] as f32).into();
    let upper: T = (BLACKBODY_CDF_DATA[index] as f32).into();

    // Linear interpolation
    let mut lerp: T =
        T::from_usize(index).unwrap() + (noise - lower.into()) / (upper - lower).into();
    if lerp.is_nan() {
        lerp = 0.0.into();
    }

    // Scale to 'temperature' using Wein's displacement law
    return lerp * (T::from_f64(BLACKBODY_CDF_TEMP).unwrap() / temp);
}

#[cfg(test)]
mod tests {
    use std::fmt::Debug;

    use rand_distr::num_traits::{real::Real, NumCast};

    use super::wavelength_to_colour;

    fn match_colours<T: Copy + Real + Debug + NumCast>() {
        let (r, g, b) = wavelength_to_colour::<T>(635.0);
        assert_eq!(r, T::from(11654).unwrap());
        assert_eq!(g, T::from(-967).unwrap());
        assert_eq!(b, T::from(-115).unwrap());

        let (r, g, b) = wavelength_to_colour::<T>(530.0);
        assert_eq!(r, T::from(-6634).unwrap());
        assert_eq!(g, T::from(11947).unwrap());
        assert_eq!(b, T::from(-1000).unwrap());

        let (r, g, b) = wavelength_to_colour::<T>(458.0);
        assert_eq!(r, T::from(392).unwrap());
        assert_eq!(g, T::from(-981).unwrap());
        assert_eq!(b, T::from(14817).unwrap());
    }

    #[test]
    fn match_colours_32() {
        match_colours::<f32>()
    }

    #[test]
    fn match_colours_64() {
        match_colours::<f64>()
    }
}