use super::{
NUM_CHANNELS,
absorption::{apply_absorption, rotate_stokes_to_plane_of_incidence},
camera::{FacetFinish, HitRecord, Ray},
color::{
apply_von_kries_white_balance, integrate_channels_to_xyz,
integrate_channels_to_xyz_families,
},
environment::{
EnvironmentSource, environment_nee_pdf, environment_white_balance,
sample_environment_channel,
},
intersect::{build_plane_soa, intersect_polyhedron_soa, shading_normal_near_edge},
refraction::{
BounceContext, BounceRay, BounceRefractionGeometry, BounceState, ExitEvent, ExitSplitCtx,
PathModeState, RayMaterialContext, RayWavelengthCache, RngDraw,
apply_partial_fresnel_bounce, apply_tir_bounce, build_ray_wavelength_cache,
compute_bounce_refraction_geometry, entry_eigenmode_selection, poynting_dir_for_mode,
},
sampling::{MODE_COUPLING_STREAM, RUSSIAN_ROULETTE_STREAM, hash_u32},
scattering::{
NeeContext, ScatterStepOutcome, apply_frosted_bounce, balance_heuristic, try_scatter_step,
},
};
use crate::{
geometry::plane::GpuFacetPlane,
optics::{
materials::{CrystalSystem, GemMaterial},
polarization::StokesVector,
},
};
use glam::Vec3;
#[must_use]
pub fn wrapped_hero_wavelengths<const N: usize>(hero_rand: f32) -> [f32; N] {
const SPECTRUM_SPAN: f32 = 780.0 - 380.0;
let channel_width = SPECTRUM_SPAN / N as f32;
let lambda_hero = hero_rand.mul_add(SPECTRUM_SPAN, 380.0);
std::array::from_fn(|k| {
let offset = (k as f32).mul_add(channel_width, lambda_hero - 380.0);
380.0 + offset.rem_euclid(SPECTRUM_SPAN)
})
}
fn accumulate_miss_radiance(
environment: EnvironmentSource<'_>,
ray_dir: Vec3,
lambdas: &[f32; NUM_CHANNELS],
stokes: &[StokesVector; NUM_CHANNELS],
mis_weight: f32,
radiance: &mut [f32; NUM_CHANNELS],
) {
let studio_rig = match environment {
EnvironmentSource::Studio {
light_yaw,
light_pitch,
..
} => Some(crate::optics::studio_rig::StudioRig::new(
light_yaw,
light_pitch,
)),
EnvironmentSource::HdrMap(_) => None,
};
for k in 0..NUM_CHANNELS {
let env_spectral =
sample_environment_channel(environment, ray_dir, lambdas[k], studio_rig.as_ref());
radiance[k] = (stokes[k].intensity() * mis_weight).mul_add(env_spectral, radiance[k]);
}
}
#[expect(
clippy::too_many_arguments,
reason = "bundles every value fixed for the trace (ctx) or the bounce (cache, geo) \
into context structs; the rest are this bounce's scalar inputs, the RNG \
stream identity, and the per-ray state a bounce can mutate (stokes, \
path_pdf, current_ray, current_k, inside_gem, is_extraordinary) -- \
collapsing those into one struct would hide which fields each bounce kind \
touches behind an opaque &mut; `exit` is the per-trace `ExitSplitCtx` \
(see its doc comment), used only by the apply_partial_fresnel_bounce \
branch -- the frosted/TIR branches never reach an exit event; `nee`/ \
`lambdas`/`radiance` are finding G8's own NEE inputs/output, used only by \
the frosted branch, mirroring try_scatter_step's identical addition for \
the Henyey-Greenstein path"
)]
fn dispatch_bounce(
ctx: &RayMaterialContext,
cache: &RayWavelengthCache,
geo: &BounceRefractionGeometry,
hit_point: Vec3,
normal: Vec3,
current_plane_normal: Vec3,
finish: FacetFinish,
rng_seed: u32,
bounce: u32,
stokes: &mut [StokesVector; NUM_CHANNELS],
path_pdf: &mut [f32; NUM_CHANNELS],
exit: &mut ExitSplitCtx<'_>,
current_ray: &mut Ray,
current_k: &mut Vec3,
inside_gem: &mut bool,
is_extraordinary: &mut bool,
nee: NeeContext<'_>,
lambdas: &[f32; NUM_CHANNELS],
radiance: &mut [f32; NUM_CHANNELS],
) -> Option<f32> {
let (new_k, new_s, new_inside_gem, is_extraordinary_update, exact_p_o, frosted_light_mis) =
if finish == FacetFinish::Frosted {
let (new_dir, new_inside_gem, is_extraordinary_update, frosted_light_mis) =
apply_frosted_bounce(
ctx,
geo,
normal,
*inside_gem,
*is_extraordinary,
rng_seed,
bounce,
stokes,
path_pdf,
nee,
lambdas,
radiance,
);
(
new_dir,
new_dir,
new_inside_gem,
is_extraordinary_update,
None,
frosted_light_mis,
)
} else if geo.sin2_t > 1.0 {
let (k_prime, exact_p_o) = apply_tir_bounce(
ctx,
geo,
*is_extraordinary,
*current_k,
normal,
stokes,
path_pdf,
);
let s_prime = poynting_dir_for_mode(ctx, geo, k_prime, *inside_gem, *is_extraordinary);
(k_prime, s_prime, *inside_gem, None, exact_p_o, None)
} else {
let bctx = BounceContext { ctx, cache, geo };
let ray = BounceRay {
k_hat: *current_k,
normal,
};
let mode_state = PathModeState {
current_plane_normal,
inside_gem: *inside_gem,
is_extraordinary: *is_extraordinary,
};
let rng = RngDraw { rng_seed, bounce };
let mut state = BounceState { stokes, path_pdf };
let mut exit_event = ExitEvent { exit, hit_point };
let (k_prime, s_prime, new_inside_gem, is_extraordinary_update, exact_p_o) =
apply_partial_fresnel_bounce(
&bctx,
ray,
mode_state,
rng,
&mut state,
&mut exit_event,
);
(
k_prime,
s_prime,
new_inside_gem,
is_extraordinary_update,
exact_p_o,
None,
)
};
let was_internal_reflection =
*inside_gem && is_extraordinary_update.is_none() && new_inside_gem == *inside_gem;
current_ray.dir = new_s;
current_ray.origin = hit_point + new_s * 1e-4;
*current_k = new_k;
*inside_gem = new_inside_gem;
if let Some(updated) = is_extraordinary_update {
*is_extraordinary = updated;
}
maybe_apply_internal_mode_coupling(
ctx,
geo.is_biaxial,
current_plane_normal,
*current_k,
stokes,
was_internal_reflection,
exact_p_o,
rng_seed,
bounce,
is_extraordinary,
);
frosted_light_mis
}
pub(super) fn apply_russian_roulette(
bounce: u32,
rng_seed: u32,
stokes: &mut [StokesVector; NUM_CHANNELS],
split_radiance: &mut [f32; NUM_CHANNELS],
) -> bool {
let max_intensity = stokes.iter().fold(0.0f32, |a, s| a.max(s.intensity()));
let q = max_intensity.clamp(0.05, 1.0);
let rr_rand =
(hash_u32(rng_seed ^ hash_u32(bounce ^ RUSSIAN_ROULETTE_STREAM)) as f32) / 4_294_967_295.0;
if rr_rand > q {
return false;
}
for s in &mut *stokes {
*s = s.scale(1.0 / q);
}
for r in &mut *split_radiance {
*r /= q;
}
true
}
#[expect(
clippy::too_many_arguments,
reason = "thin re-labeling draw: ctx/is_biaxial/current_plane_normal/new_k/stokes \
are this function's own inputs (see its doc comment), exact_p_o is the \
closed-form override of the same probability, plus the RNG stream identity"
)]
fn apply_internal_mode_coupling(
ctx: &RayMaterialContext,
is_biaxial: bool,
current_plane_normal: Vec3,
new_k: Vec3,
stokes: &[StokesVector; NUM_CHANNELS],
exact_p_o: Option<f32>,
rng_seed: u32,
bounce: u32,
) -> bool {
let p_o = exact_p_o.unwrap_or_else(|| {
if is_biaxial {
None
} else {
entry_eigenmode_selection(
ctx.c_axis,
current_plane_normal,
new_k,
stokes[ctx.hero_idx],
)
}
.map_or(0.5, |(p, ..)| p)
});
let split_rand =
(hash_u32(rng_seed ^ hash_u32(bounce ^ MODE_COUPLING_STREAM)) as f32) / 4_294_967_295.0;
split_rand >= p_o
}
#[expect(
clippy::too_many_arguments,
reason = "thin dispatcher for apply_internal_mode_coupling's own inputs plus the \
was_internal_reflection gate and RNG stream identity"
)]
fn maybe_apply_internal_mode_coupling(
ctx: &RayMaterialContext,
is_biaxial: bool,
current_plane_normal: Vec3,
new_k: Vec3,
stokes: &[StokesVector; NUM_CHANNELS],
was_internal_reflection: bool,
exact_p_o: Option<f32>,
rng_seed: u32,
bounce: u32,
is_extraordinary: &mut bool,
) {
if ctx.enable_internal_mode_coupling && ctx.is_anisotropic && was_internal_reflection {
*is_extraordinary = apply_internal_mode_coupling(
ctx,
is_biaxial,
current_plane_normal,
new_k,
stokes,
exact_p_o,
rng_seed,
bounce,
);
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PathTermination {
Escaped,
ScatterAbsorbed,
RussianRoulette,
HitCap,
}
#[expect(
clippy::too_many_arguments,
reason = "the crate's public raytracing entry point: each parameter is one \
independent input a caller supplies once per traced ray; wrapping it in \
a context struct would only make every caller build a struct instead of \
calling a function"
)]
#[must_use]
pub fn trace_spectral_ray(
initial_ray: Ray,
planes: &[GpuFacetPlane],
material: &GemMaterial,
max_bounces: u32,
environment: EnvironmentSource<'_>,
rng_seed: u32,
hero_rand: f32,
primary_hit_out: Option<&mut Option<HitRecord>>,
) -> Vec3 {
let plane_soa = build_plane_soa(planes);
trace_spectral_ray_inner(
initial_ray,
planes,
&plane_soa,
&[],
material,
max_bounces,
environment,
rng_seed,
hero_rand,
primary_hit_out,
true,
true,
matches!(environment, EnvironmentSource::HdrMap(_)),
None,
)
}
#[expect(
clippy::too_many_arguments,
reason = "see trace_spectral_ray's own reason, plus the per-facet finish slice"
)]
#[must_use]
pub fn trace_spectral_ray_with_finish(
initial_ray: Ray,
planes: &[GpuFacetPlane],
facet_finishes: &[FacetFinish],
material: &GemMaterial,
max_bounces: u32,
environment: EnvironmentSource<'_>,
rng_seed: u32,
hero_rand: f32,
primary_hit_out: Option<&mut Option<HitRecord>>,
) -> Vec3 {
let plane_soa = build_plane_soa(planes);
trace_spectral_ray_with_finish_soa(
initial_ray,
planes,
&plane_soa,
facet_finishes,
material,
max_bounces,
environment,
rng_seed,
hero_rand,
primary_hit_out,
)
}
#[expect(
clippy::too_many_arguments,
reason = "see trace_spectral_ray's own reason, plus the per-facet finish slice and \
the caller-supplied plane_soa arena alongside the planes slice it was \
built from"
)]
#[must_use]
pub fn trace_spectral_ray_with_finish_soa(
initial_ray: Ray,
planes: &[GpuFacetPlane],
plane_soa: &crate::simd::PlanesSoA32,
facet_finishes: &[FacetFinish],
material: &GemMaterial,
max_bounces: u32,
environment: EnvironmentSource<'_>,
rng_seed: u32,
hero_rand: f32,
primary_hit_out: Option<&mut Option<HitRecord>>,
) -> Vec3 {
trace_spectral_ray_inner(
initial_ray,
planes,
plane_soa,
facet_finishes,
material,
max_bounces,
environment,
rng_seed,
hero_rand,
primary_hit_out,
true,
true,
matches!(environment, EnvironmentSource::HdrMap(_)),
None,
)
}
#[expect(
clippy::too_many_arguments,
reason = "see trace_spectral_ray's own reason, plus the per-facet finish slice and \
the bounce_cost harness's own instrumentation"
)]
#[must_use]
pub fn trace_spectral_ray_with_finish_instrumented(
initial_ray: Ray,
planes: &[GpuFacetPlane],
facet_finishes: &[FacetFinish],
material: &GemMaterial,
max_bounces: u32,
environment: EnvironmentSource<'_>,
rng_seed: u32,
hero_rand: f32,
) -> (Vec3, u32, PathTermination) {
let plane_soa = build_plane_soa(planes);
let mut termination = (max_bounces, PathTermination::HitCap);
let radiance = trace_spectral_ray_inner(
initial_ray,
planes,
&plane_soa,
facet_finishes,
material,
max_bounces,
environment,
rng_seed,
hero_rand,
None,
true,
true,
matches!(environment, EnvironmentSource::HdrMap(_)),
Some(&mut termination),
);
(radiance, termination.0, termination.1)
}
fn capture_primary_hit(
primary_hit_out: &mut Option<&mut Option<HitRecord>>,
bounce: u32,
hit: Option<HitRecord>,
) {
if bounce == 0
&& let Some(slot) = primary_hit_out.as_deref_mut()
{
*slot = hit;
}
}
fn facet_finish_for(facet_finishes: &[FacetFinish], facet_idx: usize) -> FacetFinish {
facet_finishes.get(facet_idx).copied().unwrap_or_default()
}
fn apply_interior_segment(
ray_ctx: (&RayMaterialContext, &RayWavelengthCache),
k_hat: Vec3,
is_extraordinary: bool,
hit_t: f32,
inside_gem: bool,
normal: &mut Vec3,
stokes: &mut [StokesVector; NUM_CHANNELS],
) {
let (ctx, cache) = ray_ctx;
if !inside_gem {
return;
}
*normal = -*normal;
if ctx.material.scattering_sigma_s <= 0.0 {
apply_absorption(ctx, cache, k_hat, is_extraordinary, hit_t, stokes);
}
}
fn build_ray_material_context(
material: &GemMaterial,
lambdas: [f32; NUM_CHANNELS],
hero_idx: usize,
enable_internal_mode_coupling: bool,
) -> RayMaterialContext<'_> {
let c_axis = material.c_axis;
let is_anisotropic = material.crystal_system != CrystalSystem::Cubic
&& material.birefringence_delta.abs() > 1e-4;
RayMaterialContext {
material,
lambdas,
hero_idx,
c_axis,
is_anisotropic,
enable_internal_mode_coupling,
}
}
fn build_ray_context(
material: &GemMaterial,
lambdas: [f32; NUM_CHANNELS],
hero_idx: usize,
enable_internal_mode_coupling: bool,
) -> (RayMaterialContext<'_>, RayWavelengthCache) {
let mat_ctx =
build_ray_material_context(material, lambdas, hero_idx, enable_internal_mode_coupling);
let wavelength_cache = build_ray_wavelength_cache(&mat_ctx);
(mat_ctx, wavelength_cache)
}
fn record_termination(
termination_out: &mut Option<&mut (u32, PathTermination)>,
bounce: u32,
reason: PathTermination,
) {
if let Some(out) = termination_out.as_deref_mut() {
*out = (bounce, reason);
}
}
#[expect(
clippy::too_many_arguments,
reason = "the shared bounce loop every public entry point delegates to -- see \
trace_spectral_ray's own reason, plus the debug/instrumentation output \
hooks (primary_hit_out, termination_out) and the caller-supplied \
plane_soa arena alongside the planes slice it was built from"
)]
#[expect(
clippy::too_many_lines,
reason = "already at the pedantic line-length threshold; the bounce_cost harness's \
termination bookkeeping pushes it a few lines over -- see \
record_termination's doc comment for why that extraction, not further \
splitting this already heavily-decomposed loop, is the right amount of \
surgery"
)]
fn trace_spectral_ray_inner(
initial_ray: Ray,
planes: &[GpuFacetPlane],
plane_soa: &crate::simd::PlanesSoA32,
facet_finishes: &[FacetFinish],
material: &GemMaterial,
max_bounces: u32,
environment: EnvironmentSource<'_>,
rng_seed: u32,
hero_rand: f32,
mut primary_hit_out: Option<&mut Option<HitRecord>>,
enable_internal_mode_coupling: bool,
enable_exit_splitting: bool,
enable_nee: bool,
mut termination_out: Option<&mut (u32, PathTermination)>,
) -> Vec3 {
let lambdas: [f32; NUM_CHANNELS] = wrapped_hero_wavelengths(hero_rand);
let mut stokes = [StokesVector::unpolarized(1.0); NUM_CHANNELS];
let mut radiance = [0.0f32; NUM_CHANNELS];
let hero_idx: usize = 0;
let mut path_pdf = [1.0f32; NUM_CHANNELS];
let mut current_ray = initial_ray;
let mut current_k = initial_ray.dir;
let mut inside_gem = false;
let mut prev_plane_normal: Option<Vec3> = None;
let mut is_extraordinary = false;
let (mat_ctx, wavelength_cache) =
build_ray_context(material, lambdas, hero_idx, enable_internal_mode_coupling);
let exit_split_studio_rig = match environment {
EnvironmentSource::Studio {
light_yaw,
light_pitch,
..
} => Some(crate::optics::studio_rig::StudioRig::new(
light_yaw,
light_pitch,
)),
EnvironmentSource::HdrMap(_) => None,
};
let mut split_radiance = [0.0f32; NUM_CHANNELS];
let mut exit_split_ctx = ExitSplitCtx {
plane_soa,
environment,
studio_rig: exit_split_studio_rig,
split_radiance: &mut split_radiance,
enabled: enable_exit_splitting,
compat: [u8::MAX; NUM_CHANNELS],
};
let mut path_escaped = false;
let nee_ctx = NeeContext {
environment,
plane_soa,
enabled: enable_nee,
};
let mut pending_light_mis: Option<f32> = None;
for bounce in 0..max_bounces {
let phase_pdf_for_mis_this_check = pending_light_mis.take();
let hit = intersect_polyhedron_soa(current_ray, plane_soa);
capture_primary_hit(&mut primary_hit_out, bounce, hit);
let Some(hit_rec) = hit else {
let mis_weight = phase_pdf_for_mis_this_check.map_or(1.0, |phase_pdf| {
let light_pdf = environment_nee_pdf(environment, current_ray.dir);
balance_heuristic(phase_pdf, light_pdf)
});
accumulate_miss_radiance(
environment,
current_ray.dir,
&lambdas,
&stokes,
mis_weight,
&mut radiance,
);
path_escaped = true;
record_termination(&mut termination_out, bounce, PathTermination::Escaped);
break;
};
if inside_gem {
match try_scatter_step(
&mat_ctx,
&wavelength_cache,
material,
is_extraordinary,
&mut current_ray,
&mut current_k,
hit_rec.t,
rng_seed,
bounce,
&mut stokes,
&mut path_pdf,
exit_split_ctx.split_radiance,
nee_ctx,
&lambdas,
&mut radiance,
) {
ScatterStepOutcome::NotApplicable | ScatterStepOutcome::ReachedBoundary => {}
ScatterStepOutcome::ScatteredAndSurvived(phase_pdf_for_mis) => {
prev_plane_normal = None;
pending_light_mis = phase_pdf_for_mis;
continue;
}
ScatterStepOutcome::ScatteredAndTerminated => {
record_termination(
&mut termination_out,
bounce,
PathTermination::ScatterAbsorbed,
);
break;
}
}
}
let hit_point = current_ray.origin + hit_rec.t * current_ray.dir;
let mut normal = shading_normal_near_edge(
planes,
hit_point,
hit_rec.facet_idx,
hit_rec.normal,
material.edge_rounding_radius,
);
let current_plane_normal =
rotate_stokes_to_plane_of_incidence(current_k, normal, prev_plane_normal, &mut stokes);
prev_plane_normal = Some(current_plane_normal);
apply_interior_segment(
(&mat_ctx, &wavelength_cache),
current_k,
is_extraordinary,
hit_rec.t,
inside_gem,
&mut normal,
&mut stokes,
);
let geo = compute_bounce_refraction_geometry(
&mat_ctx,
&wavelength_cache,
normal,
current_k,
inside_gem,
is_extraordinary,
);
let finish = facet_finish_for(facet_finishes, hit_rec.facet_idx);
pending_light_mis = dispatch_bounce(
&mat_ctx,
&wavelength_cache,
&geo,
hit_point,
normal,
current_plane_normal,
finish,
rng_seed,
bounce,
&mut stokes,
&mut path_pdf,
&mut exit_split_ctx,
&mut current_ray,
&mut current_k,
&mut inside_gem,
&mut is_extraordinary,
nee_ctx,
&lambdas,
&mut radiance,
);
if bounce > 4
&& !apply_russian_roulette(bounce, rng_seed, &mut stokes, exit_split_ctx.split_radiance)
{
record_termination(
&mut termination_out,
bounce,
PathTermination::RussianRoulette,
);
break;
}
}
let compat = exit_split_ctx.compat;
if path_escaped {
for k in 0..NUM_CHANNELS {
radiance[k] += split_radiance[k];
}
}
let xyz = if enable_exit_splitting {
integrate_channels_to_xyz_families(&radiance, &lambdas, &path_pdf, hero_idx, compat)
} else {
integrate_channels_to_xyz(&radiance, &lambdas, &path_pdf, hero_idx)
};
apply_von_kries_white_balance(xyz, environment_white_balance(environment)).max(Vec3::ZERO)
}
#[cfg(test)]
mod mode_coupling_tests {
use super::{
super::{
environment::LightingPreset,
uniaxial_fresnel::{self, UniaxialFrame},
},
*,
};
use crate::geometry::cuts::StandardGemCuts;
fn frame_for(ang_deg: f32, c_axis: Vec3) -> (Vec3, Vec3, UniaxialFrame) {
let theta = ang_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);
(
k_hat,
normal,
UniaxialFrame::build(k_hat, normal, c_axis, cos_i, sin_i),
)
}
#[test]
fn internal_mode_coupling_draw_matches_exact_closed_form_split() {
let lambdas = [589.3f32; NUM_CHANNELS];
let zircon = GemMaterial::by_name("Zircon").expect("Zircon must be a built-in material");
let ctx = build_ray_material_context(&zircon, lambdas, 0, true);
let n_o = zircon.dispersion.evaluate(589.3);
let n_e = zircon.extraordinary_index_at(589.3, n_o);
let c_axis = Vec3::new(0.4, 0.5, 0.767_2).normalize();
let (_, _, frame) = frame_for(25.0, c_axis);
let sol = uniaxial_fresnel::internal_solve(n_o, n_o, n_e, c_axis, &frame, true);
let ro_pow = sol.r_o.norm_sqr() * sol.flux_ro;
let re_pow = sol.r_e.norm_sqr() * sol.flux_re;
let p_o_exact = ro_pow / (ro_pow + re_pow).max(1e-12);
let stokes = [StokesVector::unpolarized(1.0); NUM_CHANNELS];
let mut extraordinary_count = 0u32;
let trials = 20_000u32;
for seed in 0..trials {
if apply_internal_mode_coupling(
&ctx,
false,
Vec3::X,
Vec3::NEG_Z,
&stokes,
Some(p_o_exact),
seed,
3,
) {
extraordinary_count += 1;
}
}
let frac = f64::from(extraordinary_count) / f64::from(trials);
let expected = f64::from(1.0 - p_o_exact);
let tol = 4.0f64.mul_add(
(expected * (1.0 - expected) / f64::from(trials)).sqrt(),
0.005,
);
assert!(
(frac - expected).abs() < tol,
"draw frequency should match the exact closed-form split p_o_exact={p_o_exact} \
(expected extraordinary frac={expected}), got {frac} over {trials} trials \
(tol={tol})"
);
}
#[test]
fn internal_mode_coupling_draw_is_strongly_biased_near_a_modes_own_critical_angle() {
let lambdas = [589.3f32; NUM_CHANNELS];
let zircon = GemMaterial::by_name("Zircon").expect("Zircon must be a built-in material");
let ctx = build_ray_material_context(&zircon, lambdas, 0, true);
let n_o = zircon.dispersion.evaluate(589.3);
let n_e = zircon.extraordinary_index_at(589.3, n_o);
let c_axis = Vec3::new(0.4, 0.5, 0.767_2).normalize();
let mut best_angle = 0.0f32;
let mut best_p_o = 0.5f32;
let mut best_skew = 0.0f32;
let steps: u16 = 4000;
for i in 0..=steps {
let ang = 20.0f32.mul_add(f32::from(i) / f32::from(steps), 20.0);
let (_, _, frame) = frame_for(ang, c_axis);
let sol = uniaxial_fresnel::internal_solve(n_o, n_o, n_e, c_axis, &frame, true);
let ro_pow = sol.r_o.norm_sqr() * sol.flux_ro;
let re_pow = sol.r_e.norm_sqr() * sol.flux_re;
let p_o = ro_pow / (ro_pow + re_pow).max(1e-12);
let skew = (p_o - 0.5).abs();
if skew > best_skew {
best_skew = skew;
best_angle = ang;
best_p_o = p_o;
}
}
assert!(
best_skew > 0.45,
"test premise: sweep should find a strongly asymmetric angle between the \
two modes' own critical angles -- best found: angle={best_angle} \
p_o={best_p_o} skew={best_skew}"
);
let stokes = [StokesVector::unpolarized(1.0); NUM_CHANNELS];
let mut extraordinary_count = 0u32;
let trials = 20_000u32;
for seed in 0..trials {
if apply_internal_mode_coupling(
&ctx,
false,
Vec3::X,
Vec3::NEG_Z,
&stokes,
Some(best_p_o),
seed,
7,
) {
extraordinary_count += 1;
}
}
let frac = f64::from(extraordinary_count) / f64::from(trials);
let expected = f64::from(1.0 - best_p_o);
let tol = 4.0f64.mul_add(
(expected * (1.0 - expected) / f64::from(trials)).sqrt(),
0.005,
);
assert!(
(frac - expected).abs() < tol,
"at angle={best_angle} (p_o_exact={best_p_o}), draw frequency should match \
expected={expected}, got {frac} over {trials} trials (tol={tol})"
);
}
fn sum_with_and_without_mode_coupling(
material: &GemMaterial,
ray: Ray,
planes: &[GpuFacetPlane],
plane_soa: &crate::simd::PlanesSoA32,
env: EnvironmentSource<'_>,
samples: u32,
seed_base: u32,
) -> (Vec3, Vec3) {
let mut sum_with = Vec3::ZERO;
let mut sum_without = Vec3::ZERO;
for i in 0..samples {
let seed = seed_base + i;
let hero_rand = (hash_u32(seed) as f32) / 4_294_967_295.0;
sum_with += trace_spectral_ray_inner(
ray,
planes,
plane_soa,
&[],
material,
12,
env,
seed,
hero_rand,
None,
true,
true,
false,
None,
);
sum_without += trace_spectral_ray_inner(
ray,
planes,
plane_soa,
&[],
material,
12,
env,
seed,
hero_rand,
None,
false,
true,
false,
None,
);
}
(sum_with, sum_without)
}
#[test]
fn internal_mode_coupling_changes_zircon_render() {
let planes = StandardGemCuts::standard_round_brilliant();
let plane_soa = build_plane_soa(&planes);
let zircon = GemMaterial::by_name("Zircon").expect("Zircon must be a built-in material");
assert!(
zircon.birefringence_delta.abs() > 0.01,
"test assumes Zircon is strongly birefringent"
);
let ray = Ray {
origin: Vec3::new(0.0, 2.5, 0.0),
dir: Vec3::new(0.18, -1.0, 0.07).normalize(),
};
let env = LightingPreset::RingLights.studio(1.0, 0.85, 0.95);
let samples = 256u32;
let (sum_with, sum_without) = sum_with_and_without_mode_coupling(
&zircon, ray, &planes, &plane_soa, env, samples, 1000,
);
let mean_with = sum_with / samples as f32;
let mean_without = sum_without / samples as f32;
let diff = (mean_with - mean_without).length();
assert!(
diff > 1e-4,
"internal mode coupling should measurably change Zircon's rendered XYZ \
(mean_with={mean_with:?}, mean_without={mean_without:?}, diff={diff})"
);
let diamond = GemMaterial::by_name("Diamond").expect("Diamond must be a built-in material");
let (sum_with_diamond, sum_without_diamond) = sum_with_and_without_mode_coupling(
&diamond, ray, &planes, &plane_soa, env, samples, 1000,
);
assert_eq!(
sum_with_diamond, sum_without_diamond,
"Diamond (cubic, is_anisotropic == false) must be bit-identical with the flag \
either way -- the mechanism must not leak into isotropic materials"
);
}
}
#[cfg(test)]
mod exit_splitting_tests {
use super::*;
use crate::{geometry::cuts::StandardGemCuts, optics::LightingPreset};
struct RenderStats {
mean: Vec3,
var: Vec3,
n: f64,
}
#[expect(
clippy::too_many_arguments,
reason = "test-only helper bundling exactly the inputs trace_spectral_ray_inner \
needs plus this sweep's sample count/seed base/on-off switch"
)]
fn render_stats(
material: &GemMaterial,
ray: Ray,
planes: &[GpuFacetPlane],
plane_soa: &crate::simd::PlanesSoA32,
env: EnvironmentSource<'_>,
samples: u32,
seed_base: u32,
enable_exit_splitting: bool,
) -> RenderStats {
let mut sum = Vec3::ZERO;
let mut sumsq_x = 0.0f64;
let mut sumsq_y = 0.0f64;
let mut sumsq_z = 0.0f64;
for i in 0..samples {
let seed = seed_base.wrapping_add(i);
let hero_rand = (hash_u32(seed) as f32) / 4_294_967_295.0;
let xyz = trace_spectral_ray_inner(
ray,
planes,
plane_soa,
&[],
material,
12,
env,
seed,
hero_rand,
None,
true,
enable_exit_splitting,
false,
None,
);
sum += xyz;
let (xd, yd, zd) = (f64::from(xyz.x), f64::from(xyz.y), f64::from(xyz.z));
sumsq_x = xd.mul_add(xd, sumsq_x);
sumsq_y = yd.mul_add(yd, sumsq_y);
sumsq_z = zd.mul_add(zd, sumsq_z);
}
let n = f64::from(samples);
let mean = sum / samples as f32;
let (mx, my, mz) = (f64::from(mean.x), f64::from(mean.y), f64::from(mean.z));
let var = Vec3::new(
mx.mul_add(-mx, sumsq_x / n).max(0.0) as f32,
my.mul_add(-my, sumsq_y / n).max(0.0) as f32,
mz.mul_add(-mz, sumsq_z / n).max(0.0) as f32,
);
RenderStats { mean, var, n }
}
fn z_scores(a: &RenderStats, b: &RenderStats) -> Vec3 {
let se =
|va: f32, na: f64, vb: f32, nb: f64| (f64::from(va) / na + f64::from(vb) / nb).sqrt();
let sx = se(a.var.x, a.n, b.var.x, b.n).max(1e-20);
let sy = se(a.var.y, a.n, b.var.y, b.n).max(1e-20);
let sz = se(a.var.z, a.n, b.var.z, b.n).max(1e-20);
Vec3::new(
(f64::from(a.mean.x - b.mean.x) / sx) as f32,
(f64::from(a.mean.y - b.mean.y) / sy) as f32,
(f64::from(a.mean.z - b.mean.z) / sz) as f32,
)
}
#[test]
fn exit_splitting_respects_bounce_budget_truncation() {
const SAMPLES: u32 = 6_000;
let planes = StandardGemCuts::standard_round_brilliant();
let plane_soa = build_plane_soa(&planes);
let ray = Ray {
origin: Vec3::new(0.0, 2.5, 0.0),
dir: Vec3::new(0.18, -1.0, 0.07).normalize(),
};
let env = LightingPreset::RingLights.studio(1.0, 0.85, 0.95);
let material = GemMaterial::by_name("Synthetic Moissanite")
.expect("Synthetic Moissanite must be a built-in material");
for max_bounces in [2u32, 4, 6, 8, 12] {
let mut sum_on = Vec3::ZERO;
let mut sum_off = Vec3::ZERO;
for i in 0..SAMPLES {
let seed_on = 10_000 + i;
let hero_rand_on = (hash_u32(seed_on) as f32) / 4_294_967_295.0;
sum_on += trace_spectral_ray_inner(
ray,
&planes,
&plane_soa,
&[],
&material,
max_bounces,
env,
seed_on,
hero_rand_on,
None,
true,
true,
false,
None,
);
let seed_off = 90_000 + i;
let hero_rand_off = (hash_u32(seed_off) as f32) / 4_294_967_295.0;
sum_off += trace_spectral_ray_inner(
ray,
&planes,
&plane_soa,
&[],
&material,
max_bounces,
env,
seed_off,
hero_rand_off,
None,
true,
false,
false,
None,
);
}
let mean_on = sum_on / SAMPLES as f32;
let mean_off = sum_off / SAMPLES as f32;
let ratio_x = mean_on.x / mean_off.x;
println!(
"max_bounces={max_bounces}: mean_on={mean_on:?} mean_off={mean_off:?} ratio_x={ratio_x:.4}"
);
assert!(
(0.97..1.03).contains(&ratio_x),
"max_bounces={max_bounces}: splitting-on/off diverged sharply \
(ratio_x={ratio_x:.4}) -- likely a bounce-budget truncation \
regression (mean_on={mean_on:?}, mean_off={mean_off:?})"
);
}
}
#[test]
fn exit_splitting_is_unbiased_and_reduces_variance() {
const SAMPLES: u32 = 16_384;
let planes = StandardGemCuts::standard_round_brilliant();
let plane_soa = build_plane_soa(&planes);
let ray = Ray {
origin: Vec3::new(0.0, 2.5, 0.0),
dir: Vec3::new(0.18, -1.0, 0.07).normalize(),
};
let env = LightingPreset::RingLights.studio(1.0, 0.85, 0.95);
for name in ["Zircon", "Rutile", "Synthetic Moissanite"] {
let material = GemMaterial::by_name(name)
.unwrap_or_else(|| panic!("{name} must be a built-in material"));
let on = render_stats(
&material, ray, &planes, &plane_soa, env, SAMPLES, 10_000, true,
);
let off = render_stats(
&material, ray, &planes, &plane_soa, env, SAMPLES, 90_000, false,
);
let z = z_scores(&on, &off);
let var_ratio = Vec3::new(
f64::from(off.var.x / on.var.x.max(1e-12)) as f32,
f64::from(off.var.y / on.var.y.max(1e-12)) as f32,
f64::from(off.var.z / on.var.z.max(1e-12)) as f32,
);
println!(
"P6 unbiasedness/variance -- {name}: mean_on={:?} mean_off={:?} z={z:?} \
var_on={:?} var_off={:?} var_ratio(off/on)={var_ratio:?}",
on.mean, off.mean, on.var, off.var
);
assert!(
z.x.abs() < 3.5 && z.y.abs() < 3.5 && z.z.abs() < 3.5,
"{name}: exit-event splitting changed the estimator's expectation \
(z={z:?}, mean_on={:?}, mean_off={:?}) -- should be unbiased",
on.mean,
off.mean
);
}
}
}
#[cfg(test)]
mod nee_tests {
use super::*;
use crate::geometry::cuts::StandardGemCuts;
#[test]
fn nee_on_and_off_converge_to_the_same_mean_on_a_uniform_hdr_map() {
const L0: f32 = 2.0;
const SAMPLES: u32 = 20_000;
const TOLERANCE: f32 = 0.05;
let planes = StandardGemCuts::standard_round_brilliant();
let plane_soa = build_plane_soa(&planes);
let material = GemMaterial::new_custom("G7 furnace probe", 1.5, 0.0, 0.0, [0.0, 0.0, 0.0])
.with_scattering(1.0, 0.2);
let env_map = crate::renderer::env_map::EnvironmentMap::uniform(4, 2, [L0, L0, L0]);
let environment = EnvironmentSource::HdrMap(&env_map);
let ray = Ray {
origin: Vec3::new(0.0, 2.5, 0.0),
dir: Vec3::new(0.1, -1.0, 0.05).normalize(),
};
let mut sum_on = Vec3::ZERO;
let mut sum_off = Vec3::ZERO;
for i in 0..SAMPLES {
let seed = 42_000 + i;
let hero_rand = (hash_u32(seed) as f32) / 4_294_967_295.0;
sum_on += trace_spectral_ray_inner(
ray,
&planes,
&plane_soa,
&[],
&material,
12,
environment,
seed,
hero_rand,
None,
true,
true,
true,
None,
);
sum_off += trace_spectral_ray_inner(
ray,
&planes,
&plane_soa,
&[],
&material,
12,
environment,
seed,
hero_rand,
None,
true,
true,
false,
None,
);
}
let mean_on = sum_on / SAMPLES as f32;
let mean_off = sum_off / SAMPLES as f32;
let rel_err = |v: f32, t: f32| (v - t).abs() / t.abs().max(1e-6);
let (ex, ey, ez) = (
rel_err(mean_on.x, mean_off.x),
rel_err(mean_on.y, mean_off.y),
rel_err(mean_on.z, mean_off.z),
);
println!(
"[G7 NEE furnace] mean_on={mean_on:?} mean_off={mean_off:?} \
rel_err=({ex:.4}, {ey:.4}, {ez:.4})"
);
assert!(
ex <= TOLERANCE && ey <= TOLERANCE && ez <= TOLERANCE,
"NEE-on and NEE-off should converge to the same mean radiance on a uniform \
HDR map (mean_on={mean_on:?}, mean_off={mean_off:?}, rel_err=({ex}, {ey}, \
{ez}), tolerance={TOLERANCE})"
);
}
#[test]
fn frosted_girdle_nee_on_and_off_converge_to_the_same_mean_on_a_uniform_hdr_map() {
use super::super::camera::Camera;
const L0: f32 = 2.0;
const SAMPLES_PER_PIXEL: u32 = 48;
const GRID: usize = 10;
const TOLERANCE: f32 = 0.06;
let planes = StandardGemCuts::standard_round_brilliant();
let plane_soa = build_plane_soa(&planes);
let finishes = crate::geometry::girdle_facet_finishes(&planes);
let material =
GemMaterial::new_custom("G8 frosted furnace probe", 1.5, 0.0, 0.0, [0.0, 0.0, 0.0]);
let env_map = crate::renderer::env_map::EnvironmentMap::uniform(4, 2, [L0, L0, L0]);
let environment = EnvironmentSource::HdrMap(&env_map);
let camera = Camera::new(0.35, 0.28, 5.0, 18.0);
let mut sum_on = Vec3::ZERO;
let mut sum_off = Vec3::ZERO;
let mut count = 0u32;
for iy in 0..GRID {
for ix in 0..GRID {
let ray =
camera.generate_ray(ix as f32, iy as f32, GRID as f32, GRID as f32, 0.5, 0.5);
for s in 0..SAMPLES_PER_PIXEL {
let pixel_id = (iy as u32) * (GRID as u32) + (ix as u32);
let seed = hash_u32(pixel_id ^ hash_u32(s ^ 0x6667_8899));
let hero_rand = (hash_u32(seed) as f32) / 4_294_967_295.0;
sum_on += trace_spectral_ray_inner(
ray,
&planes,
&plane_soa,
&finishes,
&material,
12,
environment,
seed,
hero_rand,
None,
true,
true,
true,
None,
);
sum_off += trace_spectral_ray_inner(
ray,
&planes,
&plane_soa,
&finishes,
&material,
12,
environment,
seed,
hero_rand,
None,
true,
true,
false,
None,
);
count += 1;
}
}
}
let mean_on = sum_on / count as f32;
let mean_off = sum_off / count as f32;
let rel_err = |v: f32, t: f32| (v - t).abs() / t.abs().max(1e-6);
let (ex, ey, ez) = (
rel_err(mean_on.x, mean_off.x),
rel_err(mean_on.y, mean_off.y),
rel_err(mean_on.z, mean_off.z),
);
println!(
"[G8 frosted-girdle NEE furnace] mean_on={mean_on:?} mean_off={mean_off:?} \
rel_err=({ex:.4}, {ey:.4}, {ez:.4})"
);
assert!(
ex <= TOLERANCE && ey <= TOLERANCE && ez <= TOLERANCE,
"frosted-girdle NEE-on and NEE-off should converge to the same mean radiance \
on a uniform HDR map (mean_on={mean_on:?}, mean_off={mean_off:?}, \
rel_err=({ex}, {ey}, {ez}), tolerance={TOLERANCE})"
);
}
}
#[cfg(test)]
mod p2_wave_normal_tests {
use super::{super::intersect::intersect_polyhedron, *};
use crate::optics::materials::CrystalSystem;
const HALF_THICKNESS: f32 = 1.0;
fn slab_planes() -> Vec<GpuFacetPlane> {
vec![
GpuFacetPlane::new(Vec3::Y, -HALF_THICKNESS),
GpuFacetPlane::new(Vec3::NEG_Y, -HALF_THICKNESS),
GpuFacetPlane::new(Vec3::X, -1000.0),
GpuFacetPlane::new(Vec3::NEG_X, -1000.0),
GpuFacetPlane::new(Vec3::Z, -1000.0),
GpuFacetPlane::new(Vec3::NEG_Z, -1000.0),
]
}
#[expect(
clippy::too_many_lines,
reason = "the fixed (enabled: false) ExitSplitCtx both \
apply_partial_fresnel_bounce calls require; the search loop body \
itself is unchanged"
)]
fn trace_forced_extraordinary_slab(
material: &GemMaterial,
incident_origin: Vec3,
incident_dir: Vec3,
) -> (Vec3, Vec3, Vec3, Vec3, f32) {
const SEED_SEARCH_LIMIT: u32 = 20_000;
let planes = slab_planes();
let lambdas = [589.3f32; NUM_CHANNELS]; let mat_ctx = build_ray_material_context(material, lambdas, 0, false);
let cache = build_ray_wavelength_cache(&mat_ctx);
let entry_hit = intersect_polyhedron(
Ray {
origin: incident_origin,
dir: incident_dir,
},
&planes,
)
.expect("incident ray must hit the slab's top face");
let entry_point = incident_origin + entry_hit.t * incident_dir;
let plane_soa = build_plane_soa(&planes);
let mut split_radiance = [0.0f32; NUM_CHANNELS];
let mut exit_ctx = ExitSplitCtx {
plane_soa: &plane_soa,
environment: EnvironmentSource::Studio {
preset: crate::optics::raytracer::environment::LightingPreset::RingLights,
exposure: 1.0,
light_yaw: 0.0,
light_pitch: 0.85,
},
studio_rig: None,
split_radiance: &mut split_radiance,
enabled: false,
compat: [u8::MAX; NUM_CHANNELS],
};
for seed in 0..SEED_SEARCH_LIMIT {
let mut stokes = [StokesVector::unpolarized(1.0); NUM_CHANNELS];
let mut path_pdf = [1.0f32; NUM_CHANNELS];
let entry_normal = Vec3::Y;
let geo0 = compute_bounce_refraction_geometry(
&mat_ctx,
&cache,
entry_normal,
incident_dir,
false,
false,
);
if geo0.sin2_t > 1.0 {
continue; }
let entry_plane_normal = incident_dir.cross(entry_normal).normalize_or_zero();
let bctx0 = BounceContext {
ctx: &mat_ctx,
cache: &cache,
geo: &geo0,
};
let mut state0 = BounceState {
stokes: &mut stokes,
path_pdf: &mut path_pdf,
};
let mut exit_event0 = ExitEvent {
exit: &mut exit_ctx,
hit_point: entry_point,
};
let (k1, s1, inside_gem_1, is_extraordinary_update, _) = apply_partial_fresnel_bounce(
&bctx0,
BounceRay {
k_hat: incident_dir,
normal: entry_normal,
},
PathModeState {
current_plane_normal: entry_plane_normal,
inside_gem: false,
is_extraordinary: false,
},
RngDraw {
rng_seed: seed,
bounce: 0,
},
&mut state0,
&mut exit_event0,
);
let entered_extraordinary = is_extraordinary_update == Some(true);
if !inside_gem_1 || !entered_extraordinary {
continue; }
let post_entry_origin = entry_point + s1 * 1e-4;
let exit_ray = Ray {
origin: post_entry_origin,
dir: s1,
};
let Some(exit_hit) = intersect_polyhedron(exit_ray, &planes) else {
continue;
};
let exit_point = post_entry_origin + exit_hit.t * s1;
let exit_normal = -exit_hit.normal;
let geo1 =
compute_bounce_refraction_geometry(&mat_ctx, &cache, exit_normal, k1, true, true);
if geo1.sin2_t > 1.0 {
continue; }
let exit_plane_normal = k1.cross(exit_normal).normalize_or_zero();
let bctx1 = BounceContext {
ctx: &mat_ctx,
cache: &cache,
geo: &geo1,
};
let mut state1 = BounceState {
stokes: &mut stokes,
path_pdf: &mut path_pdf,
};
let mut exit_event1 = ExitEvent {
exit: &mut exit_ctx,
hit_point: exit_point,
};
let (k2, s2, inside_gem_2, _, _) = apply_partial_fresnel_bounce(
&bctx1,
BounceRay {
k_hat: k1,
normal: exit_normal,
},
PathModeState {
current_plane_normal: exit_plane_normal,
inside_gem: true,
is_extraordinary: true,
},
RngDraw {
rng_seed: seed,
bounce: 1,
},
&mut state1,
&mut exit_event1,
);
if inside_gem_2 {
continue; }
let to_exit = exit_point - entry_point;
let along = to_exit.dot(incident_dir);
let perp = to_exit - along * incident_dir;
let lateral_displacement = perp.length();
return (k1, s1, k2, s2, lateral_displacement);
}
panic!(
"no seed in 0..{SEED_SEARCH_LIMIT} entered the extraordinary mode and \
transmitted cleanly through both slab faces -- test premise violated"
);
}
#[test]
fn plane_parallel_uniaxial_slab_extraordinary_ray_exits_parallel_and_displaced() {
let mut material = GemMaterial::by_name("Zircon")
.expect("\"Zircon\" is a built-in uniaxial material in GemMaterial::all_materials()");
assert_eq!(material.crystal_system, CrystalSystem::Tetragonal);
assert!(
material.birefringence_delta.abs() > 0.01,
"test premise: Zircon must be strongly birefringent"
);
material.c_axis = Vec3::new(0.3, 0.8, 0.5).normalize();
let incident_origin = Vec3::new(0.0, 5.0, 0.0);
let incident_dir = Vec3::new(0.3, -1.0, 0.0).normalize();
let (entry_k, entry_s, exit_k, exit_s, lateral_displacement) =
trace_forced_extraordinary_slab(&material, incident_origin, incident_dir);
assert!(
(entry_k - entry_s).length() > 1e-4,
"test premise: the extraordinary mode's walk-off should visibly separate k \
from S at entry (entry_k={entry_k:?}, entry_s={entry_s:?})"
);
let cos_parallel_k = exit_k.dot(incident_dir).clamp(-1.0, 1.0);
let cos_parallel_s = exit_s.dot(incident_dir).clamp(-1.0, 1.0);
assert!(
(1.0 - cos_parallel_k).abs() < 1e-5,
"exit wave normal k must be parallel to the incident ray within 1e-5 \
(exit_k={exit_k:?}, incident_dir={incident_dir:?}, 1-cos={})",
1.0 - cos_parallel_k
);
assert!(
(1.0 - cos_parallel_s).abs() < 1e-5,
"exit Poynting direction S (== k in air) must be parallel to the incident ray \
within 1e-5 (exit_s={exit_s:?}, incident_dir={incident_dir:?}, 1-cos={})",
1.0 - cos_parallel_s
);
assert!(
(exit_k - exit_s).length() < 1e-6,
"S must equal k exactly once back in isotropic air (exit_k={exit_k:?}, \
exit_s={exit_s:?})"
);
assert!(
lateral_displacement > 1e-4,
"the exit point must be laterally displaced from the straight-through path \
by a nonzero amount (the walk-off's real, physical effect) -- got {lateral_displacement}"
);
}
}