use super::{PathTermination, path_events::PathEvents};
use super::super::{
NUM_CHANNELS,
absorption::apply_segment_absorption,
camera::{FacetFinish, HitRecord, Ray},
environment::EnvironmentSource,
intersect::shading_normal_near_edge,
refraction::{RayMaterialContext, RayWavelengthCache, build_ray_wavelength_cache},
};
use crate::{
geometry::{
plane::GpuFacetPlane,
tool::{StoneGeometry, ToolPrimitive},
},
optics::{
fluorescence::Fluorescence,
materials::{CrystalSystem, GemMaterial},
polarization::StokesVector,
studio_rig::StudioRig,
},
};
use glam::Vec3;
mod bounce_body;
use bounce_body::trace_spectral_ray_core;
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 shading_normal_for_hit(
geom: StoneGeometry<'_>,
hit_point: Vec3,
hit_facet_idx: usize,
hit_normal: Vec3,
rounding_radius: f32,
) -> Vec3 {
if geom.is_convex() || rounding_radius <= 0.0 {
return shading_normal_near_edge(
geom.planes,
hit_point,
hit_facet_idx,
hit_normal,
rounding_radius,
);
}
if hit_facet_idx >= geom.planes.len() {
return hit_normal;
}
let (u, v) = crate::optics::raytracer::scattering::frosted_orthonormal_basis(hit_normal);
let near_tool = [u, -u, v, -v].into_iter().any(|dir| {
let probe = hit_point + dir * rounding_radius;
geom.tools.iter().any(|tool| tool.contains(probe))
});
if near_tool {
return hit_normal;
}
shading_normal_near_edge(
geom.planes,
hit_point,
hit_facet_idx,
hit_normal,
rounding_radius,
)
}
#[cfg(debug_assertions)]
fn point_in_material(geom: StoneGeometry<'_>, p: Vec3) -> bool {
geom.planes
.iter()
.all(|pl| Vec3::from_array(pl.normal).dot(p) + pl.d <= 0.0)
&& !geom.tools.iter().any(|tool| tool.contains(p))
}
#[cfg(debug_assertions)]
fn debug_assert_inside_gem_matches_geometry(
geom: StoneGeometry<'_>,
ray: Ray,
t: f32,
inside_gem: bool,
) {
if geom.is_convex() || t < 1e-3 {
return;
}
let a = point_in_material(geom, ray.origin + ray.dir * (t - 1e-4));
let b = point_in_material(geom, ray.origin + ray.dir * (t - 2e-4));
debug_assert!(
a != b || a == inside_gem,
"inside_gem = {inside_gem} disagrees with the point classification {a} at t = {t}"
);
}
fn apply_interior_segment(
ray_ctx: (&RayMaterialContext, &RayWavelengthCache),
k_hat: Vec3,
is_extraordinary: bool,
segment: (Ray, 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_segment_absorption(ctx, cache, k_hat, is_extraordinary, segment, 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"
)]
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,
primary_hit_out: Option<&mut Option<HitRecord>>,
enable_internal_mode_coupling: bool,
enable_exit_splitting: bool,
enable_nee: bool,
termination_out: Option<&mut (u32, PathTermination)>,
) -> Vec3 {
trace_spectral_ray_inner_geom(
initial_ray,
planes,
&[],
plane_soa,
facet_finishes,
material,
Fluorescence::none(),
max_bounces,
environment,
rng_seed,
hero_rand,
primary_hit_out,
enable_internal_mode_coupling,
enable_exit_splitting,
enable_nee,
termination_out,
)
}
#[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"
)]
pub(super) fn trace_spectral_ray_inner_geom(
initial_ray: Ray,
planes: &[GpuFacetPlane],
tools: &[ToolPrimitive],
plane_soa: &crate::simd::PlanesSoA32,
facet_finishes: &[FacetFinish],
material: &GemMaterial,
fluorescence: &Fluorescence,
max_bounces: u32,
environment: EnvironmentSource<'_>,
rng_seed: u32,
hero_rand: f32,
primary_hit_out: Option<&mut Option<HitRecord>>,
enable_internal_mode_coupling: bool,
enable_exit_splitting: bool,
enable_nee: bool,
termination_out: Option<&mut (u32, PathTermination)>,
) -> Vec3 {
trace_spectral_ray_inner_geom_events(
initial_ray,
planes,
tools,
plane_soa,
facet_finishes,
material,
fluorescence,
max_bounces,
environment,
rng_seed,
hero_rand,
primary_hit_out,
enable_internal_mode_coupling,
enable_exit_splitting,
enable_nee,
(termination_out, None),
)
}
#[expect(
clippy::too_many_arguments,
reason = "see trace_spectral_ray_inner_geom's own reason, plus the attribution recorder"
)]
pub(super) fn trace_spectral_ray_inner_geom_events(
initial_ray: Ray,
planes: &[GpuFacetPlane],
tools: &[ToolPrimitive],
plane_soa: &crate::simd::PlanesSoA32,
facet_finishes: &[FacetFinish],
material: &GemMaterial,
fluorescence: &Fluorescence,
max_bounces: u32,
environment: EnvironmentSource<'_>,
rng_seed: u32,
hero_rand: f32,
primary_hit_out: Option<&mut Option<HitRecord>>,
enable_internal_mode_coupling: bool,
enable_exit_splitting: bool,
enable_nee: bool,
outputs: (Option<&mut (u32, PathTermination)>, Option<&mut PathEvents>),
) -> Vec3 {
let (termination_out, events) = outputs;
let scene = TraceScene {
planes,
tools,
plane_soa,
facet_finishes,
material,
fluorescence,
max_bounces,
environment,
surface_glare: environment.surface_glare(),
};
let switches = TraceSwitches {
internal_mode_coupling: enable_internal_mode_coupling,
exit_splitting: enable_exit_splitting,
nee: enable_nee,
};
trace_spectral_ray_core(
&scene,
initial_ray,
rng_seed,
hero_rand,
switches,
(primary_hit_out, termination_out, events),
)
}
#[derive(Clone, Copy)]
struct TraceScene<'a> {
planes: &'a [GpuFacetPlane],
tools: &'a [ToolPrimitive],
plane_soa: &'a crate::simd::PlanesSoA32,
facet_finishes: &'a [FacetFinish],
material: &'a GemMaterial,
fluorescence: &'a Fluorescence,
max_bounces: u32,
environment: EnvironmentSource<'a>,
surface_glare: f32,
}
thread_local! {
static STUDIO_RIG_CACHE: std::cell::Cell<Option<(u32, u32, StudioRig)>> =
const { std::cell::Cell::new(None) };
}
fn cached_studio_rig(light_yaw: f32, light_pitch: f32) -> StudioRig {
let key = (light_yaw.to_bits(), light_pitch.to_bits());
if let Some((yaw_bits, pitch_bits, rig)) = STUDIO_RIG_CACHE.get()
&& (yaw_bits, pitch_bits) == key
{
return rig;
}
let rig = StudioRig::new(light_yaw, light_pitch);
STUDIO_RIG_CACHE.set(Some((key.0, key.1, rig)));
rig
}
#[derive(Clone, Copy)]
struct TraceSwitches {
internal_mode_coupling: bool,
exit_splitting: bool,
nee: bool,
}