use glam::Vec3;
use super::{image_types::ExtinctionReason, lighting::ExitLighting};
use crate::optics::raytracer::DEFAULT_HEAD_SHADOW_DEG;
pub(super) const HEAD_SHADOW_COS: f32 = 0.96;
const NO_HEAD_SHADOW_COS: f32 = 2.0;
#[must_use]
pub(super) fn head_shadow_cone_cos(studio_rig: bool, head_shadow_deg: f32) -> f32 {
if studio_rig
|| head_shadow_deg.is_nan()
|| head_shadow_deg.to_bits() == DEFAULT_HEAD_SHADOW_DEG.to_bits()
{
return HEAD_SHADOW_COS;
}
if head_shadow_deg <= 0.0 {
return NO_HEAD_SHADOW_COS;
}
let offset = HEAD_SHADOW_COS.acos() - 16.0f32.to_radians();
(head_shadow_deg.clamp(1.0, 89.0).to_radians() + offset).cos()
}
pub(super) fn returned_or_reason(
exit_dir: Vec3,
observer: Vec3,
lighting: &ExitLighting<'_>,
) -> Result<(), ExtinctionReason> {
if exit_dir.dot(observer) > lighting.head_shadow_cos() {
Err(ExtinctionReason::ObserverShadow)
} else if lighting.is_illuminated(exit_dir) {
Ok(())
} else {
Err(ExtinctionReason::InsufficientIllumination)
}
}
#[must_use]
pub(super) fn ray_is_visibly_returned(
exit_dir: Vec3,
cam_forward: Vec3,
lighting: &ExitLighting<'_>,
) -> bool {
returned_or_reason(exit_dir, -cam_forward, lighting).is_ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_size_is_exactly_the_literal_cosine() {
assert_eq!(
head_shadow_cone_cos(false, DEFAULT_HEAD_SHADOW_DEG).to_bits(),
0.96f32.to_bits()
);
assert_eq!(
head_shadow_cone_cos(false, f32::NAN).to_bits(),
0.96f32.to_bits()
);
}
#[test]
fn zero_size_removes_the_cone_for_a_lit_model() {
let cos = head_shadow_cone_cos(false, 0.0);
assert!(cos > 1.0, "no unit dot product may exceed {cos}");
}
#[test]
fn other_sizes_follow_the_scene_for_a_lit_model() {
let offset = HEAD_SHADOW_COS.acos() - 16.0f32.to_radians();
let cos = head_shadow_cone_cos(false, 30.0);
let expected = (30.0f32.to_radians() + offset).cos();
assert!((cos - expected).abs() < 1e-6);
}
#[test]
fn the_cone_is_continuous_through_the_default() {
let below = head_shadow_cone_cos(false, 15.999);
let above = head_shadow_cone_cos(false, 16.001);
assert!((below - 0.96).abs() < 1e-4, "{below}");
assert!((above - 0.96).abs() < 1e-4, "{above}");
assert!(above < below);
}
#[test]
fn the_cone_is_centred_on_the_observer_passed_in() {
use crate::optics::raytracer::LightingPreset;
let lighting = ExitLighting::new(
LightingPreset::IsoHemisphere
.studio(1.0, 0.85, 0.95)
.with_head_shadow(30.0),
);
let exit_dir = Vec3::new(0.1, 0.9, 0.2).normalize();
let towards_eye = exit_dir;
assert_eq!(
returned_or_reason(exit_dir, towards_eye, &lighting),
Err(ExtinctionReason::ObserverShadow)
);
assert!(!ray_is_visibly_returned(exit_dir, -towards_eye, &lighting));
let axis = exit_dir.cross(Vec3::X).normalize();
let far_observer = glam::Quat::from_axis_angle(axis, 60.0f32.to_radians()) * towards_eye;
assert_ne!(
returned_or_reason(exit_dir, far_observer, &lighting),
Err(ExtinctionReason::ObserverShadow)
);
}
#[test]
fn the_shared_helper_matches_the_original_predicate_bitwise() {
use crate::optics::raytracer::LightingPreset;
let observer = Vec3::new(0.2, -0.9, 0.35).normalize();
for preset in [
LightingPreset::RingLights,
LightingPreset::LightTent,
LightingPreset::IsoHemisphere,
] {
for deg in [0.0, 16.0, 30.0] {
let lighting =
ExitLighting::new(preset.studio(1.0, 0.85, 0.95).with_head_shadow(deg));
for i in 0..24 {
for j in 0..12 {
let azimuth = i as f32 * std::f32::consts::TAU / 24.0;
let y = (j as f32 + 0.5) / 12.0;
let r = y.mul_add(-y, 1.0).sqrt();
let exit_dir = Vec3::new(r * azimuth.cos(), y, r * azimuth.sin());
assert_eq!(
exit_dir.dot(-observer).to_bits(),
(-exit_dir).dot(observer).to_bits(),
"the eye-side dot is bit for bit the legacy one"
);
let original = (-exit_dir)
.dot(observer)
.partial_cmp(&lighting.head_shadow_cos())
!= Some(std::cmp::Ordering::Greater)
&& lighting.is_illuminated(exit_dir);
assert_eq!(
ray_is_visibly_returned(exit_dir, observer, &lighting),
original
);
assert_eq!(
returned_or_reason(exit_dir, -observer, &lighting).is_ok(),
original
);
}
}
}
}
}
#[test]
fn studio_rigs_ignore_the_size() {
for deg in [0.0, 5.0, 16.0, 40.0] {
assert_eq!(head_shadow_cone_cos(true, deg).to_bits(), 0.96f32.to_bits());
}
}
}