use super::sun::{SUN_DISC_COS, sun_radiance_factor};
use super::{
DEFAULT_HEAD_SHADOW_COSINES, EnvironmentSource, LightingModel, LightingPreset,
LightingRigParams, d65_relative_spectral_power, head_shadow_cosines,
};
use crate::optics::studio_rig::RING_LIGHT_COUNT;
use glam::Vec3;
#[inline]
pub(in super::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,
head_shadow_deg,
backdrop,
..
} => {
let rig = studio_rig
.expect("sample_environment_channel: Studio environment needs a pre-built rig");
sample_studio_environment_with_rig_shadow_tray(
dir,
lambda_nm,
preset,
exposure,
rig,
observer,
head_shadow_cosines(head_shadow_deg),
backdrop,
)
}
EnvironmentSource::HdrMap(map) => map.radiance_at(dir, lambda_nm),
}
}
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, shadow: [f32; 2]) -> f32 {
1.0 - smoothstep(shadow[0], shadow[1], d.dot(observer))
}
pub(super) fn horizon_blend(d: Vec3) -> f32 {
smoothstep(-0.05, 0.05, d.y)
}
#[derive(Clone, Copy)]
enum DirectionLighting {
Rig {
bg_val: f32,
softbox: Option<f32>,
fill: Option<f32>,
ring: [f32; RING_LIGHT_COUNT],
ring_len: usize,
ground: Option<(f32, f32)>,
},
Scaled(f32),
Banded {
red: f32,
green: f32,
blue: f32,
tray_ground: f32,
},
}
const ASET_RED_NM: f32 = 610.0;
const ASET_GREEN_NM: f32 = 540.0;
const ASET_BLUE_NM: f32 = 460.0;
const ASET_SIGMA_NM: f32 = 8.493_218;
const ASET_RED_GAIN: f32 = 8.0;
const ASET_GREEN_GAIN: f32 = 5.0;
const ASET_BLUE_GAIN: f32 = 16.0;
const ASET_EDGE_45_LO: f32 = 0.687_106_8;
const ASET_EDGE_45_HI: f32 = 0.727_106_8;
const ASET_EDGE_75_LO: f32 = 0.945_925_8;
const ASET_EDGE_75_HI: f32 = 0.985_925_8;
const fn aset_spec_input(lambda_nm: f32) -> f32 {
lambda_nm
}
fn aset_band(lambda_nm: f32, centre_nm: f32) -> f32 {
let z = (lambda_nm - centre_nm) / ASET_SIGMA_NM;
(-0.5 * z * z).exp()
}
impl DirectionLighting {
fn radiance(self, spec_power: f32, exposure: f32) -> f32 {
match self {
Self::Rig {
bg_val,
softbox,
fill,
ring,
ring_len,
ground,
} => {
let mut radiance = bg_val * spec_power;
if let Some(softbox) = softbox {
radiance = softbox.mul_add(spec_power, radiance);
}
if let Some(fill) = fill {
radiance = fill.mul_add(spec_power, radiance);
}
for &intensity in &ring[..ring_len] {
radiance = intensity.mul_add(spec_power, radiance);
}
ground.map_or(radiance, |(horizon, ground)| {
horizon.mul_add(radiance, ground * spec_power)
})
}
Self::Scaled(factor) => factor * (spec_power * exposure),
Self::Banded {
red,
green,
blue,
tray_ground,
} => {
let lambda_nm = spec_power;
let r = (red * aset_band(lambda_nm, ASET_RED_NM)) * ASET_RED_GAIN;
let g = (green * aset_band(lambda_nm, ASET_GREEN_NM)) * ASET_GREEN_GAIN;
let b = (blue * aset_band(lambda_nm, ASET_BLUE_NM)) * ASET_BLUE_GAIN;
let zones = ((r + g) + b) * exposure;
if tray_ground > 0.0 {
(tray_ground * d65_relative_spectral_power(lambda_nm)).mul_add(exposure, zones)
} else {
zones
}
}
}
}
}
fn lit_ground(horizon: f32, vis: f32, tray: f32, own_ground: f32) -> f32 {
if tray > 0.0 {
((1.0 - horizon) * vis) * tray
} else {
own_ground * (1.0 - horizon)
}
}
fn iso_hemisphere_lighting(
d: Vec3,
observer: Vec3,
shadow: [f32; 2],
tray: f32,
) -> DirectionLighting {
let horizon = horizon_blend(d);
let vis = observer_visibility(d, observer, shadow);
if tray > 0.0 {
DirectionLighting::Scaled(((1.0 - horizon) * vis).mul_add(tray, horizon * vis))
} else {
DirectionLighting::Scaled(horizon * vis)
}
}
fn light_tent_lighting(
d: Vec3,
spot_mult: f32,
tent: super::TentParams,
rig: &crate::optics::studio_rig::StudioRig,
observer: Vec3,
shadow: [f32; 2],
tray: f32,
) -> DirectionLighting {
let horizon = horizon_blend(d);
let mut walls = 0.08f32.mul_add(d.y.max(0.0), 0.14) * tent.walls;
if tent.flat > 0.0 {
walls = (0.18 * tent.walls).mul_add(tent.flat, walls * (1.0 - tent.flat));
}
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 * tent.cards).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)
* tent.spark;
let vis = observer_visibility(d, observer, shadow);
let above = ((walls + key) + spark) * (horizon * vis);
let ground = lit_ground(horizon, vis, tray, tent.ground);
DirectionLighting::Scaled(above + ground)
}
fn daylight_dome_lighting(
d: Vec3,
rig: &crate::optics::studio_rig::StudioRig,
observer: Vec3,
shadow: [f32; 2],
tray: f32,
) -> DirectionLighting {
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 vis = observer_visibility(d, observer, shadow);
let above = (sky + aureole) * (horizon * vis);
let ground = lit_ground(horizon, vis, tray, 0.04);
DirectionLighting::Scaled(above + ground)
}
fn studio_rig_lighting(
d: Vec3,
spot_mult: f32,
exposure: f32,
rig: &crate::optics::studio_rig::StudioRig,
ambient_fill: bool,
tray: f32,
) -> DirectionLighting {
let bg_val = if ambient_fill {
0.012f32.mul_add(d.y.mul_add(0.5, 0.5), 0.015).max(0.005) * exposure
} else {
0.0
};
let key_dot = d.dot(rig.key_dir).max(0.0);
let softbox = (key_dot > 0.0).then(|| key_dot.powi(28) * 12.0 * spot_mult * exposure);
let fill_dot = d.dot(rig.fill_dir).max(0.0);
let fill = (fill_dot > 0.0).then(|| fill_dot.powi(18) * 4.5 * exposure);
let mut sparks = [0.0f32; RING_LIGHT_COUNT];
let mut ring_len = 0;
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;
sparks[ring_len] = spark.powi(6) * 22.0 * spot_mult * exposure;
ring_len += 1;
}
}
let ground = (tray > 0.0).then(|| {
let horizon = horizon_blend(d);
(horizon, ((1.0 - horizon) * tray) * exposure)
});
DirectionLighting::Rig {
bg_val,
softbox,
fill,
ring: sparks,
ring_len,
ground,
}
}
fn direction_lighting(
dir: Vec3,
lighting_preset: LightingPreset,
exposure: f32,
rig: &crate::optics::studio_rig::StudioRig,
observer: Vec3,
shadow: [f32; 2],
tray: f32,
) -> DirectionLighting {
let d = dir.normalize();
let LightingRigParams {
spot_mult, tent, ..
} = lighting_preset.params();
match lighting_preset.model() {
LightingModel::Studio => studio_rig_lighting(
d,
spot_mult,
exposure,
rig,
lighting_preset.has_ambient_fill(),
tray,
),
LightingModel::IsoHemisphere => iso_hemisphere_lighting(d, observer, shadow, tray),
LightingModel::LightTent => {
light_tent_lighting(d, spot_mult, tent, rig, observer, shadow, tray)
}
LightingModel::DaylightDome => daylight_dome_lighting(d, rig, observer, shadow, tray),
LightingModel::Aset => aset_radiance(d, rig, observer, shadow, tray),
LightingModel::DaylightSun => daylight_sun_radiance(d, rig, observer, shadow, tray),
}
}
pub(super) fn unit_radiance(
dir: Vec3,
lighting_preset: LightingPreset,
rig: &crate::optics::studio_rig::StudioRig,
) -> f32 {
direction_lighting(
dir,
lighting_preset,
1.0,
rig,
Vec3::ZERO,
DEFAULT_HEAD_SHADOW_COSINES,
0.0,
)
.radiance(1.0, 1.0)
}
fn aset_radiance(
d: Vec3,
_rig: &crate::optics::studio_rig::StudioRig,
_observer: Vec3,
_shadow: [f32; 2],
tray: f32,
) -> DirectionLighting {
let horizon = horizon_blend(d);
let s45 = smoothstep(ASET_EDGE_45_LO, ASET_EDGE_45_HI, d.y);
let s75 = smoothstep(ASET_EDGE_75_LO, ASET_EDGE_75_HI, d.y);
DirectionLighting::Banded {
red: (s45 * (1.0 - s75)) * horizon,
green: (1.0 - s45) * horizon,
blue: s75 * horizon,
tray_ground: if tray > 0.0 {
(1.0 - horizon) * tray
} else {
0.0
},
}
}
fn daylight_sun_radiance(
d: Vec3,
rig: &crate::optics::studio_rig::StudioRig,
observer: Vec3,
shadow: [f32; 2],
tray: f32,
) -> DirectionLighting {
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 vis = observer_visibility(d, observer, shadow);
let above = (sky + aureole) * (horizon * vis);
let ground = lit_ground(horizon, vis, tray, 0.04);
let sun = if sun_dot >= SUN_DISC_COS {
sun_radiance_factor(rig.key_dir)
} else {
0.0
};
DirectionLighting::Scaled((above + ground) + sun)
}
#[must_use]
pub 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 {
sample_studio_environment_with_rig_shadow(
dir,
lambda_nm,
lighting_preset,
exposure,
rig,
observer,
DEFAULT_HEAD_SHADOW_COSINES,
)
}
#[must_use]
pub fn sample_studio_environment_with_rig_shadow(
dir: Vec3,
lambda_nm: f32,
lighting_preset: LightingPreset,
exposure: f32,
rig: &crate::optics::studio_rig::StudioRig,
observer: Vec3,
shadow: [f32; 2],
) -> f32 {
sample_studio_environment_with_rig_shadow_tray(
dir,
lambda_nm,
lighting_preset,
exposure,
rig,
observer,
shadow,
0.0,
)
}
#[must_use]
#[allow(
clippy::too_many_arguments,
reason = "the unsuffixed entry point plus the tray, kept flat to mirror its WGSL twin"
)]
pub fn sample_studio_environment_with_rig_shadow_tray(
dir: Vec3,
lambda_nm: f32,
lighting_preset: LightingPreset,
exposure: f32,
rig: &crate::optics::studio_rig::StudioRig,
observer: Vec3,
shadow: [f32; 2],
tray: f32,
) -> f32 {
direction_lighting(dir, lighting_preset, exposure, rig, observer, shadow, tray)
.radiance(studio_spec_power(lighting_preset, lambda_nm), exposure)
}
#[must_use]
pub fn studio_spec_power(lighting_preset: LightingPreset, lambda_nm: f32) -> f32 {
if lighting_preset == LightingPreset::Aset {
aset_spec_input(lambda_nm)
} else {
lighting_preset.spectral_power(lambda_nm)
}
}
#[must_use]
pub fn sample_studio_environment_with_rig_spec_power(
dir: Vec3,
lighting_preset: LightingPreset,
exposure: f32,
rig: &crate::optics::studio_rig::StudioRig,
observer: Vec3,
spec_power: f32,
) -> f32 {
sample_studio_environment_with_rig_spec_power_tray(
dir,
lighting_preset,
exposure,
rig,
observer,
spec_power,
0.0,
)
}
#[must_use]
pub fn sample_studio_environment_with_rig_spec_power_shadow(
dir: Vec3,
lighting_preset: LightingPreset,
exposure: f32,
rig: &crate::optics::studio_rig::StudioRig,
observer: Vec3,
spec_power: f32,
shadow: [f32; 2],
) -> f32 {
sample_studio_environment_with_rig_spec_power_shadow_tray(
dir,
lighting_preset,
exposure,
rig,
observer,
spec_power,
shadow,
0.0,
)
}
#[must_use]
#[allow(
clippy::too_many_arguments,
reason = "the unsuffixed entry point plus the tray, kept flat to mirror its WGSL twin"
)]
pub fn sample_studio_environment_with_rig_spec_power_shadow_tray(
dir: Vec3,
lighting_preset: LightingPreset,
exposure: f32,
rig: &crate::optics::studio_rig::StudioRig,
observer: Vec3,
spec_power: f32,
shadow: [f32; 2],
tray: f32,
) -> f32 {
direction_lighting(dir, lighting_preset, exposure, rig, observer, shadow, tray)
.radiance(spec_power, exposure)
}
#[must_use]
pub fn sample_studio_environment_with_rig_spec_power_tray(
dir: Vec3,
lighting_preset: LightingPreset,
exposure: f32,
rig: &crate::optics::studio_rig::StudioRig,
observer: Vec3,
spec_power: f32,
tray: f32,
) -> f32 {
direction_lighting(
dir,
lighting_preset,
exposure,
rig,
observer,
DEFAULT_HEAD_SHADOW_COSINES,
tray,
)
.radiance(spec_power, exposure)
}
#[must_use]
#[allow(
clippy::too_many_arguments,
reason = "the single-wavelength entry's parameters plus the lambda array and the tray"
)]
fn sample_studio_environment_channels<const N: usize>(
dir: Vec3,
lambdas: &[f32; N],
lighting_preset: LightingPreset,
exposure: f32,
rig: &crate::optics::studio_rig::StudioRig,
observer: Vec3,
shadow: [f32; 2],
tray: f32,
) -> [f32; N] {
let lighting = direction_lighting(dir, lighting_preset, exposure, rig, observer, shadow, tray);
let spectrum = lighting_preset.illuminant_spectrum();
let aset = lighting_preset == LightingPreset::Aset;
std::array::from_fn(|k| {
let spec_power = if aset {
aset_spec_input(lambdas[k])
} else {
spectrum.power(lambdas[k])
};
lighting.radiance(spec_power, exposure)
})
}
#[inline]
pub(in super::super) fn sample_environment_channels<const N: usize>(
environment: EnvironmentSource<'_>,
dir: Vec3,
lambdas: &[f32; N],
studio_rig: Option<&crate::optics::studio_rig::StudioRig>,
observer: Vec3,
) -> [f32; N] {
match environment {
EnvironmentSource::Studio {
preset,
exposure,
head_shadow_deg,
backdrop,
..
} => {
let rig = studio_rig
.expect("sample_environment_channels: Studio environment needs a pre-built rig");
sample_studio_environment_channels(
dir,
lambdas,
preset,
exposure,
rig,
observer,
head_shadow_cosines(head_shadow_deg),
backdrop,
)
}
EnvironmentSource::HdrMap(map) => {
let rgb = map.radiance_rgb(dir);
std::array::from_fn(|k| {
crate::renderer::env_map::rgb_to_spectral_radiance(rgb, lambdas[k])
})
}
}
}