use glam::Vec3;
use super::{
fan::FanGeometry,
lighting::ExitLighting,
ray_trace::{EntryRay, RayFate, StoneArena, trace_entry},
visibility::returned_or_reason,
};
use crate::optics::raytracer::Ray;
pub(super) struct ApertureSampleContext<'a> {
pub(super) stone: StoneArena<'a>,
pub(super) nd: f32,
pub(super) n_f: f32,
pub(super) n_c: f32,
pub(super) cam_forward: Vec3,
pub(super) cam_right: Vec3,
pub(super) cam_up: Vec3,
pub(super) fan: FanGeometry,
pub(super) lighting: &'a ExitLighting<'a>,
pub(super) want_fire: bool,
}
pub(super) enum RayClassification {
Invalid,
EntryBlocked,
Windowed,
Extinct,
Returned(ReturnedRay),
}
#[derive(Clone, Copy)]
pub(super) struct ReturnedRay {
pub(super) fire: Option<(f32, f32)>,
pub(super) transmittance: f32,
pub(super) path_len: f32,
}
pub(super) fn classify_aperture_sample(
ctx: &ApertureSampleContext,
u: f32,
v: f32,
sub_right: f32,
sub_up: f32,
) -> Option<RayClassification> {
let ray_dir = (ctx.cam_forward + sub_right * ctx.cam_right + sub_up * ctx.cam_up).normalize();
let ray_origin = ctx
.fan
.origin(ctx.cam_forward, ctx.cam_right, ctx.cam_up, u, v);
let ray = Ray {
origin: ray_origin,
dir: ray_dir,
};
classify_ray(ctx, ray, -ctx.cam_forward, None)
}
pub(super) fn classify_ray(
ctx: &ApertureSampleContext,
ray: Ray,
observer: Vec3,
mut detail: Option<&mut super::image_types::RayDetail>,
) -> Option<RayClassification> {
let hit = ctx.stone.intersect(ray);
let hit_rec = hit?;
if let Some(d) = detail.as_deref_mut() {
d.entry_facet = Some(hit_rec.facet_idx);
}
let hit_point_entry = ray.origin + hit_rec.t * ray.dir;
if detail.is_some()
&& (!hit_rec.t.is_finite() || !hit_point_entry.is_finite() || !hit_rec.normal.is_finite())
{
if let Some(d) = detail.as_deref_mut() {
d.reason = super::image_types::ExtinctionReason::Invalid;
}
return Some(RayClassification::Invalid);
}
let n_entry = hit_rec.normal;
let cos_i = (-ray.dir).dot(n_entry).clamp(0.0, 1.0);
let mut trace_detail = super::image_types::RayDetail::default();
let entry = EntryRay {
point: hit_point_entry,
dir: ray.dir,
normal: n_entry,
cos_i,
facet: hit_rec.facet_idx,
};
let fate = trace_entry(
entry,
ctx.stone,
ctx.nd,
detail.as_ref().map(|_| &mut trace_detail),
);
if let Some(d) = detail.as_deref_mut() {
d.exit_facet = trace_detail.exit_facet;
d.bounces = trace_detail.bounces;
d.reason = trace_detail.reason;
}
if trace_detail.reason == super::image_types::ExtinctionReason::Invalid {
return Some(RayClassification::Invalid);
}
match fate {
RayFate::EntryBlocked => Some(RayClassification::EntryBlocked),
RayFate::Leaked => Some(RayClassification::Windowed),
RayFate::ExitedUpward(exit_d) => {
let exit_dir = exit_d.dir;
let transmittance = exit_d.transmittance;
if let Err(reason) = returned_or_reason(exit_dir, observer, ctx.lighting) {
if let Some(d) = detail {
d.reason = reason;
}
return Some(RayClassification::Extinct);
}
let fire = if ctx.want_fire {
fire_sample(ctx, entry, transmittance)
} else {
None
};
Some(RayClassification::Returned(ReturnedRay {
fire,
transmittance,
path_len: exit_d.path_len,
}))
}
RayFate::Absorbed => Some(RayClassification::Extinct),
}
}
fn fire_sample(
ctx: &ApertureSampleContext,
entry: EntryRay,
transmittance: f32,
) -> Option<(f32, f32)> {
let fate_f = trace_entry(entry, ctx.stone, ctx.n_f, None);
let fate_c = trace_entry(entry, ctx.stone, ctx.n_c, None);
let (RayFate::ExitedUpward(exit_f), RayFate::ExitedUpward(exit_c)) = (fate_f, fate_c) else {
return None;
};
if exit_f.facet_idx != exit_c.facet_idx || exit_f.bounces != exit_c.bounces {
return None;
}
let transmittance_f = exit_f.transmittance;
let transmittance_c = exit_c.transmittance;
let exit_cos_f = exit_f.exit_cos_theta;
let exit_cos_c = exit_c.exit_cos_theta;
let cos_sep = exit_f.dir.dot(exit_c.dir).clamp(-1.0, 1.0);
let angle_deg = cos_sep.acos().to_degrees();
let energy_weight = transmittance * transmittance_f * transmittance_c;
let radiance_weight = exit_cos_f.max(0.0) * exit_cos_c.max(0.0);
let weight = energy_weight * radiance_weight;
Some((angle_deg, weight))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
geometry::{cuts::StandardGemCuts, tool::StoneGeometry},
optics::{materials::GemMaterial, raytracer::LightingPreset},
};
fn signature(value: Option<&RayClassification>) -> (u8, Option<[u32; 4]>) {
match value {
None => (0, None),
Some(RayClassification::EntryBlocked) => (1, None),
Some(RayClassification::Windowed) => (2, None),
Some(RayClassification::Extinct) => (3, None),
Some(RayClassification::Invalid) => (5, None),
Some(RayClassification::Returned(r)) => (
4,
r.fire.map(|(a, w)| {
[
a.to_bits(),
w.to_bits(),
r.transmittance.to_bits(),
r.path_len.to_bits(),
]
}),
),
}
}
#[test]
fn identical_fan_rays_have_identical_class_and_fire_bits() {
let planes = StandardGemCuts::standard_round_brilliant();
let material = GemMaterial::diamond();
let camera = crate::optics::raytracer::Camera::new(0.6, 0.45, 2.4, 42.0);
let mut extinct = 0;
let mut returned = 0;
for (preset, head_shadow) in [
(LightingPreset::RingLights, 16.0),
(LightingPreset::LightTent, 16.0),
(LightingPreset::LightTent, 30.0),
(LightingPreset::IsoHemisphere, 0.0),
] {
let prepared = super::super::prepared::PreparedEval::new(
StoneGeometry::planes_only(&planes),
&material,
preset.studio(1.0, 0.85, 0.95).with_head_shadow(head_shadow),
)
.unwrap();
let ctx = ApertureSampleContext {
stone: prepared.stone(),
nd: prepared.nd,
n_f: prepared.n_f,
n_c: prepared.n_c,
cam_forward: camera.forward,
cam_right: camera.right,
cam_up: camera.up,
fan: prepared.fan,
lighting: &prepared.lighting,
want_fire: true,
};
for u in [-0.7, 0.0, 0.4] {
for v in [-0.6, 0.0, 0.5] {
for (sub_right, sub_up) in [(0.01, 0.02), (-0.03, 0.0), (0.0, -0.02)] {
let ray = Ray {
origin: ctx.fan.origin(
ctx.cam_forward,
ctx.cam_right,
ctx.cam_up,
u,
v,
),
dir: (ctx.cam_forward
+ sub_right * ctx.cam_right
+ sub_up * ctx.cam_up)
.normalize(),
};
let mut detail = super::super::image_types::RayDetail::default();
let legacy = classify_aperture_sample(&ctx, u, v, sub_right, sub_up);
extinct += usize::from(matches!(legacy, Some(RayClassification::Extinct)));
returned +=
usize::from(matches!(legacy, Some(RayClassification::Returned(_))));
assert_eq!(
signature(legacy.as_ref()),
signature(
classify_ray(&ctx, ray, -ctx.cam_forward, Some(&mut detail))
.as_ref()
)
);
}
}
}
}
assert!(
extinct > 0 && returned > 0,
"{extinct} extinct, {returned} returned"
);
}
}