use super::super::{
NUM_CHANNELS,
camera::{FacetFinish, Ray},
environment::{environment_nee_pdf, sample_environment_channels},
refraction::{
BounceContext, BounceRay, BounceRefractionGeometry, BounceState, ExitEvent, ExitSplitCtx,
PathModeState, RayMaterialContext, RayWavelengthCache, RngDraw,
apply_partial_fresnel_bounce, apply_tir_bounce, entry_eigenmode_selection,
poynting_dir_for_mode,
},
sampling::{MODE_COUPLING_STREAM, RUSSIAN_ROULETTE_STREAM, hash_u32},
scattering::{NeeContext, apply_frosted_bounce, balance_heuristic},
};
use crate::optics::polarization::StokesVector;
use glam::Vec3;
pub(super) fn accumulate_miss_radiance(
exit: &ExitSplitCtx<'_>,
ray_dir: Vec3,
lambdas: &[f32; NUM_CHANNELS],
stokes: &[StokesVector; NUM_CHANNELS],
mis_weight: f32,
radiance: &mut [f32; NUM_CHANNELS],
) {
let env_spectral = sample_environment_channels(
exit.environment,
ray_dir,
lambdas,
exit.studio_rig.as_ref(),
exit.observer,
);
for k in 0..NUM_CHANNELS {
radiance[k] = (stokes[k].intensity() * mis_weight).mul_add(env_spectral[k], 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 the HDR-map NEE inputs/output, used only by \
the frosted branch, mirroring try_scatter_step's identical addition for \
the Henyey-Greenstein path; `incoming_light_mis` is the light-sample MIS \
carry, \
threaded through so the partial-Fresnel arm's transmit-out case can return \
it unchanged rather than silently dropping it"
)]
pub(super) 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],
incoming_light_mis: Option<(f32, Vec3)>,
) -> Option<(f32, Vec3)> {
let (new_k, new_s, new_inside_gem, is_extraordinary_update, exact_p_o, bounce_light_mis) =
if finish == FacetFinish::Frosted {
let (new_dir, new_inside_gem, is_extraordinary_update, frosted_light_mis) =
apply_frosted_bounce(
ctx,
geo,
normal,
hit_point,
*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.map(|phase_pdf| (phase_pdf, new_dir)),
)
} 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 };
dispatch_polished_bounce(
&bctx,
ray,
mode_state,
rng,
&mut state,
&mut exit_event,
(nee, incoming_light_mis),
)
};
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,
);
bounce_light_mis
}
type PolishedBounceResult = (
Vec3,
Vec3,
bool,
Option<bool>,
Option<f32>,
Option<(f32, Vec3)>,
);
fn dispatch_polished_bounce(
bctx: &BounceContext<'_, '_>,
ray: BounceRay,
mode_state: PathModeState,
rng: RngDraw,
state: &mut BounceState<'_>,
exit_event: &mut ExitEvent<'_, '_>,
nee_and_carry: (NeeContext<'_>, Option<(f32, Vec3)>),
) -> PolishedBounceResult {
let (nee, incoming_light_mis) = nee_and_carry;
let pre_bounce_inside_gem = mode_state.inside_gem;
exit_event.exit.split_mis_weight = if pre_bounce_inside_gem {
incoming_light_mis.map_or(1.0, |(phase_pdf, interior_dir)| {
balance_heuristic(
phase_pdf,
environment_nee_pdf(nee.environment, interior_dir),
)
})
} else {
1.0
};
let (k_prime, s_prime, new_inside_gem, is_extraordinary_update, exact_p_o) =
apply_partial_fresnel_bounce(bctx, ray, mode_state, rng, state, exit_event);
exit_event.exit.split_mis_weight = 1.0;
let transmit_out_of_gem =
pre_bounce_inside_gem && !new_inside_gem && is_extraordinary_update.is_none();
let bounce_light_mis = if transmit_out_of_gem {
incoming_light_mis
} else {
None
};
(
k_prime,
s_prime,
new_inside_gem,
is_extraordinary_update,
exact_p_o,
bounce_light_mis,
)
}
pub(in super::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"
)]
pub(super) 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,
);
}
}