use super::{
DIRECTION_MATCH_COS_TOL, R_UNPOL_PDF_MAX, R_UNPOL_PDF_MIN,
context::{BounceContext, BounceRay, BounceState, ExitEvent, narrow_compat},
exit_split::{ChannelTransmissionInputs, compute_channel_transmission, try_split_exit_channel},
geometry::BounceRefractionGeometry,
tir::tir_phase_delta,
};
use crate::optics::{
birefringence::BirefringenceParams,
polarization::{MuellerMatrix, StokesVector},
raytracer::NUM_CHANNELS,
};
use glam::Vec3;
#[expect(
clippy::similar_names,
reason = "r_s_k/r_p_k are the established s/p polarisation Fresnel names"
)]
pub(super) 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(R_UNPOL_PDF_MIN, R_UNPOL_PDF_MAX);
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
}
pub(super) struct RefractBounceOutcome {
pub(super) new_k: Vec3,
pub(super) new_s: Vec3,
pub(super) is_extraordinary_update: Option<bool>,
}
#[derive(Clone, Copy)]
pub(super) struct RefractSelection {
pub(super) use_extraordinary: bool,
pub(super) entry_mode_azimuth2: Option<(f32, f32)>,
}
pub(super) 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)>,
}
#[expect(
clippy::similar_names,
reason = "s/p polarisation pairs (t_s_k/t_p_k, r_s_k/r_p_k) are the established Fresnel names"
)]
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(R_UNPOL_PDF_MIN, R_UNPOL_PDF_MAX);
*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) {
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)
}