Skip to main content

geometry_dag/
simplify.rs

1//! 确定性网格聚类简化:坐标量化到 cell,每 cell 均值代表点,退化/重复三角形剔除。
2//!
3//! 与 TS 权威实现(`packages/deep-engine/src/geometry/meshletDag.ts` 的
4//! `clusterSimplify`)逐位对拍。确定性来自遍历序而非哈希序:
5//! - cell key 为 `(i64,i64,i64)`(`Math.round(x)=floor(x+0.5)` 语义);
6//! - 输出顶点序 = 源顶点序中 cell 首次出现序;
7//! - 输出三角形序 = 源三角形序(去重保留首份)。
8//!
9//! 全部几何量在 f64 下运算后按 `as f32` 截断(TS Float32Array 语义)。
10
11use crate::error::{DagError, DagResult};
12use crate::types::rustc_hash_lite::{FxHashMap, FxHashSet};
13
14/// cell key 上限(2^62):防病态输入下 `as i64` 饱和静默合并不同 cell。
15const CELL_KEY_LIMIT: f64 = 4_611_686_018_427_387_904.0; // 2^62
16
17/// 聚类简化结果(与 TS `ClusterSimplifyResult` 同构)。
18#[derive(Debug, Clone)]
19pub struct ClusterSimplifyResult {
20    /// 代表点位置(f32,由 f64 均值截断)。
21    pub positions: Vec<f32>,
22    /// 简化后索引。
23    pub indices: Vec<u32>,
24    /// 每输出三角形 → 当前层输入三角形索引(父子归属用)。
25    pub source_triangles: Vec<u32>,
26    /// 顶点相对代表点的最大位移(世界单位,f64 真误差场)。
27    pub max_displacement: f64,
28}
29
30/// TS 去重键的 Rust 形态:字符串 `${a}_${b}_${c}` 的排列组合在无分隔符歧义下
31/// 与 `(u32,u32,u32)` 元组一一对应,故直接用元组承载。
32/// 分支结构逐字对应 `meshletDag.ts` 的嵌套三元表达式(见 [`cluster_simplify`] 内注释)。
33fn ts_dedup_key(a: u32, b: u32, c: u32) -> (u32, u32, u32) {
34    if a < b {
35        if b < c {
36            (a, b, c)
37        } else if a < c {
38            (a, c, b)
39        } else {
40            (c, a, b)
41        }
42    } else if b < c {
43        (b, c, a)
44    } else if a < c {
45        (b, a, c)
46    } else {
47        (c, b, a)
48    }
49}
50
51/// 执行一轮聚类简化。`factor <= 1` 时原样返回(误差 0)。
52///
53/// # Errors
54/// 量化坐标超出 i64 安全范围(病态数值跨度)时返回 [`DagError::Overflow`]。
55pub fn cluster_simplify(positions: &[f32], indices: &[u32], factor: f64) -> DagResult<ClusterSimplifyResult> {
56    if factor <= 1.0 {
57        return Ok(ClusterSimplifyResult {
58            positions: positions.to_vec(),
59            indices: indices.to_vec(),
60            source_triangles: (0..indices.len() as u32 / 3).collect(),
61            max_displacement: 0.0,
62        });
63    }
64
65    // 源包围盒(f64 精度读取 f32 值)。
66    let mut min = [f64::INFINITY; 3];
67    let mut max = [f64::NEG_INFINITY; 3];
68    for tri in positions.chunks_exact(3) {
69        for (axis, &value) in tri.iter().enumerate() {
70            let value = f64::from(value);
71            min[axis] = min[axis].min(value);
72            max[axis] = max[axis].max(value);
73        }
74    }
75    // JS `Math.max(...) || 1`:0 与 NaN 都回退为 1。
76    let extent_raw = (max[0] - min[0]).max((max[1] - min[1]).max(max[2] - min[2]));
77    let extent = if extent_raw == 0.0 || extent_raw.is_nan() { 1.0 } else { extent_raw };
78    let cell = extent / f64::max(4.0, 24.0 / factor);
79
80    let cell_of = |x: f64, y: f64, z: f64| -> DagResult<(i64, i64, i64)> {
81        let quantize = |v: f64| -> DagResult<i64> {
82            // JS Math.round(v) = floor(v + 0.5)。
83            let rounded = (v + 0.5).floor();
84            if !rounded.is_finite() || rounded.abs() >= CELL_KEY_LIMIT {
85                return Err(DagError::overflow(
86                    "cluster cell coordinate exceeds addressable range (degenerate cell size?)",
87                ));
88            }
89            Ok(rounded as i64)
90        };
91        Ok((
92            quantize((x - min[0]) / cell)?,
93            quantize((y - min[1]) / cell)?,
94            quantize((z - min[2]) / cell)?,
95        ))
96    };
97
98    // 第一遍:每 cell 聚合均值代表点。
99    let mut rep: FxHashMap<(i64, i64, i64), [f64; 4]> = FxHashMap::default();
100    for tri in positions.chunks_exact(3) {
101        let key = cell_of(f64::from(tri[0]), f64::from(tri[1]), f64::from(tri[2]))?;
102        let bucket = rep.entry(key).or_insert([0.0; 4]);
103        bucket[0] += f64::from(tri[0]);
104        bucket[1] += f64::from(tri[1]);
105        bucket[2] += f64::from(tri[2]);
106        bucket[3] += 1.0;
107    }
108
109    // 第二遍:全网格最大位移(真误差场)。
110    let mut max_displacement = 0.0f64;
111    for tri in positions.chunks_exact(3) {
112        let key = cell_of(f64::from(tri[0]), f64::from(tri[1]), f64::from(tri[2]))?;
113        if let Some(bucket) = rep.get(&key) {
114            if bucket[3] == 0.0 {
115                continue;
116            }
117            let dx = f64::from(tri[0]) - bucket[0] / bucket[3];
118            let dy = f64::from(tri[1]) - bucket[1] / bucket[3];
119            let dz = f64::from(tri[2]) - bucket[2] / bucket[3];
120            max_displacement = max_displacement.max(crate::bounds::hypot3(dx, dy, dz));
121        }
122    }
123
124    // 第三遍:输出顶点表(源顶点序中 cell 首次出现序)。
125    // TS 语义关键点:JS 侧第四遍的面积计算读的是 f64 的 number 数组(未截断),
126    // `Float32Array.from` 只发生在函数返回时。因此这里同时维护 f64 表(判定用)
127    // 与 f32 表(返回用),两者必须在代表点上一致。
128    let mut out_positions: Vec<f32> = Vec::new();
129    let mut out_positions_f64: Vec<f64> = Vec::new();
130    let mut cell_index: FxHashMap<(i64, i64, i64), u32> = FxHashMap::default();
131    let mut remap: FxHashMap<u32, u32> = FxHashMap::default();
132    for (vertex, tri) in positions.chunks_exact(3).enumerate() {
133        let key = cell_of(f64::from(tri[0]), f64::from(tri[1]), f64::from(tri[2]))?;
134        let local = *cell_index.entry(key).or_insert_with(|| {
135            let bucket = &rep[&key];
136            // TS 语义:local = outPositions.length / 3(顶点号,不是元素槽位)。
137            let local = (out_positions.len() / 3) as u32;
138            let (rx, ry, rz) = (bucket[0] / bucket[3], bucket[1] / bucket[3], bucket[2] / bucket[3]);
139            out_positions_f64.extend_from_slice(&[rx, ry, rz]);
140            out_positions.push(rx as f32);
141            out_positions.push(ry as f32);
142            out_positions.push(rz as f32);
143            local
144        });
145        remap.insert(vertex as u32, local);
146    }
147
148    // 第四遍:三角形重映射 + 退化剔除 + 排序键去重(面积判定用 f64 代表点,见上)。
149    let mut out_indices: Vec<u32> = Vec::new();
150    let mut source_triangles: Vec<u32> = Vec::new();
151    let mut seen: FxHashSet<(u32, u32, u32)> = FxHashSet::default();
152    for (source, face) in indices.chunks_exact(3).enumerate() {
153        let a = remap[&face[0]];
154        let b = remap[&face[1]];
155        let c = remap[&face[2]];
156        let (ax, ay, az) = (
157            out_positions_f64[a as usize * 3],
158            out_positions_f64[a as usize * 3 + 1],
159            out_positions_f64[a as usize * 3 + 2],
160        );
161        let (bx, by, bz) = (
162            out_positions_f64[b as usize * 3],
163            out_positions_f64[b as usize * 3 + 1],
164            out_positions_f64[b as usize * 3 + 2],
165        );
166        let (cx, cy, cz) = (
167            out_positions_f64[c as usize * 3],
168            out_positions_f64[c as usize * 3 + 1],
169            out_positions_f64[c as usize * 3 + 2],
170        );
171        let (e1x, e1y, e1z) = (bx - ax, by - ay, bz - az);
172        let (e2x, e2y, e2z) = (cx - ax, cy - ay, cz - az);
173        let n1 = e1y * e2z - e1z * e2y;
174        let n2 = e1z * e2x - e1x * e2z;
175        let n3 = e1x * e2y - e1y * e2x;
176        let area2 = n1 * n1 + n2 * n2 + n3 * n3;
177        if a == b || b == c || a == c || area2 <= 1e-24 {
178            #[cfg(feature = "dbg_dropped")]
179            eprintln!("RUST drop face {source}: a={a} b={b} c={c} area2={area2:e} degenerateVtx={}", a == b || b == c || a == c);
180            continue; // 聚类合并出的退化/共线三角形剔除
181        }
182        #[cfg(feature = "dbg_dropped")]
183        if seen.contains(&ts_dedup_key(a, b, c)) {
184            eprintln!("RUST drop face {source}: dup key {:?}", ts_dedup_key(a, b, c));
185        }
186        // 去重键 = TS 字符串键的元组形态(生成逻辑见下方 ts_dedup_key 文档)。
187        let key = ts_dedup_key(a, b, c);
188        // TS `!(a<b) && b<c` 分支无条件给 `${b}_${c}_${a}`,当 a<c 时这不是排序序
189        // (例: (242,240,274) → "240_274_242" vs (274,240,242) → "240_242_274",
190        // 两者排序后同键但字符串不同)——权威 golden 的去重行为因此与"排序键去重"
191        // 不同,这里必须保持逐字一致,不得"修正"为完美排序。
192        if !seen.insert(key) {
193            continue; // 不同源三角形塌成同一目标三角形时只保留一份
194        }
195        #[cfg(feature = "dbg_dropped")]
196        eprintln!("RUST keep face {source}: key {a}_{b}_{c} dgcKey={key:?}");
197        source_triangles.push(source as u32);
198        out_indices.extend_from_slice(&[a, b, c]);
199    }
200
201    Ok(ClusterSimplifyResult {
202        positions: out_positions,
203        indices: out_indices,
204        source_triangles,
205        max_displacement,
206    })
207}
208
209/// 测试专用:确定性经纬球网格(与 TS 单测的 sphereGeometry 同构,无 jitter)。
210#[cfg(test)]
211pub(crate) mod test_support {
212    /// 生成经纬细分球(顶点序:ring 主序、segment 次序;三角形:上半/下半各一)。
213    pub(crate) fn test_sphere(segments: usize, rings: usize) -> (Vec<f32>, Vec<u32>) {
214        let mut positions = Vec::new();
215        let mut indices = Vec::new();
216        for r in 0..=rings {
217            let phi = (r as f64 / rings as f64) * std::f64::consts::PI;
218            for s in 0..=segments {
219                let theta = (s as f64 / segments as f64) * std::f64::consts::PI * 2.0;
220                positions.push((phi.sin() * theta.cos()) as f32);
221                positions.push(phi.cos() as f32);
222                positions.push((phi.sin() * theta.sin()) as f32);
223            }
224        }
225        let row = segments + 1;
226        for r in 0..rings {
227            for s in 0..segments {
228                let a = (r * row + s) as u32;
229                let b = a + 1;
230                let c = a + row as u32;
231                let d = c + 1;
232                if r > 0 {
233                    indices.extend_from_slice(&[a, c, b]);
234                }
235                if r < rings - 1 {
236                    indices.extend_from_slice(&[b, c, d]);
237                }
238            }
239        }
240        (positions, indices)
241    }
242}
243
244#[cfg(test)]
245mod tests {
246    use super::test_support::test_sphere;
247    use super::*;
248
249    #[test]
250    fn factor_le_one_is_identity() {
251        let positions = vec![0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0];
252        let indices = vec![0, 1, 2];
253        let r = cluster_simplify(&positions, &indices, 1.0).expect("identity");
254        assert_eq!(r.positions, positions);
255        assert_eq!(r.indices, indices);
256        assert_eq!(r.source_triangles, [0]);
257        assert_eq!(r.max_displacement, 0.0);
258    }
259
260    #[test]
261    fn degenerate_faces_are_dropped() {
262        // 共线三角形 + 重合顶点三角形:全部剔除。
263        let positions = vec![0.0f32, 0.0, 0.0, 1.0, 0.0, 0.0, 2.0, 0.0, 0.0];
264        let indices = vec![0, 1, 2, 0, 1, 2];
265        let r = cluster_simplify(&positions, &indices, 2.0).expect("simplify");
266        assert!(r.indices.is_empty());
267        assert!(r.source_triangles.is_empty());
268    }
269
270    #[test]
271    fn merging_cells_dedup_follows_ts_key_quirk() {
272        // TS 键的 quirk 实证:(0,1,2)→(0,1,2)、(0,2,3)→(0,2,3)、(1,0,2)→(0,2,1),
273        // 三键互不相同 → 三面全保留(完美排序键下第三面会与第一面撞键被剔)。
274        let positions = vec![0.0f32, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 0.0];
275        let indices = vec![0, 1, 2, 0, 2, 3, 1, 0, 2];
276        let r = cluster_simplify(&positions, &indices, 2.0).expect("simplify");
277        assert_eq!(r.indices.len() / 3, 3, "TS quirk keys differ for all three faces");
278        assert_eq!(r.source_triangles, [0, 1, 2]);
279        // 同键重复 (0,1,2)/(0,1,2) 仍然去重:键同 → 只留首份。
280        let indices = vec![0, 1, 2, 0, 1, 2];
281        let r = cluster_simplify(&positions, &indices, 2.0).expect("simplify");
282        assert_eq!(r.indices.len() / 3, 1);
283        assert_eq!(r.source_triangles, [0]);
284    }
285
286    #[test]
287    fn ts_dedup_key_matches_ts_string_semantics() {
288        // 每个 case 的期望 = TS 模板串逐字求值(非排序序)。
289        assert_eq!(ts_dedup_key(1, 2, 3), (1, 2, 3)); // a<b,b<c: "1_2_3"
290        assert_eq!(ts_dedup_key(1, 3, 2), (1, 2, 3)); // a<b,!(b<c),a<c: "1_2_3"
291        assert_eq!(ts_dedup_key(3, 1, 2), (1, 2, 3)); // !(a<b),b<c: "1_2_3"
292        assert_eq!(ts_dedup_key(2, 1, 3), (1, 3, 2)); // !(a<b),b<c: "1_3_2"
293        assert_eq!(ts_dedup_key(2, 3, 1), (1, 2, 3)); // a<b,!(b<c),!(a<c): "1_2_3"
294        assert_eq!(ts_dedup_key(3, 2, 1), (1, 2, 3)); // !(a<b),!(b<c),!(a<c): "1_2_3"
295        assert_eq!(ts_dedup_key(242, 240, 274), (240, 274, 242)); // golden 实证 quirk 面
296        assert_eq!(ts_dedup_key(274, 240, 242), (240, 242, 274)); // 同排序不同键 → 不去重
297    }
298
299    #[test]
300    fn dedup_picks_the_earliest_source_triangle() {
301        // 两个全等三角形(不同源)只留一份,source 指向最早出现的。
302        let positions = vec![0.0f32, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0];
303        let indices = vec![0, 1, 2, 0, 1, 2];
304        let r = cluster_simplify(&positions, &indices, 2.0).expect("simplify");
305        assert_eq!(r.indices.len() / 3, 1);
306        assert_eq!(r.source_triangles, [0]);
307    }
308
309    #[test]
310    fn empty_mesh_is_safe() {
311        let r = cluster_simplify(&[], &[], 2.0).expect("empty");
312        assert!(r.positions.is_empty());
313        assert_eq!(r.max_displacement, 0.0);
314    }
315
316    #[test]
317    fn error_field_tracks_displacement() {
318        // 细分球上 factor=2 的位移必为正。
319        let (positions, indices) = test_sphere(24, 12);
320        let r = cluster_simplify(&positions, &indices, 2.0).expect("simplify");
321        assert!(r.max_displacement > 0.0);
322        assert!(r.indices.len() / 3 < indices.len() / 3, "cluster must reduce triangles");
323    }
324
325}