use glam::Vec3;
use super::{
frosted::{FrostedEvent, frosted_event, path_seed},
image_types::{ExtinctionReason, RayDetail},
};
use crate::{
geometry::tool::ToolPrimitive,
optics::raytracer::{FacetFinish, HitRecord, Ray, intersect_stone::intersect_stone_soa},
simd::PlanesSoA32,
};
#[derive(Debug, Clone, Copy)]
pub(super) enum RayFate {
EntryBlocked,
Leaked,
ExitedUpward(ExitPath),
Absorbed,
}
#[derive(Debug, Clone, Copy)]
pub(super) struct ExitPath {
pub(super) dir: Vec3,
pub(super) transmittance: f32,
pub(super) exit_cos_theta: f32,
pub(super) facet_idx: usize,
pub(super) bounces: u32,
pub(super) path_len: f32,
}
fn fresnel_transmittance(n1: f32, n2: f32, cos_i: f32, cos_t: f32) -> f32 {
let denom_s = n2.mul_add(cos_t, n1 * cos_i);
let denom_p = n2.mul_add(cos_i, n1 * cos_t);
if denom_s.abs() < 1e-6 || denom_p.abs() < 1e-6 {
return 0.0;
}
let rs = (n2.mul_add(-cos_t, n1 * cos_i) / denom_s).powi(2);
let rp = (n2.mul_add(-cos_i, n1 * cos_t) / denom_p).powi(2);
let r = f32::midpoint(rs, rp);
(1.0 - r).clamp(0.0, 1.0)
}
#[derive(Clone, Copy)]
pub(super) struct StoneArena<'a> {
pub(super) plane_soa: &'a PlanesSoA32,
pub(super) tools: &'a [ToolPrimitive],
pub(super) finishes: &'a [FacetFinish],
}
impl StoneArena<'_> {
pub(super) fn intersect(&self, ray: Ray) -> Option<HitRecord> {
intersect_stone_soa(ray, self.plane_soa, self.plane_soa.len(), self.tools)
}
pub(super) fn is_frosted(&self, facet_idx: usize) -> bool {
self.finishes.get(facet_idx) == Some(&FacetFinish::Frosted)
}
}
#[derive(Clone, Copy)]
pub(super) struct EntryRay {
pub(super) point: Vec3,
pub(super) dir: Vec3,
pub(super) normal: Vec3,
pub(super) cos_i: f32,
pub(super) facet: usize,
}
fn note_reason(detail: &mut Option<&mut RayDetail>, reason: ExtinctionReason) {
if let Some(d) = detail.as_deref_mut() {
d.reason = reason;
}
}
#[derive(Clone, Copy)]
struct Interior {
hit_point: Vec3,
dir: Vec3,
entry_transmittance: f32,
seed: u32,
}
#[derive(Clone, Copy)]
struct InteriorHit {
n_out: Vec3,
cos_theta: f32,
facet: usize,
bounce: u32,
}
#[derive(Clone, Copy, Default)]
struct Escape {
leaked: bool,
exited_upwards: bool,
dir: Vec3,
transmittance: f32,
cos_theta: f32,
facet_idx: usize,
bounces: u32,
}
impl Escape {
fn record(
&mut self,
hit: InteriorHit,
out_dir: Vec3,
cos_out: f32,
transmittance: impl FnOnce() -> f32,
detail: &mut Option<&mut RayDetail>,
) {
if let Some(d) = detail.as_deref_mut() {
d.exit_facet = Some(hit.facet);
d.bounces = hit.bounce;
d.reason = ExtinctionReason::SideExit;
}
if hit.n_out.y < -0.05 {
note_reason(detail, ExtinctionReason::None);
self.leaked = true; } else if out_dir.y > 0.05 {
note_reason(detail, ExtinctionReason::None);
self.exited_upwards = true;
self.dir = out_dir;
self.transmittance = transmittance();
self.cos_theta = cos_out;
self.facet_idx = hit.facet;
self.bounces = hit.bounce;
}
}
}
fn frosted_interior(hit: InteriorHit, index: f32, seed: u32) -> FrostedEvent {
let sin2_t = (index * index) * hit.cos_theta.mul_add(-hit.cos_theta, 1.0);
frosted_event(
(index, 1.0),
hit.cos_theta,
sin2_t,
-hit.n_out,
seed,
hit.bounce + 1,
)
}
fn scatter_at_frosted(
hit: InteriorHit,
index: f32,
seed: u32,
escape: &mut Escape,
detail: &mut Option<&mut RayDetail>,
) -> Option<Vec3> {
let event = frosted_interior(hit, index, seed);
if event.crossed {
escape.record(hit, event.dir, event.dir.dot(hit.n_out), || 1.0, detail);
return None;
}
if let Some(d) = detail.as_deref_mut() {
d.bounces = hit.bounce + 1;
}
Some(event.dir)
}
pub(super) fn trace_entry(
entry: EntryRay,
stone: StoneArena<'_>,
index: f32,
mut detail: Option<&mut RayDetail>,
) -> RayFate {
if !stone.is_frosted(entry.facet) {
return trace_wavelength_detail(
entry.point,
entry.dir,
entry.normal,
entry.cos_i,
stone,
index,
detail,
);
}
note_reason(&mut detail, ExtinctionReason::BounceBudget);
let seed = path_seed(entry.point, entry.dir);
let sin2_t = (1.0 / (index * index)) * entry.cos_i.mul_add(-entry.cos_i, 1.0);
let event = frosted_event((1.0, index), entry.cos_i, sin2_t, entry.normal, seed, 0);
if event.crossed {
return trace_interior(
Interior {
hit_point: entry.point + event.dir * 1e-4,
dir: event.dir,
entry_transmittance: 1.0,
seed,
},
stone,
index,
detail,
);
}
if let Some(d) = detail {
d.exit_facet = Some(entry.facet);
d.bounces = 0;
d.reason = ExtinctionReason::SideExit;
}
RayFate::Absorbed
}
fn trace_wavelength_detail(
entry_point: Vec3,
incoming_dir: Vec3,
n_entry: Vec3,
cos_i: f32,
stone: StoneArena<'_>,
index: f32,
mut detail: Option<&mut RayDetail>,
) -> RayFate {
note_reason(&mut detail, ExtinctionReason::BounceBudget);
let sin2_t = (1.0 / (index * index)) * cos_i.mul_add(-cos_i, 1.0);
if sin2_t > 1.0 {
note_reason(&mut detail, ExtinctionReason::EntryBlocked);
return RayFate::EntryBlocked;
}
let cos_t = (1.0 - sin2_t).sqrt();
let curr_dir =
((1.0 / index) * incoming_dir + (1.0 / index).mul_add(cos_i, -cos_t) * n_entry).normalize();
let entry_transmittance = fresnel_transmittance(1.0, index, cos_i, cos_t);
let seed = if stone.finishes.is_empty() {
0
} else {
path_seed(entry_point, incoming_dir)
};
trace_interior(
Interior {
hit_point: entry_point + curr_dir * 1e-4,
dir: curr_dir,
entry_transmittance,
seed,
},
stone,
index,
detail,
)
}
fn trace_interior(
start: Interior,
stone: StoneArena<'_>,
index: f32,
mut detail: Option<&mut RayDetail>,
) -> RayFate {
let mut curr_dir = start.dir;
let mut hit_point = start.hit_point;
let sin_crit = 1.0 / index;
let mut escape = Escape::default();
let mut path_len = 0.0f32;
for bounce in 0..10 {
if let Some(d) = detail.as_deref_mut() {
d.bounces = bounce;
}
let inside_ray = Ray {
origin: hit_point,
dir: curr_dir,
};
let next_hit = stone.intersect(inside_ray);
let Some(next_rec) = next_hit else {
note_reason(&mut detail, ExtinctionReason::NoIntersection);
break;
};
if detail.is_some()
&& (!next_rec.t.is_finite()
|| !curr_dir.is_finite()
|| !hit_point.is_finite()
|| !next_rec.normal.is_finite())
{
note_reason(&mut detail, ExtinctionReason::Invalid);
return RayFate::Absorbed;
}
path_len += next_rec.t;
let next_point = inside_ray.origin + next_rec.t * inside_ray.dir;
let n_out = next_rec.normal;
let cos_theta = curr_dir.dot(n_out).clamp(0.0, 1.0);
let sin_theta = cos_theta.mul_add(-cos_theta, 1.0).max(0.0).sqrt();
let hit = InteriorHit {
n_out,
cos_theta,
facet: next_rec.facet_idx,
bounce,
};
if stone.is_frosted(hit.facet) {
let Some(dir) = scatter_at_frosted(hit, index, start.seed, &mut escape, &mut detail)
else {
break;
};
curr_dir = dir;
hit_point = next_point + curr_dir * 1e-4;
continue;
}
if sin_theta < sin_crit {
let sin2_out = (index * index) * cos_theta.mul_add(-cos_theta, 1.0);
if sin2_out <= 1.0 {
let cos_out = (1.0 - sin2_out).sqrt();
let out_dir =
(index * curr_dir + index.mul_add(-cos_theta, cos_out) * n_out).normalize();
escape.record(
hit,
out_dir,
cos_out,
|| fresnel_transmittance(index, 1.0, cos_theta, cos_out),
&mut detail,
);
break;
}
}
curr_dir = (curr_dir - 2.0 * cos_theta * n_out).normalize();
hit_point = next_point + curr_dir * 1e-4;
if let Some(d) = detail.as_deref_mut() {
d.bounces = bounce + 1;
}
}
if escape.leaked {
RayFate::Leaked
} else if escape.exited_upwards {
RayFate::ExitedUpward(ExitPath {
dir: escape.dir,
transmittance: start.entry_transmittance * escape.transmittance,
exit_cos_theta: escape.cos_theta,
facet_idx: escape.facet_idx,
bounces: escape.bounces,
path_len,
})
} else {
RayFate::Absorbed
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
color::metrics::image_types::{ExtinctionReason, RayDetail},
geometry::{cuts::StandardGemCuts, tool::StoneGeometry},
optics::{
materials::GemMaterial,
raytracer::{Camera, LightingPreset},
},
};
fn kind(fate: &RayFate) -> u8 {
match fate {
RayFate::EntryBlocked => 0,
RayFate::Leaked => 1,
RayFate::ExitedUpward(_) => 2,
RayFate::Absorbed => 3,
}
}
#[test]
fn detail_reasons_match_fates() {
let planes = StandardGemCuts::emerald_cut();
let material = GemMaterial::diamond();
let prepared = super::super::prepared::PreparedEval::new(
StoneGeometry::planes_only(&planes),
&material,
LightingPreset::IsoHemisphere.studio(1.0, 0.85, 0.95),
)
.unwrap();
let stone = prepared.stone();
let camera = Camera::new(0.4, 0.9, 2.4, 42.0);
let (mut leaked, mut upward, mut absorbed) = (0, 0, 0);
for gy in 0..48 {
for gx in 0..48 {
let ray = camera.generate_ray(gx as f32, gy as f32, 48.0, 48.0, 0.0, 0.0);
let Some(hit) = stone.intersect(ray) else {
continue;
};
let entry = ray.origin + hit.t * ray.dir;
let cos_i = (-ray.dir).dot(hit.normal).clamp(0.0, 1.0);
let entry_ray = EntryRay {
point: entry,
dir: ray.dir,
normal: hit.normal,
cos_i,
facet: hit.facet_idx,
};
let plain = trace_entry(entry_ray, stone, prepared.nd, None);
let mut detail = RayDetail::default();
let detailed = trace_entry(entry_ray, stone, prepared.nd, Some(&mut detail));
assert_eq!(kind(&plain), kind(&detailed), "pixel {gx},{gy}");
match detailed {
RayFate::EntryBlocked => {
assert_eq!(detail.reason, ExtinctionReason::EntryBlocked);
}
RayFate::Leaked => {
leaked += 1;
assert_eq!(detail.reason, ExtinctionReason::None, "pixel {gx},{gy}");
assert!(detail.exit_facet.is_some());
}
RayFate::ExitedUpward(path) => {
upward += 1;
assert_eq!(detail.reason, ExtinctionReason::None, "pixel {gx},{gy}");
assert_eq!(detail.exit_facet, Some(path.facet_idx));
assert_eq!(detail.bounces, path.bounces);
}
RayFate::Absorbed => {
absorbed += 1;
assert_ne!(detail.reason, ExtinctionReason::None, "pixel {gx},{gy}");
match detail.reason {
ExtinctionReason::BounceBudget => assert_eq!(detail.bounces, 10),
ExtinctionReason::SideExit => assert!(detail.exit_facet.is_some()),
_ => {}
}
}
}
}
}
assert!(
leaked > 0 && upward > 0,
"grid saw {leaked} leaked, {upward} upward, {absorbed} absorbed"
);
}
}