use super::{
PathTermination,
bounce::{accumulate_miss_radiance, apply_russian_roulette, dispatch_bounce},
wrapped_hero_wavelengths,
};
use super::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, fill_backdrop,
},
intersect::{intersect_polyhedron_soa, shading_normal_near_edge},
refraction::{
ExitSplitCtx, RayMaterialContext, RayWavelengthCache, build_ray_wavelength_cache,
compute_bounce_refraction_geometry,
},
scattering::{NeeContext, ScatterStepOutcome, balance_heuristic, try_scatter_step},
};
use crate::{
geometry::plane::GpuFacetPlane,
optics::{
materials::{CrystalSystem, GemMaterial},
polarization::StokesVector,
},
};
use glam::Vec3;
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);
}
}
pub(super) 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"
)]
pub(super) 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 mut nee_xyz = Vec3::ZERO;
let hero_idx: usize = 0;
let mut path_pdf = [1.0f32; NUM_CHANNELS];
let mut current_ray = initial_ray;
let observer = -initial_ray.dir;
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,
observer,
split_radiance: &mut split_radiance,
enabled: enable_exit_splitting,
compat: [u8::MAX; NUM_CHANNELS],
split_mis_weight: 1.0,
};
let mut path_escaped = false;
let nee_ctx = NeeContext {
environment,
plane_soa,
enabled: enable_nee,
};
let mut pending_light_mis: Option<(f32, Vec3)> = 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 {
if bounce == 0 && fill_backdrop(environment, &lambdas, &mut radiance) {
path_escaped = true;
record_termination(&mut termination_out, bounce, PathTermination::Escaped);
break;
}
let mis_weight =
phase_pdf_for_mis_this_check.map_or(1.0, |(phase_pdf, interior_dir)| {
let light_pdf = environment_nee_pdf(environment, interior_dir);
balance_heuristic(phase_pdf, light_pdf)
});
accumulate_miss_radiance(
&exit_split_ctx,
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 nee_xyz,
enable_exit_splitting,
exit_split_ctx.compat,
facet_finishes,
) {
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,
phase_pdf_for_mis_this_check,
);
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)
};
let xyz = xyz + nee_xyz;
match environment {
EnvironmentSource::Studio { .. } => {
apply_von_kries_white_balance(xyz, environment_white_balance(environment))
}
EnvironmentSource::HdrMap(_) => xyz,
}
.max(Vec3::ZERO)
}