use crate::error::{DagError, DagResult};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct MeshletBounds {
pub sphere: [f32; 4],
pub aabb_min: [f32; 3],
pub aabb_max: [f32; 3],
pub cone: [f32; 4],
}
impl MeshletBounds {
#[must_use]
pub fn to_flat(&self) -> [f32; 16] {
[
self.sphere[0], self.sphere[1], self.sphere[2], self.sphere[3],
self.aabb_min[0], self.aabb_min[1], self.aabb_min[2], 0.0,
self.aabb_max[0], self.aabb_max[1], self.aabb_max[2], 0.0,
self.cone[0], self.cone[1], self.cone[2], self.cone[3],
]
}
}
#[must_use]
pub fn triangle_normal(positions: &[f32], a: u32, b: u32, c: u32) -> Option<[f64; 3]> {
let (ao, bo, co) = (a as usize * 3, b as usize * 3, c as usize * 3);
let ax = f64::from(positions[ao]);
let ay = f64::from(positions[ao + 1]);
let az = f64::from(positions[ao + 2]);
let ab = [
f64::from(positions[bo]) - ax,
f64::from(positions[bo + 1]) - ay,
f64::from(positions[bo + 2]) - az,
];
let ac = [
f64::from(positions[co]) - ax,
f64::from(positions[co + 1]) - ay,
f64::from(positions[co + 2]) - az,
];
let cross = [
ab[1] * ac[2] - ab[2] * ac[1],
ab[2] * ac[0] - ab[0] * ac[2],
ab[0] * ac[1] - ab[1] * ac[0],
];
let length = hypot3(cross[0], cross[1], cross[2]);
let scale = f64::max(hypot2(ab[0], ab[1]), hypot2(ac[0], ac[1]));
if !length.is_finite() || length <= scale * scale * 1e-12 {
return None;
}
Some([cross[0] / length, cross[1] / length, cross[2] / length])
}
pub fn compute_meshlet_bounds(
positions: &[f32],
vertices: &[u32],
triangle_normals: &[[f64; 3]],
has_degenerate_triangle: bool,
) -> DagResult<MeshletBounds> {
let mut min = [f64::INFINITY; 3];
let mut max = [f64::NEG_INFINITY; 3];
for &vertex in vertices {
let offset = vertex as usize * 3;
for (axis, value) in positions[offset..offset + 3].iter().enumerate() {
let value = f64::from(*value);
min[axis] = min[axis].min(value);
max[axis] = max[axis].max(value);
}
}
let center: [f32; 3] = [
((min[0] + max[0]) * 0.5) as f32,
((min[1] + max[1]) * 0.5) as f32,
((min[2] + max[2]) * 0.5) as f32,
];
let mut radius = 0.0f64;
for &vertex in vertices {
let offset = vertex as usize * 3;
let dx = f64::from(positions[offset]) - f64::from(center[0]);
let dy = f64::from(positions[offset + 1]) - f64::from(center[1]);
let dz = f64::from(positions[offset + 2]) - f64::from(center[2]);
radius = radius.max(hypot3(dx, dy, dz));
}
let conservative_radius = next_float32(radius);
if !conservative_radius.is_finite() {
return Err(DagError::overflow(
"Meshlet sphere cannot be represented by finite float32 bounds.",
));
}
let cone = compute_normal_cone(triangle_normals, has_degenerate_triangle);
Ok(MeshletBounds {
sphere: [center[0], center[1], center[2], conservative_radius],
aabb_min: [min[0] as f32, min[1] as f32, min[2] as f32],
aabb_max: [max[0] as f32, max[1] as f32, max[2] as f32],
cone,
})
}
fn adjacent_float32(value: f32, direction: i32) -> f32 {
if value == 0.0 {
return if direction > 0 { f32::from_bits(1) } else { f32::from_bits(1).copysign(-1.0) };
}
let bits = value.to_bits();
let next = if (value > 0.0) == (direction > 0) { bits + 1 } else { bits - 1 };
f32::from_bits(next)
}
pub(crate) fn next_float32(value: f64) -> f32 {
let rounded = value as f32;
if !rounded.is_finite() || f64::from(rounded) >= value {
return rounded;
}
adjacent_float32(rounded, 1)
}
fn previous_float32(value: f64) -> f32 {
adjacent_float32(value as f32, -1)
}
fn compute_normal_cone(normals: &[[f64; 3]], disabled: bool) -> [f32; 4] {
if disabled || normals.is_empty() {
return [0.0, 0.0, 1.0, -1.0];
}
let mut sum = [0.0f64; 3];
for normal in normals {
sum[0] += normal[0];
sum[1] += normal[1];
sum[2] += normal[2];
}
let length = hypot3(sum[0], sum[1], sum[2]);
if length <= 1e-12 {
return [0.0, 0.0, 1.0, -1.0];
}
let axis: [f32; 3] = [
(sum[0] / length) as f32,
(sum[1] / length) as f32,
(sum[2] / length) as f32,
];
let mut cutoff = 1.0f64;
for normal in normals {
let dot = f64::from(axis[0]) * normal[0]
+ f64::from(axis[1]) * normal[1]
+ f64::from(axis[2]) * normal[2];
cutoff = cutoff.min(dot);
}
if cutoff <= 0.0 {
return [axis[0], axis[1], axis[2], -1.0];
}
[axis[0], axis[1], axis[2], previous_float32(cutoff.min(1.0))]
}
#[inline]
#[must_use]
pub fn hypot3(x: f64, y: f64, z: f64) -> f64 {
js_hypot(&[x, y, z])
}
#[inline]
#[must_use]
pub fn hypot2(x: f64, y: f64) -> f64 {
js_hypot(&[x, y])
}
#[inline]
fn js_hypot(args: &[f64]) -> f64 {
let mut max = 0.0f64;
for &value in args {
if value.is_infinite() {
return f64::INFINITY;
}
max = if max < value.abs() { value.abs() } else { max };
}
if max == 0.0 {
return 0.0;
}
let mut sum = 0.0f64;
for &value in args {
let scaled = value / max;
sum += scaled * scaled;
}
max * sum.sqrt()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn adjacent_float32_steps_one_ulp() {
let one = 1.0f32;
assert_eq!(adjacent_float32(one, 1).to_bits(), one.to_bits() + 1);
assert_eq!(adjacent_float32(one, -1).to_bits(), one.to_bits() - 1);
assert_eq!(adjacent_float32(0.0, 1), f32::from_bits(1));
assert_eq!(adjacent_float32(0.0, -1), -f32::from_bits(1));
assert!(adjacent_float32(f32::MAX, 1).is_infinite());
}
#[test]
fn next_float32_conservative_rounding() {
assert_eq!(next_float32(2.0), 2.0f32);
let v = 1.0 + f64::EPSILON; assert!(f64::from(next_float32(v)) >= v);
}
#[test]
fn triangle_normal_rejects_collinear_and_repeated() {
let positions = [0.0f32, 0.0, 0.0, 1.0, 0.0, 0.0, 2.0, 0.0, 0.0, 0.0, 1.0, 0.0];
assert!(triangle_normal(&positions, 0, 1, 2).is_none()); assert!(triangle_normal(&positions, 0, 0, 1).is_none()); let n = triangle_normal(&positions, 0, 1, 3).expect("valid triangle");
assert!((hypot3(n[0], n[1], n[2]) - 1.0).abs() < 1e-12);
}
#[test]
fn bounds_disable_cone_on_degenerate() {
let positions = [0.0f32, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0];
let bounds = compute_meshlet_bounds(&positions, &[0, 1, 2], &[], true).expect("bounds");
assert_eq!(bounds.cone, [0.0, 0.0, 1.0, -1.0]);
}
#[test]
fn hypot_matches_reference() {
assert_eq!(hypot3(3.0, 4.0, 0.0), 5.0);
assert_eq!(hypot2(3.0, 4.0), 5.0);
assert_eq!(hypot3(0.0, 0.0, 0.0), 0.0);
assert!(hypot3(f64::INFINITY, 1.0, 2.0).is_infinite());
}
}