use super::{
NUM_CHANNELS,
absorption::spectral_absorption,
camera::Ray,
refraction::{RayMaterialContext, RayWavelengthCache},
};
use crate::optics::{
birefringence::{
AbsorptionTensor3, BirefringenceParams, assigned_mode_alpha, assigned_mode_e_field_uniaxial,
},
materials::AbsorptionUnit,
zoning::{MAX_ZONES, ZoneAbsorption, ZoneKernel, ZoneKernelF32},
};
use glam::Vec3;
pub const ZONE_SLOTS: usize = MAX_ZONES + 1;
pub type ZoneAlphas = [[f32; NUM_CHANNELS]; ZONE_SLOTS];
pub type ZoneLengthsMm = [f32; ZONE_SLOTS];
enum ZoneKernels {
Single(Box<ZoneKernelF32>),
Double(Box<ZoneKernel>),
}
pub struct ZonedCache {
kernels: ZoneKernels,
tensors: Vec<[AbsorptionTensor3; NUM_CHANNELS]>,
}
fn channel_tensors(
zone: &ZoneAbsorption,
lambdas: &[f32; NUM_CHANNELS],
c_axis: Vec3,
is_biaxial: bool,
) -> [AbsorptionTensor3; NUM_CHANNELS] {
let t = &zone.tensor;
std::array::from_fn(|k| {
let alpha_o = spectral_absorption(&t.o_ray, lambdas[k]);
let alpha_e = spectral_absorption(&t.e_ray, lambdas[k]);
let alpha_beta = if is_biaxial {
t.beta_ray
.as_deref()
.map(|bands| spectral_absorption(bands, lambdas[k]))
} else {
None
};
alpha_beta.map_or_else(
|| AbsorptionTensor3::uniaxial(alpha_o, alpha_e, c_axis),
|beta| AbsorptionTensor3::biaxial(alpha_o, beta, alpha_e, c_axis),
)
})
}
pub(super) fn build_zoned_cache(ctx: &RayMaterialContext, is_biaxial: bool) -> Option<ZonedCache> {
let zoned = ctx.material.zoning.as_ref()?;
debug_assert!(
ctx.material.absorption_unit == AbsorptionUnit::PerMm,
"a zoned material must be PerMm (its zones are in millimetres)"
);
debug_assert!(
zoned.validate().is_ok(),
"invalid zoning installed on a material: {:?}",
zoned.validate()
);
let kernels = ZoneKernelF32::new(zoned).map_or_else(
|| ZoneKernels::Double(Box::new(ZoneKernel::new(zoned))),
|kernel| ZoneKernels::Single(Box::new(kernel)),
);
let tensors = (0..=zoned.zones.len().min(MAX_ZONES))
.filter_map(|i| zoned.zone_absorption(i))
.map(|zone| channel_tensors(zone, &ctx.lambdas, ctx.c_axis, is_biaxial))
.collect();
Some(ZonedCache { kernels, tensors })
}
impl ZonedCache {
pub(crate) fn zone_alphas(
&self,
ctx: &RayMaterialContext,
cache: &RayWavelengthCache,
k_hat: Vec3,
is_extraordinary: bool,
) -> ZoneAlphas {
let c_axis = ctx.c_axis;
let mut out = [[0.0f32; NUM_CHANNELS]; ZONE_SLOTS];
if !ctx.is_anisotropic {
let (eigen_a, eigen_b) = cache.hero_indicatrix.map_or_else(
|| {
(
BirefringenceParams::ordinary_eigen_polarization(k_hat, c_axis),
BirefringenceParams::extraordinary_eigen_polarization(k_hat, c_axis),
)
},
|ind| ind.eigen_polarizations(k_hat),
);
for (slot, tensors) in out.iter_mut().zip(&self.tensors) {
for (alpha, tensor) in slot.iter_mut().zip(tensors) {
*alpha = f32::midpoint(
tensor.quadratic_form(eigen_a),
tensor.quadratic_form(eigen_b),
);
}
}
return out;
}
let e_mode_hat = cache.hero_indicatrix.map_or_else(
|| {
let ordinary_hero = cache.n_o_ch[ctx.hero_idx];
let extraordinary_hero = ctx
.material
.extraordinary_index_at(ctx.lambdas[ctx.hero_idx], ordinary_hero);
assigned_mode_e_field_uniaxial(
k_hat,
c_axis,
is_extraordinary,
ordinary_hero,
extraordinary_hero,
)
},
|ind| ind.assigned_mode_e_field(k_hat, is_extraordinary),
);
for (slot, tensors) in out.iter_mut().zip(&self.tensors) {
for (alpha, tensor) in slot.iter_mut().zip(tensors) {
*alpha = assigned_mode_alpha(tensor, e_mode_hat);
}
}
out
}
pub(crate) fn lengths_mm(
&self,
origin: Vec3,
dir: Vec3,
t0: f32,
t1: f32,
path_scale: f32,
) -> ZoneLengthsMm {
let mut out = [0.0f32; ZONE_SLOTS];
let scaled = (t1 - t0) * path_scale;
if scaled.is_nan() || scaled <= 0.0 {
return out;
}
let from = (origin + dir * t0) * path_scale;
let to = (origin + dir * t1) * path_scale;
let raw: [f32; ZONE_SLOTS] = match &self.kernels {
ZoneKernels::Single(kernel) => kernel.lengths(from, to),
ZoneKernels::Double(kernel) => {
let lengths = kernel.lengths(from.as_dvec3(), to.as_dvec3());
#[expect(
clippy::cast_possible_truncation,
reason = "zone path lengths in model units fit f32; the GPU kernels use f32"
)]
let narrowed = lengths.map(|l| l as f32);
narrowed
}
};
let total: f32 = raw.iter().sum();
if total.is_nan() || total <= 0.0 {
out[0] = scaled;
return out;
}
for (o, r) in out.iter_mut().zip(raw) {
*o = scaled * (r / total);
}
out
}
}
pub fn optical_depths(
alphas: &ZoneAlphas,
lengths: &ZoneLengthsMm,
sigma_s: f32,
) -> [f32; NUM_CHANNELS] {
let mut depth = [0.0f32; NUM_CHANNELS];
for (zone_alphas, &len) in alphas.iter().zip(lengths) {
for (d, &alpha) in depth.iter_mut().zip(zone_alphas) {
*d = (alpha + sigma_s).mul_add(len, *d);
}
}
depth
}
pub(super) fn segment_optical_depths(
ctx: &RayMaterialContext,
cache: &RayWavelengthCache,
k_hat: Vec3,
is_extraordinary: bool,
ray: Ray,
path_len: f32,
) -> Option<[f32; NUM_CHANNELS]> {
let zoned = cache.zoned.as_ref()?;
let alphas = zoned.zone_alphas(ctx, cache, k_hat, is_extraordinary);
let lengths = zoned.lengths_mm(
ray.origin,
ray.dir,
0.0,
path_len,
ctx.material.absorption_path_scale,
);
Some(optical_depths(&alphas, &lengths, 0.0))
}
pub fn local_extinction(
zoned: &ZonedCache,
alphas: &ZoneAlphas,
ray: Ray,
t: f32,
hit_t: f32,
path_scale: f32,
sigma_s: f32,
) -> [f32; NUM_CHANNELS] {
let delta = (1e-4 * hit_t).max(1e-7);
let t_a = (t - delta).max(0.0);
let t_b = (t + delta).min(hit_t);
let lengths = zoned.lengths_mm(ray.origin, ray.dir, t_a, t_b, path_scale);
let total: f32 = lengths.iter().sum();
let depth = optical_depths(alphas, &lengths, sigma_s);
if total > 0.0 {
depth.map(|d| d / total)
} else {
std::array::from_fn(|k| alphas[0][k] + sigma_s)
}
}