use glam::Vec3;
use crate::optics::raytracer::{
FRESNEL_BRANCH_STREAM, FROSTED_DIR_U_STREAM, FROSTED_DIR_V_STREAM, cosine_weighted_hemisphere,
hash_u32,
refraction::{R_UNPOL_PDF_MAX, R_UNPOL_PDF_MIN},
};
#[derive(Clone, Copy, Debug)]
pub(super) struct FrostedEvent {
pub(super) dir: Vec3,
pub(super) crossed: bool,
}
pub(super) fn path_seed(origin: Vec3, dir: Vec3) -> u32 {
[origin.x, origin.y, origin.z, dir.x, dir.y, dir.z]
.into_iter()
.fold(0x9E37_79B9_u32, |seed, component| {
hash_u32(seed ^ component.to_bits())
})
}
const fn draw(seed: u32, bounce: u32, salt: u32) -> f32 {
(hash_u32(seed ^ hash_u32(bounce ^ salt)) as f32) / 4_294_967_295.0
}
pub(super) fn frosted_event(
(n1, n2): (f32, f32),
cos_i: f32,
sin2_t: f32,
normal: Vec3,
seed: u32,
bounce: u32,
) -> FrostedEvent {
let u1 = draw(seed, bounce, FROSTED_DIR_U_STREAM);
let u2 = draw(seed, bounce, FROSTED_DIR_V_STREAM);
if sin2_t > 1.0 {
return FrostedEvent {
dir: cosine_weighted_hemisphere(u1, u2, normal),
crossed: false,
};
}
let cos_t = (1.0 - sin2_t).max(0.0).sqrt();
let r_s = f32::mul_add(n2, -cos_t, n1 * cos_i) / f32::mul_add(n2, cos_t, n1 * cos_i);
let r_p = f32::mul_add(n1, -cos_t, n2 * cos_i) / f32::mul_add(n1, cos_t, n2 * cos_i);
let r_unpol = (0.5 * r_p.mul_add(r_p, r_s * r_s)).clamp(R_UNPOL_PDF_MIN, R_UNPOL_PDF_MAX);
let branch = draw(seed, bounce, FRESNEL_BRANCH_STREAM);
if branch < r_unpol {
FrostedEvent {
dir: cosine_weighted_hemisphere(u1, u2, normal),
crossed: false,
}
} else {
FrostedEvent {
dir: cosine_weighted_hemisphere(u1, u2, -normal),
crossed: true,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::optics::{
materials::GemMaterial,
polarization::StokesVector,
raytracer::{
BounceRefractionGeometry, LightingPreset, RayMaterialContext, apply_frosted_bounce,
build_plane_soa, scattering::NeeContext,
},
};
fn bits(v: Vec3) -> [u32; 3] {
v.to_array().map(f32::to_bits)
}
fn render_tracer(
(n1, n2): (f32, f32),
cos_i: f32,
sin2_t: f32,
normal: Vec3,
inside_gem: bool,
seed: u32,
bounce: u32,
) -> (Vec3, bool) {
let material = GemMaterial::new_custom("frosted metrics", 1.8, 0.02, 0.0, [0.0; 3]);
let geo = BounceRefractionGeometry {
cos_i,
n1,
n2,
sin2_t,
..Default::default()
};
let ctx = RayMaterialContext {
material: &material,
lambdas: [550.0; 8],
hero_idx: 0,
c_axis: Vec3::Y,
is_anisotropic: false,
enable_internal_mode_coupling: true,
};
let mut stokes: [StokesVector; 8] = std::array::from_fn(|_| StokesVector::unpolarized(1.0));
let mut path_pdf = [1.0f32; 8];
let plane_soa = build_plane_soa(&[]);
let nee = NeeContext {
environment: LightingPreset::Daylight.studio(1.0, 0.0, 0.0),
plane_soa: &plane_soa,
tools: &[],
enabled: false,
};
let mut radiance = [0.0f32; 8];
let (dir, new_inside_gem, _, _) = apply_frosted_bounce(
&ctx,
&geo,
normal,
Vec3::ZERO,
inside_gem,
false,
seed,
bounce,
&mut stokes,
&mut path_pdf,
nee,
&ctx.lambdas,
&mut radiance,
);
(dir, new_inside_gem != inside_gem)
}
#[test]
fn a_frosted_event_is_the_render_tracers_frosted_bounce() {
let outward = Vec3::new(0.3, 0.8, 0.2).normalize();
let mut crossed = 0;
let mut stayed = 0;
for index in [1.45f32, 1.77, 2.42] {
for cos_i in [0.15f32, 0.4, 0.7, 0.95] {
for seed in [1u32, 7, 0xDEAD_BEEF, 123_456_789] {
for bounce in 0..4u32 {
let sin2_in = (1.0 / (index * index)) * cos_i.mul_add(-cos_i, 1.0);
let ours =
frosted_event((1.0, index), cos_i, sin2_in, outward, seed, bounce);
let reference = render_tracer(
(1.0, index),
cos_i,
sin2_in,
outward,
false,
seed,
bounce,
);
assert_eq!(bits(ours.dir), bits(reference.0));
assert_eq!(ours.crossed, reference.1);
let sin2_out = (index * index) * cos_i.mul_add(-cos_i, 1.0);
let ours =
frosted_event((index, 1.0), cos_i, sin2_out, -outward, seed, bounce);
let reference = render_tracer(
(index, 1.0),
cos_i,
sin2_out,
-outward,
true,
seed,
bounce,
);
assert_eq!(bits(ours.dir), bits(reference.0));
assert_eq!(ours.crossed, reference.1);
if ours.crossed {
crossed += 1;
} else {
stayed += 1;
}
}
}
}
}
assert!(
crossed > 0 && stayed > 0,
"{crossed} crossed, {stayed} stayed"
);
}
#[test]
fn the_path_seed_is_deterministic_and_ray_dependent() {
let (origin, dir) = (Vec3::new(0.1, 2.0, -0.3), Vec3::NEG_Y);
assert_eq!(path_seed(origin, dir), path_seed(origin, dir));
assert_ne!(
path_seed(origin, dir),
path_seed(origin + Vec3::X * 1e-3, dir)
);
assert_ne!(path_seed(origin, dir), path_seed(origin, Vec3::NEG_X));
}
}