use bevy::prelude::*;
use crate::celestial::NIGHT_SKY_COLOUR;
pub const DISPLAY_MAX: f32 = 4096.0;
pub fn display_value(luminance: f32, exposure: f32) -> f32 {
(luminance * exposure).min(DISPLAY_MAX)
}
pub fn luminance_for_display(display: f32, exposure: f32) -> f32 {
if exposure <= 0.0 {
return 0.0;
}
(display.clamp(0.0, DISPLAY_MAX) / exposure).max(0.0)
}
pub fn light_intensity(luminance: f32, distance: f32, albedo: f32) -> f32 {
if albedo <= 0.0 || distance <= 0.0 {
return 0.0;
}
let four_pi_sq = 4.0 * std::f32::consts::PI * std::f32::consts::PI;
four_pi_sq * distance * distance * luminance.max(0.0) / albedo
}
pub fn light_illuminance(intensity: f32, distance: f32) -> f32 {
if distance <= 0.0 {
return 0.0;
}
intensity.max(0.0) / (4.0 * std::f32::consts::PI * distance * distance)
}
pub fn night_light_colour() -> Vec3 {
NIGHT_SKY_COLOUR
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_round_trip_holds() {
for exposure in [0.01_f32, 0.5, 22.4, 4096.0] {
for display in [0.001_f32, 0.05, 0.18, 0.9, 1.0] {
let luminance = luminance_for_display(display, exposure);
let back = display_value(luminance, exposure);
assert!(
(back - display).abs() <= 1e-3 * display.max(1e-6),
"{display} at {exposure} came back as {back}"
);
}
}
}
#[test]
fn a_lights_power_is_right() {
for (luminance, distance, albedo) in [
(1.0_f32, 3.0_f32, 0.2_f32),
(0.5, 10.0, 0.5),
(12.0, 1.5, 0.8),
] {
let intensity = light_intensity(luminance, distance, albedo);
let illuminance = light_illuminance(intensity, distance);
let got = illuminance * albedo / std::f32::consts::PI;
assert!(
(got - luminance).abs() <= 1e-4 * luminance,
"{luminance} wanted, {got} got (intensity {intensity})"
);
}
}
#[test]
fn the_units_match_bevys_own_calibration() {
let exposure = bevy::camera::Exposure { ev100: 12.0 }.exposure();
let intensity = 1_000_000.0_f32;
let illuminance = light_illuminance(intensity, 5.0);
let luminance = illuminance * 0.5 / std::f32::consts::PI;
let value = display_value(luminance, exposure);
assert!(
(0.001..0.5).contains(&value),
"a 1e6 lm cinema light reads at {value} (the luminance {luminance})"
);
let back = light_intensity(luminance, 5.0, 0.5);
assert!(
(back - intensity).abs() <= 1.0,
"the round trip gave {back} against {intensity}"
);
}
#[test]
fn the_degenerate_cases_are_zero() {
assert_eq!(light_intensity(1.0, 0.0, 0.5), 0.0);
assert_eq!(light_intensity(1.0, 3.0, 0.0), 0.0);
assert_eq!(light_illuminance(1.0, 0.0), 0.0);
assert_eq!(light_intensity(-1.0, 3.0, 0.5), 0.0);
}
#[test]
fn a_light_reads_the_same_at_every_hour() {
for exposure in [0.0008_f32, 0.001, 0.05, 1.34, 6.0, 22.4] {
for (display, distance, albedo) in [
(0.18_f32, 1.92_f32, 0.14_f32),
(0.06, 1.92, 0.14),
(0.5, 4.0, 0.4),
] {
let luminance = luminance_for_display(display, exposure);
let intensity = light_intensity(luminance, distance, albedo);
let got = light_illuminance(intensity, distance) * albedo / std::f32::consts::PI;
let read = display_value(got, exposure);
assert!(
(read - display).abs() <= 1e-3 * display,
"at the exposure {exposure} a surface asked to read at {display} reads at {read}"
);
}
}
}
#[test]
fn the_night_colour_is_the_scotopic_one() {
let c = night_light_colour();
assert!(c.x > 0.0 && c.y > 0.0 && c.z > 0.0);
assert!(c.z > c.y, "the scotopic colour is cool: {c:?}");
assert!(c.z > c.x, "the scotopic colour is cool: {c:?}");
}
}