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::shading_normal_near_edge,
intersect_stone::intersect_stone_soa,
refraction::{
ExitSplitCtx, RayMaterialContext, RayWavelengthCache, build_ray_wavelength_cache,
compute_bounce_refraction_geometry,
},
sampling::{
FLUORESCENCE_DIR_U_STREAM, FLUORESCENCE_DIR_V_STREAM, FLUORESCENCE_DISTANCE_STREAM,
FLUORESCENCE_EMITTER_STREAM, FLUORESCENCE_EXCITATION_STREAM, hash_u32,
},
scattering::{NeeContext, ScatterStepOutcome, balance_heuristic, try_scatter_step},
};
use crate::{
geometry::{
plane::GpuFacetPlane,
tool::{StoneGeometry, ToolPrimitive},
},
optics::{
fluorescence::{Fluorescence, sample_vertex},
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 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,
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"
)]
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 {
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),
)
}
#[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,
}
#[derive(Clone, Copy)]
struct TraceSwitches {
internal_mode_coupling: bool,
exit_splitting: bool,
nee: bool,
}
const FLUORESCENCE_SAMPLING_CAP: f32 = 2.0;
fn unit_draw(rng_seed: u32, bounce: u32, stream: u32) -> f32 {
(hash_u32(rng_seed ^ hash_u32(bounce ^ stream)) as f32) / 4_294_967_295.0
}
fn depolarize_and_scale(stokes: &mut [StokesVector; NUM_CHANNELS], weight: f32) {
for s in &mut *stokes {
*s = StokesVector::unpolarized(s.intensity() * weight);
}
}
fn scale_stokes(stokes: &mut [StokesVector; NUM_CHANNELS], weight: f32) {
for s in &mut *stokes {
*s = s.scale(weight);
}
}
fn apply_surface_glare(stokes: &mut [StokesVector; NUM_CHANNELS], surface_glare: f32) {
for s in &mut *stokes {
*s = s.scale(surface_glare);
}
}
#[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_core(
scene: &TraceScene<'_>,
initial_ray: Ray,
rng_seed: u32,
hero_rand: f32,
switches: TraceSwitches,
outputs: (
Option<&mut Option<HitRecord>>,
Option<&mut (u32, PathTermination)>,
),
) -> Vec3 {
let TraceScene {
planes,
tools,
plane_soa,
facet_finishes,
material,
fluorescence,
max_bounces,
environment,
surface_glare,
} = *scene;
let TraceSwitches {
internal_mode_coupling: enable_internal_mode_coupling,
exit_splitting: enable_exit_splitting,
nee: enable_nee,
} = switches;
let (mut primary_hit_out, mut termination_out) = outputs;
let surface_glare = surface_glare.clamp(0.0, 1.0);
let fluorescent = !fluorescence.is_empty();
let enable_exit_splitting = enable_exit_splitting && !fluorescent;
let (camera_wavelength, camera_weight) = if fluorescent {
fluorescence.camera_wavelength(hero_rand)
} else {
(0.0, 1.0)
};
let mut lambdas: [f32; NUM_CHANNELS] = if fluorescent {
[camera_wavelength; NUM_CHANNELS]
} else {
wrapped_hero_wavelengths(hero_rand)
};
let camera_lambdas = lambdas;
debug_assert!(
!fluorescent
|| material.absorption_unit == crate::optics::materials::AbsorptionUnit::PerMm
|| material.absorption_path_scale.to_bits() == 1.0_f32.to_bits(),
"fluorescent emitters need a PerMm material (or an unscaled bare one)"
);
let fluorescence_rate = if fluorescent {
fluorescence.pseudo_extinction(camera_lambdas[0])
} else {
0.0
};
let mut fluorescence_armed = fluorescence_rate > 0.0;
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 (mut mat_ctx, mut 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,
tools,
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,
tools,
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_stone_soa(current_ray, plane_soa, planes.len(), tools);
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;
};
#[cfg(debug_assertions)]
debug_assert_inside_gem_matches_geometry(
StoneGeometry { planes, tools },
current_ray,
hit_rec.t,
inside_gem,
);
let mut segment_t = hit_rec.t;
let mut vertex_t = None;
let mut sampling_rate = 0.0f32;
let path_scale = material.absorption_path_scale;
if fluorescence_armed && inside_gem && hit_rec.t > 0.0 {
let segment_scaled = hit_rec.t * path_scale;
sampling_rate = fluorescence_rate.min(FLUORESCENCE_SAMPLING_CAP / segment_scaled);
let u = unit_draw(rng_seed, bounce, FLUORESCENCE_DISTANCE_STREAM);
let t_scaled = -((1.0 - u).max(1e-7).ln()) / sampling_rate;
if t_scaled < segment_scaled {
segment_t = t_scaled / path_scale;
vertex_t = Some(t_scaled);
}
}
let origin_before = current_ray.origin;
if inside_gem {
match try_scatter_step(
&mat_ctx,
&wavelength_cache,
material,
is_extraordinary,
&mut current_ray,
&mut current_k,
segment_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 => {
if let Some(t_scaled) = vertex_t {
if material.scattering_sigma_s <= 0.0 {
apply_absorption(
&mat_ctx,
&wavelength_cache,
current_k,
is_extraordinary,
segment_t,
&mut stokes,
);
}
let Some(vertex) = sample_vertex(
fluorescence,
camera_lambdas[0],
unit_draw(rng_seed, bounce, FLUORESCENCE_EMITTER_STREAM),
unit_draw(rng_seed, bounce, FLUORESCENCE_EXCITATION_STREAM),
) else {
record_termination(
&mut termination_out,
bounce,
PathTermination::RussianRoulette,
);
break;
};
let weight = (fluorescence_rate / sampling_rate)
* (sampling_rate * t_scaled).exp()
* vertex.energy_ratio;
depolarize_and_scale(&mut stokes, weight);
let z = (-2.0f32)
.mul_add(unit_draw(rng_seed, bounce, FLUORESCENCE_DIR_U_STREAM), 1.0);
let phi = std::f32::consts::TAU
* unit_draw(rng_seed, bounce, FLUORESCENCE_DIR_V_STREAM);
#[expect(
clippy::suboptimal_flops,
reason = "transport results are pinned bit for bit (goldens, twins); a fused `1 - z*z` rounds once and moves the last bit"
)]
let r = (1.0 - z * z).max(0.0).sqrt();
let new_dir = Vec3::new(r * phi.cos(), r * phi.sin(), z);
current_ray.origin += segment_t * current_ray.dir;
current_ray.dir = new_dir;
current_k = new_dir;
prev_plane_normal = None;
lambdas = [vertex.lambda_ex; NUM_CHANNELS];
(mat_ctx, wavelength_cache) = build_ray_context(
material,
lambdas,
hero_idx,
enable_internal_mode_coupling,
);
fluorescence_armed = false;
continue;
}
if sampling_rate > 0.0 {
let g = (sampling_rate * hit_rec.t * path_scale).exp();
scale_stokes(&mut stokes, g);
}
}
ScatterStepOutcome::ScatteredAndSurvived(phase_pdf_for_mis) => {
if sampling_rate > 0.0 {
let s = (current_ray.origin - origin_before).length() * path_scale;
scale_stokes(&mut stokes, (sampling_rate * s).exp());
}
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_for_hit(
StoneGeometry { planes, tools },
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 == 0 && !inside_gem && surface_glare < 1.0 && finish != FacetFinish::Frosted {
apply_surface_glare(&mut stokes, surface_glare);
}
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, &camera_lambdas, &path_pdf, hero_idx, compat)
} else {
integrate_channels_to_xyz(&radiance, &camera_lambdas, &path_pdf, hero_idx)
};
let xyz = xyz + nee_xyz;
let xyz = if fluorescent {
xyz * camera_weight
} else {
xyz
};
match environment {
EnvironmentSource::Studio { .. } => {
apply_von_kries_white_balance(xyz, environment_white_balance(environment))
}
EnvironmentSource::HdrMap(_) => xyz,
}
.max(Vec3::ZERO)
}