use super::{
DEFAULT_HEAD_SHADOW_COSINES, EnvironmentSource, LightingModel, LightingPreset,
LightingRigParams, 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,
..
} => {
let rig = studio_rig
.expect("sample_environment_channel: Studio environment needs a pre-built rig");
sample_studio_environment_with_rig_shadow(
dir,
lambda_nm,
preset,
exposure,
rig,
observer,
head_shadow_cosines(head_shadow_deg),
)
}
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],
pub(in super::super) sun: Option<SunNeeRadiance>,
}
#[derive(Clone, Copy)]
pub(in super::super) struct SunNeeRadiance {
geometry: f32,
exposure: f32,
preset: LightingPreset,
}
impl EnvNeeSample {
#[must_use]
pub(in super::super) fn radiance(&self, lambda_nm: f32) -> f32 {
self.sun.map_or_else(
|| crate::renderer::env_map::rgb_to_spectral_radiance(self.rgb, lambda_nm),
|sun| sun.geometry * (sun.preset.spectral_power(lambda_nm) * sun.exposure),
)
}
}
#[must_use]
pub(in super::super) fn environment_supports_nee(environment: EnvironmentSource<'_>) -> bool {
match environment {
EnvironmentSource::Studio { preset, .. } => preset.model() == LightingModel::DaylightSun,
EnvironmentSource::HdrMap(_) => true,
}
}
#[must_use]
pub(in super::super) fn sample_environment_for_nee(
environment: EnvironmentSource<'_>,
u0: f32,
u1: f32,
) -> Option<EnvNeeSample> {
match environment {
EnvironmentSource::Studio {
preset,
exposure,
light_yaw,
light_pitch,
..
} if preset.model() == LightingModel::DaylightSun => {
let key_dir = key_dir_only(light_yaw, light_pitch);
Some(EnvNeeSample {
dir: sun_cone_direction(key_dir, u0, u1),
pdf: 1.0 / SUN_SOLID_ANGLE,
rgb: [0.0; 3],
sun: Some(SunNeeRadiance {
geometry: sun_radiance_factor(key_dir),
exposure,
preset,
}),
})
}
EnvironmentSource::Studio { .. } => None,
EnvironmentSource::HdrMap(map) => {
let (dir, rgb, pdf) = map.sample(u0, u1);
(pdf > 0.0).then_some(EnvNeeSample {
dir,
pdf,
rgb,
sun: None,
})
}
}
}
fn key_dir_only(light_yaw: f32, light_pitch: f32) -> Vec3 {
let cos_pitch = light_pitch.cos();
let sin_pitch = light_pitch.sin();
let cos_yaw = light_yaw.cos();
let sin_yaw = light_yaw.sin();
Vec3::new(cos_pitch * sin_yaw, sin_pitch, cos_pitch * cos_yaw).normalize()
}
#[cfg(test)]
pub(super) fn key_dir_only_for_test(light_yaw: f32, light_pitch: f32) -> Vec3 {
key_dir_only(light_yaw, light_pitch)
}
#[must_use]
pub(in super::super) fn environment_nee_pdf(environment: EnvironmentSource<'_>, dir: Vec3) -> f32 {
match environment {
EnvironmentSource::Studio {
preset,
light_yaw,
light_pitch,
..
} if preset.model() == LightingModel::DaylightSun => {
sun_nee_pdf(key_dir_only(light_yaw, light_pitch), dir)
}
EnvironmentSource::Studio { .. } => 0.0,
EnvironmentSource::HdrMap(map) => map.pdf(dir),
}
}
pub(super) const SUN_DISC_COS: f32 = 0.999_988_9;
pub(super) const SUN_ONE_MINUS_COS: f32 = 1.108_646_4e-5;
pub(super) const SUN_SOLID_ANGLE: f32 = 6.965_831e-5;
pub(super) const SUN_RADIANCE: f32 = 40_000.0;
pub(super) fn sun_radiance_factor(key_dir: Vec3) -> f32 {
SUN_RADIANCE * horizon_blend(key_dir)
}
pub(super) fn sun_cone_direction(key_dir: Vec3, u0: f32, u1: f32) -> Vec3 {
let one_minus_cos = u0 * SUN_ONE_MINUS_COS;
let cos_t = 1.0 - one_minus_cos;
let sin_t = (one_minus_cos * (2.0 - one_minus_cos)).max(0.0).sqrt();
let phi = 2.0 * std::f32::consts::PI * u1;
let (sin_p, cos_p) = phi.sin_cos();
let a = if key_dir.x.abs() > 0.9 {
Vec3::Y
} else {
Vec3::X
};
let t = (a - key_dir * key_dir.dot(a)).normalize_or_zero();
let b = key_dir.cross(t);
(t * (sin_t * cos_p) + b * (sin_t * sin_p) + key_dir * cos_t).normalize_or_zero()
}
pub(super) fn sun_nee_pdf(key_dir: Vec3, dir: Vec3) -> f32 {
if dir.dot(key_dir) >= SUN_DISC_COS {
1.0 / SUN_SOLID_ANGLE
} else {
0.0
}
}
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))
}
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),
Banded { red: f32, green: f32, blue: 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,
} => {
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),
Self::Banded { red, green, blue } => {
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;
((r + g) + b) * exposure
}
}
}
}
fn iso_hemisphere_lighting(d: Vec3, observer: Vec3, shadow: [f32; 2]) -> DirectionLighting {
DirectionLighting::Scaled(horizon_blend(d) * observer_visibility(d, observer, shadow))
}
fn light_tent_lighting(
d: Vec3,
spot_mult: f32,
tent: super::TentParams,
rig: &crate::optics::studio_rig::StudioRig,
observer: Vec3,
shadow: [f32; 2],
) -> 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 above = ((walls + key) + spark) * (horizon * observer_visibility(d, observer, shadow));
let ground = tent.ground * (1.0 - horizon);
DirectionLighting::Scaled(above + ground)
}
fn daylight_dome_lighting(
d: Vec3,
rig: &crate::optics::studio_rig::StudioRig,
observer: Vec3,
shadow: [f32; 2],
) -> 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 above = (sky + aureole) * (horizon * observer_visibility(d, observer, shadow));
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,
ambient_fill: bool,
) -> 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;
}
}
DirectionLighting::Rig {
bg_val,
softbox,
fill,
ring: sparks,
ring_len,
}
}
fn direction_lighting(
dir: Vec3,
lighting_preset: LightingPreset,
exposure: f32,
rig: &crate::optics::studio_rig::StudioRig,
observer: Vec3,
shadow: [f32; 2],
) -> 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(),
),
LightingModel::IsoHemisphere => iso_hemisphere_lighting(d, observer, shadow),
LightingModel::LightTent => light_tent_lighting(d, spot_mult, tent, rig, observer, shadow),
LightingModel::DaylightDome => daylight_dome_lighting(d, rig, observer, shadow),
LightingModel::Aset => aset_radiance(d, rig, observer, shadow),
LightingModel::DaylightSun => daylight_sun_radiance(d, rig, observer, shadow),
}
}
fn aset_radiance(
d: Vec3,
_rig: &crate::optics::studio_rig::StudioRig,
_observer: Vec3,
_shadow: [f32; 2],
) -> 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,
}
}
fn daylight_sun_radiance(
d: Vec3,
rig: &crate::optics::studio_rig::StudioRig,
observer: Vec3,
shadow: [f32; 2],
) -> 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 above = (sky + aureole) * (horizon * observer_visibility(d, observer, shadow));
let ground = 0.04 * (1.0 - horizon);
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 {
direction_lighting(dir, lighting_preset, exposure, rig, observer, shadow)
.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 {
direction_lighting(
dir,
lighting_preset,
exposure,
rig,
observer,
DEFAULT_HEAD_SHADOW_COSINES,
)
.radiance(spec_power, 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,
shadow: [f32; 2],
) -> [f32; N] {
let lighting = direction_lighting(dir, lighting_preset, exposure, rig, observer, shadow);
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,
..
} => {
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),
)
}
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])
})
}
}
}