use super::{
DIRECTION_MATCH_COS_TOL, R_UNPOL_PDF_MAX, R_UNPOL_PDF_MIN, R_UNPOL_SELECT_MAX,
R_UNPOL_SELECT_MIN,
context::{BounceRay, BounceState, ExitEvent, RngDraw, UniaxialBounceContext},
exit_split::try_split_exit_channel,
};
use crate::optics::{
polarization::StokesVector,
raytracer::{
NUM_CHANNELS,
sampling::{FRESNEL_BRANCH_STREAM, hash_u32},
uniaxial_fresnel,
},
};
use glam::Vec3;
#[expect(
clippy::similar_names,
reason = "n_o/n_e are the established ordinary/extraordinary index names; ro_pow/re_pow are the established ordinary/extraordinary reflected-power names"
)]
pub(super) 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>) {
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)]
pub(super) struct HeroInternalSolve {
pub(super) hero: usize,
pub(super) is_extraordinary: bool,
pub(super) sol_hero: uniaxial_fresnel::InternalPolarizationSolution,
pub(super) r_branch: f32,
}
#[expect(
clippy::similar_names,
reason = "n_o/n_e are the established ordinary/extraordinary index names"
)]
pub(super) 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(R_UNPOL_PDF_MIN, R_UNPOL_PDF_MAX);
}
}
#[expect(
clippy::similar_names,
reason = "ts_n/tp_n are the established s/p polarisation normalised-amplitude names"
)]
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)
}
#[expect(
clippy::similar_names,
reason = "n_o/n_e are the established ordinary/extraordinary index names"
)]
pub(super) fn apply_uniaxial_internal_transmit_channels(
ubctx: &UniaxialBounceContext<'_, '_>,
hero_solve: &HeroInternalSolve,
ray: BounceRay,
state: &mut BounceState<'_>,
exit_event: &mut ExitEvent<'_, '_>,
) {
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(R_UNPOL_PDF_MIN, R_UNPOL_PDF_MAX);
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(R_UNPOL_PDF_MIN, R_UNPOL_PDF_MAX);
path_pdf[k] *= t_unpol_k;
}
}