#[must_use]
pub fn rgb_to_spectral_radiance(rgb: [f32; 3], lambda_nm: f32) -> f32 {
let r = rgb[0].max(0.0);
let g = rgb[1].max(0.0);
let b = rgb[2].max(0.0);
r.mul_add(
asymmetric_gaussian(lambda_nm, 615.0, 45.0, 65.0),
g.mul_add(
asymmetric_gaussian(lambda_nm, 545.0, 45.0, 45.0),
b * asymmetric_gaussian(lambda_nm, 465.0, 40.0, 45.0),
),
)
}
#[inline]
fn asymmetric_gaussian(x: f32, mu: f32, sigma_lo: f32, sigma_hi: f32) -> f32 {
let sigma = if x < mu { sigma_lo } else { sigma_hi };
let t = (x - mu) / sigma;
(-0.5 * t * t).exp()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn zero_rgb_gives_zero_spectrum() {
for lambda in [380.0, 500.0, 560.0, 650.0, 780.0] {
assert_eq!(rgb_to_spectral_radiance([0.0, 0.0, 0.0], lambda), 0.0);
}
}
#[test]
fn negative_components_are_clamped_not_propagated() {
let v = rgb_to_spectral_radiance([-1.0, -2.0, -3.0], 550.0);
assert_eq!(v, 0.0);
assert!(!v.is_nan());
}
#[test]
fn result_is_always_finite_and_non_negative() {
for lambda in [300.0, 380.0, 450.0, 550.0, 650.0, 780.0, 900.0] {
let v = rgb_to_spectral_radiance([1.0, 2.5, 100.0], lambda);
assert!(v.is_finite());
assert!(v >= 0.0);
}
}
#[test]
fn red_weight_dominates_the_red_end_of_the_spectrum() {
let red_only = rgb_to_spectral_radiance([1.0, 0.0, 0.0], 615.0);
let blue_only = rgb_to_spectral_radiance([0.0, 0.0, 1.0], 615.0);
assert!(
red_only > blue_only,
"at 615nm the red basis peak should dominate the blue basis's tail"
);
}
}