use super::{
classify::{ApertureSampleContext, RayClassification, classify_ray},
image::offset,
image_types::{OpticalImage, OpticalImageError, OpticalImageRequest, OpticalPixel},
prepared::PreparedEval,
scintillation::{
SCINT_TEMPORAL_YAW_OFFSETS_DEG, combine_scintillation_pct, spatial_scintillation_pct,
temporal_scintillation_pct,
},
};
use crate::optics::raytracer::{Camera, DEFAULT_FOV_DEG};
const POSES: u8 = 5;
const BASE_POSE: usize = 2;
pub const SCINTILLATION_WINDOW_RADIUS: usize = 2;
pub(super) fn temporal_cameras(pose: [f32; 3], fov_tan: f32) -> [Camera; 5] {
SCINT_TEMPORAL_YAW_OFFSETS_DEG.map(|offset_deg| {
let mut camera = Camera::new(
pose[0] + offset_deg.to_radians(),
pose[1],
pose[2],
DEFAULT_FOV_DEG,
);
camera.fov_tan = fov_tan;
camera
})
}
pub(super) struct TemporalSampler<'a> {
ctx: ApertureSampleContext<'a>,
cameras: [Camera; 5],
projection: [u32; 2],
origin: [u32; 2],
scale: [f32; 2],
}
impl<'a> TemporalSampler<'a> {
pub(super) fn new(
request: &OpticalImageRequest<'_>,
prepared: &'a PreparedEval<'a>,
pose: [f32; 3],
projection: [u32; 2],
origin: [u32; 2],
scale: [f32; 2],
) -> Self {
let c = request.camera;
Self {
ctx: ApertureSampleContext {
stone: prepared.stone(),
nd: prepared.nd,
n_f: prepared.n_f,
n_c: prepared.n_c,
cam_forward: c.forward,
cam_right: c.right,
cam_up: c.up,
fan: prepared.fan,
lighting: &prepared.lighting,
want_fire: false,
},
cameras: temporal_cameras(pose, c.fov_tan),
projection,
origin,
scale,
}
}
fn returned_at(&self, index: usize, x: u32, y: u32) -> Option<bool> {
let (jx, jy) = offset(0);
let ray = self.cameras[index].generate_ray(
(x as f32 + jx).mul_add(self.scale[0], self.origin[0] as f32 - 0.5),
(y as f32 + jy).mul_add(self.scale[1], self.origin[1] as f32 - 0.5),
self.projection[0] as f32,
self.projection[1] as f32,
0.0,
0.0,
);
classify_ray(&self.ctx, ray, -ray.dir, None)
.map(|class| matches!(class, RayClassification::Returned(_)))
}
pub(super) fn apply(
&self,
x: u32,
y: u32,
pixel: &mut OpticalPixel,
cancelled: &impl Fn() -> bool,
) -> Result<(), OpticalImageError> {
if cancelled() {
return Err(OpticalImageError::Cancelled);
}
let Some(base) = self.returned_at(BASE_POSE, x, y) else {
return Ok(());
};
let mut returned = u8::from(base);
for index in (0..self.cameras.len()).filter(|i| *i != BASE_POSE) {
if cancelled() {
return Err(OpticalImageError::Cancelled);
}
returned += u8::from(self.returned_at(index, x, y) == Some(true));
}
pixel.scint_returned = returned;
pixel.scint_poses = POSES;
Ok(())
}
}
fn brilliance_fractions(image: &OpticalImage) -> Vec<Option<f32>> {
image
.pixels
.iter()
.map(|p| (p.hits > 0).then(|| p.fractions()[2]))
.collect()
}
fn window_spatial(
fractions: &[Option<f32>],
width: usize,
height: usize,
x: usize,
y: usize,
) -> f32 {
let mut sum = 0.0_f32;
let mut sum_sq = 0.0_f32;
let mut count = 0_u32;
for wy in y.saturating_sub(SCINTILLATION_WINDOW_RADIUS)
..(y + SCINTILLATION_WINDOW_RADIUS + 1).min(height)
{
for wx in x.saturating_sub(SCINTILLATION_WINDOW_RADIUS)
..(x + SCINTILLATION_WINDOW_RADIUS + 1).min(width)
{
if let Some(f) = fractions[wy * width + wx] {
sum += f;
sum_sq = f.mul_add(f, sum_sq);
count += 1;
}
}
}
spatial_scintillation_pct(sum, sum_sq, count)
}
#[must_use]
pub fn scintillation_spatial(image: &OpticalImage) -> Vec<f32> {
let (width, height) = (image.width as usize, image.height as usize);
let mut out = vec![0.0; image.pixels.len()];
if width.checked_mul(height) != Some(image.pixels.len()) {
return out;
}
let fractions = brilliance_fractions(image);
for y in 0..height {
for x in 0..width {
if fractions[y * width + x].is_some() {
out[y * width + x] = window_spatial(&fractions, width, height, x, y);
}
}
}
out
}
#[must_use]
pub fn scintillation_spatial_at(image: &OpticalImage, i: usize) -> f32 {
let (width, height) = (image.width as usize, image.height as usize);
if width.checked_mul(height) != Some(image.pixels.len()) || i >= image.pixels.len() {
return 0.0;
}
let (col, row) = (i % width, i / width);
let (x0, x1) = (
col.saturating_sub(SCINTILLATION_WINDOW_RADIUS),
(col + SCINTILLATION_WINDOW_RADIUS + 1).min(width),
);
let (y0, y1) = (
row.saturating_sub(SCINTILLATION_WINDOW_RADIUS),
(row + SCINTILLATION_WINDOW_RADIUS + 1).min(height),
);
let (frame_w, frame_h) = (x1 - x0, y1 - y0);
let mut local = Vec::with_capacity(frame_w * frame_h);
for wy in y0..y1 {
for wx in x0..x1 {
let pixel = &image.pixels[wy * width + wx];
local.push((pixel.hits > 0).then(|| pixel.fractions()[2]));
}
}
if local[(row - y0) * frame_w + (col - x0)].is_none() {
return 0.0;
}
window_spatial(&local, frame_w, frame_h, col - x0, row - y0)
}
#[must_use]
pub fn scintillation_index(image: &OpticalImage, spatial: &[f32], i: usize) -> Option<f32> {
let variance = image.temporal_variance(i)?;
let spatial_pct = spatial.get(i).copied().unwrap_or(0.0);
Some(combine_scintillation_pct(
spatial_pct,
temporal_scintillation_pct(variance, 1),
))
}
#[must_use]
pub fn scintillation_index_at(image: &OpticalImage, i: usize) -> Option<f32> {
let variance = image.temporal_variance(i)?;
Some(combine_scintillation_pct(
scintillation_spatial_at(image, i),
temporal_scintillation_pct(variance, 1),
))
}
pub(super) fn image_scintillation_pct(image: &OpticalImage) -> f32 {
let mut sum = 0.0_f32;
let mut sum_sq = 0.0_f32;
let mut count = 0_u32;
for p in image.pixels.iter().filter(|p| p.hits > 0) {
let f = p.fractions()[2];
sum += f;
sum_sq = f.mul_add(f, sum_sq);
count += 1;
}
let mut variance_sum = 0.0_f32;
let mut poses_pixels = 0_u32;
for i in 0..image.pixels.len() {
if let Some(variance) = image.temporal_variance(i) {
variance_sum += variance;
poses_pixels += 1;
}
}
combine_scintillation_pct(
spatial_scintillation_pct(sum, sum_sq, count),
temporal_scintillation_pct(variance_sum, poses_pixels),
)
}
#[cfg(test)]
#[path = "image_scint_tests.rs"]
mod tests;