use super::*;
use crate::optics::studio_rig::StudioRig;
use crate::render_setup::Backdrop;
const LAMBDAS: [f32; 3] = [450.0, 550.0, 650.0];
fn probe_dirs() -> Vec<Vec3> {
let mut out = Vec::new();
for y in [
-0.98f32, -0.7, -0.35, -0.1, -0.049, 0.0, 0.049, 0.1, 0.35, 0.7, 0.98,
] {
let radius = y.mul_add(-y, 1.0).sqrt();
for (hx, hz) in [(1.0f32, 0.0f32), (0.6, 0.8), (-0.8, -0.6)] {
out.push(Vec3::new(hx * radius, y, hz * radius));
}
}
out
}
#[test]
fn a_zero_tray_is_the_unsuffixed_entry_point() {
let rig = StudioRig::new(0.84, 0.94);
let observer = Vec3::new(0.1, 0.95, 0.3).normalize();
for preset in LightingPreset::ALL {
for dir in probe_dirs() {
for lambda in LAMBDAS {
let plain = sample_studio_environment_with_rig_shadow(
dir,
lambda,
preset,
1.3,
&rig,
observer,
DEFAULT_HEAD_SHADOW_COSINES,
);
let tray = sample_studio_environment_with_rig_shadow_tray(
dir,
lambda,
preset,
1.3,
&rig,
observer,
DEFAULT_HEAD_SHADOW_COSINES,
0.0,
);
assert_eq!(plain.to_bits(), tray.to_bits(), "{preset:?}");
let spec = studio_spec_power(preset, lambda);
let a = sample_studio_environment_with_rig_spec_power(
dir, preset, 1.3, &rig, observer, spec,
);
let b = sample_studio_environment_with_rig_spec_power_tray(
dir, preset, 1.3, &rig, observer, spec, 0.0,
);
assert_eq!(a.to_bits(), b.to_bits(), "{preset:?}");
let shadow = head_shadow_cosines(12.0);
let c = sample_studio_environment_with_rig_spec_power_shadow(
dir, preset, 1.3, &rig, observer, spec, shadow,
);
let d = sample_studio_environment_with_rig_spec_power_shadow_tray(
dir, preset, 1.3, &rig, observer, spec, shadow, 0.0,
);
assert_eq!(c.to_bits(), d.to_bits(), "{preset:?}");
}
}
}
}
#[test]
fn the_tray_is_seen_by_every_bounce() {
let rig = StudioRig::new(0.4, 0.35);
let observer = Vec3::new(0.1, 0.95, 0.3).normalize();
for preset in LightingPreset::ALL {
let source = preset.studio(1.2, 0.4, 0.35).with_backdrop(0.5);
for dir in probe_dirs() {
let channels = sample_environment_channels(source, dir, &LAMBDAS, Some(&rig), observer);
for (&lambda, &value) in LAMBDAS.iter().zip(&channels) {
let single = sample_environment_channel(source, dir, lambda, Some(&rig), observer);
let direct = sample_studio_environment_with_rig_shadow_tray(
dir,
lambda,
preset,
1.2,
&rig,
observer,
DEFAULT_HEAD_SHADOW_COSINES,
0.5,
);
assert_eq!(value.to_bits(), single.to_bits(), "{preset:?} {dir:?}");
assert_eq!(value.to_bits(), direct.to_bits(), "{preset:?} {dir:?}");
}
}
let down = -Vec3::Y;
let with_tray = sample_environment_channels(source, down, &LAMBDAS, Some(&rig), observer);
let as_lit = sample_environment_channels(
source.with_backdrop(0.0),
down,
&LAMBDAS,
Some(&rig),
observer,
);
let s = if preset == LightingPreset::Aset {
d65_relative_spectral_power(550.0)
} else {
preset.spectral_power(550.0)
};
let expected = 0.5 * 1.2 * s;
assert!(
(with_tray[1] - expected).abs() <= expected.mul_add(1e-5, 1e-9),
"{preset:?}: {} vs {expected}",
with_tray[1]
);
if !preset.is_uv_lamp() && preset != LightingPreset::WhiteTray {
assert_ne!(
with_tray[1].to_bits(),
as_lit[1].to_bits(),
"{preset:?}: the tray must replace the own ground"
);
}
}
}
#[test]
fn iso_hemisphere_is_one_above_and_the_tray_below() {
let tray = 0.5;
let spd = d65_relative_spectral_power(550.0);
let rig = StudioRig::new(0.0, 0.0);
let sample = |dir: Vec3| {
sample_studio_environment_with_rig_shadow_tray(
dir,
550.0,
LightingPreset::IsoHemisphere,
1.0,
&rig,
Vec3::ZERO,
DEFAULT_HEAD_SHADOW_COSINES,
tray,
)
};
assert!((sample(Vec3::Y) - spd).abs() < 1e-5);
assert!(tray.mul_add(-spd, sample(-Vec3::Y)).abs() < 1e-5);
assert!(1.0_f32.midpoint(tray).mul_add(-spd, sample(Vec3::X)).abs() < 1e-5);
let double = sample_studio_environment_with_rig_shadow_tray(
-Vec3::Y,
550.0,
LightingPreset::IsoHemisphere,
2.0,
&rig,
Vec3::ZERO,
DEFAULT_HEAD_SHADOW_COSINES,
tray,
);
assert!((2.0 * tray).mul_add(-spd, double).abs() < 1e-5);
}
#[test]
fn the_tray_replaces_the_ground_of_every_model() {
let rig = StudioRig::new(0.4, 0.35);
let down = -Vec3::Y;
let tray = 0.7;
for preset in LightingPreset::ALL {
let expected = if preset == LightingPreset::Aset {
tray * d65_relative_spectral_power(550.0)
} else {
tray * preset.spectral_power(550.0)
};
let got = sample_studio_environment_with_rig_shadow_tray(
down,
550.0,
preset,
1.0,
&rig,
Vec3::ZERO,
DEFAULT_HEAD_SHADOW_COSINES,
tray,
);
assert!(
(got - expected).abs() <= expected * 1e-5,
"{preset:?}: {got} vs {expected}"
);
}
}
#[test]
fn the_head_shadow_darkens_the_tray_of_the_lit_models_only() {
let rig = StudioRig::new(0.4, 0.35);
let observer = Vec3::new(0.0, -0.4, 1.0).normalize();
let sample = |preset: LightingPreset| {
sample_studio_environment_with_rig_shadow_tray(
observer,
550.0,
preset,
1.0,
&rig,
observer,
DEFAULT_HEAD_SHADOW_COSINES,
0.5,
)
};
for preset in [
LightingPreset::IsoHemisphere,
LightingPreset::LightTent,
LightingPreset::DaylightDome,
LightingPreset::DaylightSun,
] {
assert_eq!(sample(preset), 0.0, "{preset:?}");
}
assert!(sample(LightingPreset::RingLights) > 0.0);
}
#[test]
fn tray_resolve_matches_closed_forms() {
use std::f32::consts::PI as PI32;
let (yaw, pitch) = (0.84f32, 72.0f32.to_radians());
let key_y = StudioRig::new(yaw, pitch).key_dir.y;
let iso = rig_irradiance_over_pi(LightingPreset::IsoHemisphere, yaw, pitch);
assert!((iso - 0.9995).abs() < 1e-3, "iso {iso}");
let dome = rig_irradiance_over_pi(LightingPreset::DaylightDome, yaw, pitch);
let expected = 0.188_50f32.mul_add(key_y, 0.397_94) / PI32;
assert!(
(dome / expected - 1.0).abs() < 0.01,
"dome {dome} vs {expected}"
);
let sun = rig_irradiance_over_pi(LightingPreset::DaylightSun, yaw, pitch);
let added = (sun - dome) * PI32;
let expected_sun = sun::SUN_RADIANCE * sun::SUN_SOLID_ANGLE * key_y;
assert!(
(added / expected_sun - 1.0).abs() < 1e-4,
"sun {added} vs {expected_sun}"
);
let white = rig_irradiance_over_pi(LightingPreset::WhiteTray, yaw, pitch);
assert!((white - 0.9).abs() < 0.02 * 0.9, "white tray {white}");
assert_eq!(
rig_irradiance_over_pi(LightingPreset::Aset, yaw, pitch).to_bits(),
1.0f32.to_bits()
);
}
#[test]
fn tray_radiance_is_albedo_times_irradiance_and_zero_for_as_lit() {
let (yaw, pitch) = (0.84f32, 0.94f32);
for preset in LightingPreset::ALL {
let e = rig_irradiance_over_pi(preset, yaw, pitch);
assert!(e.is_finite() && e > 0.0, "{preset:?}: {e}");
assert_eq!(
tray_radiance(TRAY_ALBEDO_WHITE, preset, yaw, pitch).to_bits(),
(TRAY_ALBEDO_WHITE * e).to_bits()
);
assert_eq!(
rig_irradiance_over_pi(preset, yaw, pitch).to_bits(),
e.to_bits()
);
}
assert_eq!(
tray_radiance(0.0, LightingPreset::LightTent, yaw, pitch).to_bits(),
0.0f32.to_bits()
);
}
#[test]
fn tray_resolve_does_not_wobble() {
for preset in LightingPreset::ALL {
for pitch_deg in [30.0f32, 54.0, 72.0] {
let pitch = pitch_deg.to_radians();
let a = rig_irradiance_over_pi(preset, 0.8, pitch);
let b = rig_irradiance_over_pi(preset, 0.8 + 1.0f32.to_radians(), pitch);
assert!(
(a - b).abs() / a < 0.005,
"{preset:?} at pitch {pitch_deg}: {a} vs {b}"
);
}
}
}
#[test]
fn backdrop_levels_indices_and_default() {
const ALL: [Backdrop; 4] = [
Backdrop::AsLit,
Backdrop::Grey,
Backdrop::White,
Backdrop::Black,
];
assert_eq!(Backdrop::default(), Backdrop::White);
assert_eq!(ALL.map(Backdrop::index), [0, 1, 2, 3]);
assert_eq!(
ALL.map(|b| b.albedo().to_bits()),
[0.0f32, 0.18, 0.90, 0.02].map(f32::to_bits)
);
for b in ALL {
assert_eq!(Backdrop::from_index(b.index()), b);
}
assert_eq!(Backdrop::from_index(-1), Backdrop::White);
assert_eq!(Backdrop::from_index(99), Backdrop::White);
let preset = LightingPreset::IsoHemisphere;
assert_eq!(Backdrop::AsLit.resolve(preset, 0.8, 1.2).to_bits(), 0);
assert_eq!(
Backdrop::White.resolve(preset, 0.8, 1.2).to_bits(),
tray_radiance(TRAY_ALBEDO_WHITE, preset, 0.8, 1.2).to_bits()
);
assert_eq!(
Backdrop::Grey.level().to_bits(),
Backdrop::Grey
.resolve(
LightingPreset::default(),
48.0f32.to_radians(),
72.0f32.to_radians()
)
.to_bits()
);
}