use super::color::illuminant_white_balance;
use crate::renderer::env_map::EnvironmentMap;
use glam::Vec3;
const CIE_D65_SPD_380_780_10NM: [f32; 41] = [
49.9755, 54.6482, 82.7549, 91.4860, 93.4318, 86.6823, 104.865, 117.008, 117.812, 114.861,
115.923, 108.811, 109.354, 107.802, 104.790, 107.689, 104.405, 104.046, 100.000, 96.3342,
95.7880, 88.6856, 90.0062, 89.5991, 87.6987, 83.2886, 83.6992, 80.0268, 80.2146, 82.2778,
78.2842, 69.7213, 71.6091, 74.3496, 61.6045, 69.8856, 75.0870, 63.5928, 46.4182, 66.8054,
63.3828,
];
#[must_use]
pub fn d65_relative_spectral_power(lambda_nm: f32) -> f32 {
const START_NM: f32 = 380.0;
const STEP_NM: f32 = 10.0;
const LAST_INDEX: usize = CIE_D65_SPD_380_780_10NM.len() - 1;
let clamped = lambda_nm.clamp(START_NM, STEP_NM.mul_add(LAST_INDEX as f32, START_NM));
let position = (clamped - START_NM) / STEP_NM;
let index0 = (position.floor() as usize).min(LAST_INDEX - 1);
let index1 = index0 + 1;
let frac = position - index0 as f32;
let v0 = CIE_D65_SPD_380_780_10NM[index0];
let v1 = CIE_D65_SPD_380_780_10NM[index1];
frac.mul_add(v1 - v0, v0) / 100.0
}
#[must_use]
pub fn blackbody_spectrum(lambda_nm: f32, temp_k: f32) -> f32 {
let t_k = temp_k.max(1000.0);
let h_c_k = 14_388_000.0_f32; let exp_val = (h_c_k / (lambda_nm * t_k)).min(80.0).exp();
let exp_560 = (h_c_k / (560.0 * t_k)).min(80.0).exp();
let denom = (exp_val - 1.0).max(1e-6);
let denom_560 = (exp_560 - 1.0).max(1e-6);
let ratio = denom_560 / denom;
((560.0 / lambda_nm).powi(5) * ratio).clamp(0.01, 20.0)
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct LightingRigParams {
pub temp_k: f32,
pub spot_mult: f32,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum LightingPreset {
#[default]
Daylight,
Incandescent,
RingLights,
DarkSpotlight,
IsoHemisphere,
LightTent,
DaylightDome,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum LightingModel {
Studio,
IsoHemisphere,
LightTent,
DaylightDome,
}
impl LightingModel {
#[must_use]
pub const fn gpu_id(self) -> u32 {
match self {
Self::Studio => 0,
Self::IsoHemisphere => 1,
Self::LightTent => 2,
Self::DaylightDome => 3,
}
}
}
impl LightingPreset {
pub const ALL: [Self; 7] = [
Self::Daylight,
Self::Incandescent,
Self::RingLights,
Self::DarkSpotlight,
Self::IsoHemisphere,
Self::LightTent,
Self::DaylightDome,
];
#[must_use]
pub const fn params(self) -> LightingRigParams {
match self {
Self::Incandescent => LightingRigParams {
temp_k: 3200.0,
spot_mult: 1.2,
},
Self::RingLights => LightingRigParams {
temp_k: 5000.0,
spot_mult: 1.6,
},
Self::DarkSpotlight => LightingRigParams {
temp_k: 6000.0,
spot_mult: 2.4,
},
Self::LightTent => LightingRigParams {
temp_k: 5000.0,
spot_mult: 1.0,
},
Self::Daylight | Self::IsoHemisphere | Self::DaylightDome => LightingRigParams {
temp_k: 6500.0,
spot_mult: 1.0,
},
}
}
#[must_use]
pub const fn label(self) -> &'static str {
match self {
Self::Daylight => "D65 Daylight (6500K)",
Self::Incandescent => "Incandescent (3200K)",
Self::RingLights => "Gem Studio Ring Lights",
Self::DarkSpotlight => "Dramatic Dark Spotlight",
Self::IsoHemisphere => "ISO hemisphere (GemRay-style)",
Self::LightTent => "Light tent + black cards",
Self::DaylightDome => "Daylight sky + sun",
}
}
#[must_use]
pub fn from_label(label: &str) -> Self {
match label {
"Incandescent (3200K)" => Self::Incandescent,
"Gem Studio Ring Lights" => Self::RingLights,
"Dramatic Dark Spotlight" => Self::DarkSpotlight,
"ISO hemisphere (GemRay-style)" | "ISO hemisphere" => Self::IsoHemisphere,
"Light tent + black cards" | "Soft dome + ring lights" => Self::LightTent,
"Daylight sky + sun" | "Daylight dome + sun" => Self::DaylightDome,
_ => Self::Daylight,
}
}
#[must_use]
pub const fn index(self) -> i32 {
match self {
Self::Daylight => 0,
Self::Incandescent => 1,
Self::RingLights => 2,
Self::DarkSpotlight => 3,
Self::IsoHemisphere => 4,
Self::LightTent => 5,
Self::DaylightDome => 6,
}
}
#[must_use]
pub const fn from_index(index: i32) -> Self {
match index {
1 => Self::Incandescent,
2 => Self::RingLights,
3 => Self::DarkSpotlight,
4 => Self::IsoHemisphere,
5 => Self::LightTent,
6 => Self::DaylightDome,
_ => Self::Daylight,
}
}
#[must_use]
pub const fn model(self) -> LightingModel {
match self {
Self::Daylight | Self::Incandescent | Self::RingLights | Self::DarkSpotlight => {
LightingModel::Studio
}
Self::IsoHemisphere => LightingModel::IsoHemisphere,
Self::LightTent => LightingModel::LightTent,
Self::DaylightDome => LightingModel::DaylightDome,
}
}
#[must_use]
pub const fn uses_d65(self) -> bool {
matches!(
self,
Self::Daylight | Self::IsoHemisphere | Self::DaylightDome
)
}
#[must_use]
pub fn spectral_power(self, lambda_nm: f32) -> f32 {
if self.uses_d65() {
d65_relative_spectral_power(lambda_nm)
} else {
blackbody_spectrum(lambda_nm, self.params().temp_k)
}
}
#[must_use]
pub const fn studio(
self,
exposure: f32,
light_yaw: f32,
light_pitch: f32,
) -> EnvironmentSource<'static> {
EnvironmentSource::Studio {
preset: self,
exposure,
light_yaw,
light_pitch,
backdrop: 0.0,
}
}
}
#[derive(Clone, Copy)]
pub enum EnvironmentSource<'a> {
Studio {
preset: LightingPreset,
exposure: f32,
light_yaw: f32,
light_pitch: f32,
backdrop: f32,
},
HdrMap(&'a EnvironmentMap),
}
pub const BACKDROP_GEMRAY_GREY: f32 = 0.23;
pub const BACKDROP_WHITE: f32 = 8.0;
impl EnvironmentSource<'_> {
#[must_use]
pub const fn with_backdrop(self, backdrop: f32) -> Self {
match self {
Self::Studio {
preset,
exposure,
light_yaw,
light_pitch,
..
} => Self::Studio {
preset,
exposure,
light_yaw,
light_pitch,
backdrop,
},
hdr @ Self::HdrMap(_) => hdr,
}
}
}
#[inline]
pub(super) fn sample_environment_channel(
environment: EnvironmentSource<'_>,
dir: Vec3,
lambda_nm: f32,
studio_rig: Option<&crate::optics::studio_rig::StudioRig>,
observer: Vec3,
) -> f32 {
match environment {
EnvironmentSource::Studio {
preset, exposure, ..
} => {
let rig = studio_rig
.expect("sample_environment_channel: Studio environment needs a pre-built rig");
sample_studio_environment_with_rig(dir, lambda_nm, preset, exposure, rig, observer)
}
EnvironmentSource::HdrMap(map) => map.radiance_at(dir, lambda_nm),
}
}
pub(super) fn fill_backdrop<const N: usize>(
environment: EnvironmentSource<'_>,
lambdas: &[f32; N],
radiance: &mut [f32; N],
) -> bool {
match environment {
EnvironmentSource::Studio {
preset, backdrop, ..
} if backdrop > 0.0 => {
for (out, &lambda_nm) in radiance.iter_mut().zip(lambdas) {
*out = backdrop * preset.spectral_power(lambda_nm);
}
true
}
_ => false,
}
}
pub(super) struct EnvNeeSample {
pub(super) dir: Vec3,
pub(super) pdf: f32,
pub(super) rgb: [f32; 3],
}
#[must_use]
pub(super) fn sample_environment_for_nee(
environment: EnvironmentSource<'_>,
u0: f32,
u1: f32,
) -> Option<EnvNeeSample> {
match environment {
EnvironmentSource::Studio { .. } => None,
EnvironmentSource::HdrMap(map) => {
let (dir, rgb, pdf) = map.sample(u0, u1);
(pdf > 0.0).then_some(EnvNeeSample { dir, pdf, rgb })
}
}
}
#[must_use]
pub(super) fn environment_nee_pdf(environment: EnvironmentSource<'_>, dir: Vec3) -> f32 {
match environment {
EnvironmentSource::Studio { .. } => 0.0,
EnvironmentSource::HdrMap(map) => map.pdf(dir),
}
}
#[inline]
pub(crate) fn environment_white_balance(environment: EnvironmentSource<'_>) -> Vec3 {
match environment {
EnvironmentSource::Studio { preset, .. } => illuminant_white_balance(preset),
EnvironmentSource::HdrMap(_) => Vec3::ONE,
}
}
#[must_use]
pub fn sample_studio_environment(
dir: Vec3,
lambda_nm: f32,
lighting_preset: LightingPreset,
exposure: f32,
light_yaw: f32,
light_pitch: f32,
) -> f32 {
sample_studio_environment_observed(
dir,
lambda_nm,
lighting_preset,
exposure,
light_yaw,
light_pitch,
Vec3::ZERO,
)
}
#[must_use]
pub fn sample_studio_environment_observed(
dir: Vec3,
lambda_nm: f32,
lighting_preset: LightingPreset,
exposure: f32,
light_yaw: f32,
light_pitch: f32,
observer: Vec3,
) -> f32 {
let rig = crate::optics::studio_rig::StudioRig::new(light_yaw, light_pitch);
sample_studio_environment_with_rig(dir, lambda_nm, lighting_preset, exposure, &rig, observer)
}
const HEAD_SHADOW_OUTER_COS: f32 = 0.951_056_5;
const HEAD_SHADOW_INNER_COS: f32 = 0.970_295_7;
const SUN_OUTER_COS: f32 = 0.997_564_1;
const SUN_INNER_COS: f32 = 0.999_390_8;
const TENT_KEY_OUTER_COS: f32 = 0.766_044_4;
const TENT_KEY_INNER_COS: f32 = 0.939_692_6;
const SPARK_OUTER_COS: f32 = 0.996_194_7;
const SPARK_INNER_COS: f32 = 0.999_390_8;
const CARD_OUTER_COS: f32 = 0.898_794;
const CARD_INNER_COS: f32 = 0.961_261_7;
const CARD_RING_SLOTS: [usize; 3] = [4, 8, 12];
fn smoothstep(e0: f32, e1: f32, x: f32) -> f32 {
let t = ((x - e0) / (e1 - e0)).clamp(0.0, 1.0);
t * t * (-2.0f32).mul_add(t, 3.0)
}
fn observer_visibility(d: Vec3, observer: Vec3) -> f32 {
1.0 - smoothstep(
HEAD_SHADOW_OUTER_COS,
HEAD_SHADOW_INNER_COS,
d.dot(observer),
)
}
fn horizon_blend(d: Vec3) -> f32 {
smoothstep(-0.05, 0.05, d.y)
}
fn sample_iso_hemisphere(d: Vec3, spec_power: f32, exposure: f32, observer: Vec3) -> f32 {
(horizon_blend(d) * observer_visibility(d, observer)) * (spec_power * exposure)
}
fn sample_light_tent(
d: Vec3,
spec_power: f32,
spot_mult: f32,
exposure: f32,
rig: &crate::optics::studio_rig::StudioRig,
observer: Vec3,
) -> f32 {
let horizon = horizon_blend(d);
let mut walls = 0.08f32.mul_add(d.y.max(0.0), 0.14);
let mut card = 0.0f32;
for slot in CARD_RING_SLOTS {
card = card.max(smoothstep(
CARD_OUTER_COS,
CARD_INNER_COS,
d.dot(rig.ring_dirs[slot]),
));
}
walls *= card.mul_add(-0.9, 1.0);
let key =
smoothstep(TENT_KEY_OUTER_COS, TENT_KEY_INNER_COS, d.dot(rig.key_dir)) * (1.4 * spot_mult);
let spark =
smoothstep(SPARK_OUTER_COS, SPARK_INNER_COS, d.dot(rig.fill_dir)) * (5.0 * spot_mult);
let above = ((walls + key) + spark) * (horizon * observer_visibility(d, observer));
let ground = 0.02 * (1.0 - horizon);
(above + ground) * (spec_power * exposure)
}
fn sample_daylight_dome(
d: Vec3,
spec_power: f32,
exposure: f32,
rig: &crate::optics::studio_rig::StudioRig,
observer: Vec3,
) -> f32 {
let horizon = horizon_blend(d);
let sun_dot = d.dot(rig.key_dir);
let sky = 0.08f32.mul_add(1.0 - d.y.max(0.0), 0.10);
let glow = sun_dot.max(0.0);
let glow2 = glow * glow;
let glow4 = glow2 * glow2;
let aureole = (glow4 * glow4) * 0.30;
let sun = smoothstep(SUN_OUTER_COS, SUN_INNER_COS, sun_dot) * 10.0;
let above = ((sky + aureole) + sun) * (horizon * observer_visibility(d, observer));
let ground = 0.04 * (1.0 - horizon);
(above + ground) * (spec_power * exposure)
}
fn sample_studio_rig(
d: Vec3,
spec_power: f32,
spot_mult: f32,
exposure: f32,
rig: &crate::optics::studio_rig::StudioRig,
) -> f32 {
let bg_val = 0.012f32.mul_add(d.y.mul_add(0.5, 0.5), 0.015).max(0.005) * exposure;
let mut radiance = bg_val * spec_power;
let key_dot = d.dot(rig.key_dir).max(0.0);
if key_dot > 0.0 {
let softbox = key_dot.powi(28) * 12.0 * spot_mult * exposure;
radiance = softbox.mul_add(spec_power, radiance);
}
let fill_dot = d.dot(rig.fill_dir).max(0.0);
if fill_dot > 0.0 {
let fill = fill_dot.powi(18) * 4.5 * exposure;
radiance = fill.mul_add(spec_power, radiance);
}
for ring_dir in rig.ring_dirs {
let ring_dot = d.dot(ring_dir).max(0.0);
if ring_dot > 0.96 {
let spark = (ring_dot - 0.96) / 0.04;
let intensity = spark.powi(6) * 22.0 * spot_mult * exposure;
radiance = intensity.mul_add(spec_power, radiance);
}
}
radiance
}
#[must_use]
fn sample_studio_environment_with_rig(
dir: Vec3,
lambda_nm: f32,
lighting_preset: LightingPreset,
exposure: f32,
rig: &crate::optics::studio_rig::StudioRig,
observer: Vec3,
) -> f32 {
let d = dir.normalize();
let LightingRigParams { spot_mult, .. } = lighting_preset.params();
let spec_power = lighting_preset.spectral_power(lambda_nm);
match lighting_preset.model() {
LightingModel::Studio => sample_studio_rig(d, spec_power, spot_mult, exposure, rig),
LightingModel::IsoHemisphere => sample_iso_hemisphere(d, spec_power, exposure, observer),
LightingModel::LightTent => {
sample_light_tent(d, spec_power, spot_mult, exposure, rig, observer)
}
LightingModel::DaylightDome => sample_daylight_dome(d, spec_power, exposure, rig, observer),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn d65_relative_spectral_power_matches_the_cie_table_at_450_and_550nm() {
let expected_450 = 117.008 / 100.0;
let expected_550 = 104.046 / 100.0;
let got_450 = d65_relative_spectral_power(450.0);
let got_550 = d65_relative_spectral_power(550.0);
assert!(
(got_450 - expected_450).abs() < 1e-4,
"450nm: got {got_450}, expected {expected_450} from the CIE D65 table"
);
assert!(
(got_550 - expected_550).abs() < 1e-4,
"550nm: got {got_550}, expected {expected_550} from the CIE D65 table"
);
assert!(
got_450 > got_550,
"D65's real spectrum has more relative power at 450nm than 550nm; \
got 450nm={got_450}, 550nm={got_550}"
);
let d65_ratio = d65_relative_spectral_power(450.0) / d65_relative_spectral_power(380.0);
let blackbody_ratio = blackbody_spectrum(450.0, 6500.0) / blackbody_spectrum(380.0, 6500.0);
assert!(
d65_ratio > blackbody_ratio * 1.5,
"test premise: the real D65 table's 450nm/380nm rise ({d65_ratio:.3}) must \
be much steeper than a smooth 6500K Planckian's ({blackbody_ratio:.3}), \
confirming the Daylight preset is no longer just a relabelled blackbody"
);
}
#[test]
fn daylight_preset_studio_environment_uses_the_d65_table_not_a_blackbody() {
let dir = Vec3::new(0.0, -1.0, 0.0);
let exposure = 1.0;
let daylight_450 =
sample_studio_environment(dir, 450.0, LightingPreset::Daylight, exposure, 0.0, 0.0);
let daylight_550 =
sample_studio_environment(dir, 550.0, LightingPreset::Daylight, exposure, 0.0, 0.0);
assert!(
daylight_450 > daylight_550,
"Daylight preset must reflect D65's own 450nm > 550nm ordering end to \
end, got 450nm={daylight_450}, 550nm={daylight_550}"
);
}
#[test]
fn backdrop_fills_the_camera_ray_only_when_set() {
let lambdas = [450.0f32, 550.0, 650.0];
let mut radiance = [0.0f32; 3];
let plain = LightingPreset::LightTent.studio(1.0, 0.4, 0.35);
assert!(!fill_backdrop(plain, &lambdas, &mut radiance));
assert_eq!(radiance, [0.0; 3]);
let carded = plain.with_backdrop(BACKDROP_GEMRAY_GREY);
assert!(fill_backdrop(carded, &lambdas, &mut radiance));
for (&value, &lambda_nm) in radiance.iter().zip(&lambdas) {
let expected =
BACKDROP_GEMRAY_GREY * LightingPreset::LightTent.spectral_power(lambda_nm);
assert!(
(value - expected).abs() < 1e-6,
"backdrop at {lambda_nm} nm: got {value}, expected {expected}"
);
}
}
#[test]
fn label_and_index_round_trip_for_every_preset() {
for (pos, &p) in LightingPreset::ALL.iter().enumerate() {
assert_eq!(LightingPreset::from_label(p.label()), p);
assert_eq!(LightingPreset::from_index(p.index()), p);
assert_eq!(p.index(), pos as i32);
}
}
#[test]
fn iso_hemisphere_is_one_above_and_zero_below() {
let val_up =
sample_studio_environment(Vec3::Y, 550.0, LightingPreset::IsoHemisphere, 1.0, 0.0, 0.0);
let expected = d65_relative_spectral_power(550.0);
assert!(
(val_up - expected).abs() < 1e-4,
"up direction should match d65_relative_spectral_power(550): got {val_up}, expected {expected}"
);
let val_down = sample_studio_environment(
-Vec3::Y,
550.0,
LightingPreset::IsoHemisphere,
1.0,
0.0,
0.0,
);
assert_eq!(val_down, 0.0, "down direction should be 0");
}
#[test]
fn observer_head_shadow_darkens_only_the_lit_models() {
let observer = Vec3::new(0.2, 0.9, -0.3).normalize();
let perp = observer.cross(Vec3::X).normalize();
let (sin_a, cos_a) = 20.0f32.to_radians().sin_cos();
let outside = observer.mul_add(Vec3::splat(cos_a), perp * sin_a);
let ring_with = sample_studio_environment_observed(
observer,
560.0,
LightingPreset::RingLights,
1.0,
0.4,
0.35,
observer,
);
let ring_without =
sample_studio_environment(observer, 560.0, LightingPreset::RingLights, 1.0, 0.4, 0.35);
assert_eq!(
ring_with.to_bits(),
ring_without.to_bits(),
"the studio rig must ignore the observer"
);
for preset in [
LightingPreset::IsoHemisphere,
LightingPreset::LightTent,
LightingPreset::DaylightDome,
] {
let at_eye = sample_studio_environment_observed(
observer, 560.0, preset, 1.0, 0.4, 0.35, observer,
);
assert_eq!(
at_eye, 0.0,
"{preset:?}: the eye direction must be fully shadowed"
);
let clear = sample_studio_environment_observed(
outside, 560.0, preset, 1.0, 0.4, 0.35, observer,
);
let unobserved = sample_studio_environment(outside, 560.0, preset, 1.0, 0.4, 0.35);
assert_eq!(
clear.to_bits(),
unobserved.to_bits(),
"{preset:?}: outside the cone the observer must not matter"
);
assert!(
unobserved > 0.0,
"{preset:?}: a lit direction above the girdle must be lit"
);
}
}
#[test]
fn legacy_lit_model_labels_resolve_to_the_renamed_presets() {
assert_eq!(
LightingPreset::from_label("ISO hemisphere"),
LightingPreset::IsoHemisphere
);
assert_eq!(
LightingPreset::from_label("Soft dome + ring lights"),
LightingPreset::LightTent
);
assert_eq!(
LightingPreset::from_label("Daylight dome + sun"),
LightingPreset::DaylightDome
);
}
#[test]
fn new_models_never_exceed_their_documented_peak() {
let mut max_iso = 0.0f32;
let mut max_soft = 0.0f32;
let mut max_daylight = 0.0f32;
for i in 0..2000 {
let phi = (i as f32 + 0.5) * (std::f32::consts::PI * (3.0 - 5.0f32.sqrt()));
let y = (i as f32 + 0.5).mul_add(-(2.0 / 2000.0), 1.0);
let r = y.mul_add(-y, 1.0).max(0.0).sqrt();
let dir = Vec3::new(r * phi.cos(), y, r * phi.sin());
let v_iso = sample_studio_environment(
dir,
560.0,
LightingPreset::IsoHemisphere,
1.0,
0.4,
0.35,
);
let v_soft =
sample_studio_environment(dir, 560.0, LightingPreset::LightTent, 1.0, 0.4, 0.35);
let v_daylight =
sample_studio_environment(dir, 560.0, LightingPreset::DaylightDome, 1.0, 0.4, 0.35);
max_iso = max_iso.max(v_iso);
max_soft = max_soft.max(v_soft);
max_daylight = max_daylight.max(v_daylight);
}
assert!(
max_iso <= 1.0 + 1e-5,
"ISO peak must not exceed 1.0, got {max_iso}"
);
assert!(
max_soft <= 5.5,
"Light tent peak must not exceed 5.5, got {max_soft}"
);
assert!(
max_daylight <= 10.5,
"Daylight dome peak must not exceed 10.5, got {max_daylight}"
);
}
#[test]
#[allow(clippy::unreadable_literal)]
fn studio_presets_are_unchanged_by_the_split() {
const BASELINE_BITS: [u32; 192] = [
1021303258, 1023655294, 1023453223, 1021200980, 1023595102, 1023378703, 1021099049,
1023535114, 1023261534, 1020997253, 1023475205, 1023144520, 1021568336, 1023811298,
1023605575, 1022776057, 1024522062, 1024299704, 1020689822, 1023178377, 1022791133,
1027755127, 1030009411, 1029658622, 1044895793, 1047214420, 1046853619, 1020392899,
1022828889, 1022449825, 1020281301, 1022697534, 1022321544, 1020178664, 1022576726,
1022203564, 1061137584, 1063361422, 1063015371, 1083905861, 1085705250, 1085425247,
1019902579, 1022251765, 1021886209, 1046107587, 1048608372, 1048246560, 1076756649,
1078775455, 1078461309, 1093086489, 1095026093, 1094724272, 1100967472, 1102817210,
1102529373, 1102501900, 1104623284, 1104293176, 1100418366, 1102170893, 1101898183,
1101296236, 1103204175, 1102907281, 1072082398, 1074250299, 1074042064, 1075709089,
1077542440, 1077257152, 1081883524, 1083470199, 1083242507, 1084303182, 1086172911,
1085881963, 1078120144, 1080380336, 1080028628, 1050792594, 1052670085, 1052377929,
1047922901, 1049676716, 1049454620, 1044118990, 1046300096, 1045960695, 1018488179,
1020586967, 1020260376, 1018134305, 1020170445, 1019853601, 1018031797, 1020049789,
1019735769, 1075090799, 1076814691, 1076546437, 1068234283, 1070229409, 1069918948,
1017725102, 1019688798, 1019383227, 1017689159, 1019646492, 1019341912, 1017521023,
1019448590, 1019148641, 1028692337, 1031112541, 1030735933, 1017420802, 1019330627,
1019033440, 1017213944, 1019087147, 1018795658, 1039204672, 1041094120, 1040876564,
1018976767, 1021162052, 1020822000, 1016907249, 1018726157, 1018443117, 1016805017,
1018605826, 1018325602, 1016702784, 1018485494, 1018208087, 1026205460, 1028185399,
1027877301, 1016519388, 1018269632, 1017997277, 1016396089, 1018124504, 1017855546,
1016303883, 1018015974, 1017749556, 1016191626, 1017883843, 1017620518, 1016089640,
1017763802, 1017503286, 1015987162, 1017643183, 1017385490, 1015884931, 1017522852,
1017267976, 1015807868, 1017432147, 1017179394, 1015680468, 1017282193, 1017032949,
1015578235, 1017161861, 1016915434, 1015476004, 1017041531, 1016797920, 1015373773,
1016921202, 1016680407, 1015271540, 1016800870, 1016562892, 1015169308, 1016680540,
1016445378, 1015067077, 1016560210, 1016327864, 1014908123, 1016439880, 1016210351,
1014703658, 1016319549, 1016092836,
];
let mut idx = 0;
for k in 0..64 {
let phi = (k as f32 + 0.5) * (std::f32::consts::PI * (3.0 - 5.0f32.sqrt()));
let y = (k as f32 + 0.5).mul_add(-(2.0 / 64.0), 1.0);
let r = y.mul_add(-y, 1.0).max(0.0).sqrt();
let dir = Vec3::new(r * phi.cos(), y, r * phi.sin());
for &lambda in &[450.0f32, 550.0, 650.0] {
let val = sample_studio_environment(
dir,
lambda,
LightingPreset::RingLights,
1.2,
0.4,
0.35,
);
assert_eq!(
val.to_bits(),
BASELINE_BITS[idx],
"divergence at dir {k}, lambda {lambda}: got {val} (bits {}), expected bits {}",
val.to_bits(),
BASELINE_BITS[idx]
);
idx += 1;
}
}
}
}