Skip to main content

geometry_dag/
bounds.rs

1//! 簇包围球 / AABB / 法向锥计算。
2//!
3//! 与 TS 权威实现(`packages/deep-engine/src/geometry/meshletBounds.ts`)逐位对拍:
4//! TS 中 `Float32Array` 元素读出即 f64,全部运算在 f64 下进行,写回时经 `Math.fround`
5//! (round-to-nearest-even 的 f64→f32)。Rust 侧对应:f32 读入 `as f64` 提升、f64 运算、
6//! 写回 `as f32`。相邻 f32(ulp 步进)用位操作复刻 TS 的 `ADJACENT_WORDS` 技巧。
7
8use crate::error::{DagError, DagResult};
9
10/// 单个簇的渲染剔除包围体。
11#[derive(Debug, Clone, Copy, PartialEq)]
12pub struct MeshletBounds {
13    /// 包围球 `[cx, cy, cz, r]`,半径为保守 f32(向上含 1 ulp 余量)。
14    pub sphere: [f32; 4],
15    /// AABB 最小角点。
16    pub aabb_min: [f32; 3],
17    /// AABB 最大角点。
18    pub aabb_max: [f32; 3],
19    /// 法向锥 `[nx, ny, nz, cutoff]`;cutoff 为 -1 表示锥剔除禁用。
20    pub cone: [f32; 4],
21}
22
23impl MeshletBounds {
24    /// 平铺为 16 个 f32(与 TS `appendBounds` 顺序一致:sphere, aabbMin+0, aabbMax+0, cone)。
25    #[must_use]
26    pub fn to_flat(&self) -> [f32; 16] {
27        [
28            self.sphere[0], self.sphere[1], self.sphere[2], self.sphere[3],
29            self.aabb_min[0], self.aabb_min[1], self.aabb_min[2], 0.0,
30            self.aabb_max[0], self.aabb_max[1], self.aabb_max[2], 0.0,
31            self.cone[0], self.cone[1], self.cone[2], self.cone[3],
32        ]
33    }
34}
35
36/// 三角形单位法线;退化(重合顶点或近共线)返回 `None`。
37///
38/// 与 TS `triangleNormal` 一致:全部在 f64 下计算,`length <= scale²·1e-12` 判退化。
39#[must_use]
40pub fn triangle_normal(positions: &[f32], a: u32, b: u32, c: u32) -> Option<[f64; 3]> {
41    let (ao, bo, co) = (a as usize * 3, b as usize * 3, c as usize * 3);
42    let ax = f64::from(positions[ao]);
43    let ay = f64::from(positions[ao + 1]);
44    let az = f64::from(positions[ao + 2]);
45    let ab = [
46        f64::from(positions[bo]) - ax,
47        f64::from(positions[bo + 1]) - ay,
48        f64::from(positions[bo + 2]) - az,
49    ];
50    let ac = [
51        f64::from(positions[co]) - ax,
52        f64::from(positions[co + 1]) - ay,
53        f64::from(positions[co + 2]) - az,
54    ];
55    let cross = [
56        ab[1] * ac[2] - ab[2] * ac[1],
57        ab[2] * ac[0] - ab[0] * ac[2],
58        ab[0] * ac[1] - ab[1] * ac[0],
59    ];
60    let length = hypot3(cross[0], cross[1], cross[2]);
61    let scale = f64::max(hypot2(ab[0], ab[1]), hypot2(ac[0], ac[1]));
62    if !length.is_finite() || length <= scale * scale * 1e-12 {
63        return None;
64    }
65    Some([cross[0] / length, cross[1] / length, cross[2] / length])
66}
67
68/// 计算簇包围体。`vertices` 为全局顶点表(与 TS `pending.vertices` 同序)。
69///
70/// # Errors
71/// 包围球半径无法表示为有限 f32 时返回 [`DagError::Overflow`]。
72pub fn compute_meshlet_bounds(
73    positions: &[f32],
74    vertices: &[u32],
75    triangle_normals: &[[f64; 3]],
76    has_degenerate_triangle: bool,
77) -> DagResult<MeshletBounds> {
78    let mut min = [f64::INFINITY; 3];
79    let mut max = [f64::NEG_INFINITY; 3];
80    for &vertex in vertices {
81        let offset = vertex as usize * 3;
82        for (axis, value) in positions[offset..offset + 3].iter().enumerate() {
83            let value = f64::from(*value);
84            min[axis] = min[axis].min(value);
85            max[axis] = max[axis].max(value);
86        }
87    }
88    let center: [f32; 3] = [
89        ((min[0] + max[0]) * 0.5) as f32,
90        ((min[1] + max[1]) * 0.5) as f32,
91        ((min[2] + max[2]) * 0.5) as f32,
92    ];
93    let mut radius = 0.0f64;
94    for &vertex in vertices {
95        let offset = vertex as usize * 3;
96        let dx = f64::from(positions[offset]) - f64::from(center[0]);
97        let dy = f64::from(positions[offset + 1]) - f64::from(center[1]);
98        let dz = f64::from(positions[offset + 2]) - f64::from(center[2]);
99        radius = radius.max(hypot3(dx, dy, dz));
100    }
101    let conservative_radius = next_float32(radius);
102    if !conservative_radius.is_finite() {
103        return Err(DagError::overflow(
104            "Meshlet sphere cannot be represented by finite float32 bounds.",
105        ));
106    }
107    let cone = compute_normal_cone(triangle_normals, has_degenerate_triangle);
108    Ok(MeshletBounds {
109        sphere: [center[0], center[1], center[2], conservative_radius],
110        aabb_min: [min[0] as f32, min[1] as f32, min[2] as f32],
111        aabb_max: [max[0] as f32, max[1] as f32, max[2] as f32],
112        cone,
113    })
114}
115
116/// f32 邻域步进:与 TS `adjacentFloat32` 一致(位操作 ±1 ulp;零值走向最小次正规数)。
117fn adjacent_float32(value: f32, direction: i32) -> f32 {
118    if value == 0.0 {
119        return if direction > 0 { f32::from_bits(1) } else { f32::from_bits(1).copysign(-1.0) };
120    }
121    let bits = value.to_bits();
122    let next = if (value > 0.0) == (direction > 0) { bits + 1 } else { bits - 1 };
123    f32::from_bits(next)
124}
125
126/// TS `nextFloat32`:fround 后若已 ≥ 原值则用之,否则向上 1 ulp。
127pub(crate) fn next_float32(value: f64) -> f32 {
128    let rounded = value as f32;
129    if !rounded.is_finite() || f64::from(rounded) >= value {
130        return rounded;
131    }
132    adjacent_float32(rounded, 1)
133}
134
135/// TS `previousFloat32`:fround 后向下 1 ulp。
136fn previous_float32(value: f64) -> f32 {
137    adjacent_float32(value as f32, -1)
138}
139
140/// 法向锥:非退化三角形单位法线的和归一化,`cutoff = min(1, min(axis·n))`(向下 1 ulp 保守)。
141fn compute_normal_cone(normals: &[[f64; 3]], disabled: bool) -> [f32; 4] {
142    if disabled || normals.is_empty() {
143        return [0.0, 0.0, 1.0, -1.0];
144    }
145    let mut sum = [0.0f64; 3];
146    for normal in normals {
147        sum[0] += normal[0];
148        sum[1] += normal[1];
149        sum[2] += normal[2];
150    }
151    let length = hypot3(sum[0], sum[1], sum[2]);
152    if length <= 1e-12 {
153        return [0.0, 0.0, 1.0, -1.0];
154    }
155    let axis: [f32; 3] = [
156        (sum[0] / length) as f32,
157        (sum[1] / length) as f32,
158        (sum[2] / length) as f32,
159    ];
160    let mut cutoff = 1.0f64;
161    for normal in normals {
162        let dot = f64::from(axis[0]) * normal[0]
163            + f64::from(axis[1]) * normal[1]
164            + f64::from(axis[2]) * normal[2];
165        cutoff = cutoff.min(dot);
166    }
167    if cutoff <= 0.0 {
168        return [axis[0], axis[1], axis[2], -1.0];
169    }
170    [axis[0], axis[1], axis[2], previous_float32(cutoff.min(1.0))]
171}
172
173/// V8 `Math.hypot` 的逐位复刻(`src/builtins/math.tq` MathHypot):
174/// 取绝对值最大值做缩放,平 f64 方和,再正确舍入 `sqrt`。
175///
176/// 与嵌套 `hypot2(hypot2(x,y),z)` 不同,该算法对 f64 的舍入路径是确定的;
177/// 法向锥轴这类和值恰好跨零的临界点上,任何 1 ulp 偏差都会翻转结果符号,
178/// golden 对拍(quick_sphere 逐位过、synthetic50k 曾因此单字段翻符号)已实证。
179#[inline]
180#[must_use]
181pub fn hypot3(x: f64, y: f64, z: f64) -> f64 {
182    js_hypot(&[x, y, z])
183}
184
185/// V8 `Math.hypot(x, y)` 双参数形态(同一 MathHypot 算法)。
186#[inline]
187#[must_use]
188pub fn hypot2(x: f64, y: f64) -> f64 {
189    js_hypot(&[x, y])
190}
191
192/// V8 MathHypot builtin 的确定性算法:max 缩放 + Σ(x/max)² + sqrt。
193#[inline]
194fn js_hypot(args: &[f64]) -> f64 {
195    let mut max = 0.0f64;
196    for &value in args {
197        if value.is_infinite() {
198            return f64::INFINITY;
199        }
200        max = if max < value.abs() { value.abs() } else { max };
201    }
202    if max == 0.0 {
203        return 0.0;
204    }
205    let mut sum = 0.0f64;
206    for &value in args {
207        let scaled = value / max;
208        sum += scaled * scaled;
209    }
210    max * sum.sqrt()
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216
217    #[test]
218    fn adjacent_float32_steps_one_ulp() {
219        let one = 1.0f32;
220        assert_eq!(adjacent_float32(one, 1).to_bits(), one.to_bits() + 1);
221        assert_eq!(adjacent_float32(one, -1).to_bits(), one.to_bits() - 1);
222        // 零值:走向最小次正规数
223        assert_eq!(adjacent_float32(0.0, 1), f32::from_bits(1));
224        assert_eq!(adjacent_float32(0.0, -1), -f32::from_bits(1));
225        // 最大正数向上溢出为无穷(TS 同语义:bits+1 翻到 Inf 编码)
226        assert!(adjacent_float32(f32::MAX, 1).is_infinite());
227    }
228
229    #[test]
230    fn next_float32_conservative_rounding() {
231        // 整数可精确表示:直接用 rounded
232        assert_eq!(next_float32(2.0), 2.0f32);
233        // 不可精确表示的 f64:向上取下一个 f32
234        let v = 1.0 + f64::EPSILON; // 略大于 1.0
235        assert!(f64::from(next_float32(v)) >= v);
236    }
237
238    #[test]
239    fn triangle_normal_rejects_collinear_and_repeated() {
240        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];
241        assert!(triangle_normal(&positions, 0, 1, 2).is_none()); // 共线
242        assert!(triangle_normal(&positions, 0, 0, 1).is_none()); // 重合顶点
243        let n = triangle_normal(&positions, 0, 1, 3).expect("valid triangle");
244        assert!((hypot3(n[0], n[1], n[2]) - 1.0).abs() < 1e-12);
245    }
246
247    #[test]
248    fn bounds_disable_cone_on_degenerate() {
249        let positions = [0.0f32, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0];
250        let bounds = compute_meshlet_bounds(&positions, &[0, 1, 2], &[], true).expect("bounds");
251        assert_eq!(bounds.cone, [0.0, 0.0, 1.0, -1.0]);
252    }
253
254    #[test]
255    fn hypot_matches_reference() {
256        assert_eq!(hypot3(3.0, 4.0, 0.0), 5.0);
257        assert_eq!(hypot2(3.0, 4.0), 5.0);
258        assert_eq!(hypot3(0.0, 0.0, 0.0), 0.0);
259        assert!(hypot3(f64::INFINITY, 1.0, 2.0).is_infinite());
260    }
261}