Skip to content

Commit 3045a69

Browse files
committed
[add] physics collision filter backend support
1 parent 7ffecad commit 3045a69

4 files changed

Lines changed: 208 additions & 0 deletions

File tree

crates/lambda-rs-platform/src/physics/rapier2d.rs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,8 @@ impl PhysicsBackend2D {
304304
/// - `density`: The density in kg/m².
305305
/// - `friction`: The friction coefficient (unitless).
306306
/// - `restitution`: The restitution coefficient in `[0.0, 1.0]`.
307+
/// - `collision_group`: The collider membership bitfield.
308+
/// - `collision_mask`: The collider interaction mask bitfield.
307309
///
308310
/// # Returns
309311
/// Returns a `(slot_index, slot_generation)` pair for the created collider.
@@ -322,6 +324,8 @@ impl PhysicsBackend2D {
322324
density: f32,
323325
friction: f32,
324326
restitution: f32,
327+
collision_group: u32,
328+
collision_mask: u32,
325329
) -> Result<(u32, u32), Collider2DBackendError> {
326330
return self.attach_collider_2d(
327331
parent_slot_index,
@@ -332,6 +336,8 @@ impl PhysicsBackend2D {
332336
density,
333337
friction,
334338
restitution,
339+
collision_group,
340+
collision_mask,
335341
);
336342
}
337343

@@ -350,6 +356,8 @@ impl PhysicsBackend2D {
350356
/// - `density`: The density in kg/m².
351357
/// - `friction`: The friction coefficient (unitless).
352358
/// - `restitution`: The restitution coefficient in `[0.0, 1.0]`.
359+
/// - `collision_group`: The collider membership bitfield.
360+
/// - `collision_mask`: The collider interaction mask bitfield.
353361
///
354362
/// # Returns
355363
/// Returns a `(slot_index, slot_generation)` pair for the created collider.
@@ -369,6 +377,8 @@ impl PhysicsBackend2D {
369377
density: f32,
370378
friction: f32,
371379
restitution: f32,
380+
collision_group: u32,
381+
collision_mask: u32,
372382
) -> Result<(u32, u32), Collider2DBackendError> {
373383
return self.attach_collider_2d(
374384
parent_slot_index,
@@ -379,6 +389,8 @@ impl PhysicsBackend2D {
379389
density,
380390
friction,
381391
restitution,
392+
collision_group,
393+
collision_mask,
382394
);
383395
}
384396

@@ -398,6 +410,8 @@ impl PhysicsBackend2D {
398410
/// - `density`: The density in kg/m².
399411
/// - `friction`: The friction coefficient (unitless).
400412
/// - `restitution`: The restitution coefficient in `[0.0, 1.0]`.
413+
/// - `collision_group`: The collider membership bitfield.
414+
/// - `collision_mask`: The collider interaction mask bitfield.
401415
///
402416
/// # Returns
403417
/// Returns a `(slot_index, slot_generation)` pair for the created collider.
@@ -417,6 +431,8 @@ impl PhysicsBackend2D {
417431
density: f32,
418432
friction: f32,
419433
restitution: f32,
434+
collision_group: u32,
435+
collision_mask: u32,
420436
) -> Result<(u32, u32), Collider2DBackendError> {
421437
let rapier_builder = if half_height == 0.0 {
422438
ColliderBuilder::ball(radius)
@@ -433,6 +449,8 @@ impl PhysicsBackend2D {
433449
density,
434450
friction,
435451
restitution,
452+
collision_group,
453+
collision_mask,
436454
);
437455
}
438456

@@ -452,6 +470,8 @@ impl PhysicsBackend2D {
452470
/// - `density`: The density in kg/m².
453471
/// - `friction`: The friction coefficient (unitless).
454472
/// - `restitution`: The restitution coefficient in `[0.0, 1.0]`.
473+
/// - `collision_group`: The collider membership bitfield.
474+
/// - `collision_mask`: The collider interaction mask bitfield.
455475
///
456476
/// # Returns
457477
/// Returns a `(slot_index, slot_generation)` pair for the created collider.
@@ -472,6 +492,8 @@ impl PhysicsBackend2D {
472492
density: f32,
473493
friction: f32,
474494
restitution: f32,
495+
collision_group: u32,
496+
collision_mask: u32,
475497
) -> Result<(u32, u32), Collider2DBackendError> {
476498
let rapier_vertices: Vec<Vector> = vertices
477499
.iter()
@@ -493,6 +515,8 @@ impl PhysicsBackend2D {
493515
density,
494516
friction,
495517
restitution,
518+
collision_group,
519+
collision_mask,
496520
);
497521
}
498522

@@ -1018,6 +1042,8 @@ impl PhysicsBackend2D {
10181042
/// - `density`: The requested density in kg/m².
10191043
/// - `friction`: The friction coefficient (unitless).
10201044
/// - `restitution`: The restitution coefficient in `[0.0, 1.0]`.
1045+
/// - `collision_group`: The collider membership bitfield.
1046+
/// - `collision_mask`: The collider interaction mask bitfield.
10211047
///
10221048
/// # Returns
10231049
/// Returns a `(slot_index, slot_generation)` pair for the created collider.
@@ -1035,13 +1061,17 @@ impl PhysicsBackend2D {
10351061
density: f32,
10361062
friction: f32,
10371063
restitution: f32,
1064+
collision_group: u32,
1065+
collision_mask: u32,
10381066
) -> Result<(u32, u32), Collider2DBackendError> {
10391067
let (rapier_parent_handle, rapier_density) = self
10401068
.prepare_parent_body_for_collider_attachment_2d(
10411069
parent_slot_index,
10421070
parent_slot_generation,
10431071
density,
10441072
)?;
1073+
let interaction_groups =
1074+
build_collision_groups_2d(collision_group, collision_mask);
10451075

10461076
let rapier_collider = rapier_builder
10471077
.translation(Vector::new(local_offset[0], local_offset[1]))
@@ -1051,6 +1081,8 @@ impl PhysicsBackend2D {
10511081
.friction_combine_rule(CoefficientCombineRule::Multiply)
10521082
.restitution(restitution)
10531083
.restitution_combine_rule(CoefficientCombineRule::Max)
1084+
.collision_groups(interaction_groups)
1085+
.solver_groups(interaction_groups)
10541086
.build();
10551087

10561088
let rapier_handle = self.colliders.insert_with_parent(
@@ -1251,6 +1283,25 @@ fn build_rapier_rigid_body(
12511283
}
12521284
}
12531285

1286+
/// Converts public collision filter bitfields into Rapier interaction groups.
1287+
///
1288+
/// # Arguments
1289+
/// - `collision_group`: The collider membership bitfield.
1290+
/// - `collision_mask`: The collider interaction mask bitfield.
1291+
///
1292+
/// # Returns
1293+
/// Returns Rapier interaction groups using AND-based matching.
1294+
fn build_collision_groups_2d(
1295+
collision_group: u32,
1296+
collision_mask: u32,
1297+
) -> InteractionGroups {
1298+
return InteractionGroups::new(
1299+
Group::from_bits_retain(collision_group),
1300+
Group::from_bits_retain(collision_mask),
1301+
InteractionTestMode::And,
1302+
);
1303+
}
1304+
12541305
/// Validates a 2D position.
12551306
///
12561307
/// # Arguments

crates/lambda-rs/src/physics/collider_2d.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,8 @@ impl Collider2DBuilder {
345345
self.material.density(),
346346
self.material.friction(),
347347
self.material.restitution(),
348+
self.collision_filter.group,
349+
self.collision_filter.mask,
348350
)
349351
.map_err(map_backend_error)?,
350352
ColliderShape2D::Rectangle {
@@ -362,6 +364,8 @@ impl Collider2DBuilder {
362364
self.material.density(),
363365
self.material.friction(),
364366
self.material.restitution(),
367+
self.collision_filter.group,
368+
self.collision_filter.mask,
365369
)
366370
.map_err(map_backend_error)?,
367371
ColliderShape2D::Capsule {
@@ -379,6 +383,8 @@ impl Collider2DBuilder {
379383
self.material.density(),
380384
self.material.friction(),
381385
self.material.restitution(),
386+
self.collision_filter.group,
387+
self.collision_filter.mask,
382388
)
383389
.map_err(map_backend_error)?,
384390
ColliderShape2D::ConvexPolygon { vertices } => world
@@ -392,6 +398,8 @@ impl Collider2DBuilder {
392398
self.material.density(),
393399
self.material.friction(),
394400
self.material.restitution(),
401+
self.collision_filter.group,
402+
self.collision_filter.mask,
395403
)
396404
.map_err(map_backend_error)?,
397405
};
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
//! 2D collision filter integration tests.
2+
//!
3+
//! These tests validate that collider group and mask settings affect physical
4+
//! contact generation through the public API.
5+
6+
use lambda::physics::{
7+
Collider2DBuilder,
8+
CollisionFilter,
9+
PhysicsWorld2D,
10+
PhysicsWorld2DBuilder,
11+
RigidBody2D,
12+
RigidBody2DBuilder,
13+
RigidBodyType,
14+
};
15+
16+
const DEFAULT_STEP_COUNT: u32 = 240;
17+
18+
/// Steps a world forward for the given number of fixed timesteps.
19+
///
20+
/// # Arguments
21+
/// - `world`: The world to step.
22+
/// - `steps`: The number of steps to execute.
23+
///
24+
/// # Returns
25+
/// Returns `()` after stepping the world.
26+
fn step_world(world: &mut PhysicsWorld2D, steps: u32) {
27+
for _ in 0..steps {
28+
world.step();
29+
}
30+
31+
return;
32+
}
33+
34+
/// Creates a static ground body with the provided collision filter.
35+
///
36+
/// # Arguments
37+
/// - `world`: The world that will own the body.
38+
/// - `filter`: The collision filter to apply to the ground collider.
39+
///
40+
/// # Returns
41+
/// Returns the created rigid body handle.
42+
fn build_ground(
43+
world: &mut PhysicsWorld2D,
44+
filter: CollisionFilter,
45+
) -> RigidBody2D {
46+
let ground = RigidBody2DBuilder::new(RigidBodyType::Static)
47+
.with_position(0.0, -1.0)
48+
.build(world)
49+
.unwrap();
50+
51+
Collider2DBuilder::rectangle(20.0, 0.5)
52+
.with_collision_filter(filter)
53+
.build(world, ground)
54+
.unwrap();
55+
56+
return ground;
57+
}
58+
59+
/// Creates a dynamic ball body with the provided collision filter.
60+
///
61+
/// # Arguments
62+
/// - `world`: The world that will own the body.
63+
/// - `filter`: The collision filter to apply to the ball collider.
64+
///
65+
/// # Returns
66+
/// Returns the created rigid body handle.
67+
fn build_ball(
68+
world: &mut PhysicsWorld2D,
69+
filter: CollisionFilter,
70+
) -> RigidBody2D {
71+
let ball = RigidBody2DBuilder::new(RigidBodyType::Dynamic)
72+
.with_position(0.0, 4.0)
73+
.build(world)
74+
.unwrap();
75+
76+
Collider2DBuilder::circle(0.5)
77+
.with_collision_filter(filter)
78+
.build(world, ball)
79+
.unwrap();
80+
81+
return ball;
82+
}
83+
84+
/// Allows collisions when both colliders' group and mask settings match.
85+
#[test]
86+
fn physics_2d_matching_collision_filters_allow_contact() {
87+
let mut world = PhysicsWorld2DBuilder::new().build().unwrap();
88+
89+
build_ground(
90+
&mut world,
91+
CollisionFilter {
92+
group: 0b0001,
93+
mask: 0b0010,
94+
},
95+
);
96+
let ball = build_ball(
97+
&mut world,
98+
CollisionFilter {
99+
group: 0b0010,
100+
mask: 0b0001,
101+
},
102+
);
103+
104+
step_world(&mut world, DEFAULT_STEP_COUNT);
105+
106+
let position = ball.position(&world).unwrap();
107+
108+
assert!(
109+
position[1] > -0.25,
110+
"ball did not collide with the ground: y={}",
111+
position[1],
112+
);
113+
114+
return;
115+
}
116+
117+
/// Prevents collisions when the colliders' groups and masks do not overlap.
118+
#[test]
119+
fn physics_2d_mismatched_collision_filters_prevent_contact() {
120+
let mut world = PhysicsWorld2DBuilder::new().build().unwrap();
121+
122+
build_ground(
123+
&mut world,
124+
CollisionFilter {
125+
group: 0b0001,
126+
mask: 0b0001,
127+
},
128+
);
129+
let ball = build_ball(
130+
&mut world,
131+
CollisionFilter {
132+
group: 0b0010,
133+
mask: 0b0010,
134+
},
135+
);
136+
137+
step_world(&mut world, DEFAULT_STEP_COUNT);
138+
139+
let position = ball.position(&world).unwrap();
140+
141+
assert!(
142+
position[1] < -5.0,
143+
"ball unexpectedly collided with the ground: y={}",
144+
position[1],
145+
);
146+
147+
return;
148+
}

crates/lambda-rs/tests/physics_2d/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
//! surface, including cross-crate wiring through `lambda-rs-platform`.
55
66
mod colliders;
7+
mod collision_filters;
78
mod compound_colliders;
89
mod materials;
910

0 commit comments

Comments
 (0)