use super::{
camera::camera_view_basis,
classify::{ApertureSampleContext, RayClassification, classify_aperture_sample},
fan::{FanGeometry, GRID_DISC_RADIUS_SQ},
lighting::ExitLighting,
scintillation::{
TemporalPoseContext, cell_temporal_variance, combine_scintillation_pct,
spatial_scintillation_pct, temporal_scintillation_pct,
},
types::GemOpticalMetrics,
};
use crate::{
geometry::plane::GpuFacetPlane,
optics::{
materials::GemMaterial,
raytracer::{EnvironmentSource, build_plane_soa},
},
};
const FIRE_DEGREES_TO_DISPLAY_SCALE: f32 = 275.0;
#[derive(Default)]
struct MetricsAccumulators {
total_rays: u32,
windowed_rays: u32,
extinct_rays: u32,
returned_rays: u32,
fire_energy_weighted_sum_deg: f32,
dbg_fire_qualifying: u32,
dbg_fire_angle_sum_unweighted: f32,
dbg_fire_transmittance_sum: f32,
cell_fraction_sum: f32,
cell_fraction_sum_sq: f32,
cell_count: u32,
temporal_variance_sum: f32,
}
fn log_fire_diagnostics(
material_name: &str,
n_total: f32,
fire_index: f32,
acc: &MetricsAccumulators,
) {
let mean_angle_unweighted = if acc.dbg_fire_qualifying > 0 {
acc.dbg_fire_angle_sum_unweighted / acc.dbg_fire_qualifying as f32
} else {
0.0
};
let mean_transmittance = if acc.dbg_fire_qualifying > 0 {
acc.dbg_fire_transmittance_sum / acc.dbg_fire_qualifying as f32
} else {
0.0
};
eprintln!(
"DIAG material={material_name} n_total={n_total} returned_rays={} fire_qualifying={} mean_angle_unweighted={mean_angle_unweighted:.4} mean_transmittance={mean_transmittance:.4} weighted_sum={:.4} fire_index={fire_index:.4}",
acc.returned_rays, acc.dbg_fire_qualifying, acc.fire_energy_weighted_sum_deg
);
}
fn log_scintillation_diagnostics(
material_name: &str,
spatial_scint_pct: f32,
temporal_pct: f32,
scintillation_pct: f32,
) {
eprintln!(
"DIAG-SCINT material={material_name} spatial={spatial_scint_pct:.4} temporal={temporal_pct:.4} combined={scintillation_pct:.4}"
);
}
struct GridEvalSetup<'a> {
grid_size: i32,
aperture_samples: [(f32, f32); 5],
temporal_ctx: TemporalPoseContext<'a>,
aperture_ctx: ApertureSampleContext<'a>,
}
fn build_grid_eval_setup<'a>(
plane_soa: &'a crate::simd::PlanesSoA32,
fan: FanGeometry,
material: &GemMaterial,
cam_yaw: f32,
cam_pitch: f32,
environment: EnvironmentSource<'a>,
) -> GridEvalSetup<'a> {
let nd = material.dispersion.evaluate(589.3).max(1.1);
let n_f = material.dispersion.evaluate(486.1).max(1.001);
let n_c = material.dispersion.evaluate(656.3).max(1.001);
let grid_size = 18;
let (cam_forward, cam_right, cam_up) = camera_view_basis(cam_yaw, cam_pitch);
let lighting = ExitLighting::new(environment);
let aperture_samples = [
(0.0f32, 0.0f32),
(0.08, 0.0),
(-0.08, 0.0),
(0.0, 0.08),
(0.0, -0.08),
];
let temporal_ctx = TemporalPoseContext {
plane_soa,
nd,
cam_yaw,
cam_pitch,
fan,
lighting: lighting.clone(),
};
let aperture_ctx = ApertureSampleContext {
plane_soa,
nd,
n_f,
n_c,
cam_forward,
cam_right,
cam_up,
fan,
lighting,
};
GridEvalSetup {
grid_size,
aperture_samples,
temporal_ctx,
aperture_ctx,
}
}
#[must_use]
pub fn evaluate_gem_optical_metrics(
planes: &[GpuFacetPlane],
material: &GemMaterial,
cam_yaw: f32,
cam_pitch: f32,
environment: EnvironmentSource<'_>,
) -> GemOpticalMetrics {
if planes.is_empty() {
return GemOpticalMetrics {
brilliance_pct: 85.0,
fire_index: 25.0,
scintillation_pct: 75.0,
windowing_pct: 5.0,
extinction_pct: 5.0,
};
}
let mut acc = MetricsAccumulators::default();
let diag_fire_debug = std::env::var("DIAG_FIRE_DEBUG").is_ok();
let plane_soa = build_plane_soa(planes);
let fan = FanGeometry::for_planes(planes);
let setup = build_grid_eval_setup(&plane_soa, fan, material, cam_yaw, cam_pitch, environment);
for ix in 0..setup.grid_size {
for iz in 0..setup.grid_size {
let u = ((ix as f32 + 0.5) / (setup.grid_size as f32)).mul_add(2.0, -1.0);
let v = ((iz as f32 + 0.5) / (setup.grid_size as f32)).mul_add(2.0, -1.0);
if v.mul_add(v, u * u) > GRID_DISC_RADIUS_SQ {
continue; }
let mut cell_total = 0u32;
let mut cell_returned = 0u32;
for &(dx_sub, dz_sub) in &setup.aperture_samples {
let Some(classification) =
classify_aperture_sample(&setup.aperture_ctx, u, v, dx_sub, dz_sub)
else {
continue;
};
acc.total_rays += 1;
cell_total += 1;
match classification {
RayClassification::EntryBlocked => {}
RayClassification::Windowed => acc.windowed_rays += 1,
RayClassification::Extinct => acc.extinct_rays += 1,
RayClassification::Returned(fire) => {
acc.returned_rays += 1;
cell_returned += 1;
if let Some((angle_deg, weight)) = fire {
acc.fire_energy_weighted_sum_deg =
f32::mul_add(angle_deg, weight, acc.fire_energy_weighted_sum_deg);
if diag_fire_debug {
acc.dbg_fire_qualifying += 1;
acc.dbg_fire_angle_sum_unweighted += angle_deg;
acc.dbg_fire_transmittance_sum += weight;
}
}
}
}
}
if cell_total > 0 {
let frac = cell_returned as f32 / cell_total as f32;
acc.cell_fraction_sum += frac;
acc.cell_fraction_sum_sq = frac.mul_add(frac, acc.cell_fraction_sum_sq);
acc.cell_count += 1;
acc.temporal_variance_sum += cell_temporal_variance(&setup.temporal_ctx, u, v);
}
}
}
let n_total = acc.total_rays.max(1) as f32;
let windowing_pct = (acc.windowed_rays as f32 / n_total * 100.0).clamp(0.0, 100.0);
let extinction_pct = (acc.extinct_rays as f32 / n_total * 100.0).clamp(0.0, 100.0);
let brilliance_pct = (acc.returned_rays as f32 / n_total * 100.0).clamp(0.0, 100.0);
let fire_index =
(acc.fire_energy_weighted_sum_deg / n_total * FIRE_DEGREES_TO_DISPLAY_SCALE).max(0.1);
if diag_fire_debug {
log_fire_diagnostics(&material.name, n_total, fire_index, &acc);
}
let spatial_scint_pct = spatial_scintillation_pct(
acc.cell_fraction_sum,
acc.cell_fraction_sum_sq,
acc.cell_count,
);
let temporal_pct = temporal_scintillation_pct(acc.temporal_variance_sum, acc.cell_count);
let scintillation_pct = combine_scintillation_pct(spatial_scint_pct, temporal_pct);
if diag_fire_debug {
log_scintillation_diagnostics(
&material.name,
spatial_scint_pct,
temporal_pct,
scintillation_pct,
);
}
GemOpticalMetrics {
brilliance_pct,
fire_index,
scintillation_pct,
windowing_pct,
extinction_pct,
}
}