use super::{
NUM_CHANNELS,
absorption::spectral_absorption,
camera::Ray,
environment::{EnvironmentSource, sample_environment_channel},
intersect::intersect_polyhedron_soa,
sampling::{BIREFRINGENT_SPLIT_STREAM, FRESNEL_BRANCH_STREAM, hash_u32},
uniaxial_fresnel::{self, UniaxialFrame},
};
use crate::optics::{
birefringence::{AbsorptionTensor3, BiaxialIndicatrix, BirefringenceParams},
materials::GemMaterial,
polarization::{MuellerMatrix, StokesVector},
studio_rig::StudioRig,
};
use glam::Vec3;
#[inline]
pub(crate) fn tir_phase_delta(n1k: f32, cos_i: f32, sin_i: f32) -> f32 {
let tan_half_delta_k = (cos_i * (n1k * n1k * sin_i).mul_add(sin_i, -1.0).max(0.0).sqrt())
/ (n1k * sin_i * sin_i).max(1e-6);
2.0 * tan_half_delta_k.atan()
}
pub(crate) struct RayMaterialContext<'a> {
pub(crate) material: &'a GemMaterial,
pub(crate) lambdas: [f32; NUM_CHANNELS],
pub(crate) hero_idx: usize,
pub(crate) c_axis: Vec3,
pub(crate) is_anisotropic: bool,
pub(crate) enable_internal_mode_coupling: bool,
}
pub(crate) struct RayWavelengthCache {
pub(crate) n_o_ch: [f32; NUM_CHANNELS],
pub(crate) hero_indicatrix: Option<BiaxialIndicatrix>,
pub(crate) biaxial_ch: [Option<BiaxialIndicatrix>; NUM_CHANNELS],
pub(crate) tensor_ch: [AbsorptionTensor3; NUM_CHANNELS],
}
pub(super) struct ExitSplitCtx<'a> {
pub(super) plane_soa: &'a crate::simd::PlanesSoA32,
pub(super) environment: EnvironmentSource<'a>,
pub(super) studio_rig: Option<StudioRig>,
pub(super) observer: Vec3,
pub(super) split_radiance: &'a mut [f32; NUM_CHANNELS],
pub(super) enabled: bool,
pub(super) compat: [u8; NUM_CHANNELS],
}
const _: () = assert!(
NUM_CHANNELS <= 8,
"ExitSplitCtx::compat packs one bit per channel into a u8"
);
pub(super) struct BounceContext<'m, 'b> {
pub(super) ctx: &'b RayMaterialContext<'m>,
pub(super) cache: &'b RayWavelengthCache,
pub(super) geo: &'b BounceRefractionGeometry,
}
pub(super) struct UniaxialBounceContext<'m, 'b> {
pub(super) ctx: &'b RayMaterialContext<'m>,
pub(super) geo: &'b BounceRefractionGeometry,
pub(super) frame: &'b UniaxialFrame,
}
#[derive(Clone, Copy)]
pub(super) struct BounceRay {
pub(super) k_hat: Vec3,
pub(super) normal: Vec3,
}
pub(super) struct BounceState<'a> {
pub(super) stokes: &'a mut [StokesVector; NUM_CHANNELS],
pub(super) path_pdf: &'a mut [f32; NUM_CHANNELS],
}
pub(super) struct ExitEvent<'a, 'b> {
pub(super) exit: &'a mut ExitSplitCtx<'b>,
pub(super) hit_point: Vec3,
}
#[derive(Clone, Copy)]
pub(super) struct RngDraw {
pub(super) rng_seed: u32,
pub(super) bounce: u32,
}
#[derive(Clone, Copy)]
pub(super) struct PathModeState {
pub(super) current_plane_normal: Vec3,
pub(super) inside_gem: bool,
pub(super) is_extraordinary: bool,
}
pub(super) fn narrow_compat(
compat: &mut [u8; NUM_CHANNELS],
dirs: &[Option<Vec3>; NUM_CHANNELS],
hero: usize,
hero_match: [bool; NUM_CHANNELS],
) {
const DIRECTION_MATCH_COS_TOL: f32 = 1.0 - 1e-6;
for a in 0..NUM_CHANNELS {
for b in (a + 1)..NUM_CHANNELS {
let matches = if a == hero {
hero_match[b]
} else if b == hero {
hero_match[a]
} else if let (Some(da), Some(db)) = (dirs[a], dirs[b]) {
da.dot(db) >= DIRECTION_MATCH_COS_TOL
} else {
true
};
if !matches {
compat[a] &= !(1u8 << b);
compat[b] &= !(1u8 << a);
}
}
}
}
struct ChannelTransmissionInputs {
n1k: f32,
n2k: f32,
cos_i: f32,
cos_t_k: f32,
r_unpol: f32,
entering_anisotropic: bool,
entry_mode_azimuth2: Option<(f32, f32)>,
incident_stokes_k: StokesVector,
}
fn compute_channel_transmission(inputs: &ChannelTransmissionInputs) -> (StokesVector, f32) {
let &ChannelTransmissionInputs {
n1k,
n2k,
cos_i,
cos_t_k,
r_unpol,
entering_anisotropic,
entry_mode_azimuth2,
incident_stokes_k,
} = inputs;
let t_s_k = (2.0 * n1k * cos_i) / f32::mul_add(n2k, cos_t_k, n1k * cos_i);
let t_p_k = (2.0 * n1k * cos_i) / f32::mul_add(n1k, cos_t_k, n2k * cos_i);
let trans_matrix_k =
MuellerMatrix::fresnel_transmission(n1k, n2k, cos_i, cos_t_k, t_s_k, t_p_k);
let incident_k =
if entering_anisotropic && let Some((cos_2psi_x, sin_2psi_x)) = entry_mode_azimuth2 {
let i_k = incident_stokes_k.i;
StokesVector::new(i_k, i_k * cos_2psi_x, i_k * sin_2psi_x, 0.0)
} else {
incident_stokes_k
};
let transmitted = incident_k
.apply_matrix(&trans_matrix_k)
.scale(1.0 / (1.0 - r_unpol));
let r_s_k = f32::mul_add(n2k, -cos_t_k, n1k * cos_i) / f32::mul_add(n2k, cos_t_k, n1k * cos_i);
let r_p_k = f32::mul_add(n1k, -cos_t_k, n2k * cos_i) / f32::mul_add(n1k, cos_t_k, n2k * cos_i);
let r_unpol_k = (0.5 * r_p_k.mul_add(r_p_k, r_s_k * r_s_k)).clamp(1e-4, 1.0 - 1e-4);
(transmitted, r_unpol_k)
}
pub(super) fn try_split_exit_channel(
exit: &mut ExitSplitCtx<'_>,
hit_point: Vec3,
k: usize,
lambda_k: f32,
dir_k: Vec3,
transmitted_intensity: f32,
) {
let probe = Ray {
origin: hit_point + dir_k * 1e-4,
dir: dir_k,
};
if intersect_polyhedron_soa(probe, exit.plane_soa).is_some() {
return;
}
let env_spectral = sample_environment_channel(
exit.environment,
dir_k,
lambda_k,
exit.studio_rig.as_ref(),
exit.observer,
);
exit.split_radiance[k] = f32::mul_add(
transmitted_intensity.max(0.0),
env_spectral,
exit.split_radiance[k],
);
}
pub(super) fn build_ray_wavelength_cache(ctx: &RayMaterialContext) -> RayWavelengthCache {
let material = ctx.material;
let (n_o_ch, _n_eff_ch_unused_theta_c_independent_half) =
per_channel_uniaxial_indices(ctx, 0.0);
let biaxial_ch: [Option<BiaxialIndicatrix>; NUM_CHANNELS] =
std::array::from_fn(|k| material.biaxial_indicatrix(ctx.lambdas[k]));
let hero_indicatrix = biaxial_ch[ctx.hero_idx];
let is_biaxial = hero_indicatrix.is_some();
let abs_o = &material.absorption.o_ray;
let abs_e = &material.absorption.e_ray;
let abs_beta = material.absorption.beta_ray.as_deref();
let alpha_o_ch: [f32; NUM_CHANNELS] =
std::array::from_fn(|k| spectral_absorption(abs_o, ctx.lambdas[k]));
let alpha_e_ch: [f32; NUM_CHANNELS] =
std::array::from_fn(|k| spectral_absorption(abs_e, ctx.lambdas[k]));
let alpha_beta_ch: [Option<f32>; NUM_CHANNELS] = std::array::from_fn(|k| {
if is_biaxial {
abs_beta.map(|bands| spectral_absorption(bands, ctx.lambdas[k]))
} else {
None
}
});
let tensor_ch: [AbsorptionTensor3; NUM_CHANNELS] = std::array::from_fn(|k| {
alpha_beta_ch[k].map_or_else(
|| AbsorptionTensor3::uniaxial(alpha_o_ch[k], alpha_e_ch[k], ctx.c_axis),
|beta| AbsorptionTensor3::biaxial(alpha_o_ch[k], beta, alpha_e_ch[k], ctx.c_axis),
)
});
RayWavelengthCache {
n_o_ch,
hero_indicatrix,
biaxial_ch,
tensor_ch,
}
}
#[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>,
}
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(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]);
let n_e_k = material.extraordinary_index_at(ctx.lambdas[k], n_o_k);
n_o_ch[k] = n_o_k;
n_eff_ch[k] = if is_anisotropic {
BirefringenceParams::effective_extraordinary_index(n_o_k, n_e_k, theta_c)
} else {
n_o_k
};
}
(n_o_ch, n_eff_ch)
}
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];
let n_e_k = material.extraordinary_index_at(ctx.lambdas[k], n_o_k);
n_eff_ch[k] = if is_anisotropic {
BirefringenceParams::effective_extraordinary_index(n_o_k, n_e_k, theta_c)
} else {
n_o_k
};
}
n_eff_ch
}
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)
}
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)
}
pub(super) 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
}
}
pub(super) fn apply_tir_bounce(
ctx: &RayMaterialContext,
geo: &BounceRefractionGeometry,
is_extraordinary: bool,
k_hat: Vec3,
normal: Vec3,
stokes: &mut [StokesVector; NUM_CHANNELS],
path_pdf: &mut [f32; NUM_CHANNELS],
) -> (Vec3, Option<f32>) {
if let Some(frame) = geo.uniaxial_frame {
let mut exact_p_o = None;
for k in 0..NUM_CHANNELS {
let n_inc = geo.n1_ch[k];
let n_o_k = geo.n_o_ch[k];
let n_e_k = ctx.material.extraordinary_index_at(ctx.lambdas[k], n_o_k);
let sol = uniaxial_fresnel::internal_solve(
n_inc,
n_o_k,
n_e_k,
ctx.c_axis,
&frame,
!is_extraordinary,
);
let flux_inc = sol.flux_inc.max(1e-12);
let ro_pow = sol.r_o.norm_sqr() * sol.flux_ro;
let re_pow = sol.r_e.norm_sqr() * sol.flux_re;
let r_total_k = ((ro_pow + re_pow) / flux_inc).min(1.0);
stokes[k] = stokes[k].scale(r_total_k);
if geo.sin2_t_ch[k] <= 1.0 {
path_pdf[k] *= r_total_k.clamp(1e-4, 1.0 - 1e-4);
}
if k == ctx.hero_idx {
exact_p_o = Some(ro_pow / (ro_pow + re_pow).max(1e-12));
}
}
return (k_hat - 2.0 * k_hat.dot(normal) * normal, exact_p_o);
}
for k in 0..NUM_CHANNELS {
let n1k = geo.n1_ch[k];
let n2k = geo.n2_ch[k];
if geo.sin2_t_ch[k] > 1.0 {
let delta_k = tir_phase_delta(n1k, geo.cos_i, geo.sin_i);
let tir_matrix_k = MuellerMatrix::tir_retardation(delta_k);
stokes[k] = stokes[k].apply_matrix(&tir_matrix_k);
} else {
let cos_t_k = (1.0 - geo.sin2_t_ch[k]).max(0.0).sqrt();
let r_s_k = f32::mul_add(n2k, -cos_t_k, n1k * geo.cos_i)
/ f32::mul_add(n2k, cos_t_k, n1k * geo.cos_i);
let r_p_k = f32::mul_add(n1k, -cos_t_k, n2k * geo.cos_i)
/ f32::mul_add(n1k, cos_t_k, n2k * geo.cos_i);
let refl_matrix_k = MuellerMatrix::fresnel_reflection(r_s_k, r_p_k);
stokes[k] = stokes[k].apply_matrix(&refl_matrix_k);
let r_unpol_k = (0.5 * r_p_k.mul_add(r_p_k, r_s_k * r_s_k)).clamp(1e-4, 1.0 - 1e-4);
path_pdf[k] *= r_unpol_k;
}
}
(k_hat - 2.0 * k_hat.dot(normal) * normal, None)
}
fn apply_partial_reflect_bounce(
geo: &BounceRefractionGeometry,
r_unpol: f32,
k_hat: Vec3,
normal: Vec3,
stokes: &mut [StokesVector; NUM_CHANNELS],
path_pdf: &mut [f32; NUM_CHANNELS],
) -> Vec3 {
for k in 0..NUM_CHANNELS {
let n1k = geo.n1_ch[k];
let n2k = geo.n2_ch[k];
let refl_matrix_k = if geo.sin2_t_ch[k] > 1.0 {
let delta_k = tir_phase_delta(n1k, geo.cos_i, geo.sin_i);
MuellerMatrix::tir_retardation(delta_k)
} else {
let cos_t_k = (1.0 - geo.sin2_t_ch[k]).max(0.0).sqrt();
let r_s_k = f32::mul_add(n2k, -cos_t_k, n1k * geo.cos_i)
/ f32::mul_add(n2k, cos_t_k, n1k * geo.cos_i);
let r_p_k = f32::mul_add(n1k, -cos_t_k, n2k * geo.cos_i)
/ f32::mul_add(n1k, cos_t_k, n2k * geo.cos_i);
let r_unpol_k = (0.5 * r_p_k.mul_add(r_p_k, r_s_k * r_s_k)).clamp(1e-4, 1.0 - 1e-4);
path_pdf[k] *= r_unpol_k;
MuellerMatrix::fresnel_reflection(r_s_k, r_p_k)
};
stokes[k] = stokes[k].apply_matrix(&refl_matrix_k).scale(1.0 / r_unpol);
}
k_hat - 2.0 * k_hat.dot(normal) * normal
}
struct RefractBounceOutcome {
new_k: Vec3,
new_s: Vec3,
is_extraordinary_update: Option<bool>,
}
#[derive(Clone, Copy)]
struct RefractSelection {
use_extraordinary: bool,
entry_mode_azimuth2: Option<(f32, f32)>,
}
fn apply_refract_bounce(
bctx: &BounceContext<'_, '_>,
r_unpol: f32,
ray: BounceRay,
inside_gem: bool,
selection: RefractSelection,
state: &mut BounceState<'_>,
exit_event: &mut ExitEvent<'_, '_>,
) -> RefractBounceOutcome {
let ctx = bctx.ctx;
let geo = bctx.geo;
let BounceRay { k_hat, normal } = ray;
let RefractSelection {
use_extraordinary,
entry_mode_azimuth2,
} = selection;
let c_axis = ctx.c_axis;
let entering_anisotropic = !inside_gem && ctx.is_anisotropic;
let (new_k, final_refr_dir) = if let (true, Some(ind)) =
(entering_anisotropic && geo.is_biaxial, geo.hero_indicatrix)
{
let n2_hero_dir = if use_extraordinary {
geo.n_biax_b_hero
} else {
geo.n_biax_a_hero
};
let eta_dir = geo.n1 / n2_hero_dir;
let sin2_t_dir = (eta_dir * eta_dir * geo.cos_i.mul_add(-geo.cos_i, 1.0)).min(1.0);
let cos_t_dir = (1.0 - sin2_t_dir).max(0.0).sqrt();
let refr_wave_dir =
(eta_dir * k_hat + f32::mul_add(eta_dir, geo.cos_i, -cos_t_dir) * normal).normalize();
(
refr_wave_dir,
ind.mode_poynting_dir(refr_wave_dir, use_extraordinary),
)
} else {
let n2_hero_dir = if entering_anisotropic && !use_extraordinary {
geo.n_o_hero
} else {
geo.n2
};
let eta_dir = geo.n1 / n2_hero_dir;
let sin2_t_dir = (eta_dir * eta_dir * geo.cos_i.mul_add(-geo.cos_i, 1.0)).min(1.0);
let cos_t_dir = (1.0 - sin2_t_dir).max(0.0).sqrt();
let refr_wave_dir =
(eta_dir * k_hat + f32::mul_add(eta_dir, geo.cos_i, -cos_t_dir) * normal).normalize();
let s = if entering_anisotropic && use_extraordinary {
BirefringenceParams::extraordinary_poynting_dir(
refr_wave_dir,
c_axis,
geo.n_o_hero,
geo.n_e_hero,
)
} else {
refr_wave_dir
};
(refr_wave_dir, s)
};
let decision = RefractDecision {
entering_anisotropic,
use_extraordinary,
entry_mode_azimuth2,
final_refr_dir,
r_unpol,
};
let mut dirs = [None; NUM_CHANNELS];
let mut hero_match = [false; NUM_CHANNELS];
for k in 0..NUM_CHANNELS {
let (dir_k, matches_hero) =
apply_refract_channel(bctx, k, ray, inside_gem, &decision, state, exit_event);
dirs[k] = dir_k;
hero_match[k] = matches_hero;
}
if exit_event.exit.enabled && !inside_gem {
narrow_compat(&mut exit_event.exit.compat, &dirs, ctx.hero_idx, hero_match);
}
RefractBounceOutcome {
new_k,
new_s: final_refr_dir,
is_extraordinary_update: entering_anisotropic.then_some(use_extraordinary),
}
}
#[derive(Clone, Copy)]
struct RefractDecision {
entering_anisotropic: bool,
use_extraordinary: bool,
entry_mode_azimuth2: Option<(f32, f32)>,
final_refr_dir: Vec3,
r_unpol: f32,
}
struct ChannelRefractionGeometry {
n1k: f32,
n2k: f32,
cos_t_k: f32,
refr_wave_dir_k: Vec3,
final_dir_k: Vec3,
}
fn compute_channel_refraction_geometry(
bctx: &BounceContext<'_, '_>,
k: usize,
ray: BounceRay,
decision: &RefractDecision,
) -> Option<ChannelRefractionGeometry> {
let (cache, geo) = (bctx.cache, bctx.geo);
let BounceRay { k_hat, normal } = ray;
let &RefractDecision {
entering_anisotropic,
use_extraordinary,
..
} = decision;
let material = bctx.ctx.material;
let c_axis = bctx.ctx.c_axis;
let n1k = geo.n1_ch[k];
let n2k = if entering_anisotropic && !use_extraordinary {
if geo.is_biaxial {
geo.n_biax_a_ch[k]
} else {
geo.n_o_ch[k]
}
} else {
geo.n2_ch[k]
};
let sin2_t_k = (n1k / n2k).powi(2) * geo.cos_i.mul_add(-geo.cos_i, 1.0);
if sin2_t_k > 1.0 {
return None;
}
let cos_t_k = (1.0 - sin2_t_k).max(0.0).sqrt();
let eta_dir_k = n1k / n2k;
let refr_wave_dir_k =
(eta_dir_k * k_hat + f32::mul_add(eta_dir_k, geo.cos_i, -cos_t_k) * normal).normalize();
let final_dir_k = if let (true, Some(ind_k)) =
(entering_anisotropic && geo.is_biaxial, cache.biaxial_ch[k])
{
ind_k.mode_poynting_dir(refr_wave_dir_k, use_extraordinary)
} else if entering_anisotropic && use_extraordinary {
let n_e_k = geo.n_o_ch[k] + material.birefringence_delta;
BirefringenceParams::extraordinary_poynting_dir(
refr_wave_dir_k,
c_axis,
geo.n_o_ch[k],
n_e_k,
)
} else {
refr_wave_dir_k
};
Some(ChannelRefractionGeometry {
n1k,
n2k,
cos_t_k,
refr_wave_dir_k,
final_dir_k,
})
}
struct ChannelTransmitMatchInputs {
n1k: f32,
n2k: f32,
cos_i: f32,
cos_t_k: f32,
r_unpol: f32,
entering_anisotropic: bool,
entry_mode_azimuth2: Option<(f32, f32)>,
}
fn apply_channel_transmission_match(
inputs: &ChannelTransmitMatchInputs,
stokes_k: &mut StokesVector,
path_pdf_k: &mut f32,
) {
let &ChannelTransmitMatchInputs {
n1k,
n2k,
cos_i,
cos_t_k,
r_unpol,
entering_anisotropic,
entry_mode_azimuth2,
} = inputs;
let t_s_k = (2.0 * n1k * cos_i) / f32::mul_add(n2k, cos_t_k, n1k * cos_i);
let t_p_k = (2.0 * n1k * cos_i) / f32::mul_add(n1k, cos_t_k, n2k * cos_i);
let trans_matrix_k =
MuellerMatrix::fresnel_transmission(n1k, n2k, cos_i, cos_t_k, t_s_k, t_p_k);
let incident_k =
if entering_anisotropic && let Some((cos_2psi_x, sin_2psi_x)) = entry_mode_azimuth2 {
let i_k = stokes_k.i;
StokesVector::new(i_k, i_k * cos_2psi_x, i_k * sin_2psi_x, 0.0)
} else {
*stokes_k
};
*stokes_k = incident_k
.apply_matrix(&trans_matrix_k)
.scale(1.0 / (1.0 - r_unpol));
let r_s_k = f32::mul_add(n2k, -cos_t_k, n1k * cos_i) / f32::mul_add(n2k, cos_t_k, n1k * cos_i);
let r_p_k = f32::mul_add(n1k, -cos_t_k, n2k * cos_i) / f32::mul_add(n1k, cos_t_k, n2k * cos_i);
let r_unpol_k = (0.5 * r_p_k.mul_add(r_p_k, r_s_k * r_s_k)).clamp(1e-4, 1.0 - 1e-4);
*path_pdf_k *= 1.0 - r_unpol_k;
}
struct ChannelMismatchInputs {
n1k: f32,
n2k: f32,
cos_i: f32,
cos_t_k: f32,
r_unpol: f32,
entering_anisotropic: bool,
entry_mode_azimuth2: Option<(f32, f32)>,
inside_gem: bool,
lambda_k: f32,
refr_wave_dir_k: Vec3,
original_stokes_k: StokesVector,
}
fn apply_channel_chromatic_termination(
inputs: &ChannelMismatchInputs,
k: usize,
stokes_k: &mut StokesVector,
path_pdf_k: &mut f32,
exit_event: &mut ExitEvent<'_, '_>,
) {
let &ChannelMismatchInputs {
n1k,
n2k,
cos_i,
cos_t_k,
r_unpol,
entering_anisotropic,
entry_mode_azimuth2,
inside_gem,
lambda_k,
refr_wave_dir_k,
original_stokes_k,
} = inputs;
let prefix_path_pdf_k = *path_pdf_k;
*stokes_k = stokes_k.scale(0.0);
*path_pdf_k = 0.0;
let (exit, hit_point) = (&mut *exit_event.exit, exit_event.hit_point);
if exit.enabled {
let is_exit_event = inside_gem && !entering_anisotropic;
let (transmitted, r_unpol_k) = compute_channel_transmission(&ChannelTransmissionInputs {
n1k,
n2k,
cos_i,
cos_t_k,
r_unpol,
entering_anisotropic,
entry_mode_azimuth2,
incident_stokes_k: original_stokes_k,
});
*path_pdf_k = prefix_path_pdf_k * (1.0 - r_unpol_k);
if is_exit_event && original_stokes_k.intensity() > 0.0 {
try_split_exit_channel(
exit,
hit_point,
k,
lambda_k,
refr_wave_dir_k,
transmitted.intensity(),
);
}
}
}
fn apply_refract_channel(
bctx: &BounceContext<'_, '_>,
k: usize,
ray: BounceRay,
inside_gem: bool,
decision: &RefractDecision,
state: &mut BounceState<'_>,
exit_event: &mut ExitEvent<'_, '_>,
) -> (Option<Vec3>, bool) {
const DIRECTION_MATCH_COS_TOL: f32 = 1.0 - 1e-6;
let ctx = bctx.ctx;
let &RefractDecision {
entering_anisotropic,
entry_mode_azimuth2,
final_refr_dir,
r_unpol,
..
} = decision;
let (stokes, path_pdf) = (&mut *state.stokes, &mut *state.path_pdf);
let original_stokes_k = stokes[k];
let Some(geometry) = compute_channel_refraction_geometry(bctx, k, ray, decision) else {
stokes[k] = stokes[k].scale(0.0);
path_pdf[k] = 0.0;
return (None, false);
};
let ChannelRefractionGeometry {
n1k,
n2k,
cos_t_k,
refr_wave_dir_k,
final_dir_k,
} = geometry;
let direction_matches = final_dir_k.dot(final_refr_dir) >= DIRECTION_MATCH_COS_TOL;
if direction_matches {
apply_channel_transmission_match(
&ChannelTransmitMatchInputs {
n1k,
n2k,
cos_i: bctx.geo.cos_i,
cos_t_k,
r_unpol,
entering_anisotropic,
entry_mode_azimuth2,
},
&mut stokes[k],
&mut path_pdf[k],
);
} else {
apply_channel_chromatic_termination(
&ChannelMismatchInputs {
n1k,
n2k,
cos_i: bctx.geo.cos_i,
cos_t_k,
r_unpol,
entering_anisotropic,
entry_mode_azimuth2,
inside_gem,
lambda_k: ctx.lambdas[k],
refr_wave_dir_k,
original_stokes_k,
},
k,
&mut stokes[k],
&mut path_pdf[k],
exit_event,
);
}
(Some(final_dir_k), direction_matches)
}
#[must_use]
pub(super) fn entry_eigenmode_selection(
c_axis: Vec3,
current_plane_normal: Vec3,
k_hat: Vec3,
hero_stokes: StokesVector,
) -> Option<(f32, f32, f32)> {
if hero_stokes.i <= 1e-7 || current_plane_normal.length_squared() <= 1e-6 {
return None;
}
let (q, u) = (hero_stokes.q, hero_stokes.u);
if q.mul_add(q, u * u) <= 1e-12 {
return None;
}
let o_hat = BirefringenceParams::ordinary_eigen_polarization(k_hat, c_axis);
let s_hat = current_plane_normal;
let p_hat = k_hat.cross(s_hat);
let s_comp = o_hat.dot(s_hat);
let p_comp = o_hat.dot(p_hat);
let cos_2psi_o = s_comp.mul_add(s_comp, -(p_comp * p_comp));
let sin_2psi_o = 2.0 * s_comp * p_comp;
let p_o = (0.5 + 0.5 * q.mul_add(cos_2psi_o, u * sin_2psi_o) / hero_stokes.i).clamp(0.0, 1.0);
Some((p_o, cos_2psi_o, sin_2psi_o))
}
fn apply_uniaxial_entry_bounce_isotropic_fallback(
geo: &BounceRefractionGeometry,
k_hat: Vec3,
normal: Vec3,
rng_seed: u32,
bounce: u32,
stokes: &mut [StokesVector; NUM_CHANNELS],
path_pdf: &mut [f32; NUM_CHANNELS],
) -> (Vec3, Vec3, bool, Option<bool>) {
const R_UNPOL_SELECT_MIN: f32 = 0.02;
const R_UNPOL_SELECT_MAX: f32 = 0.98;
let n1 = 1.0f32;
let n2_hero = geo.n_o_hero;
let cos_i = geo.cos_i;
let sin2_t = (n1 / n2_hero).powi(2) * sin_i_sq(cos_i);
let cos_t = (1.0 - sin2_t).max(0.0).sqrt();
let r_s_hero = n2_hero.mul_add(-cos_t, n1 * cos_i) / n2_hero.mul_add(cos_t, n1 * cos_i);
let r_p_hero = n1.mul_add(-cos_t, n2_hero * cos_i) / n1.mul_add(cos_t, n2_hero * cos_i);
let r_unpol = (0.5 * r_p_hero.mul_add(r_p_hero, r_s_hero * r_s_hero))
.clamp(R_UNPOL_SELECT_MIN, R_UNPOL_SELECT_MAX);
let rng_bounce =
(hash_u32(rng_seed ^ hash_u32(bounce ^ FRESNEL_BRANCH_STREAM)) as f32) / 4_294_967_295.0;
if rng_bounce < r_unpol {
for k in 0..NUM_CHANNELS {
let n2k = geo.n_o_ch[k];
let sin2_t_k = (n1 / n2k).powi(2) * sin_i_sq(cos_i);
let cos_t_k = (1.0 - sin2_t_k).max(0.0).sqrt();
let r_s_k = n2k.mul_add(-cos_t_k, n1 * cos_i) / n2k.mul_add(cos_t_k, n1 * cos_i);
let r_p_k = n1.mul_add(-cos_t_k, n2k * cos_i) / n1.mul_add(cos_t_k, n2k * cos_i);
let refl_matrix_k = MuellerMatrix::fresnel_reflection(r_s_k, r_p_k);
let r_unpol_k = (0.5 * r_p_k.mul_add(r_p_k, r_s_k * r_s_k)).clamp(1e-4, 1.0 - 1e-4);
stokes[k] = stokes[k].apply_matrix(&refl_matrix_k).scale(1.0 / r_unpol);
path_pdf[k] *= r_unpol_k;
}
let new_k = k_hat - 2.0 * k_hat.dot(normal) * normal;
(new_k, new_k, false, None)
} else {
for k in 0..NUM_CHANNELS {
let n2k = geo.n_o_ch[k];
let sin2_t_k = (n1 / n2k).powi(2) * sin_i_sq(cos_i);
let cos_t_k = (1.0 - sin2_t_k).max(0.0).sqrt();
let t_s_k = (2.0 * n1 * cos_i) / n1.mul_add(cos_i, n2k * cos_t_k);
let t_p_k = (2.0 * n1 * cos_i) / n2k.mul_add(cos_i, n1 * cos_t_k);
let trans_matrix_k =
MuellerMatrix::fresnel_transmission(n1, n2k, cos_i, cos_t_k, t_s_k, t_p_k);
stokes[k] = stokes[k]
.apply_matrix(&trans_matrix_k)
.scale(1.0 / (1.0 - r_unpol));
let r_s_k = n2k.mul_add(-cos_t_k, n1 * cos_i) / n2k.mul_add(cos_t_k, n1 * cos_i);
let r_p_k = n1.mul_add(-cos_t_k, n2k * cos_i) / n1.mul_add(cos_t_k, n2k * cos_i);
let r_unpol_k = (0.5 * r_p_k.mul_add(r_p_k, r_s_k * r_s_k)).clamp(1e-4, 1.0 - 1e-4);
path_pdf[k] *= 1.0 - r_unpol_k;
}
let eta = n1 / n2_hero;
let new_k = (eta * k_hat + f32::mul_add(eta, cos_i, -cos_t) * normal).normalize();
(new_k, new_k, true, Some(false))
}
}
#[inline]
fn sin_i_sq(cos_i: f32) -> f32 {
cos_i.mul_add(-cos_i, 1.0)
}
fn apply_uniaxial_entry_bounce(
ubctx: &UniaxialBounceContext<'_, '_>,
ray: BounceRay,
rng: RngDraw,
state: &mut BounceState<'_>,
exit: &mut ExitSplitCtx<'_>,
) -> (Vec3, Vec3, bool, Option<bool>) {
const R_UNPOL_SELECT_MIN: f32 = 0.02;
const R_UNPOL_SELECT_MAX: f32 = 0.98;
let (ctx, geo, frame) = (ubctx.ctx, ubctx.geo, ubctx.frame);
let BounceRay { k_hat, normal } = ray;
let RngDraw { rng_seed, bounce } = rng;
let hero = ctx.hero_idx;
let c_axis = ctx.c_axis;
let n_o_hero = geo.n_o_ch[hero];
let n_e_hero = ctx
.material
.extraordinary_index_at(ctx.lambdas[hero], n_o_hero);
if k_hat.cross(c_axis).length_squared() < 1e-6 {
return apply_uniaxial_entry_bounce_isotropic_fallback(
geo,
k_hat,
normal,
rng_seed,
bounce,
state.stokes,
state.path_pdf,
);
}
let inc_frame = uniaxial_fresnel::entry_incidence_frame(1.0, frame);
let (sol_s_hero, sol_p_hero) = uniaxial_fresnel::entry_solve_pair_with_incidence(
&inc_frame, 1.0, n_o_hero, n_e_hero, c_axis, frame,
);
let reflect_mueller_hero = uniaxial_fresnel::jones_to_mueller(
sol_s_hero.r_s,
sol_p_hero.r_s,
sol_s_hero.r_p,
sol_p_hero.r_p,
);
let r_branch = reflect_mueller_hero.col(0)[0].clamp(R_UNPOL_SELECT_MIN, R_UNPOL_SELECT_MAX);
let inc_flux = 0.5 * frame.cos_i;
let p_o_raw = uniaxial_fresnel::mode_power(
sol_s_hero.t_o,
sol_p_hero.t_o,
sol_s_hero.flux_o,
inc_flux,
state.stokes[hero],
);
let p_e_raw = uniaxial_fresnel::mode_power(
sol_s_hero.t_e,
sol_p_hero.t_e,
sol_s_hero.flux_e,
inc_flux,
state.stokes[hero],
);
let p_o_hero =
(p_o_raw / (p_o_raw + p_e_raw).max(1e-12)).clamp(R_UNPOL_SELECT_MIN, R_UNPOL_SELECT_MAX);
let mode_split_rand = (hash_u32(rng_seed ^ hash_u32(bounce ^ BIREFRINGENT_SPLIT_STREAM))
as f32)
/ 4_294_967_295.0;
let use_extraordinary = mode_split_rand < (1.0 - p_o_hero);
let p_mode_hero_frac = if use_extraordinary {
1.0 - p_o_hero
} else {
p_o_hero
};
let n2_hero_dir = if use_extraordinary {
geo.n2
} else {
geo.n_o_hero
};
let eta_dir = geo.n1 / n2_hero_dir;
let sin2_t_dir = (eta_dir * eta_dir * geo.cos_i.mul_add(-geo.cos_i, 1.0)).min(1.0);
let cos_t_dir = (1.0 - sin2_t_dir).max(0.0).sqrt();
let refr_wave_dir =
(eta_dir * k_hat + f32::mul_add(eta_dir, geo.cos_i, -cos_t_dir) * normal).normalize();
let final_refr_dir = if use_extraordinary {
BirefringenceParams::extraordinary_poynting_dir(
refr_wave_dir,
c_axis,
geo.n_o_hero,
geo.n_e_hero,
)
} else {
refr_wave_dir
};
let p_axis_transmitted = refr_wave_dir.cross(frame.s_axis);
let rng_bounce =
(hash_u32(rng_seed ^ hash_u32(bounce ^ FRESNEL_BRANCH_STREAM)) as f32) / 4_294_967_295.0;
if rng_bounce < r_branch {
apply_uniaxial_entry_reflect_channels(ubctx, &inc_frame, r_branch, state);
let new_k = k_hat - 2.0 * k_hat.dot(normal) * normal;
(new_k, new_k, false, None)
} else {
let mode = EntryTransmitMode {
use_extraordinary,
final_refr_dir,
p_axis_transmitted,
p_mode_hero_frac,
inc_flux,
r_branch,
};
apply_uniaxial_entry_transmit_channels(ubctx, &inc_frame, ray, &mode, state, exit);
(refr_wave_dir, final_refr_dir, true, Some(use_extraordinary))
}
}
fn apply_uniaxial_entry_reflect_channels(
ubctx: &UniaxialBounceContext<'_, '_>,
inc_frame: &uniaxial_fresnel::EntryIncidenceFrame,
r_branch: f32,
state: &mut BounceState<'_>,
) {
let ctx = ubctx.ctx;
let geo = ubctx.geo;
let frame = ubctx.frame;
let c_axis = ctx.c_axis;
let stokes = &mut *state.stokes;
let path_pdf = &mut *state.path_pdf;
for k in 0..NUM_CHANNELS {
let n_o_k = geo.n_o_ch[k];
let n_e_k = ctx.material.extraordinary_index_at(ctx.lambdas[k], n_o_k);
let (sol_s_k, sol_p_k) = uniaxial_fresnel::entry_solve_pair_with_incidence(
inc_frame, 1.0, n_o_k, n_e_k, c_axis, frame,
);
let mueller_k =
uniaxial_fresnel::jones_to_mueller(sol_s_k.r_s, sol_p_k.r_s, sol_s_k.r_p, sol_p_k.r_p);
let r_unpol_k = mueller_k.col(0)[0].clamp(1e-4, 1.0 - 1e-4);
stokes[k] = stokes[k].apply_matrix(&mueller_k).scale(1.0 / r_branch);
path_pdf[k] *= r_unpol_k;
}
}
struct EntryTransmitMode {
use_extraordinary: bool,
final_refr_dir: Vec3,
p_axis_transmitted: Vec3,
p_mode_hero_frac: f32,
inc_flux: f32,
r_branch: f32,
}
fn apply_uniaxial_entry_transmit_channels(
ubctx: &UniaxialBounceContext<'_, '_>,
inc_frame: &uniaxial_fresnel::EntryIncidenceFrame,
ray: BounceRay,
mode: &EntryTransmitMode,
state: &mut BounceState<'_>,
exit: &mut ExitSplitCtx<'_>,
) {
const DIRECTION_MATCH_COS_TOL: f32 = 1.0 - 1e-6;
let (ctx, geo, frame) = (ubctx.ctx, ubctx.geo, ubctx.frame);
let c_axis = ctx.c_axis;
let BounceRay { k_hat, normal } = ray;
let &EntryTransmitMode {
use_extraordinary,
final_refr_dir,
p_axis_transmitted,
p_mode_hero_frac,
inc_flux,
r_branch,
} = mode;
let (stokes, path_pdf) = (&mut *state.stokes, &mut *state.path_pdf);
let mut dirs = [None; NUM_CHANNELS];
let mut hero_match = [false; NUM_CHANNELS];
for k in 0..NUM_CHANNELS {
let n_o_k = geo.n_o_ch[k];
let n2k_dir = if use_extraordinary {
geo.n2_ch[k]
} else {
n_o_k
};
let sin2_t_k = (geo.n1 / n2k_dir).powi(2) * geo.cos_i.mul_add(-geo.cos_i, 1.0);
if sin2_t_k > 1.0 {
stokes[k] = stokes[k].scale(0.0);
path_pdf[k] = 0.0;
continue;
}
let cos_t_k = (1.0 - sin2_t_k).max(0.0).sqrt();
let eta_dir_k = geo.n1 / n2k_dir;
let refr_wave_dir_k =
(eta_dir_k * k_hat + f32::mul_add(eta_dir_k, geo.cos_i, -cos_t_k) * normal).normalize();
let final_dir_k = if use_extraordinary {
let n_e_eff_k = n_o_k + ctx.material.birefringence_delta;
BirefringenceParams::extraordinary_poynting_dir(
refr_wave_dir_k,
c_axis,
n_o_k,
n_e_eff_k,
)
} else {
refr_wave_dir_k
};
dirs[k] = Some(final_dir_k);
let direction_matches = final_dir_k.dot(final_refr_dir) >= DIRECTION_MATCH_COS_TOL;
hero_match[k] = direction_matches;
if !direction_matches {
stokes[k] = stokes[k].scale(0.0);
if exit.enabled {
let n_e_k = ctx.material.extraordinary_index_at(ctx.lambdas[k], n_o_k);
let (sol_s_k, sol_p_k) = uniaxial_fresnel::entry_solve_pair_with_incidence(
inc_frame, 1.0, n_o_k, n_e_k, c_axis, frame,
);
let mueller_k = uniaxial_fresnel::jones_to_mueller(
sol_s_k.r_s,
sol_p_k.r_s,
sol_s_k.r_p,
sol_p_k.r_p,
);
let t_unpol_k = (1.0 - mueller_k.col(0)[0]).clamp(1e-4, 1.0 - 1e-4);
path_pdf[k] *= t_unpol_k;
} else {
path_pdf[k] = 0.0;
}
continue;
}
let n_e_k = ctx.material.extraordinary_index_at(ctx.lambdas[k], n_o_k);
let (sol_s_k, sol_p_k) = uniaxial_fresnel::entry_solve_pair_with_incidence(
inc_frame, 1.0, n_o_k, n_e_k, c_axis, frame,
);
let (t_s_row, t_p_row, flux_k, mode_dir) = if use_extraordinary {
(sol_s_k.t_e, sol_p_k.t_e, sol_s_k.flux_e, sol_s_k.e_hat)
} else {
(sol_s_k.t_o, sol_p_k.t_o, sol_s_k.flux_o, sol_s_k.o_hat)
};
let p_mode_k = uniaxial_fresnel::mode_power(t_s_row, t_p_row, flux_k, inc_flux, stokes[k]);
let deposit_i = p_mode_k / p_mode_hero_frac;
let (cos_2psi, sin_2psi) =
uniaxial_fresnel::azimuth2_in_frame(mode_dir, frame.s_axis, p_axis_transmitted);
stokes[k] = StokesVector::new(deposit_i, deposit_i * cos_2psi, deposit_i * sin_2psi, 0.0)
.scale(1.0 / (1.0 - r_branch));
let mueller_k =
uniaxial_fresnel::jones_to_mueller(sol_s_k.r_s, sol_p_k.r_s, sol_s_k.r_p, sol_p_k.r_p);
let t_unpol_k = (1.0 - mueller_k.col(0)[0]).clamp(1e-4, 1.0 - 1e-4);
path_pdf[k] *= t_unpol_k;
}
if exit.enabled {
narrow_compat(&mut exit.compat, &dirs, ctx.hero_idx, hero_match);
}
}
fn apply_uniaxial_internal_bounce(
ubctx: &UniaxialBounceContext<'_, '_>,
is_extraordinary: bool,
ray: BounceRay,
rng: RngDraw,
state: &mut BounceState<'_>,
exit_event: &mut ExitEvent<'_, '_>,
) -> (Vec3, Vec3, bool, Option<bool>, Option<f32>) {
const R_UNPOL_SELECT_MIN: f32 = 0.02;
const R_UNPOL_SELECT_MAX: f32 = 0.98;
let (ctx, geo, frame) = (ubctx.ctx, ubctx.geo, ubctx.frame);
let BounceRay { k_hat, normal } = ray;
let RngDraw { rng_seed, bounce } = rng;
let hero = ctx.hero_idx;
let c_axis = ctx.c_axis;
let n_inc_hero = geo.n1_ch[hero];
let n_o_hero = geo.n_o_ch[hero];
let n_e_hero = ctx
.material
.extraordinary_index_at(ctx.lambdas[hero], n_o_hero);
let sol_hero = uniaxial_fresnel::internal_solve(
n_inc_hero,
n_o_hero,
n_e_hero,
c_axis,
frame,
!is_extraordinary,
);
let flux_inc_hero = sol_hero.flux_inc.max(1e-12);
let ro_pow = sol_hero.r_o.norm_sqr() * sol_hero.flux_ro;
let re_pow = sol_hero.r_e.norm_sqr() * sol_hero.flux_re;
let r_branch =
((ro_pow + re_pow) / flux_inc_hero).clamp(R_UNPOL_SELECT_MIN, R_UNPOL_SELECT_MAX);
let p_o_exact = ro_pow / (ro_pow + re_pow).max(1e-12);
let rng_bounce =
(hash_u32(rng_seed ^ hash_u32(bounce ^ FRESNEL_BRANCH_STREAM)) as f32) / 4_294_967_295.0;
let hero_solve = HeroInternalSolve {
hero,
is_extraordinary,
sol_hero,
r_branch,
};
if rng_bounce < r_branch {
apply_uniaxial_internal_reflect_channels(ubctx, &hero_solve, state);
let new_k = k_hat - 2.0 * k_hat.dot(normal) * normal;
(new_k, new_k, true, None, Some(p_o_exact))
} else {
let eta_dir = geo.n1 / geo.n2;
let sin2_t_dir = (eta_dir * eta_dir * geo.cos_i.mul_add(-geo.cos_i, 1.0)).min(1.0);
let cos_t_dir = (1.0 - sin2_t_dir).max(0.0).sqrt();
let refr_wave_dir =
(eta_dir * k_hat + f32::mul_add(eta_dir, geo.cos_i, -cos_t_dir) * normal).normalize();
apply_uniaxial_internal_transmit_channels(ubctx, &hero_solve, ray, state, exit_event);
(refr_wave_dir, refr_wave_dir, false, None, None)
}
}
#[derive(Clone, Copy)]
struct HeroInternalSolve {
hero: usize,
is_extraordinary: bool,
sol_hero: uniaxial_fresnel::InternalPolarizationSolution,
r_branch: f32,
}
fn apply_uniaxial_internal_reflect_channels(
ubctx: &UniaxialBounceContext<'_, '_>,
hero_solve: &HeroInternalSolve,
state: &mut BounceState<'_>,
) {
let ctx = ubctx.ctx;
let geo = ubctx.geo;
let frame = ubctx.frame;
let c_axis = ctx.c_axis;
let &HeroInternalSolve {
hero,
is_extraordinary,
sol_hero,
r_branch,
} = hero_solve;
let stokes = &mut *state.stokes;
let path_pdf = &mut *state.path_pdf;
for k in 0..NUM_CHANNELS {
let sol = if k == hero {
sol_hero
} else {
let n_inc_k = geo.n1_ch[k];
let n_o_k = geo.n_o_ch[k];
let n_e_k = ctx.material.extraordinary_index_at(ctx.lambdas[k], n_o_k);
uniaxial_fresnel::internal_solve(
n_inc_k,
n_o_k,
n_e_k,
c_axis,
frame,
!is_extraordinary,
)
};
let flux_inc = sol.flux_inc.max(1e-12);
let r_total_k = (sol
.r_e
.norm_sqr()
.mul_add(sol.flux_re, sol.r_o.norm_sqr() * sol.flux_ro)
/ flux_inc)
.min(1.0);
stokes[k] = stokes[k].scale(r_total_k / r_branch);
path_pdf[k] *= r_total_k.clamp(1e-4, 1.0 - 1e-4);
}
}
fn compute_uniaxial_exit_transmission(
sol: uniaxial_fresnel::InternalPolarizationSolution,
r_branch: f32,
incident_i: f32,
) -> (StokesVector, f32) {
let flux_inc = sol.flux_inc.max(1e-12);
let ts_n = sol.t_s.scale((sol.flux_ts / flux_inc).sqrt());
let tp_n = sol.t_p.scale((sol.flux_tp / flux_inc).sqrt());
let i_unit = ts_n.norm_sqr() + tp_n.norm_sqr();
let q_unit = ts_n.norm_sqr() - tp_n.norm_sqr();
let cross = ts_n.mul(tp_n.conj());
let u_unit = 2.0 * cross.re;
let v_unit = -2.0 * cross.im;
let transmitted = StokesVector::new(
incident_i * i_unit,
incident_i * q_unit,
incident_i * u_unit,
incident_i * v_unit,
)
.scale(1.0 / (1.0 - r_branch));
(transmitted, i_unit)
}
fn apply_uniaxial_internal_transmit_channels(
ubctx: &UniaxialBounceContext<'_, '_>,
hero_solve: &HeroInternalSolve,
ray: BounceRay,
state: &mut BounceState<'_>,
exit_event: &mut ExitEvent<'_, '_>,
) {
const DIRECTION_MATCH_COS_TOL: f32 = 1.0 - 1e-6;
let (ctx, geo, frame) = (ubctx.ctx, ubctx.geo, ubctx.frame);
let c_axis = ctx.c_axis;
let &HeroInternalSolve {
hero,
is_extraordinary,
sol_hero,
r_branch,
} = hero_solve;
let BounceRay { k_hat, normal } = ray;
let (stokes, path_pdf) = (&mut *state.stokes, &mut *state.path_pdf);
let (exit, hit_point) = (&mut *exit_event.exit, exit_event.hit_point);
let eta_dir = geo.n1 / geo.n2;
let sin2_t_dir = (eta_dir * eta_dir * geo.cos_i.mul_add(-geo.cos_i, 1.0)).min(1.0);
let cos_t_dir = (1.0 - sin2_t_dir).max(0.0).sqrt();
let hero_refr_dir =
(eta_dir * k_hat + f32::mul_add(eta_dir, geo.cos_i, -cos_t_dir) * normal).normalize();
for k in 0..NUM_CHANNELS {
let original_stokes_i = stokes[k].i;
let n_inc_k = geo.n1_ch[k];
let n2k = geo.n2_ch[k];
let sin2_t_k = (n_inc_k / n2k).powi(2) * geo.cos_i.mul_add(-geo.cos_i, 1.0);
if sin2_t_k > 1.0 {
stokes[k] = stokes[k].scale(0.0);
path_pdf[k] = 0.0;
continue;
}
let cos_t_k = (1.0 - sin2_t_k).max(0.0).sqrt();
let eta_dir_k = n_inc_k / n2k;
let refr_wave_dir_k =
(eta_dir_k * k_hat + f32::mul_add(eta_dir_k, geo.cos_i, -cos_t_k) * normal).normalize();
let direction_matches = refr_wave_dir_k.dot(hero_refr_dir) >= DIRECTION_MATCH_COS_TOL;
let sol = if k == hero {
sol_hero
} else {
let n_o_k = geo.n_o_ch[k];
let n_e_k = ctx.material.extraordinary_index_at(ctx.lambdas[k], n_o_k);
uniaxial_fresnel::internal_solve(
n_inc_k,
n_o_k,
n_e_k,
c_axis,
frame,
!is_extraordinary,
)
};
if !direction_matches {
let prefix_path_pdf_k = path_pdf[k];
stokes[k] = stokes[k].scale(0.0);
path_pdf[k] = 0.0;
if exit.enabled {
let (transmitted, i_unit) =
compute_uniaxial_exit_transmission(sol, r_branch, original_stokes_i);
let t_unpol_k = i_unit.clamp(1e-4, 1.0 - 1e-4);
path_pdf[k] = prefix_path_pdf_k * t_unpol_k;
if original_stokes_i > 0.0 {
try_split_exit_channel(
exit,
hit_point,
k,
ctx.lambdas[k],
refr_wave_dir_k,
transmitted.intensity(),
);
}
}
continue;
}
let (transmitted, i_unit) =
compute_uniaxial_exit_transmission(sol, r_branch, original_stokes_i);
stokes[k] = transmitted;
let t_unpol_k = i_unit.clamp(1e-4, 1.0 - 1e-4);
path_pdf[k] *= t_unpol_k;
}
}
type FresnelBounceResult = (Vec3, Vec3, bool, Option<bool>, Option<f32>);
fn try_dispatch_uniaxial_bounce(
bctx: &BounceContext<'_, '_>,
ray: BounceRay,
mode_state: PathModeState,
entering_anisotropic: bool,
rng: RngDraw,
state: &mut BounceState<'_>,
exit_event: &mut ExitEvent<'_, '_>,
) -> Option<FresnelBounceResult> {
let (ctx, geo) = (bctx.ctx, bctx.geo);
let BounceRay { k_hat, .. } = ray;
let PathModeState {
inside_gem,
is_extraordinary,
..
} = mode_state;
if entering_anisotropic && let Some(frame) = geo.uniaxial_frame {
let ubctx = UniaxialBounceContext {
ctx,
geo,
frame: &frame,
};
let (new_k, new_s, new_inside_gem, is_extraordinary_update) =
apply_uniaxial_entry_bounce(&ubctx, ray, rng, state, exit_event.exit);
return Some((new_k, new_s, new_inside_gem, is_extraordinary_update, None));
}
if inside_gem
&& let Some(frame) = geo.uniaxial_frame
&& k_hat.cross(ctx.c_axis).length_squared() > 1e-6
{
let ubctx = UniaxialBounceContext {
ctx,
geo,
frame: &frame,
};
return Some(apply_uniaxial_internal_bounce(
&ubctx,
is_extraordinary,
ray,
rng,
state,
exit_event,
));
}
None
}
struct EntryModeSelectionInputs<'m, 'b> {
ctx: &'b RayMaterialContext<'m>,
geo: &'b BounceRefractionGeometry,
entering_anisotropic: bool,
current_plane_normal: Vec3,
k_hat: Vec3,
hero_stokes: StokesVector,
is_extraordinary: bool,
rng_seed: u32,
bounce: u32,
}
fn resolve_entry_mode_selection(
inputs: &EntryModeSelectionInputs<'_, '_>,
) -> (bool, Option<(f32, f32)>) {
let &EntryModeSelectionInputs {
ctx,
geo,
entering_anisotropic,
current_plane_normal,
k_hat,
hero_stokes,
is_extraordinary,
rng_seed,
bounce,
} = inputs;
let entry_selection = if entering_anisotropic && !geo.is_biaxial {
entry_eigenmode_selection(ctx.c_axis, current_plane_normal, k_hat, hero_stokes)
} else {
None
};
let p_o = entry_selection.map_or(0.5, |(p, ..)| p);
let mode_split_rand = (hash_u32(rng_seed ^ hash_u32(bounce ^ BIREFRINGENT_SPLIT_STREAM))
as f32)
/ 4_294_967_295.0;
let use_extraordinary = if entering_anisotropic {
mode_split_rand < (1.0 - p_o)
} else {
is_extraordinary
};
let entry_mode_azimuth2 = entry_selection.map(|(_, cos_2psi_o, sin_2psi_o)| {
if use_extraordinary {
(-cos_2psi_o, -sin_2psi_o)
} else {
(cos_2psi_o, sin_2psi_o)
}
});
(use_extraordinary, entry_mode_azimuth2)
}
fn compute_entry_reflect_probability(
geo: &BounceRefractionGeometry,
entering_anisotropic: bool,
use_extraordinary: bool,
min: f32,
max: f32,
) -> f32 {
let ordinary_selected = entering_anisotropic && !geo.is_biaxial && !use_extraordinary;
let n2 = if ordinary_selected {
geo.n_o_hero
} else {
geo.n2
};
let sin2_t = if ordinary_selected {
let eta = geo.n1 / n2;
eta * eta * geo.cos_i.mul_add(-geo.cos_i, 1.0)
} else {
geo.sin2_t
};
let cos_t = (1.0 - sin2_t).max(0.0).sqrt();
let r_s =
f32::mul_add(n2, -cos_t, geo.n1 * geo.cos_i) / f32::mul_add(n2, cos_t, geo.n1 * geo.cos_i);
let r_p =
f32::mul_add(geo.n1, -cos_t, n2 * geo.cos_i) / f32::mul_add(geo.n1, cos_t, n2 * geo.cos_i);
let r_unpol_raw = 0.5 * r_p.mul_add(r_p, r_s * r_s);
r_unpol_raw.clamp(min, max)
}
pub(super) fn apply_partial_fresnel_bounce(
bctx: &BounceContext<'_, '_>,
ray: BounceRay,
mode_state: PathModeState,
rng: RngDraw,
state: &mut BounceState<'_>,
exit_event: &mut ExitEvent<'_, '_>,
) -> (Vec3, Vec3, bool, Option<bool>, Option<f32>) {
const R_UNPOL_SELECT_MIN: f32 = 0.02;
const R_UNPOL_SELECT_MAX: f32 = 0.98;
let (ctx, geo) = (bctx.ctx, bctx.geo);
let BounceRay { k_hat, normal } = ray;
let PathModeState {
current_plane_normal,
inside_gem,
is_extraordinary,
} = mode_state;
let RngDraw { rng_seed, bounce } = rng;
let entering_anisotropic = !inside_gem && ctx.is_anisotropic;
if let Some(result) = try_dispatch_uniaxial_bounce(
bctx,
ray,
mode_state,
entering_anisotropic,
rng,
state,
exit_event,
) {
return result;
}
let (use_extraordinary, entry_mode_azimuth2) =
resolve_entry_mode_selection(&EntryModeSelectionInputs {
ctx,
geo,
entering_anisotropic,
current_plane_normal,
k_hat,
hero_stokes: state.stokes[ctx.hero_idx],
is_extraordinary,
rng_seed,
bounce,
});
let r_unpol = compute_entry_reflect_probability(
geo,
entering_anisotropic,
use_extraordinary,
R_UNPOL_SELECT_MIN,
R_UNPOL_SELECT_MAX,
);
let rng_bounce =
(hash_u32(rng_seed ^ hash_u32(bounce ^ FRESNEL_BRANCH_STREAM)) as f32) / 4_294_967_295.0;
if rng_bounce < r_unpol {
let new_k =
apply_partial_reflect_bounce(geo, r_unpol, k_hat, normal, state.stokes, state.path_pdf);
let new_s = poynting_dir_for_mode(ctx, geo, new_k, inside_gem, is_extraordinary);
(new_k, new_s, inside_gem, None, None)
} else {
let selection = RefractSelection {
use_extraordinary,
entry_mode_azimuth2,
};
let outcome =
apply_refract_bounce(bctx, r_unpol, ray, inside_gem, selection, state, exit_event);
(
outcome.new_k,
outcome.new_s,
!inside_gem,
outcome.is_extraordinary_update,
None,
)
}
}
#[cfg(test)]
mod tir_phase_retardation_tests {
use super::*;
#[test]
fn tir_phase_delta_is_nonzero_past_critical_angle() {
let n1k = 2.42f32;
let cos_i = 60.0f32.to_radians().cos();
let sin_i = 60.0f32.to_radians().sin();
assert!(
n1k * sin_i > 1.0,
"test setup must be past the critical angle"
);
let delta = tir_phase_delta(n1k, cos_i, sin_i);
assert!(
delta.abs() > 1e-3,
"TIR phase retardation should be nonzero past the critical angle (got {delta})"
);
let tir_matrix = MuellerMatrix::tir_retardation(delta);
let linear_45 = StokesVector::new(1.0, 0.0, 1.0, 0.0);
let out = linear_45.apply_matrix(&tir_matrix);
assert!(
out.v.abs() > 1e-3,
"a nonzero TIR phase retardation must convert some linear polarization to circular (V) (got V={})",
out.v
);
}
#[test]
fn tir_phase_delta_is_finite_near_grazing_incidence() {
let n1k = 2.42f32;
let cos_i = 0.001f32;
let sin_i = cos_i.mul_add(-cos_i, 1.0).sqrt();
let delta = tir_phase_delta(n1k, cos_i, sin_i);
assert!(
delta.is_finite(),
"delta must remain finite near grazing incidence (got {delta})"
);
}
}
#[cfg(test)]
mod p2_fresnel_energy_conservation_tests {
use crate::optics::materials::GemMaterial;
#[test]
fn ordinary_and_extraordinary_indices_each_conserve_energy_at_several_angles() {
let material = GemMaterial::by_name("Zircon").expect("Zircon must be a built-in material");
let n_o = material.dispersion.evaluate(589.3);
let n_e = n_o + material.birefringence_delta;
assert!(
(n_o - n_e).abs() > 0.01,
"test premise: Zircon must be strongly birefringent"
);
for n2 in [n_o, n_e] {
for angle_deg in [0.0f32, 15.0, 30.0, 45.0, 60.0, 75.0] {
let n1 = 1.0f32; let cos_i = angle_deg.to_radians().cos();
let sin_i = angle_deg.to_radians().sin();
let eta = n1 / n2;
let sin2_t = eta * eta * sin_i * sin_i;
assert!(
sin2_t <= 1.0,
"air->crystal entry (n1 == 1.0 < n2) should never TIR"
);
let cos_t = (1.0 - sin2_t).sqrt();
let r_s =
f32::mul_add(n2, -cos_t, n1 * cos_i) / f32::mul_add(n2, cos_t, n1 * cos_i);
let r_p =
f32::mul_add(n1, -cos_t, n2 * cos_i) / f32::mul_add(n1, cos_t, n2 * cos_i);
let t_s = (2.0 * n1 * cos_i) / f32::mul_add(n2, cos_t, n1 * cos_i);
let t_p = (2.0 * n1 * cos_i) / f32::mul_add(n1, cos_t, n2 * cos_i);
let r_unpol = 0.5 * r_p.mul_add(r_p, r_s * r_s);
let factor = (n2 * cos_t) / (n1 * cos_i).max(1e-6);
let t_unpol = 0.5 * f32::mul_add(t_p * t_p, factor, t_s * t_s * factor);
assert!(
(r_unpol + t_unpol - 1.0).abs() < 1e-6,
"R + T should equal 1 at n2={n2}, angle={angle_deg} deg \
(R={r_unpol}, T={t_unpol}, R+T={})",
r_unpol + t_unpol
);
}
}
}
}
#[cfg(test)]
mod p2_uniaxial_internal_wiring_energy_conservation_tests {
use super::*;
use crate::optics::materials::GemMaterial;
#[test]
#[expect(
clippy::too_many_lines,
reason = "the empty-arena/disabled `ExitSplitCtx` this test's \
`apply_uniaxial_internal_transmit_channels` call requires \
(splitting itself is irrelevant to this test's own energy-conservation \
sweep, which only reads the hero channel -- see that setup's own \
comment) adds to an already-long sweep body"
)]
fn uniaxial_internal_bounce_wiring_conserves_energy_at_exit_interface() {
let zircon = GemMaterial::by_name("Zircon").expect("Zircon must be a built-in material");
let n_o = zircon.dispersion.evaluate(589.3);
let n_e = zircon.extraordinary_index_at(589.3, n_o);
let lambdas = [589.3f32; NUM_CHANNELS];
let axes = [Vec3::X, Vec3::Y, Vec3::new(0.4, 0.5, 0.767_2).normalize()];
let r_branch = 0.5f32;
let mut worst = 0.0f32;
for &c_axis in &axes {
let ctx = RayMaterialContext {
material: &zircon,
lambdas,
hero_idx: 0,
c_axis,
is_anisotropic: true,
enable_internal_mode_coupling: true,
};
for angle_deg in [5.0f32, 15.0, 25.0, 35.0, 45.0, 55.0, 65.0] {
let theta = angle_deg.to_radians();
let cos_i = theta.cos();
let sin_i = theta.sin();
let k_hat = Vec3::new(sin_i, 0.0, cos_i);
let normal = Vec3::new(0.0, 0.0, -1.0);
if k_hat.cross(c_axis).length_squared() < 1e-4 {
continue;
}
let frame = UniaxialFrame::build(k_hat, normal, c_axis, cos_i, sin_i);
for is_extraordinary in [false, true] {
let n_inc = if is_extraordinary {
let cos_kc = frame.gamma.mul_add(frame.cos_i, frame.alpha * frame.sin_i);
let sin2 = cos_kc.mul_add(-cos_kc, 1.0).max(0.0);
1.0 / (cos_kc * cos_kc / (n_o * n_o) + sin2 / (n_e * n_e)).sqrt()
} else {
n_o
};
let geo = BounceRefractionGeometry {
cos_i,
sin_i,
n1: n_inc,
n2: 1.0,
n1_ch: [n_inc; NUM_CHANNELS],
n2_ch: [1.0; NUM_CHANNELS],
n_o_ch: [n_o; NUM_CHANNELS],
..BounceRefractionGeometry::default()
};
let sol_hero = uniaxial_fresnel::internal_solve(
n_inc,
n_o,
n_e,
c_axis,
&frame,
!is_extraordinary,
);
let mut stokes_r = [StokesVector::unpolarized(1.0); NUM_CHANNELS];
let mut pdf_r = [1.0f32; NUM_CHANNELS];
let ubctx = UniaxialBounceContext {
ctx: &ctx,
geo: &geo,
frame: &frame,
};
let hero_solve = HeroInternalSolve {
hero: ctx.hero_idx,
is_extraordinary,
sol_hero,
r_branch,
};
let mut state_r = BounceState {
stokes: &mut stokes_r,
path_pdf: &mut pdf_r,
};
apply_uniaxial_internal_reflect_channels(&ubctx, &hero_solve, &mut state_r);
let mut stokes_t = [StokesVector::unpolarized(1.0); NUM_CHANNELS];
let mut pdf_t = [1.0f32; NUM_CHANNELS];
let empty_soa = crate::simd::PlanesSoA32::from_normals_d(std::iter::empty(), 0);
let mut split_radiance_t = [0.0f32; NUM_CHANNELS];
let mut exit_ctx = ExitSplitCtx {
plane_soa: &empty_soa,
environment: EnvironmentSource::Studio {
preset: crate::optics::LightingPreset::RingLights,
exposure: 1.0,
light_yaw: 0.0,
light_pitch: 0.85,
backdrop: 0.0,
},
studio_rig: None,
observer: Vec3::ZERO,
split_radiance: &mut split_radiance_t,
enabled: false,
compat: [u8::MAX; NUM_CHANNELS],
};
let mut state_t = BounceState {
stokes: &mut stokes_t,
path_pdf: &mut pdf_t,
};
let mut exit_event_t = ExitEvent {
exit: &mut exit_ctx,
hit_point: Vec3::ZERO,
};
apply_uniaxial_internal_transmit_channels(
&ubctx,
&hero_solve,
BounceRay { k_hat, normal },
&mut state_t,
&mut exit_event_t,
);
let reflected = stokes_r[0].i * r_branch;
let transmitted = stokes_t[0].i * (1.0 - r_branch);
let err = (reflected + transmitted - 1.0).abs();
worst = worst.max(err);
}
}
}
println!(
"uniaxial_internal_bounce_wiring_conserves_energy_at_exit_interface: worst_err={worst}"
);
assert!(
worst < 5e-6,
"reflected + transmitted power should equal incident power (1.0) through \
the actual CPU wiring, worst_err={worst}"
);
}
}
#[cfg(test)]
mod entry_eigenmode_selection_tests {
use super::*;
#[test]
fn unpolarized_light_returns_none() {
let c_axis = Vec3::Y;
let current_plane_normal = Vec3::X;
let k_hat = Vec3::NEG_Z;
let stokes = StokesVector::unpolarized(1.0);
assert!(
entry_eigenmode_selection(c_axis, current_plane_normal, k_hat, stokes).is_none(),
"unpolarized light must not be weighted -- it must fall back to flat 50/50"
);
}
#[test]
fn degenerate_plane_of_incidence_returns_none() {
let c_axis = Vec3::Y;
let current_plane_normal = Vec3::ZERO;
let k_hat = Vec3::NEG_Z;
let stokes = StokesVector::new(1.0, 1.0, 0.0, 0.0);
assert!(entry_eigenmode_selection(c_axis, current_plane_normal, k_hat, stokes).is_none());
}
#[test]
fn fully_polarized_along_ordinary_axis_gives_p_o_one() {
let c_axis = Vec3::Y;
let k_hat = Vec3::NEG_Z;
let current_plane_normal = Vec3::X;
let stokes = StokesVector::new(1.0, 1.0, 0.0, 0.0); let (p_o, cos_2psi_o, sin_2psi_o) =
entry_eigenmode_selection(c_axis, current_plane_normal, k_hat, stokes)
.expect("polarized light with a well-defined frame must select Some");
assert!((p_o - 1.0).abs() < 1e-4, "p_o should be ~1.0, got {p_o}");
assert!((cos_2psi_o - 1.0).abs() < 1e-4, "got {cos_2psi_o}");
assert!(sin_2psi_o.abs() < 1e-4, "got {sin_2psi_o}");
}
#[test]
fn fully_polarized_along_extraordinary_axis_gives_p_o_zero() {
let c_axis = Vec3::Y;
let k_hat = Vec3::NEG_Z;
let current_plane_normal = Vec3::X;
let stokes = StokesVector::new(1.0, -1.0, 0.0, 0.0); let (p_o, ..) = entry_eigenmode_selection(c_axis, current_plane_normal, k_hat, stokes)
.expect("polarized light with a well-defined frame must select Some");
assert!(p_o.abs() < 1e-4, "p_o should be ~0.0, got {p_o}");
}
}
#[cfg(test)]
mod entry_mode_projection_tests {
use super::*;
fn oblique_frame() -> (Vec3, Vec3, Vec3) {
let c_axis = Vec3::Y;
let k_hat = Vec3::NEG_Z;
let current_plane_normal = Vec3::new(0.6, 0.8, 0.0);
(c_axis, current_plane_normal, k_hat)
}
#[test]
fn projected_stokes_is_fully_linear_at_several_dop_values() {
let (c_axis, current_plane_normal, k_hat) = oblique_frame();
let psi = 25.0f32.to_radians();
let (cos_2psi, sin_2psi) = ((2.0 * psi).cos(), (2.0 * psi).sin());
for dop in [0.05f32, 0.3, 0.6, 0.9, 1.0] {
let i = 1.0f32;
let q = i * dop * cos_2psi;
let u = i * dop * sin_2psi;
let stokes = StokesVector::new(i, q, u, 0.0);
let (_, cos_2psi_o, sin_2psi_o) =
entry_eigenmode_selection(c_axis, current_plane_normal, k_hat, stokes)
.expect("nonzero DoP with a well-defined frame must select Some");
for (cos_x, sin_x) in [(cos_2psi_o, sin_2psi_o), (-cos_2psi_o, -sin_2psi_o)] {
let projected = StokesVector::new(i, i * cos_x, i * sin_x, 0.0);
assert!(
projected.v.abs() < 1e-6,
"projected V must be exactly zero at dop={dop}, got {}",
projected.v
);
let lin_energy = projected.q.mul_add(projected.q, projected.u * projected.u);
assert!(
projected.i.mul_add(-projected.i, lin_energy).abs() < 1e-4,
"projected Stokes vector must be fully linearly polarized (Q^2+U^2 \
== I^2) at dop={dop}: I={}, Q={}, U={}, Q^2+U^2={lin_energy}",
projected.i,
projected.q,
projected.u
);
}
}
}
#[test]
fn mode_selection_draw_is_an_unbiased_estimator_at_several_dop_values() {
const T_O: f32 = 0.8;
const T_E: f32 = 0.35;
const TRIALS: u32 = 20_000;
let (c_axis, current_plane_normal, k_hat) = oblique_frame();
let psi = 25.0f32.to_radians();
let (cos_2psi, sin_2psi) = ((2.0 * psi).cos(), (2.0 * psi).sin());
for dop in [0.05f32, 0.3, 0.6, 0.9, 1.0] {
let i = 1.0f32;
let q = i * dop * cos_2psi;
let u = i * dop * sin_2psi;
let stokes = StokesVector::new(i, q, u, 0.0);
let (p_o, ..) = entry_eigenmode_selection(c_axis, current_plane_normal, k_hat, stokes)
.expect("nonzero DoP with a well-defined frame must select Some");
let target = i * p_o.mul_add(T_O, (1.0 - p_o) * T_E);
let mut sum = 0.0f64;
for bounce in 0..TRIALS {
let mode_split_rand = (hash_u32(hash_u32(bounce ^ BIREFRINGENT_SPLIT_STREAM))
as f32)
/ 4_294_967_295.0;
let use_extraordinary = mode_split_rand < (1.0 - p_o);
let estimate = if use_extraordinary { T_E * i } else { T_O * i };
sum += f64::from(estimate);
}
let mean = (sum / f64::from(TRIALS)) as f32;
assert!(
(mean - target).abs() < 0.01,
"mode-selection draw should be an unbiased estimator of the \
polarization-weighted mixture at dop={dop}: p_o={p_o}, target={target}, \
Monte Carlo mean={mean} over {TRIALS} trials"
);
}
}
}