use super::{
camera::camera_view_basis,
fan::FanGeometry,
lighting::ExitLighting,
ray_trace::{EntryRay, RayFate, StoneArena, trace_entry},
visibility::ray_is_visibly_returned,
};
use crate::optics::raytracer::Ray;
use glam::Vec3;
pub(super) const SCINT_TEMPORAL_YAW_OFFSETS_DEG: [f32; 5] = [-3.0, -1.5, 0.0, 1.5, 3.0];
const SCINT_SPATIAL_WEIGHT: f32 = 0.6;
const SCINT_TEMPORAL_WEIGHT: f32 = 0.4;
pub(super) struct TemporalPoseContext<'a> {
pub(super) stone: StoneArena<'a>,
pub(super) nd: f32,
bases: [(Vec3, Vec3, Vec3); 5],
pub(super) fan: FanGeometry,
pub(super) lighting: &'a ExitLighting<'a>,
}
impl<'a> TemporalPoseContext<'a> {
pub(super) fn new(
stone: StoneArena<'a>,
nd: f32,
cam_yaw: f32,
cam_pitch: f32,
fan: FanGeometry,
lighting: &'a ExitLighting<'a>,
) -> Self {
let bases = SCINT_TEMPORAL_YAW_OFFSETS_DEG
.map(|offset_deg| camera_view_basis(cam_yaw + offset_deg.to_radians(), cam_pitch));
Self {
stone,
nd,
bases,
fan,
lighting,
}
}
}
#[must_use]
fn cell_returned_at_yaw_offset(
ctx: &TemporalPoseContext,
offset_index: usize,
u: f32,
v: f32,
) -> bool {
let (forward, right, up) = ctx.bases[offset_index];
let ray = Ray {
origin: ctx.fan.origin(forward, right, up, u, v),
dir: forward,
};
let Some(hit_rec) = ctx.stone.intersect(ray) else {
return false;
};
let hit_point = ray.origin + hit_rec.t * ray.dir;
let n_entry = hit_rec.normal;
let cos_i = (-ray.dir).dot(n_entry).clamp(0.0, 1.0);
let entry = EntryRay {
point: hit_point,
dir: ray.dir,
normal: n_entry,
cos_i,
facet: hit_rec.facet_idx,
};
match trace_entry(entry, ctx.stone, ctx.nd, None) {
RayFate::ExitedUpward(exit) => ray_is_visibly_returned(exit.dir, forward, ctx.lighting),
RayFate::EntryBlocked | RayFate::Leaked | RayFate::Absorbed => false,
}
}
#[must_use]
pub(super) fn cell_temporal_variance(ctx: &TemporalPoseContext, u: f32, v: f32) -> f32 {
let mut temporal_returned = 0u32;
for offset_index in 0..SCINT_TEMPORAL_YAW_OFFSETS_DEG.len() {
if cell_returned_at_yaw_offset(ctx, offset_index, u, v) {
temporal_returned += 1;
}
}
let temporal_p = temporal_returned as f32 / SCINT_TEMPORAL_YAW_OFFSETS_DEG.len() as f32;
temporal_p.mul_add(-temporal_p, temporal_p)
}
#[must_use]
pub(super) fn spatial_scintillation_pct(
cell_fraction_sum: f32,
cell_fraction_sum_sq: f32,
cell_count: u32,
) -> f32 {
if cell_count == 0 {
return 0.0;
}
let mean_frac = cell_fraction_sum / cell_count as f32;
if mean_frac <= 1e-4 {
return 0.0;
}
let variance = mean_frac
.mul_add(-mean_frac, cell_fraction_sum_sq / cell_count as f32)
.max(0.0);
let std_dev = variance.sqrt();
let coefficient_of_variation = std_dev / mean_frac;
(coefficient_of_variation / (1.0 + coefficient_of_variation) * 100.0).clamp(0.0, 100.0)
}
#[must_use]
pub(super) fn temporal_scintillation_pct(temporal_variance_sum: f32, cell_count: u32) -> f32 {
if cell_count == 0 {
return 0.0;
}
((temporal_variance_sum / cell_count as f32) / 0.24 * 100.0).clamp(0.0, 100.0)
}
#[must_use]
pub(super) fn combine_scintillation_pct(spatial_pct: f32, temporal_pct: f32) -> f32 {
SCINT_TEMPORAL_WEIGHT
.mul_add(temporal_pct, SCINT_SPATIAL_WEIGHT * spatial_pct)
.clamp(0.0, 100.0)
}