use super::{
super::{
NUM_CHANNELS,
absorption::channel_absorption_alphas_assigned,
camera::{FacetFinish, Ray},
environment::{EnvironmentSource, sample_environment_for_nee},
intersect_stone::intersect_stone_soa,
refraction::{RayMaterialContext, RayWavelengthCache},
sampling::{NEE_ENV_DIR_U_STREAM, NEE_ENV_DIR_V_STREAM, hash_u32},
},
HG_NEE_MAX_CROSSINGS, NeeContext, balance_heuristic,
hg::henyey_greenstein_phase,
};
use crate::optics::{polarization::StokesVector, raytracer::camera::HitRecord};
use glam::Vec3;
const SHADOW_PROBE_OFFSET: f32 = 1e-4;
#[derive(Clone, Copy)]
pub(crate) enum ShadowAlphas<'a> {
Fixed(&'a [f32; NUM_CHANNELS]),
AssignedMode {
ctx: &'a RayMaterialContext<'a>,
cache: &'a RayWavelengthCache,
is_extraordinary: bool,
},
}
impl ShadowAlphas<'_> {
pub(super) fn at(self, dir: Vec3) -> [f32; NUM_CHANNELS] {
match self {
Self::Fixed(alphas) => *alphas,
Self::AssignedMode {
ctx,
cache,
is_extraordinary,
} => channel_absorption_alphas_assigned(ctx, cache, dir, is_extraordinary),
}
}
}
#[expect(
clippy::too_many_arguments,
reason = "mirrors try_scatter_step's own argument shape -- context, this bounce's own \
geometric/RNG state, and the per-channel accumulators an NEE contribution \
reads/writes; see that function's identical justification"
)]
pub(crate) fn nee_contribution_hg_scatter(
nee: NeeContext<'_>,
lambdas: &[f32; NUM_CHANNELS],
n_inside_hero: f32,
scatter_point: Vec3,
scatter_dir_in: Vec3,
g: f32,
rng_seed: u32,
bounce: u32,
stokes: &[StokesVector; NUM_CHANNELS],
radiance: &mut [f32; NUM_CHANNELS],
shadow_alphas: ShadowAlphas<'_>,
sigma_s: f32,
absorption_path_scale: f32,
facet_finishes: &[FacetFinish],
) {
if !nee.enabled || !matches!(nee.environment, EnvironmentSource::HdrMap(_)) {
return;
}
let u0 =
(hash_u32(rng_seed ^ hash_u32(bounce ^ NEE_ENV_DIR_U_STREAM)) as f32) / 4_294_967_295.0;
let u1 =
(hash_u32(rng_seed ^ hash_u32(bounce ^ NEE_ENV_DIR_V_STREAM)) as f32) / 4_294_967_295.0;
let Some(sample) = sample_environment_for_nee(nee.environment, u0, u1) else {
return;
};
let probe = Ray {
origin: scatter_point + sample.dir * SHADOW_PROBE_OFFSET,
dir: sample.dir,
};
let Some(first) = intersect_stone_soa(probe, nee.plane_soa, nee.plane_soa.len(), nee.tools)
else {
return;
};
let Some(exit) = (if nee.tools.is_empty() {
single_exit(first, sample.dir, n_inside_hero, facet_finishes)
} else {
walk_exits(nee, probe, first, n_inside_hero, facet_finishes)
}) else {
return;
};
let ExitProbe {
normal: exit_normal,
t_material: hit_t,
cos_i,
cos_t,
t_unpol,
} = exit;
let phase_cos = sample.dir.dot(scatter_dir_in);
let phase_val = henyey_greenstein_phase(phase_cos, g);
let mis_weight = balance_heuristic(sample.pdf, phase_val);
if mis_weight <= 0.0 {
return;
}
let refracted_dir = (n_inside_hero * sample.dir
- n_inside_hero.mul_add(cos_i, -cos_t) * exit_normal)
.normalize_or_zero();
let EnvironmentSource::HdrMap(env_map) = nee.environment else {
return;
};
let env_rgb = env_map.radiance_rgb(refracted_dir);
let alphas = shadow_alphas.at(sample.dir);
let hit_t_scaled = hit_t * absorption_path_scale;
let sigma_s_abs = sigma_s / absorption_path_scale;
let mut trans_args = [0f32; NUM_CHANNELS];
for k in 0..NUM_CHANNELS {
trans_args[k] = -(alphas[k] + sigma_s_abs) * hit_t_scaled;
}
let transmittance = crate::simd::exp_f32x8(trans_args);
let common = t_unpol * phase_val * mis_weight / sample.pdf;
for k in 0..NUM_CHANNELS {
let env_k = crate::renderer::env_map::rgb_to_spectral_radiance(env_rgb, lambdas[k]);
radiance[k] =
(stokes[k].intensity() * transmittance[k] * common * env_k).mul_add(1.0, radiance[k]);
}
}
struct ExitProbe {
normal: Vec3,
t_material: f32,
cos_i: f32,
cos_t: f32,
t_unpol: f32,
}
fn exit_transmittance(n_inside: f32, cos_i: f32) -> Option<(f32, f32)> {
let sin2_t = (n_inside * n_inside * cos_i.mul_add(-cos_i, 1.0)).min(1.0);
if sin2_t >= 1.0 {
return None;
}
let cos_t = (1.0 - sin2_t).max(0.0).sqrt();
let r_s = n_inside.mul_add(cos_i, -cos_t) / n_inside.mul_add(cos_i, cos_t);
let r_p = n_inside.mul_add(-cos_t, cos_i) / n_inside.mul_add(cos_t, cos_i);
let r_unpol = (0.5 * r_p.mul_add(r_p, r_s * r_s)).clamp(0.0, 1.0);
Some((1.0 - r_unpol, cos_t))
}
fn hit_is_frosted(facet_finishes: &[FacetFinish], hit: HitRecord) -> bool {
facet_finishes
.get(hit.facet_idx)
.copied()
.unwrap_or_default()
== FacetFinish::Frosted
}
fn single_exit(
hit: HitRecord,
dir: Vec3,
n_inside: f32,
facet_finishes: &[FacetFinish],
) -> Option<ExitProbe> {
if hit_is_frosted(facet_finishes, hit) {
return None;
}
let cos_i = dir.dot(hit.normal).clamp(0.0, 1.0);
let (t_unpol, cos_t) = exit_transmittance(n_inside, cos_i)?;
Some(ExitProbe {
normal: hit.normal,
t_material: hit.t + SHADOW_PROBE_OFFSET,
cos_i,
cos_t,
t_unpol,
})
}
fn walk_exits(
nee: NeeContext<'_>,
probe: Ray,
first: HitRecord,
n_inside: f32,
facet_finishes: &[FacetFinish],
) -> Option<ExitProbe> {
let dir = probe.dir;
let mut ray = probe;
let mut hit = first;
let mut crossings = 1;
let mut chain = 1.0f32;
let mut material_len = 0.0f32;
let mut last: Option<(Vec3, f32, f32)>;
loop {
if hit_is_frosted(facet_finishes, hit) {
return None;
}
let cos = dir.dot(hit.normal);
if cos > 0.0 {
material_len += hit.t + SHADOW_PROBE_OFFSET;
let cos_i = cos.clamp(0.0, 1.0);
let (t, cos_t) = exit_transmittance(n_inside, cos_i)?;
chain *= t;
last = Some((hit.normal, cos_i, cos_t));
} else {
let (t, _) = exit_transmittance(n_inside, (-cos).clamp(0.0, 1.0))?;
chain *= t;
last = None;
}
ray = Ray {
origin: ray.origin + hit.t * dir + dir * SHADOW_PROBE_OFFSET,
dir,
};
let Some(next) = intersect_stone_soa(ray, nee.plane_soa, nee.plane_soa.len(), nee.tools)
else {
break;
};
crossings += 1;
if crossings > HG_NEE_MAX_CROSSINGS {
return None;
}
hit = next;
}
let (normal, cos_i, cos_t) = last?;
Some(ExitProbe {
normal,
t_material: material_len,
cos_i,
cos_t,
t_unpol: chain,
})
}
#[cfg(test)]
mod walk_tests {
use super::*;
use crate::{
geometry::{plane::GpuFacetPlane, tool::ToolPrimitive},
optics::raytracer::{build_plane_soa, environment::LightingPreset},
};
#[test]
fn through_groove_probe_matches_analytic_segments_and_interfaces() {
let planes: Vec<GpuFacetPlane> = [
Vec3::X,
Vec3::NEG_X,
Vec3::Y,
Vec3::NEG_Y,
Vec3::Z,
Vec3::NEG_Z,
]
.into_iter()
.map(|n| GpuFacetPlane::new(n, -0.5))
.collect();
let soa = build_plane_soa(&planes);
let tools = [ToolPrimitive::cylinder(Vec3::ZERO, Vec3::Z, 0.1, 2.0)];
let env = LightingPreset::Daylight.studio(1.0, 0.4, 0.35);
let nee = NeeContext {
environment: env,
plane_soa: &soa,
tools: &tools,
enabled: true,
};
let n = 1.76f32;
let start = Vec3::new(-0.4, 0.05, 0.0);
let probe = Ray {
origin: start + Vec3::X * SHADOW_PROBE_OFFSET,
dir: Vec3::X,
};
let first = intersect_stone_soa(probe, &soa, planes.len(), &tools).expect("groove wall");
let exit = walk_exits(nee, probe, first, n, &[]).expect("probe escapes");
let half_chord = 0.05f32.mul_add(-0.05, 0.1f32 * 0.1).sqrt();
let seg_a = (-half_chord) - (-0.4);
let seg_b = 0.5 - half_chord;
assert!(
(exit.t_material - (seg_a + seg_b)).abs() < 5e-5,
"material path {} vs {}",
exit.t_material,
seg_a + seg_b
);
let cos_wall = (0.05f32 / 0.1).mul_add(-(0.05f32 / 0.1), 1.0).sqrt();
let t_wall = exit_transmittance(n, cos_wall).expect("no TIR").0;
let t_face = exit_transmittance(n, 1.0).expect("no TIR").0;
let analytic = t_wall * t_wall * t_face;
assert!(
(exit.t_unpol - analytic).abs() < 1e-4,
"chain {} vs {}",
exit.t_unpol,
analytic
);
assert!(exit.t_material > first.t + 0.4);
}
}