wow_wmo/
bsp.rs

1//! BSP tree point query for WMO groups
2//!
3//! WMO groups use BSP (Binary Space Partitioning) trees for efficient point containment
4//! and collision testing. The BSP tree partitions space using axis-aligned planes,
5//! allowing rapid determination of which faces/triangles contain a given point.
6//!
7//! # Algorithm
8//!
9//! 1. Start at the root node (index 0)
10//! 2. If the node is a leaf, collect it as a candidate
11//! 3. If the plane is Z-axis aligned, traverse both children (for height queries)
12//! 4. Otherwise, compare the point's coordinate to the plane distance and traverse
13//!    the appropriate child
14//! 5. After collecting candidate leaves, perform ray-triangle intersection
15//!    (shooting a ray in negative Z direction) to find the closest triangle
16//!
17//! # Reference
18//!
19//! Based on noclip.website's WMO BSP implementation:
20//! <https://github.com/magcius/noclip.website/blob/master/rust/src/wow/wmo.rs>
21
22use crate::types::Vec3;
23use crate::wmo_group_types::WmoBspNode;
24
25/// Axis type for BSP plane classification
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum BspAxisType {
28    /// Plane normal points along X axis
29    X,
30    /// Plane normal points along Y axis
31    Y,
32    /// Plane normal points along Z axis
33    Z,
34    /// Non-axis-aligned plane (should not occur in WMO BSP)
35    Other,
36}
37
38/// Extension trait for `WmoBspNode` to add query functionality
39pub trait BspNodeExt {
40    /// Check if this node is a leaf node
41    fn is_leaf(&self) -> bool;
42
43    /// Get the axis type of this node's plane
44    fn get_axis_type(&self) -> BspAxisType;
45
46    /// Get the negative child index (-1 means no child)
47    fn negative_child(&self) -> i16;
48
49    /// Get the positive child index (-1 means no child)
50    fn positive_child(&self) -> i16;
51}
52
53impl BspNodeExt for WmoBspNode {
54    fn is_leaf(&self) -> bool {
55        // A leaf node has face references (num_faces > 0) or both children are -1
56        self.num_faces > 0 || (self.children[0] == -1 && self.children[1] == -1)
57    }
58
59    fn get_axis_type(&self) -> BspAxisType {
60        let nx = self.plane.normal.x.abs();
61        let ny = self.plane.normal.y.abs();
62        let nz = self.plane.normal.z.abs();
63
64        // The plane is axis-aligned if one component is ~1 and others are ~0
65        const EPSILON: f32 = 0.0001;
66
67        if nx > 1.0 - EPSILON && ny < EPSILON && nz < EPSILON {
68            BspAxisType::X
69        } else if ny > 1.0 - EPSILON && nx < EPSILON && nz < EPSILON {
70            BspAxisType::Y
71        } else if nz > 1.0 - EPSILON && nx < EPSILON && ny < EPSILON {
72            BspAxisType::Z
73        } else {
74            BspAxisType::Other
75        }
76    }
77
78    fn negative_child(&self) -> i16 {
79        self.children[0]
80    }
81
82    fn positive_child(&self) -> i16 {
83        self.children[1]
84    }
85}
86
87/// BSP tree wrapper for efficient spatial queries
88#[derive(Debug, Clone)]
89pub struct BspTree {
90    nodes: Vec<WmoBspNode>,
91}
92
93impl BspTree {
94    /// Create a new BSP tree from nodes
95    pub fn new(nodes: Vec<WmoBspNode>) -> Self {
96        Self { nodes }
97    }
98
99    /// Create an empty BSP tree
100    pub fn empty() -> Self {
101        Self { nodes: Vec::new() }
102    }
103
104    /// Check if the tree is empty
105    pub fn is_empty(&self) -> bool {
106        self.nodes.is_empty()
107    }
108
109    /// Get the number of nodes
110    pub fn len(&self) -> usize {
111        self.nodes.len()
112    }
113
114    /// Get a node by index
115    pub fn get_node(&self, index: usize) -> Option<&WmoBspNode> {
116        self.nodes.get(index)
117    }
118
119    /// Query the BSP tree to find all leaf nodes that might contain the point.
120    ///
121    /// Returns indices of leaf nodes that need to be checked for triangle intersection.
122    pub fn query_point(&self, point: &[f32; 3]) -> Vec<usize> {
123        let mut leaves = Vec::new();
124        if !self.nodes.is_empty() {
125            self.query_recursive(point, 0, &mut leaves);
126        }
127        leaves
128    }
129
130    /// Recursive BSP traversal
131    fn query_recursive(&self, point: &[f32; 3], node_index: i16, leaves: &mut Vec<usize>) {
132        if node_index < 0 {
133            return;
134        }
135
136        let idx = node_index as usize;
137        if idx >= self.nodes.len() {
138            return;
139        }
140
141        let node = &self.nodes[idx];
142
143        // If this is a leaf, collect it
144        if node.is_leaf() {
145            leaves.push(idx);
146            return;
147        }
148
149        let axis = node.get_axis_type();
150
151        // For Z-axis planes, traverse both children (needed for height-based queries)
152        // This is because we want to find all triangles above/below the point
153        if axis == BspAxisType::Z {
154            self.query_recursive(point, node.negative_child(), leaves);
155            self.query_recursive(point, node.positive_child(), leaves);
156        } else {
157            // For X/Y planes, only traverse the side the point is on
158            let component = match axis {
159                BspAxisType::X => point[0],
160                BspAxisType::Y => point[1],
161                _ => {
162                    // Non-axis-aligned: use signed distance
163                    let dist = node.plane.normal.x * point[0]
164                        + node.plane.normal.y * point[1]
165                        + node.plane.normal.z * point[2]
166                        - node.plane.distance;
167                    if dist < 0.0 {
168                        self.query_recursive(point, node.negative_child(), leaves);
169                    } else {
170                        self.query_recursive(point, node.positive_child(), leaves);
171                    }
172                    return;
173                }
174            };
175
176            if component < node.plane.distance {
177                self.query_recursive(point, node.negative_child(), leaves);
178            } else {
179                self.query_recursive(point, node.positive_child(), leaves);
180            }
181        }
182    }
183
184    /// Find the closest triangle below the given point using ray-triangle intersection.
185    ///
186    /// Shoots a ray in the negative Z direction from the point and finds the
187    /// first triangle it intersects.
188    ///
189    /// # Arguments
190    ///
191    /// * `point` - The query point
192    /// * `vertices` - All vertices in the WMO group
193    /// * `indices` - Triangle indices (3 per triangle)
194    ///
195    /// # Returns
196    ///
197    /// The index of the closest triangle (in terms of triangle count, not face index),
198    /// or `None` if no triangle is below the point.
199    pub fn pick_closest_tri_neg_z(
200        &self,
201        point: &[f32; 3],
202        vertices: &[Vec3],
203        indices: &[u16],
204    ) -> Option<usize> {
205        let leaf_indices = self.query_point(point);
206
207        let mut closest_t = f32::NEG_INFINITY;
208        let mut closest_tri = None;
209
210        for leaf_idx in leaf_indices {
211            let node = &self.nodes[leaf_idx];
212            if node.num_faces == 0 {
213                continue;
214            }
215
216            // Check each face in this leaf
217            for face_offset in 0..node.num_faces {
218                let face_index = node.first_face as usize + face_offset as usize;
219                let tri_start = face_index * 3;
220
221                if tri_start + 2 >= indices.len() {
222                    continue;
223                }
224
225                let i0 = indices[tri_start] as usize;
226                let i1 = indices[tri_start + 1] as usize;
227                let i2 = indices[tri_start + 2] as usize;
228
229                if i0 >= vertices.len() || i1 >= vertices.len() || i2 >= vertices.len() {
230                    continue;
231                }
232
233                let v0 = &vertices[i0];
234                let v1 = &vertices[i1];
235                let v2 = &vertices[i2];
236
237                // Ray-triangle intersection: ray from point going in -Z direction
238                if let Some(t) = ray_triangle_intersect_neg_z(point, v0, v1, v2) {
239                    // t should be positive (triangle is below the point)
240                    // We want the closest triangle (smallest positive t)
241                    if t > 0.0 && t > closest_t {
242                        closest_t = t;
243                        closest_tri = Some(face_index);
244                    }
245                }
246            }
247        }
248
249        closest_tri
250    }
251}
252
253/// Ray-triangle intersection for a ray going in the negative Z direction.
254///
255/// Uses the Möller–Trumbore algorithm optimized for -Z direction rays.
256///
257/// # Returns
258///
259/// The t parameter (distance in Z) if intersection exists, `None` otherwise.
260fn ray_triangle_intersect_neg_z(origin: &[f32; 3], v0: &Vec3, v1: &Vec3, v2: &Vec3) -> Option<f32> {
261    // Ray direction is (0, 0, -1)
262    let edge1 = [v1.x - v0.x, v1.y - v0.y, v1.z - v0.z];
263    let edge2 = [v2.x - v0.x, v2.y - v0.y, v2.z - v0.z];
264
265    // h = dir × edge2 = (0,0,-1) × edge2
266    let h = [edge2[1], -edge2[0], 0.0];
267
268    let a = edge1[0] * h[0] + edge1[1] * h[1] + edge1[2] * h[2];
269
270    const EPSILON: f32 = 0.000001;
271    if a > -EPSILON && a < EPSILON {
272        return None; // Ray is parallel to triangle
273    }
274
275    let f = 1.0 / a;
276    let s = [origin[0] - v0.x, origin[1] - v0.y, origin[2] - v0.z];
277
278    let u = f * (s[0] * h[0] + s[1] * h[1] + s[2] * h[2]);
279    if !(0.0..=1.0).contains(&u) {
280        return None;
281    }
282
283    // q = s × edge1
284    let q = [
285        s[1] * edge1[2] - s[2] * edge1[1],
286        s[2] * edge1[0] - s[0] * edge1[2],
287        s[0] * edge1[1] - s[1] * edge1[0],
288    ];
289
290    // v = f * dot(dir, q) = f * (0*q[0] + 0*q[1] + (-1)*q[2])
291    let v = f * (-q[2]);
292    if v < 0.0 || u + v > 1.0 {
293        return None;
294    }
295
296    // t = f * dot(edge2, q)
297    let t = f * (edge2[0] * q[0] + edge2[1] * q[1] + edge2[2] * q[2]);
298
299    if t > EPSILON { Some(t) } else { None }
300}
301
302/// Check if a point is inside a WMO group using BSP tree and ray-casting
303pub fn point_in_group(point: &[f32; 3], bsp: &BspTree, vertices: &[Vec3], indices: &[u16]) -> bool {
304    bsp.pick_closest_tri_neg_z(point, vertices, indices)
305        .is_some()
306}
307
308#[cfg(test)]
309mod tests {
310    use super::*;
311    use crate::wmo_group_types::WmoPlane;
312
313    fn make_plane(axis: BspAxisType, dist: f32) -> WmoPlane {
314        let normal = match axis {
315            BspAxisType::X => Vec3 {
316                x: 1.0,
317                y: 0.0,
318                z: 0.0,
319            },
320            BspAxisType::Y => Vec3 {
321                x: 0.0,
322                y: 1.0,
323                z: 0.0,
324            },
325            BspAxisType::Z => Vec3 {
326                x: 0.0,
327                y: 0.0,
328                z: 1.0,
329            },
330            BspAxisType::Other => Vec3 {
331                x: 0.577,
332                y: 0.577,
333                z: 0.577,
334            },
335        };
336        WmoPlane {
337            normal,
338            distance: dist,
339        }
340    }
341
342    #[test]
343    fn test_bsp_node_is_leaf() {
344        let leaf = WmoBspNode {
345            plane: make_plane(BspAxisType::X, 0.0),
346            children: [-1, -1],
347            first_face: 0,
348            num_faces: 2,
349        };
350        assert!(leaf.is_leaf());
351
352        let internal = WmoBspNode {
353            plane: make_plane(BspAxisType::X, 0.0),
354            children: [1, 2],
355            first_face: 0,
356            num_faces: 0,
357        };
358        assert!(!internal.is_leaf());
359    }
360
361    #[test]
362    fn test_bsp_axis_type() {
363        let x_node = WmoBspNode {
364            plane: make_plane(BspAxisType::X, 5.0),
365            children: [-1, -1],
366            first_face: 0,
367            num_faces: 0,
368        };
369        assert_eq!(x_node.get_axis_type(), BspAxisType::X);
370
371        let y_node = WmoBspNode {
372            plane: make_plane(BspAxisType::Y, 5.0),
373            children: [-1, -1],
374            first_face: 0,
375            num_faces: 0,
376        };
377        assert_eq!(y_node.get_axis_type(), BspAxisType::Y);
378
379        let z_node = WmoBspNode {
380            plane: make_plane(BspAxisType::Z, 5.0),
381            children: [-1, -1],
382            first_face: 0,
383            num_faces: 0,
384        };
385        assert_eq!(z_node.get_axis_type(), BspAxisType::Z);
386    }
387
388    #[test]
389    fn test_empty_bsp() {
390        let bsp = BspTree::empty();
391        assert!(bsp.is_empty());
392        assert_eq!(bsp.query_point(&[0.0, 0.0, 0.0]), Vec::<usize>::new());
393    }
394
395    #[test]
396    fn test_single_leaf_bsp() {
397        let nodes = vec![WmoBspNode {
398            plane: make_plane(BspAxisType::X, 0.0),
399            children: [-1, -1],
400            first_face: 0,
401            num_faces: 1,
402        }];
403        let bsp = BspTree::new(nodes);
404
405        let leaves = bsp.query_point(&[0.0, 0.0, 0.0]);
406        assert_eq!(leaves.len(), 1);
407        assert_eq!(leaves[0], 0);
408    }
409
410    #[test]
411    fn test_simple_x_split() {
412        // Root splits at x=0, left child at index 1, right child at index 2
413        let nodes = vec![
414            WmoBspNode {
415                plane: make_plane(BspAxisType::X, 0.0),
416                children: [1, 2],
417                first_face: 0,
418                num_faces: 0,
419            },
420            WmoBspNode {
421                plane: make_plane(BspAxisType::X, 0.0),
422                children: [-1, -1],
423                first_face: 0,
424                num_faces: 1,
425            },
426            WmoBspNode {
427                plane: make_plane(BspAxisType::X, 0.0),
428                children: [-1, -1],
429                first_face: 1,
430                num_faces: 1,
431            },
432        ];
433        let bsp = BspTree::new(nodes);
434
435        // Point with x < 0 should go to negative child (index 1)
436        let leaves_neg = bsp.query_point(&[-5.0, 0.0, 0.0]);
437        assert_eq!(leaves_neg, vec![1]);
438
439        // Point with x > 0 should go to positive child (index 2)
440        let leaves_pos = bsp.query_point(&[5.0, 0.0, 0.0]);
441        assert_eq!(leaves_pos, vec![2]);
442    }
443
444    #[test]
445    fn test_z_split_traverses_both() {
446        // Z-axis split should traverse both children
447        let nodes = vec![
448            WmoBspNode {
449                plane: make_plane(BspAxisType::Z, 0.0),
450                children: [1, 2],
451                first_face: 0,
452                num_faces: 0,
453            },
454            WmoBspNode {
455                plane: make_plane(BspAxisType::X, 0.0),
456                children: [-1, -1],
457                first_face: 0,
458                num_faces: 1,
459            },
460            WmoBspNode {
461                plane: make_plane(BspAxisType::X, 0.0),
462                children: [-1, -1],
463                first_face: 1,
464                num_faces: 1,
465            },
466        ];
467        let bsp = BspTree::new(nodes);
468
469        // Should return both children for Z-split
470        let mut leaves = bsp.query_point(&[0.0, 0.0, 5.0]);
471        leaves.sort();
472        assert_eq!(leaves, vec![1, 2]);
473    }
474
475    #[test]
476    fn test_ray_triangle_intersect() {
477        // Triangle in XY plane at z=0
478        let v0 = Vec3 {
479            x: -1.0,
480            y: -1.0,
481            z: 0.0,
482        };
483        let v1 = Vec3 {
484            x: 1.0,
485            y: -1.0,
486            z: 0.0,
487        };
488        let v2 = Vec3 {
489            x: 0.0,
490            y: 1.0,
491            z: 0.0,
492        };
493
494        // Point above center of triangle
495        let hit = ray_triangle_intersect_neg_z(&[0.0, 0.0, 5.0], &v0, &v1, &v2);
496        assert!(hit.is_some());
497        assert!((hit.unwrap() - 5.0).abs() < 0.001);
498
499        // Point outside triangle bounds
500        let miss = ray_triangle_intersect_neg_z(&[10.0, 10.0, 5.0], &v0, &v1, &v2);
501        assert!(miss.is_none());
502
503        // Point below triangle (should not hit with -Z ray)
504        let below = ray_triangle_intersect_neg_z(&[0.0, 0.0, -5.0], &v0, &v1, &v2);
505        assert!(below.is_none());
506    }
507}