use super::color::illuminant_white_balance;
use crate::renderer::env_map::EnvironmentMap;
use glam::Vec3;
mod rig;
mod spectral;
#[cfg(test)]
mod tests;
pub(super) use rig::{
environment_nee_pdf, environment_supports_nee, fill_backdrop, sample_environment_channel,
sample_environment_channels, sample_environment_for_nee,
};
#[cfg(feature = "gpu")]
#[must_use]
pub(crate) fn sun_nee_reference_sample(key_dir: Vec3, u0: f32, u1: f32) -> (Vec3, f32) {
let dir = rig::sun_cone_direction(key_dir, u0, u1);
(dir, rig::sun_nee_pdf(key_dir, dir))
}
#[cfg(feature = "gpu")]
#[must_use]
pub(crate) fn sun_nee_reference_factor(key_dir: Vec3) -> f32 {
rig::sun_radiance_factor(key_dir)
}
#[cfg(feature = "gpu")]
#[must_use]
pub(crate) fn sun_nee_reference_pdf(key_dir: Vec3, dir: Vec3) -> f32 {
rig::sun_nee_pdf(key_dir, dir)
}
pub use rig::{
sample_studio_environment_with_rig, sample_studio_environment_with_rig_shadow,
sample_studio_environment_with_rig_spec_power, studio_spec_power,
};
use spectral::{BlackbodyNorm, IlluminantSpectrum};
pub use spectral::{blackbody_spectrum, d65_relative_spectral_power};
pub const DEFAULT_HEAD_SHADOW_DEG: f32 = 16.0;
pub const DEFAULT_HEAD_SHADOW_COSINES: [f32; 2] = [0.951_056_5, 0.970_295_7];
pub const MAX_HEAD_SHADOW_DEG: f32 = 88.0;
pub const MIN_HEAD_SHADOW_DEG: f32 = 3.0;
#[must_use]
pub fn head_shadow_cosines(deg: f32) -> [f32; 2] {
if deg.is_nan() || deg.to_bits() == DEFAULT_HEAD_SHADOW_DEG.to_bits() {
return DEFAULT_HEAD_SHADOW_COSINES;
}
if deg <= 0.0 {
return [2.0, 3.0];
}
let deg = deg.clamp(MIN_HEAD_SHADOW_DEG, MAX_HEAD_SHADOW_DEG);
[
(deg + 2.0).to_radians().cos(),
(deg - 2.0).to_radians().cos(),
]
}
const fn clamp_head_shadow_deg(deg: f32) -> f32 {
if deg.is_nan() {
DEFAULT_HEAD_SHADOW_DEG
} else if deg <= 0.0 {
0.0
} else if deg < MIN_HEAD_SHADOW_DEG {
MIN_HEAD_SHADOW_DEG
} else if deg > MAX_HEAD_SHADOW_DEG {
MAX_HEAD_SHADOW_DEG
} else {
deg
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct LightingRigParams {
pub temp_k: f32,
pub spot_mult: f32,
pub tent: TentParams,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct TentParams {
pub walls: f32,
pub cards: f32,
pub spark: f32,
pub ground: f32,
pub flat: f32,
}
impl TentParams {
pub const DEFAULT: Self = Self {
walls: 1.0,
cards: 1.0,
spark: 1.0,
ground: 0.02,
flat: 0.0,
};
#[must_use]
pub const fn to_array(self) -> [f32; 4] {
[self.walls, self.cards, self.spark, self.ground]
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum LightingPreset {
Daylight,
Incandescent,
RingLights,
DarkSpotlight,
IsoHemisphere,
#[default]
LightTent,
DaylightDome,
UvLamp365,
UvLamp395,
DaylightSun,
Aset,
ShopLights,
WindowDaylight,
WhiteTray,
IlluminantA,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum LightingModel {
Studio,
IsoHemisphere,
LightTent,
DaylightDome,
Aset,
DaylightSun,
}
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,
Self::Aset => 4,
Self::DaylightSun => 5,
}
}
}
impl LightingPreset {
pub const ALL: [Self; 15] = [
Self::LightTent,
Self::IsoHemisphere,
Self::WhiteTray,
Self::ShopLights,
Self::WindowDaylight,
Self::DaylightDome,
Self::DaylightSun,
Self::Daylight,
Self::Incandescent,
Self::IlluminantA,
Self::RingLights,
Self::DarkSpotlight,
Self::Aset,
Self::UvLamp365,
Self::UvLamp395,
];
#[must_use]
pub const fn params(self) -> LightingRigParams {
match self {
Self::Incandescent => LightingRigParams {
temp_k: 3200.0,
spot_mult: 1.2,
tent: TentParams::DEFAULT,
},
Self::RingLights => LightingRigParams {
temp_k: 5000.0,
spot_mult: 1.6,
tent: TentParams::DEFAULT,
},
Self::DarkSpotlight => LightingRigParams {
temp_k: 6000.0,
spot_mult: 2.4,
tent: TentParams::DEFAULT,
},
Self::LightTent => LightingRigParams {
temp_k: 5000.0,
spot_mult: 1.0,
tent: TentParams::DEFAULT,
},
Self::ShopLights => LightingRigParams {
temp_k: 5000.0,
spot_mult: 1.0,
tent: TentParams {
walls: 3.0,
cards: 0.0,
spark: 1.0,
ground: 0.05,
flat: 0.0,
},
},
Self::WindowDaylight => LightingRigParams {
temp_k: 5000.0,
spot_mult: 1.0,
tent: TentParams {
walls: 0.4,
cards: 0.0,
spark: 0.0,
ground: 0.03,
flat: 0.0,
},
},
Self::WhiteTray => LightingRigParams {
temp_k: 5000.0,
spot_mult: 0.0,
tent: TentParams {
walls: 5.0,
cards: 0.0,
spark: 0.0,
ground: 0.8,
flat: 1.0,
},
},
Self::IlluminantA => LightingRigParams {
temp_k: 2856.0,
spot_mult: 1.2,
tent: TentParams::DEFAULT,
},
Self::Daylight
| Self::IsoHemisphere
| Self::DaylightDome
| Self::DaylightSun
| Self::Aset
| Self::UvLamp365
| Self::UvLamp395 => LightingRigParams {
temp_k: 6500.0,
spot_mult: 1.0,
tent: TentParams::DEFAULT,
},
}
}
#[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 => "Grading tray (D65 hemisphere + head shadow)",
Self::LightTent => "Light tent + black cards",
Self::DaylightDome => "Daylight sky (no sun)",
Self::DaylightSun => "Daylight sky + direct sun",
Self::Aset => "Contrast view (ASET-style)",
Self::ShopLights => "Jewellery shop (diffuse + spots)",
Self::WindowDaylight => "Window daylight",
Self::WhiteTray => "White tray (lit from below)",
Self::IlluminantA => "Incandescent A (2856K)",
Self::UvLamp365 => "UV lamp 365 nm",
Self::UvLamp395 => "UV lamp 395 nm",
}
}
#[must_use]
pub fn from_label(label: &str) -> Self {
match label {
"D65 Daylight (6500K)" | "D65 Daylight (5500K)" => Self::Daylight,
"Incandescent (3200K)" => Self::Incandescent,
"Gem Studio Ring Lights" => Self::RingLights,
"Dramatic Dark Spotlight" => Self::DarkSpotlight,
"Grading tray (D65 hemisphere + head shadow)"
| "ISO hemisphere"
| "ISO hemisphere (GemRay-style)" => Self::IsoHemisphere,
"Daylight sky (no sun)" | "Daylight sky + sun" | "Daylight dome + sun" => {
Self::DaylightDome
}
"Daylight sky + direct sun" => Self::DaylightSun,
"Contrast view (ASET-style)" => Self::Aset,
"Jewellery shop (diffuse + spots)" => Self::ShopLights,
"Window daylight" => Self::WindowDaylight,
"White tray (lit from below)" => Self::WhiteTray,
"Incandescent A (2856K)" => Self::IlluminantA,
"UV lamp 365 nm" => Self::UvLamp365,
"UV lamp 395 nm" => Self::UvLamp395,
_ => Self::LightTent,
}
}
#[must_use]
pub const fn index(self) -> i32 {
match self {
Self::LightTent => 0,
Self::IsoHemisphere => 1,
Self::WhiteTray => 2,
Self::ShopLights => 3,
Self::WindowDaylight => 4,
Self::DaylightDome => 5,
Self::DaylightSun => 6,
Self::Daylight => 7,
Self::Incandescent => 8,
Self::IlluminantA => 9,
Self::RingLights => 10,
Self::DarkSpotlight => 11,
Self::Aset => 12,
Self::UvLamp365 => 13,
Self::UvLamp395 => 14,
}
}
#[must_use]
pub const fn from_index(index: i32) -> Self {
match index {
1 => Self::IsoHemisphere,
2 => Self::WhiteTray,
3 => Self::ShopLights,
4 => Self::WindowDaylight,
5 => Self::DaylightDome,
6 => Self::DaylightSun,
7 => Self::Daylight,
8 => Self::Incandescent,
9 => Self::IlluminantA,
10 => Self::RingLights,
11 => Self::DarkSpotlight,
12 => Self::Aset,
13 => Self::UvLamp365,
14 => Self::UvLamp395,
_ => Self::LightTent,
}
}
#[must_use]
pub const fn model(self) -> LightingModel {
match self {
Self::Daylight
| Self::Incandescent
| Self::RingLights
| Self::DarkSpotlight
| Self::IlluminantA
| Self::UvLamp365
| Self::UvLamp395 => LightingModel::Studio,
Self::IsoHemisphere => LightingModel::IsoHemisphere,
Self::LightTent | Self::ShopLights | Self::WindowDaylight | Self::WhiteTray => {
LightingModel::LightTent
}
Self::DaylightDome => LightingModel::DaylightDome,
Self::Aset => LightingModel::Aset,
Self::DaylightSun => LightingModel::DaylightSun,
}
}
#[must_use]
pub const fn uses_d65(self) -> bool {
matches!(
self,
Self::Daylight
| Self::IsoHemisphere
| Self::DaylightDome
| Self::DaylightSun
| Self::ShopLights
| Self::WindowDaylight
| Self::WhiteTray
| Self::Aset
)
}
#[must_use]
pub const fn is_uv_lamp(self) -> bool {
matches!(self, Self::UvLamp365 | Self::UvLamp395)
}
#[must_use]
pub const fn uv_line(self) -> Option<(f32, f32)> {
match self {
Self::UvLamp365 => Some((365.0, 10.0)),
Self::UvLamp395 => Some((395.0, 12.0)),
_ => None,
}
}
#[must_use]
pub const fn uses_white_balance(self) -> bool {
!self.uses_d65() && !self.is_uv_lamp() && !matches!(self, Self::Aset)
}
#[must_use]
pub const fn has_ambient_fill(self) -> bool {
!self.is_uv_lamp()
}
#[must_use]
pub fn spectral_power(self, lambda_nm: f32) -> f32 {
self.illuminant_spectrum().power(lambda_nm)
}
fn illuminant_spectrum(self) -> IlluminantSpectrum {
if self.uses_d65() {
IlluminantSpectrum::D65
} else if let Some((centre_nm, fwhm_nm)) = self.uv_line() {
IlluminantSpectrum::Gaussian {
centre_nm,
sigma_nm: fwhm_nm / (2.0 * (2.0 * std::f32::consts::LN_2).sqrt()),
}
} else {
IlluminantSpectrum::Blackbody(BlackbodyNorm::new(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,
surface_glare: 1.0,
head_shadow_deg: DEFAULT_HEAD_SHADOW_DEG,
}
}
}
#[derive(Clone, Copy)]
pub enum EnvironmentSource<'a> {
Studio {
preset: LightingPreset,
exposure: f32,
light_yaw: f32,
light_pitch: f32,
backdrop: f32,
surface_glare: f32,
head_shadow_deg: f32,
},
HdrMap(&'a EnvironmentMap),
}
pub const BACKDROP_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,
surface_glare,
head_shadow_deg,
..
} => Self::Studio {
preset,
exposure,
light_yaw,
light_pitch,
backdrop,
surface_glare,
head_shadow_deg,
},
hdr @ Self::HdrMap(_) => hdr,
}
}
#[must_use]
pub const fn with_head_shadow(self, head_shadow_deg: f32) -> Self {
match self {
Self::Studio {
preset,
exposure,
light_yaw,
light_pitch,
backdrop,
surface_glare,
..
} => Self::Studio {
preset,
exposure,
light_yaw,
light_pitch,
backdrop,
surface_glare,
head_shadow_deg: clamp_head_shadow_deg(head_shadow_deg),
},
hdr @ Self::HdrMap(_) => hdr,
}
}
#[must_use]
pub const fn head_shadow_deg(&self) -> f32 {
match *self {
Self::Studio {
head_shadow_deg, ..
} => head_shadow_deg,
Self::HdrMap(_) => DEFAULT_HEAD_SHADOW_DEG,
}
}
#[must_use]
pub const fn with_surface_glare(self, surface_glare: f32) -> Self {
match self {
Self::Studio {
preset,
exposure,
light_yaw,
light_pitch,
backdrop,
head_shadow_deg,
..
} => Self::Studio {
preset,
exposure,
light_yaw,
light_pitch,
backdrop,
surface_glare: clamp_surface_glare(surface_glare),
head_shadow_deg,
},
hdr @ Self::HdrMap(_) => hdr,
}
}
#[must_use]
pub const fn lighting_preset(&self) -> Option<LightingPreset> {
match *self {
Self::Studio { preset, .. } => Some(preset),
Self::HdrMap(_) => None,
}
}
#[must_use]
pub const fn surface_glare(&self) -> f32 {
match *self {
Self::Studio { surface_glare, .. } => surface_glare,
Self::HdrMap(_) => 1.0,
}
}
}
const fn clamp_surface_glare(value: f32) -> f32 {
if value >= 1.0 || value.is_nan() {
1.0
} else if value > 0.0 {
value
} else {
0.0
}
}
#[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)
}