use super::{EnvironmentSource, LightingModel, LightingPreset, LightingRigParams};
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, ..
} => {
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(in super::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 => {
let spectrum = preset.illuminant_spectrum();
for (out, &lambda_nm) in radiance.iter_mut().zip(lambdas) {
*out = backdrop * spectrum.power(lambda_nm);
}
true
}
_ => false,
}
}
pub(in super::super) struct EnvNeeSample {
pub(in super::super) dir: Vec3,
pub(in super::super) pdf: f32,
pub(in super::super) rgb: [f32; 3],
}
#[must_use]
pub(in super::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(in super::super) fn environment_nee_pdf(environment: EnvironmentSource<'_>, dir: Vec3) -> f32 {
match environment {
EnvironmentSource::Studio { .. } => 0.0,
EnvironmentSource::HdrMap(map) => map.pdf(dir),
}
}
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)
}
#[derive(Clone, Copy)]
enum DirectionLighting {
Rig {
bg_val: f32,
softbox: Option<f32>,
fill: Option<f32>,
ring: [f32; RING_LIGHT_COUNT],
ring_len: usize,
},
Scaled(f32),
}
impl DirectionLighting {
fn radiance(self, spec_power: f32, exposure: f32) -> f32 {
match self {
Self::Rig {
bg_val,
softbox,
fill,
ring,
ring_len,
} => {
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);
}
radiance
}
Self::Scaled(factor) => factor * (spec_power * exposure),
}
}
}
fn iso_hemisphere_lighting(d: Vec3, observer: Vec3) -> DirectionLighting {
DirectionLighting::Scaled(horizon_blend(d) * observer_visibility(d, observer))
}
fn light_tent_lighting(
d: Vec3,
spot_mult: f32,
rig: &crate::optics::studio_rig::StudioRig,
observer: Vec3,
) -> DirectionLighting {
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);
DirectionLighting::Scaled(above + ground)
}
fn daylight_dome_lighting(
d: Vec3,
rig: &crate::optics::studio_rig::StudioRig,
observer: Vec3,
) -> 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 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);
DirectionLighting::Scaled(above + ground)
}
fn studio_rig_lighting(
d: Vec3,
spot_mult: f32,
exposure: f32,
rig: &crate::optics::studio_rig::StudioRig,
) -> DirectionLighting {
let bg_val = 0.012f32.mul_add(d.y.mul_add(0.5, 0.5), 0.015).max(0.005) * exposure;
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 ring = [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;
ring[ring_len] = spark.powi(6) * 22.0 * spot_mult * exposure;
ring_len += 1;
}
}
DirectionLighting::Rig {
bg_val,
softbox,
fill,
ring,
ring_len,
}
}
fn direction_lighting(
dir: Vec3,
lighting_preset: LightingPreset,
exposure: f32,
rig: &crate::optics::studio_rig::StudioRig,
observer: Vec3,
) -> DirectionLighting {
let d = dir.normalize();
let LightingRigParams { spot_mult, .. } = lighting_preset.params();
match lighting_preset.model() {
LightingModel::Studio => studio_rig_lighting(d, spot_mult, exposure, rig),
LightingModel::IsoHemisphere => iso_hemisphere_lighting(d, observer),
LightingModel::LightTent => light_tent_lighting(d, spot_mult, rig, observer),
LightingModel::DaylightDome => daylight_dome_lighting(d, rig, observer),
}
}
#[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 {
direction_lighting(dir, lighting_preset, exposure, rig, observer)
.radiance(lighting_preset.spectral_power(lambda_nm), exposure)
}
#[must_use]
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,
) -> [f32; N] {
let lighting = direction_lighting(dir, lighting_preset, exposure, rig, observer);
let spectrum = lighting_preset.illuminant_spectrum();
std::array::from_fn(|k| lighting.radiance(spectrum.power(lambdas[k]), 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, ..
} => {
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)
}
EnvironmentSource::HdrMap(map) => std::array::from_fn(|k| map.radiance_at(dir, lambdas[k])),
}
}