Skip to content

Commit 8bf77e0

Browse files
authored
Improved contact types (#616)
# Objective A lot of Avian's current contact types store some redundant data, aren't properly optimized, and aren't explicit enough about what they represent. This PR is a clean-up pass to make the types a bit smaller and clearer. ## Solution ### `Contacts` - Replace `total_normal_impulse` property with `total_normal_impulse`, `total_normal_impulse_magnitude`, and `max_normal_impulse` helpers - Deprecate `total_normal_force` helper - You can just divide by the substep timestep - Remove `total_tangent_impulse` property and `total_friction_force` helper - These could previously technically be wrong/misleading, since each contact point can have a different tangent direction, especially in 3D, and they can also change between substeps. The tangent impulse magnitudes of each individual point can still be accessed. ### `ContactManifold` - Rename `ContactManifold::contacts` to `ContactManifold::points` - Use an `ArrayVec` with a capacity of 2 instead of a `Vec` to store 2D `ContactManifold` points directly on the stack - Replace the local `normal1` and `normal2` with a single world-space `normal` - Internals only need the world-space normal, and it makes e.g. contact modification more straightforward - Add `total_normal_impulse` and `max_normal_impulse` helpers ### `ContactData` - Rename `ContactData` to `ContactPoint` (represents a point in a contact manifold) - Another option would be `ManifoldPoint` like in Box2D; not sure which one is better - Rename `point1` and `point2` to `local_point1` and `local_point2` for explicitness - Remove `normal1` and `normal2` from `ContactPoint`, since the normal is already stored in the `ContactManifold` ### Other - Many documentation improvements - Contact force debug rendering uses per-point forces instead of the total force for the contact pair --- ## Migration Guide There have been several changes to Avian's contact types to make them more optimized and clearer. ### `Contacts` - The `total_normal_impulse` property has been replaced with a `total_normal_impulse` helper method. - The `total_normal_force` helper has been deprecated. Instead, just divide the impulse by the substep timestep. - The `total_tangent_impulse` property and `total_friction_force` helper have been removed for being inaccurate/misleading. The tangent impulse magnitudes of each individual point can still be accessed. ### `ContactManifold` - `ContactManifold::contacts` has been renamed to `ContactManifold::points`. - The local `normal1` and `normal2` have been replaced with a single world-space `normal`, pointing from the first shape to the second. ### `ContactData` - `ContactData` has been renamed to `ContactPoint`, since it specifically represents a point in a contact manifold, not general contact data. - `point1` and `point2` have been renamed to `local_point1` and `local_point2` for explicitness - `normal1` and `normal2` have been removed, since the normal is already stored in the `ContactManifold`.
1 parent 95bbd06 commit 8bf77e0

File tree

14 files changed

+270
-262
lines changed

14 files changed

+270
-262
lines changed

crates/avian2d/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ nalgebra = { version = "0.33", features = ["convert-glam029"], optional = true }
7979
serde = { version = "1", features = ["derive"], optional = true }
8080
derive_more = "1"
8181
indexmap = "2.0.0"
82+
arrayvec = "0.7"
8283
fxhash = "0.2.1"
8384
itertools = "0.13"
8485
bitflags = "2.5.0"

crates/avian2d/examples/custom_collider.rs

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -79,31 +79,28 @@ impl AnyCollider for CircleCollider {
7979
let sum_radius = self.radius + other.radius;
8080

8181
if distance_squared < (sum_radius + prediction_distance).powi(2) {
82-
let normal1 = if distance_squared != 0.0 {
82+
let local_normal1 = if distance_squared != 0.0 {
8383
delta_pos.normalize_or_zero()
8484
} else {
8585
Vector::X
8686
};
87-
let normal2 = delta_rot.inverse() * (-normal1);
88-
let point1 = normal1 * self.radius;
89-
let point2 = normal2 * other.radius;
87+
let local_normal2 = delta_rot.inverse() * (-local_normal1);
88+
let local_point1 = local_normal1 * self.radius;
89+
let local_point2 = local_normal2 * other.radius;
9090

9191
vec![ContactManifold {
9292
index: 0,
93-
normal1,
94-
normal2,
95-
contacts: vec![ContactData {
96-
feature_id1: PackedFeatureId::face(0),
97-
feature_id2: PackedFeatureId::face(0),
98-
point1,
99-
point2,
100-
normal1,
101-
normal2,
93+
normal: rotation1 * local_normal1,
94+
points: avian2d::data_structures::ArrayVec::from_iter([ContactPoint {
95+
local_point1,
96+
local_point2,
10297
penetration: sum_radius - distance_squared.sqrt(),
103-
// Impulses are computed by the constraint solver
98+
// Impulses are computed by the constraint solver.
10499
normal_impulse: 0.0,
105100
tangent_impulse: 0.0,
106-
}],
101+
feature_id1: PackedFeatureId::face(0),
102+
feature_id2: PackedFeatureId::face(0),
103+
}]),
107104
}]
108105
} else {
109106
vec![]

crates/avian2d/examples/kinematic_character_2d/plugin.rs

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -269,12 +269,7 @@ fn kinematic_controller_collisions(
269269
bodies: Query<&RigidBody>,
270270
collider_parents: Query<&ColliderParent, Without<Sensor>>,
271271
mut character_controllers: Query<
272-
(
273-
&mut Position,
274-
&Rotation,
275-
&mut LinearVelocity,
276-
Option<&MaxSlopeAngle>,
277-
),
272+
(&mut Position, &mut LinearVelocity, Option<&MaxSlopeAngle>),
278273
(With<RigidBody>, With<CharacterController>),
279274
>,
280275
time: Res<Time>,
@@ -295,7 +290,7 @@ fn kinematic_controller_collisions(
295290
let character_rb: RigidBody;
296291
let is_other_dynamic: bool;
297292

298-
let (mut position, rotation, mut linear_velocity, max_slope_angle) =
293+
let (mut position, mut linear_velocity, max_slope_angle) =
299294
if let Ok(character) = character_controllers.get_mut(collider_parent1.get()) {
300295
is_first = true;
301296
character_rb = *bodies.get(collider_parent1.get()).unwrap();
@@ -323,15 +318,15 @@ fn kinematic_controller_collisions(
323318
// Each contact in a single manifold shares the same contact normal.
324319
for manifold in contacts.manifolds.iter() {
325320
let normal = if is_first {
326-
-manifold.global_normal1(rotation)
321+
-manifold.normal
327322
} else {
328-
-manifold.global_normal2(rotation)
323+
manifold.normal
329324
};
330325

331326
let mut deepest_penetration: Scalar = Scalar::MIN;
332327

333328
// Solve each penetrating contact in the manifold.
334-
for contact in manifold.contacts.iter() {
329+
for contact in manifold.points.iter() {
335330
if contact.penetration > 0.0 {
336331
position.0 += normal * contact.penetration;
337332
}

crates/avian2d/examples/one_way_platform_2d.rs

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ fn pass_through_one_way_platform(
187187
// It can have read-only access to queries, resources, and other system parameters.
188188
#[derive(SystemParam)]
189189
struct PlatformerCollisionHooks<'w, 's> {
190-
one_way_platforms_query: Query<'w, 's, Read<OneWayPlatform>>,
190+
one_way_platforms_query: Query<'w, 's, (Read<OneWayPlatform>, Read<GlobalTransform>)>,
191191
// NOTE: This precludes a `OneWayPlatform` passing through a `OneWayPlatform`.
192192
other_colliders_query: Query<
193193
'w,
@@ -255,19 +255,24 @@ impl CollisionHooks for PlatformerCollisionHooks<'_, '_> {
255255

256256
// First, figure out which entity is the one-way platform, and which is the other.
257257
// Choose the appropriate normal for pass-through depending on which is which.
258-
let (platform_entity, one_way_platform, other_entity, relevant_normal) =
259-
if let Ok(one_way_platform) = self.one_way_platforms_query.get(contacts.entity1) {
258+
let (platform_entity, one_way_platform, platform_transform, other_entity, relevant_normal) =
259+
if let Ok((one_way_platform, platform_transform)) =
260+
self.one_way_platforms_query.get(contacts.entity1)
261+
{
260262
(
261263
contacts.entity1,
262264
one_way_platform,
265+
platform_transform,
263266
contacts.entity2,
264267
RelevantNormal::Normal1,
265268
)
266-
} else if let Ok(one_way_platform) = self.one_way_platforms_query.get(contacts.entity2)
269+
} else if let Ok((one_way_platform, platform_transform)) =
270+
self.one_way_platforms_query.get(contacts.entity2)
267271
{
268272
(
269273
contacts.entity2,
270274
one_way_platform,
275+
platform_transform,
271276
contacts.entity1,
272277
RelevantNormal::Normal2,
273278
)
@@ -280,7 +285,7 @@ impl CollisionHooks for PlatformerCollisionHooks<'_, '_> {
280285
if one_way_platform.0.contains(&other_entity) {
281286
let any_penetrating = contacts.manifolds.iter().any(|manifold| {
282287
manifold
283-
.contacts
288+
.points
284289
.iter()
285290
.any(|contact| contact.penetration > 0.0)
286291
});
@@ -314,13 +319,14 @@ impl CollisionHooks for PlatformerCollisionHooks<'_, '_> {
314319
Err(_) | Ok(None) | Ok(Some(PassThroughOneWayPlatform::ByNormal)) => {
315320
// If all contact normals are in line with the local up vector of this platform,
316321
// then this collision should occur: the entity is on top of the platform.
322+
let platform_up = platform_transform.up().truncate().adjust_precision();
317323
if contacts.manifolds.iter().all(|manifold| {
318324
let normal = match relevant_normal {
319-
RelevantNormal::Normal1 => manifold.normal1,
320-
RelevantNormal::Normal2 => manifold.normal2,
325+
RelevantNormal::Normal1 => manifold.normal,
326+
RelevantNormal::Normal2 => -manifold.normal,
321327
};
322328

323-
normal.length() > Scalar::EPSILON && normal.dot(Vector::Y) >= 0.5
329+
normal.length() > Scalar::EPSILON && normal.dot(platform_up) >= 0.5
324330
}) {
325331
true
326332
} else {

crates/avian3d/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ nalgebra = { version = "0.33", features = ["convert-glam029"], optional = true }
8181
serde = { version = "1", features = ["derive"], optional = true }
8282
derive_more = "1"
8383
indexmap = "2.0.0"
84+
arrayvec = "0.7"
8485
fxhash = "0.2.1"
8586
itertools = "0.13"
8687
bitflags = "2.5.0"

crates/avian3d/examples/kinematic_character_3d/plugin.rs

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -285,12 +285,7 @@ fn kinematic_controller_collisions(
285285
bodies: Query<&RigidBody>,
286286
collider_parents: Query<&ColliderParent, Without<Sensor>>,
287287
mut character_controllers: Query<
288-
(
289-
&mut Position,
290-
&Rotation,
291-
&mut LinearVelocity,
292-
Option<&MaxSlopeAngle>,
293-
),
288+
(&mut Position, &mut LinearVelocity, Option<&MaxSlopeAngle>),
294289
(With<RigidBody>, With<CharacterController>),
295290
>,
296291
time: Res<Time>,
@@ -311,7 +306,7 @@ fn kinematic_controller_collisions(
311306
let character_rb: RigidBody;
312307
let is_other_dynamic: bool;
313308

314-
let (mut position, rotation, mut linear_velocity, max_slope_angle) =
309+
let (mut position, mut linear_velocity, max_slope_angle) =
315310
if let Ok(character) = character_controllers.get_mut(collider_parent1.get()) {
316311
is_first = true;
317312
character_rb = *bodies.get(collider_parent1.get()).unwrap();
@@ -339,15 +334,15 @@ fn kinematic_controller_collisions(
339334
// Each contact in a single manifold shares the same contact normal.
340335
for manifold in contacts.manifolds.iter() {
341336
let normal = if is_first {
342-
-manifold.global_normal1(rotation)
337+
-manifold.normal
343338
} else {
344-
-manifold.global_normal2(rotation)
339+
manifold.normal
345340
};
346341

347342
let mut deepest_penetration: Scalar = Scalar::MIN;
348343

349344
// Solve each penetrating contact in the manifold.
350-
for contact in manifold.contacts.iter() {
345+
for contact in manifold.points.iter() {
351346
if contact.penetration > 0.0 {
352347
position.0 += normal * contact.penetration;
353348
}

src/collision/contact_query.rs

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ pub fn contact(
9090
let normal1: Vector = (rotation1.inverse() * Vector::from(contact.normal1)).normalize();
9191
let normal2: Vector = (rotation2.inverse() * Vector::from(contact.normal2)).normalize();
9292

93-
// Make sure normals are valid
93+
// Make sure the normal is valid
9494
if !normal1.is_normalized() || !normal2.is_normalized() {
9595
return None;
9696
}
@@ -156,8 +156,12 @@ pub fn contact_manifolds(
156156
rotation2: impl Into<Rotation>,
157157
prediction_distance: Scalar,
158158
) -> Vec<ContactManifold> {
159-
let isometry1 = make_isometry(position1.into(), rotation1.into());
160-
let isometry2 = make_isometry(position2.into(), rotation2.into());
159+
let position1: Position = position1.into();
160+
let position2: Position = position2.into();
161+
let rotation1: Rotation = rotation1.into();
162+
let rotation2: Rotation = rotation2.into();
163+
let isometry1 = make_isometry(position1, rotation1);
164+
let isometry2 = make_isometry(position2, rotation2);
161165
let isometry12 = isometry1.inv_mul(&isometry2);
162166

163167
// TODO: Reuse manifolds from previous frame to improve performance
@@ -184,22 +188,25 @@ pub fn contact_manifolds(
184188
shape2,
185189
prediction_distance,
186190
) {
187-
let normal1 = Vector::from(contact.normal1);
188-
let normal2 = Vector::from(contact.normal2);
191+
let normal = rotation1 * Vector::from(contact.normal1);
189192

190-
// Make sure normals are valid
191-
if !normal1.is_normalized() || !normal2.is_normalized() {
193+
// Make sure the normal is valid
194+
if !normal.is_normalized() {
192195
return vec![];
193196
}
194197

195198
return vec![ContactManifold {
196-
normal1,
197-
normal2,
198-
contacts: vec![ContactData::new(
199+
normal,
200+
#[cfg(feature = "2d")]
201+
points: arrayvec::ArrayVec::from_iter([ContactPoint::new(
202+
contact.point1.into(),
203+
contact.point2.into(),
204+
-contact.dist,
205+
)]),
206+
#[cfg(feature = "3d")]
207+
points: vec![ContactPoint::new(
199208
contact.point1.into(),
200209
contact.point2.into(),
201-
normal1,
202-
normal2,
203210
-contact.dist,
204211
)],
205212
index: 0,
@@ -215,34 +222,27 @@ pub fn contact_manifolds(
215222
.filter_map(|manifold| {
216223
let subpos1 = manifold.subshape_pos1.unwrap_or_default();
217224
let subpos2 = manifold.subshape_pos2.unwrap_or_default();
218-
let normal1: Vector = subpos1
225+
let local_normal: Vector = subpos1
219226
.rotation
220227
.transform_vector(&manifold.local_n1)
221228
.normalize()
222229
.into();
223-
let normal2: Vector = subpos2
224-
.rotation
225-
.transform_vector(&manifold.local_n2)
226-
.normalize()
227-
.into();
230+
let normal = rotation1 * local_normal;
228231

229-
// Make sure normals are valid
230-
if !normal1.is_normalized() || !normal2.is_normalized() {
232+
// Make sure the normal is valid
233+
if !normal.is_normalized() {
231234
return None;
232235
}
233236

234237
let manifold = ContactManifold {
235-
normal1,
236-
normal2,
237-
contacts: manifold
238+
normal,
239+
points: manifold
238240
.contacts()
239241
.iter()
240242
.map(|contact| {
241-
ContactData::new(
243+
ContactPoint::new(
242244
subpos1.transform_point(&contact.local_p1).into(),
243245
subpos2.transform_point(&contact.local_p2).into(),
244-
normal1,
245-
normal2,
246246
-contact.dist,
247247
)
248248
.with_feature_ids(contact.fid1.into(), contact.fid2.into())

0 commit comments

Comments
 (0)