Skip to main content

box3d_rust/
sensor.rs

1//! Port of box3d-cpp-reference/src/sensor.c + sensor.h.
2//!
3//! SPDX-FileCopyrightText: 2025 Erin Catto
4//! SPDX-License-Identifier: MIT
5
6use crate::bitset::BitSet;
7use crate::body::get_body_transform;
8use crate::compound::overlap_compound;
9use crate::constants::MAX_SHAPE_CAST_POINTS;
10use crate::core::NULL_INDEX;
11use crate::distance::{make_proxy, ShapeProxy};
12use crate::events::{SensorBeginTouchEvent, SensorEndTouchEvent};
13use crate::geometry::{overlap_capsule, overlap_sphere, ShapeType};
14use crate::height_field::overlap_height_field;
15use crate::hull::{get_hull_points, overlap_hull};
16use crate::id::ShapeId;
17use crate::math_functions::{
18    inv_mul_transforms, min_int, to_relative_transform, transform_point, Transform, POS_ZERO,
19    TRANSFORM_IDENTITY,
20};
21use crate::mesh::{overlap_mesh, Mesh};
22use crate::shape::{shape_flags, should_shapes_collide, Shape, ShapeGeometry};
23use crate::solver_set::DISABLED_SET;
24use crate::types::BodyType;
25use crate::world::World;
26
27/// Used to track shapes that hit sensors using time of impact. (b3SensorHit)
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub struct SensorHit {
30    pub sensor_id: i32,
31    pub visitor_id: i32,
32}
33
34impl Default for SensorHit {
35    fn default() -> Self {
36        SensorHit {
37            sensor_id: NULL_INDEX,
38            visitor_id: NULL_INDEX,
39        }
40    }
41}
42
43/// (b3Visitor)
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub struct Visitor {
46    pub shape_id: i32,
47    pub generation: u16,
48}
49
50impl Default for Visitor {
51    fn default() -> Self {
52        Visitor {
53            shape_id: NULL_INDEX,
54            generation: 0,
55        }
56    }
57}
58
59/// Sensors are shapes that live in the broad-phase but never have contacts.
60/// (b3Sensor)
61#[derive(Debug, Clone, Default)]
62pub struct Sensor {
63    pub hits: Vec<Visitor>,
64    pub overlaps1: Vec<Visitor>,
65    pub overlaps2: Vec<Visitor>,
66    pub shape_id: i32,
67}
68
69impl Sensor {
70    pub fn new(shape_id: i32) -> Sensor {
71        Sensor {
72            hits: Vec::with_capacity(4),
73            overlaps1: Vec::with_capacity(16),
74            overlaps2: Vec::with_capacity(16),
75            shape_id,
76        }
77    }
78}
79
80/// (b3SensorTaskContext)
81#[derive(Debug, Clone, Default)]
82pub struct SensorTaskContext {
83    pub event_bits: BitSet,
84}
85
86/// Shape proxy for a convex visitor. (b3MakeShapeProxy)
87fn make_shape_proxy(shape: &Shape) -> ShapeProxy {
88    match &shape.geometry {
89        ShapeGeometry::Capsule(capsule) => {
90            make_proxy(&[capsule.center1, capsule.center2], capsule.radius)
91        }
92        ShapeGeometry::Sphere(sphere) => make_proxy(&[sphere.center], sphere.radius),
93        ShapeGeometry::Hull(hull) => {
94            let points = get_hull_points(hull);
95            make_proxy(points, 0.0)
96        }
97        _ => {
98            debug_assert!(false, "make_shape_proxy: unsupported visitor shape type");
99            ShapeProxy::default()
100        }
101    }
102}
103
104/// Overlap test with the visitor expressed in the sensor's local frame.
105/// (static b3OverlapSensor)
106fn overlap_sensor(
107    sensor_shape: &Shape,
108    sensor_transform: Transform,
109    visitor_shape: &Shape,
110    visitor_transform: Transform,
111) -> bool {
112    let proxy = make_shape_proxy(visitor_shape);
113    let relative_transform = inv_mul_transforms(sensor_transform, visitor_transform);
114
115    let mut local_proxy = ShapeProxy {
116        count: min_int(proxy.count, MAX_SHAPE_CAST_POINTS as i32),
117        radius: proxy.radius,
118        ..ShapeProxy::default()
119    };
120    for i in 0..local_proxy.count as usize {
121        local_proxy.points[i] = transform_point(relative_transform, proxy.points[i]);
122    }
123
124    match &sensor_shape.geometry {
125        ShapeGeometry::Capsule(capsule) => {
126            overlap_capsule(capsule, TRANSFORM_IDENTITY, &local_proxy)
127        }
128        ShapeGeometry::Compound(compound) => {
129            overlap_compound(compound, TRANSFORM_IDENTITY, &local_proxy)
130        }
131        ShapeGeometry::HeightField(hf) => {
132            overlap_height_field(hf, TRANSFORM_IDENTITY, &local_proxy)
133        }
134        ShapeGeometry::Hull(hull) => overlap_hull(hull, TRANSFORM_IDENTITY, &local_proxy),
135        ShapeGeometry::Mesh { data, scale } => {
136            let mesh = Mesh::new(data, *scale);
137            overlap_mesh(&mesh, TRANSFORM_IDENTITY, &local_proxy)
138        }
139        ShapeGeometry::Sphere(sphere) => overlap_sphere(sphere, TRANSFORM_IDENTITY, &local_proxy),
140    }
141}
142
143/// Broad-phase visitor filter + exact overlap for one candidate.
144fn sensor_accepts_visitor(
145    world: &World,
146    sensor_shape: &Shape,
147    sensor_transform: Transform,
148    visitor_shape_id: i32,
149) -> bool {
150    let sensor_shape_id = sensor_shape.id;
151    if visitor_shape_id == sensor_shape_id {
152        return false;
153    }
154
155    let other_shape = &world.shapes[visitor_shape_id as usize];
156    let other_type = other_shape.shape_type();
157    let sensor_type = sensor_shape.shape_type();
158    if (other_type == ShapeType::Mesh || other_type == ShapeType::Height)
159        && (sensor_type == ShapeType::Mesh || sensor_type == ShapeType::Height)
160    {
161        return false;
162    }
163
164    if (other_shape.flags & shape_flags::ENABLE_SENSOR_EVENTS) == 0 {
165        return false;
166    }
167    if other_shape.body_id == sensor_shape.body_id {
168        return false;
169    }
170    if !should_shapes_collide(sensor_shape.filter, other_shape.filter) {
171        return false;
172    }
173
174    if (sensor_shape.flags & shape_flags::ENABLE_CUSTOM_FILTERING) != 0
175        || (other_shape.flags & shape_flags::ENABLE_CUSTOM_FILTERING) != 0
176    {
177        if let Some(custom_filter_fcn) = world.custom_filter_fcn {
178            let id_a = ShapeId {
179                index1: sensor_shape_id + 1,
180                world0: world.world_id,
181                generation: sensor_shape.generation,
182            };
183            let id_b = ShapeId {
184                index1: visitor_shape_id + 1,
185                world0: world.world_id,
186                generation: other_shape.generation,
187            };
188            if !custom_filter_fcn(world, id_a, id_b, world.custom_filter_context) {
189                return false;
190            }
191        }
192    }
193
194    let other_transform =
195        to_relative_transform(get_body_transform(world, other_shape.body_id), POS_ZERO);
196    overlap_sensor(sensor_shape, sensor_transform, other_shape, other_transform)
197}
198
199/// Serial sensor overlap pass for `[start, end)`. (static b3SensorTask)
200fn sensor_task(world: &mut World, start_index: usize, end_index: usize) {
201    debug_assert!(start_index < end_index);
202
203    for sensor_index in start_index..end_index {
204        {
205            let sensor = &mut world.sensors[sensor_index];
206            std::mem::swap(&mut sensor.overlaps1, &mut sensor.overlaps2);
207            sensor.overlaps2.clear();
208            sensor.overlaps2.append(&mut sensor.hits);
209        }
210
211        let shape_id = world.sensors[sensor_index].shape_id;
212        let body_id = world.shapes[shape_id as usize].body_id;
213        let body_set_index = world.bodies[body_id as usize].set_index;
214        let sensor_events_enabled =
215            (world.shapes[shape_id as usize].flags & shape_flags::ENABLE_SENSOR_EVENTS) != 0;
216
217        if body_set_index == DISABLED_SET || !sensor_events_enabled {
218            if !world.sensors[sensor_index].overlaps1.is_empty() {
219                world.sensor_task_contexts[0]
220                    .event_bits
221                    .set_bit(sensor_index as u32);
222            }
223            continue;
224        }
225
226        let transform = to_relative_transform(get_body_transform(world, body_id), POS_ZERO);
227        debug_assert!(world.shapes[shape_id as usize].sensor_index == sensor_index as i32);
228
229        let query_bounds = world.shapes[shape_id as usize].aabb;
230        let mask_bits = world.shapes[shape_id as usize].filter.mask_bits;
231
232        let mut candidates = Vec::new();
233        for tree_index in 0..BodyType::Dynamic as usize + 1 {
234            world.broad_phase.trees[tree_index].query(
235                query_bounds,
236                mask_bits,
237                false,
238                |_proxy_id, user_data| {
239                    candidates.push(user_data as i32);
240                    true
241                },
242            );
243        }
244
245        for visitor_shape_id in candidates {
246            let accepted = {
247                let sensor_shape = &world.shapes[shape_id as usize];
248                sensor_accepts_visitor(world, sensor_shape, transform, visitor_shape_id)
249            };
250            if accepted {
251                let generation = world.shapes[visitor_shape_id as usize].generation;
252                world.sensors[sensor_index].overlaps2.push(Visitor {
253                    shape_id: visitor_shape_id,
254                    generation,
255                });
256            }
257        }
258
259        world.sensors[sensor_index]
260            .overlaps2
261            .sort_unstable_by_key(|a| a.shape_id);
262
263        {
264            let overlaps = &mut world.sensors[sensor_index].overlaps2;
265            let mut unique_count = 0usize;
266            for i in 0..overlaps.len() {
267                if unique_count == 0 || overlaps[i].shape_id != overlaps[unique_count - 1].shape_id
268                {
269                    overlaps[unique_count] = overlaps[i];
270                    unique_count += 1;
271                }
272            }
273            overlaps.truncate(unique_count);
274        }
275
276        let count1 = world.sensors[sensor_index].overlaps1.len();
277        let count2 = world.sensors[sensor_index].overlaps2.len();
278        if count1 != count2 {
279            world.sensor_task_contexts[0]
280                .event_bits
281                .set_bit(sensor_index as u32);
282        } else {
283            let changed = (0..count1).any(|i| {
284                let s1 = world.sensors[sensor_index].overlaps1[i];
285                let s2 = world.sensors[sensor_index].overlaps2[i];
286                s1.shape_id != s2.shape_id || s1.generation != s2.generation
287            });
288            if changed {
289                world.sensor_task_contexts[0]
290                    .event_bits
291                    .set_bit(sensor_index as u32);
292            }
293        }
294    }
295}
296
297fn publish_sensor_events(world: &mut World) {
298    let bits: Vec<u64> = {
299        let bit_set = &world.sensor_task_contexts[0].event_bits;
300        (0..bit_set.block_count())
301            .map(|k| bit_set.block(k))
302            .collect()
303    };
304    let world_id = world.world_id;
305    let end_index = world.end_event_array_index as usize;
306
307    for (k, mut word) in bits.into_iter().enumerate() {
308        while word != 0 {
309            let ctz = word.trailing_zeros();
310            let sensor_index = (64 * k as u32 + ctz) as usize;
311
312            let shape_id = world.sensors[sensor_index].shape_id;
313            let sensor_generation = world.shapes[shape_id as usize].generation;
314            let sensor_id = ShapeId {
315                index1: shape_id + 1,
316                world0: world_id,
317                generation: sensor_generation,
318            };
319
320            let count1 = world.sensors[sensor_index].overlaps1.len();
321            let count2 = world.sensors[sensor_index].overlaps2.len();
322
323            let mut index1 = 0usize;
324            let mut index2 = 0usize;
325            while index1 < count1 && index2 < count2 {
326                let r1 = world.sensors[sensor_index].overlaps1[index1];
327                let r2 = world.sensors[sensor_index].overlaps2[index2];
328                if r1.shape_id == r2.shape_id {
329                    if r1.generation < r2.generation {
330                        world.sensor_end_events[end_index].push(SensorEndTouchEvent {
331                            sensor_shape_id: sensor_id,
332                            visitor_shape_id: ShapeId {
333                                index1: r1.shape_id + 1,
334                                world0: world_id,
335                                generation: r1.generation,
336                            },
337                        });
338                        index1 += 1;
339                    } else if r1.generation > r2.generation {
340                        world.sensor_begin_events.push(SensorBeginTouchEvent {
341                            sensor_shape_id: sensor_id,
342                            visitor_shape_id: ShapeId {
343                                index1: r2.shape_id + 1,
344                                world0: world_id,
345                                generation: r2.generation,
346                            },
347                        });
348                        index2 += 1;
349                    } else {
350                        index1 += 1;
351                        index2 += 1;
352                    }
353                } else if r1.shape_id < r2.shape_id {
354                    world.sensor_end_events[end_index].push(SensorEndTouchEvent {
355                        sensor_shape_id: sensor_id,
356                        visitor_shape_id: ShapeId {
357                            index1: r1.shape_id + 1,
358                            world0: world_id,
359                            generation: r1.generation,
360                        },
361                    });
362                    index1 += 1;
363                } else {
364                    world.sensor_begin_events.push(SensorBeginTouchEvent {
365                        sensor_shape_id: sensor_id,
366                        visitor_shape_id: ShapeId {
367                            index1: r2.shape_id + 1,
368                            world0: world_id,
369                            generation: r2.generation,
370                        },
371                    });
372                    index2 += 1;
373                }
374            }
375
376            while index1 < count1 {
377                let r1 = world.sensors[sensor_index].overlaps1[index1];
378                world.sensor_end_events[end_index].push(SensorEndTouchEvent {
379                    sensor_shape_id: sensor_id,
380                    visitor_shape_id: ShapeId {
381                        index1: r1.shape_id + 1,
382                        world0: world_id,
383                        generation: r1.generation,
384                    },
385                });
386                index1 += 1;
387            }
388
389            while index2 < count2 {
390                let r2 = world.sensors[sensor_index].overlaps2[index2];
391                world.sensor_begin_events.push(SensorBeginTouchEvent {
392                    sensor_shape_id: sensor_id,
393                    visitor_shape_id: ShapeId {
394                        index1: r2.shape_id + 1,
395                        world0: world_id,
396                        generation: r2.generation,
397                    },
398                });
399                index2 += 1;
400            }
401
402            word &= word - 1;
403        }
404    }
405}
406
407/// Query all sensors for overlaps and emit begin/end events. (b3OverlapSensors)
408pub fn overlap_sensors(world: &mut World) {
409    let sensor_count = world.sensors.len();
410    if sensor_count == 0 {
411        return;
412    }
413
414    debug_assert!(!world.sensor_task_contexts.is_empty());
415    world.sensor_task_contexts[0]
416        .event_bits
417        .set_bit_count_and_clear(sensor_count as u32);
418
419    sensor_task(world, 0, sensor_count);
420    publish_sensor_events(world);
421}
422
423/// Flush pending end events and remove a sensor from the dense array.
424/// (b3DestroySensor)
425pub fn destroy_sensor(world: &mut World, sensor_shape_id: i32) {
426    let sensor_index = world.shapes[sensor_shape_id as usize].sensor_index;
427    debug_assert!(sensor_index != NULL_INDEX);
428
429    let world_id = world.world_id;
430    let generation = world.shapes[sensor_shape_id as usize].generation;
431    let end_index = world.end_event_array_index as usize;
432
433    let overlaps: Vec<_> = world.sensors[sensor_index as usize].overlaps2.clone();
434    for visitor in overlaps {
435        world.sensor_end_events[end_index].push(SensorEndTouchEvent {
436            sensor_shape_id: ShapeId {
437                index1: sensor_shape_id + 1,
438                world0: world_id,
439                generation,
440            },
441            visitor_shape_id: ShapeId {
442                index1: visitor.shape_id + 1,
443                world0: world_id,
444                generation: visitor.generation,
445            },
446        });
447    }
448
449    world.sensors[sensor_index as usize].hits.clear();
450    world.sensors[sensor_index as usize].overlaps1.clear();
451    world.sensors[sensor_index as usize].overlaps2.clear();
452
453    let last = world.sensors.len() as i32 - 1;
454    world.sensors.swap_remove(sensor_index as usize);
455    if sensor_index < last {
456        let moved_shape_id = world.sensors[sensor_index as usize].shape_id;
457        world.shapes[moved_shape_id as usize].sensor_index = sensor_index;
458    }
459}