use super::context::{RayMaterialContext, RayWavelengthCache};
use crate::optics::{
birefringence::{BiaxialIndicatrix, BirefringenceParams},
raytracer::{NUM_CHANNELS, uniaxial_fresnel::UniaxialFrame},
};
use glam::Vec3;
#[derive(Clone, Copy, Default)]
pub(crate) struct BounceRefractionGeometry {
pub(crate) cos_i: f32,
pub(crate) sin_i: f32,
pub(crate) is_biaxial: bool,
pub(crate) n_o_hero: f32,
pub(crate) n_e_hero: f32,
pub(crate) hero_indicatrix: Option<BiaxialIndicatrix>,
pub(crate) n_biax_a_hero: f32,
pub(crate) n_biax_b_hero: f32,
pub(crate) n_o_ch: [f32; NUM_CHANNELS],
pub(crate) n_biax_a_ch: [f32; NUM_CHANNELS],
pub(crate) n1: f32,
pub(crate) n2: f32,
pub(crate) sin2_t: f32,
pub(crate) n1_ch: [f32; NUM_CHANNELS],
pub(crate) n2_ch: [f32; NUM_CHANNELS],
pub(crate) sin2_t_ch: [f32; NUM_CHANNELS],
pub(crate) uniaxial_frame: Option<UniaxialFrame>,
}
#[expect(
clippy::similar_names,
reason = "n_o/n_e are the established ordinary/extraordinary index names"
)]
pub(crate) fn theta_c_for_bounce(
ctx: &RayMaterialContext,
normal: Vec3,
k_hat: Vec3,
cos_i: f32,
inside_gem: bool,
is_biaxial: bool,
n_o_hero_seed: f32,
) -> f32 {
let material = ctx.material;
let c_axis = ctx.c_axis;
let is_anisotropic = ctx.is_anisotropic;
if !inside_gem && is_anisotropic && !is_biaxial {
let n_e_hero_seed =
material.extraordinary_index_at(ctx.lambdas[ctx.hero_idx], n_o_hero_seed);
let mut n_guess = n_o_hero_seed;
let mut theta = 0.0f32;
for _ in 0..2 {
let eta_guess = 1.0 / n_guess;
let sin2_t_guess = eta_guess * eta_guess * cos_i.mul_add(-cos_i, 1.0);
if sin2_t_guess > 1.0 {
break;
}
let cos_t_guess = (1.0 - sin2_t_guess).max(0.0).sqrt();
let wave_dir_guess =
(eta_guess * k_hat + eta_guess.mul_add(cos_i, -cos_t_guess) * normal).normalize();
let cos_theta_wave = wave_dir_guess.dot(c_axis).clamp(-1.0, 1.0).abs();
theta = cos_theta_wave.acos();
n_guess = BirefringenceParams::effective_extraordinary_index(
n_o_hero_seed,
n_e_hero_seed,
theta,
);
}
theta
} else {
k_hat.dot(c_axis).clamp(-1.0, 1.0).abs().acos()
}
}
pub(super) fn per_channel_ordinary_indices(ctx: &RayMaterialContext) -> [f32; NUM_CHANNELS] {
std::array::from_fn(|k| ctx.material.dispersion.evaluate(ctx.lambdas[k]))
}
#[cfg(feature = "gpu")]
#[expect(
clippy::similar_names,
reason = "n_o/n_e are the established ordinary/extraordinary index names"
)]
pub(crate) fn per_channel_uniaxial_indices(
ctx: &RayMaterialContext,
theta_c: f32,
) -> ([f32; NUM_CHANNELS], [f32; NUM_CHANNELS]) {
let material = ctx.material;
let is_anisotropic = ctx.is_anisotropic;
let mut n_o_ch = [0.0f32; NUM_CHANNELS];
let mut n_eff_ch = [0.0f32; NUM_CHANNELS];
for k in 0..NUM_CHANNELS {
let n_o_k = material.dispersion.evaluate(ctx.lambdas[k]);
n_o_ch[k] = n_o_k;
n_eff_ch[k] = if is_anisotropic {
let n_e_k = material.extraordinary_index_at(ctx.lambdas[k], n_o_k);
BirefringenceParams::effective_extraordinary_index(n_o_k, n_e_k, theta_c)
} else {
n_o_k
};
}
(n_o_ch, n_eff_ch)
}
#[expect(
clippy::similar_names,
reason = "n_o/n_e are the established ordinary/extraordinary index names"
)]
fn per_channel_effective_extraordinary_indices(
ctx: &RayMaterialContext,
n_o_ch: &[f32; NUM_CHANNELS],
theta_c: f32,
) -> [f32; NUM_CHANNELS] {
let material = ctx.material;
let is_anisotropic = ctx.is_anisotropic;
let mut n_eff_ch = [0.0f32; NUM_CHANNELS];
for k in 0..NUM_CHANNELS {
let n_o_k = n_o_ch[k];
n_eff_ch[k] = if is_anisotropic {
let n_e_k = material.extraordinary_index_at(ctx.lambdas[k], n_o_k);
BirefringenceParams::effective_extraordinary_index(n_o_k, n_e_k, theta_c)
} else {
n_o_k
};
}
n_eff_ch
}
#[expect(
clippy::similar_names,
reason = "mode A/mode B wave directions are the established paired names"
)]
fn hero_biaxial_wave_dirs(
cache: &RayWavelengthCache,
normal: Vec3,
k_hat: Vec3,
cos_i: f32,
inside_gem: bool,
is_biaxial: bool,
n_o_hero_seed: f32,
) -> (Option<BiaxialIndicatrix>, Vec3, Vec3) {
let hero_indicatrix = if is_biaxial {
cache.hero_indicatrix
} else {
None
};
let (wave_dir_a_hero, wave_dir_b_hero) =
hero_indicatrix.map_or((Vec3::ZERO, Vec3::ZERO), |ind| {
if inside_gem {
(k_hat, k_hat)
} else {
let (_, dir_a) = ind.resolve_entry_mode(k_hat, normal, cos_i, n_o_hero_seed, false);
let (_, dir_b) = ind.resolve_entry_mode(k_hat, normal, cos_i, n_o_hero_seed, true);
(dir_a, dir_b)
}
});
(hero_indicatrix, wave_dir_a_hero, wave_dir_b_hero)
}
#[expect(
clippy::similar_names,
reason = "mode A/mode B wave directions and index arrays are the established paired names"
)]
fn per_channel_biaxial_indices(
cache: &RayWavelengthCache,
is_biaxial: bool,
wave_dir_a_hero: Vec3,
wave_dir_b_hero: Vec3,
) -> ([f32; NUM_CHANNELS], [f32; NUM_CHANNELS]) {
let mut n_biax_a_ch = [0.0f32; NUM_CHANNELS];
let mut n_biax_b_ch = [0.0f32; NUM_CHANNELS];
if is_biaxial {
for k in 0..NUM_CHANNELS {
if let Some(ind_k) = cache.biaxial_ch[k] {
n_biax_a_ch[k] = ind_k.wave_indices(wave_dir_a_hero).1;
n_biax_b_ch[k] = ind_k.wave_indices(wave_dir_b_hero).0;
}
}
}
(n_biax_a_ch, n_biax_b_ch)
}
fn per_channel_medium_indices(
inside_gem: bool,
n_medium_ch: [f32; NUM_CHANNELS],
cos_i: f32,
) -> (
[f32; NUM_CHANNELS],
[f32; NUM_CHANNELS],
[f32; NUM_CHANNELS],
) {
let mut n1_ch = [0.0f32; NUM_CHANNELS];
let mut n2_ch = [0.0f32; NUM_CHANNELS];
let mut sin2_t_ch = [0.0f32; NUM_CHANNELS];
for k in 0..NUM_CHANNELS {
let n1k = if inside_gem { n_medium_ch[k] } else { 1.0 };
let n2k = if inside_gem { 1.0 } else { n_medium_ch[k] };
let etak = n1k / n2k;
n1_ch[k] = n1k;
n2_ch[k] = n2k;
sin2_t_ch[k] = etak * etak * cos_i.mul_add(-cos_i, 1.0);
}
(n1_ch, n2_ch, sin2_t_ch)
}
#[expect(
clippy::similar_names,
reason = "n_o/n_e are the established ordinary/extraordinary index names; mode A/mode B wave directions are the established paired names; mode A/mode B index arrays are the established paired names; mode A/mode B hero indices are the established paired names"
)]
pub(in crate::optics::raytracer) fn compute_bounce_refraction_geometry(
ctx: &RayMaterialContext,
cache: &RayWavelengthCache,
normal: Vec3,
k_hat: Vec3,
inside_gem: bool,
is_extraordinary: bool,
) -> BounceRefractionGeometry {
let material = ctx.material;
let hero_idx = ctx.hero_idx;
let is_anisotropic = ctx.is_anisotropic;
let cos_i = (-k_hat).dot(normal).clamp(0.0, 1.0);
let sin_i = cos_i.mul_add(-cos_i, 1.0).max(0.0).sqrt();
let is_biaxial = material.biaxial_delta_beta_alpha.is_some();
let n_o_hero_seed = cache.n_o_ch[hero_idx];
let theta_c = theta_c_for_bounce(
ctx,
normal,
k_hat,
cos_i,
inside_gem,
is_biaxial,
n_o_hero_seed,
);
let n_o_ch = cache.n_o_ch;
let n_eff_ch = per_channel_effective_extraordinary_indices(ctx, &n_o_ch, theta_c);
let n_o_hero = n_o_ch[hero_idx];
let n_e_hero = n_o_hero + material.birefringence_delta;
let (hero_indicatrix, wave_dir_a_hero, wave_dir_b_hero) = hero_biaxial_wave_dirs(
cache,
normal,
k_hat,
cos_i,
inside_gem,
is_biaxial,
n_o_hero_seed,
);
let (n_biax_a_ch, n_biax_b_ch) =
per_channel_biaxial_indices(cache, is_biaxial, wave_dir_a_hero, wave_dir_b_hero);
let n_biax_a_hero = n_biax_a_ch[hero_idx];
let n_biax_b_hero = n_biax_b_ch[hero_idx];
let n_mode_a_ch = if is_biaxial { n_biax_a_ch } else { n_o_ch };
let n_mode_b_ch = if is_biaxial { n_biax_b_ch } else { n_eff_ch };
let n_medium_ch: [f32; NUM_CHANNELS] = if is_anisotropic && inside_gem && !is_extraordinary {
n_mode_a_ch
} else {
n_mode_b_ch
};
let n_medium_hero = n_medium_ch[hero_idx];
let n1 = if inside_gem { n_medium_hero } else { 1.0 };
let n2 = if inside_gem { 1.0 } else { n_medium_hero };
let eta = n1 / n2;
let sin2_t = eta * eta * cos_i.mul_add(-cos_i, 1.0);
let (n1_ch, n2_ch, sin2_t_ch) = per_channel_medium_indices(inside_gem, n_medium_ch, cos_i);
let uniaxial_frame = (is_anisotropic && !is_biaxial)
.then(|| UniaxialFrame::build(k_hat, normal, material.c_axis, cos_i, sin_i));
BounceRefractionGeometry {
cos_i,
sin_i,
is_biaxial,
n_o_hero,
n_e_hero,
hero_indicatrix,
n_biax_a_hero,
n_biax_b_hero,
n_o_ch,
n_biax_a_ch,
n1,
n2,
sin2_t,
n1_ch,
n2_ch,
sin2_t_ch,
uniaxial_frame,
}
}
#[must_use]
pub(crate) fn poynting_dir_for_mode(
ctx: &RayMaterialContext,
geo: &BounceRefractionGeometry,
k: Vec3,
inside_gem: bool,
is_extraordinary: bool,
) -> Vec3 {
if !inside_gem || !ctx.is_anisotropic {
return k;
}
if geo.is_biaxial {
geo.hero_indicatrix
.map_or(k, |ind| ind.mode_poynting_dir(k, is_extraordinary))
} else if is_extraordinary {
BirefringenceParams::extraordinary_poynting_dir(k, ctx.c_axis, geo.n_o_hero, geo.n_e_hero)
} else {
k
}
}