Skip to content

Commit 0474c72

Browse files
authored
Per-manifold material properties and tangent velocity (#660)
# Objective Follow-up to #610. Now that we have contact modification hooks, we should add support for modifying the surface properties of contacts. This can enable use cases like conveyor belts or non-uniform friction and restitution for different parts of a mesh. ## Solution Add `friction`, `restitution`, and `tangent_speed`/`tangent_velocity` properties to `ContactManifold`. These are copied over for each `ContactConstraint`. > [!NOTE] > **Why not have a hook for modifying `ContactConstraint` directly instead?** > > While we could technically do this right now, once we have wide SIMD constraints, `ContactConstraint` will also have its own SIMD version that is not as user-friendly and depends on feature flags. Providing hooks for modifying that is not very viable, and it would not be good for UX. Splitting contact modification into two hooks for different types of contact types could also be confusing for users. The new *tangent velocity* is a way to emulate relative movement of contact surfaces, which is often used for conveyor belts. A new `conveyor_belt` example has been added to demonstrate this. https://github.com/user-attachments/assets/3f07b4f1-6ff9-4b00-80db-5500ba43940b Engines like Rapier, Jolt, and Box2D also have tangent velocity and other material properties, but the capabilities for contact modification are different. - **Rapier**: Friction, restitution, and tangent velocity can be set for each solver contact point. - **Jolt**: Friction, restitution, and tangent velocity can be set for each contact manifold. - **Box2D**: Friction, restitution, and tangent velocity cannot be modified in pre-solve hooks, *but* friction and restitution have their own optional callbacks for basic mixing logic (I believe Jolt also has this), and chain shapes support per-segment materials. So, surface properties can only be set per shape (pair), but it is still decently flexible for 2D. I opted for Jolt's approach of per-manifold configuration for now. It lets you have different material properties for e.g. different triangles of a trimesh, while keeping memory usage fairly minimal by not storing the properties for each individual contact point. I think this is a good balance of flexibility and cost. If important use cases that require per-point surface properties arise, we could switch to that, but I'd prefer to keep things more simple and minimal for now.
1 parent 8bf77e0 commit 0474c72

File tree

9 files changed

+382
-119
lines changed

9 files changed

+382
-119
lines changed

crates/avian2d/examples/custom_collider.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -88,10 +88,8 @@ impl AnyCollider for CircleCollider {
8888
let local_point1 = local_normal1 * self.radius;
8989
let local_point2 = local_normal2 * other.radius;
9090

91-
vec![ContactManifold {
92-
index: 0,
93-
normal: rotation1 * local_normal1,
94-
points: avian2d::data_structures::ArrayVec::from_iter([ContactPoint {
91+
vec![ContactManifold::new(
92+
[ContactPoint {
9593
local_point1,
9694
local_point2,
9795
penetration: sum_radius - distance_squared.sqrt(),
@@ -100,8 +98,10 @@ impl AnyCollider for CircleCollider {
10098
tangent_impulse: 0.0,
10199
feature_id1: PackedFeatureId::face(0),
102100
feature_id2: PackedFeatureId::face(0),
103-
}]),
104-
}]
101+
}],
102+
rotation1 * local_normal1,
103+
0,
104+
)]
105105
} else {
106106
vec![]
107107
}

crates/avian3d/Cargo.toml

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,14 @@ required-features = ["3d", "default-collider"]
119119
name = "chain_3d"
120120
required-features = ["3d", "default-collider"]
121121

122+
[[example]]
123+
name = "collider_constructors"
124+
required-features = ["3d", "default-collider", "bevy_scene"]
125+
126+
[[example]]
127+
name = "conveyor_belt"
128+
required-features = ["3d", "default-collider"]
129+
122130
[[example]]
123131
name = "cubes"
124132
required-features = ["3d", "default-collider"]
@@ -155,10 +163,6 @@ required-features = ["3d", "default-collider", "bevy_picking"]
155163
name = "trimesh_shapes_3d"
156164
required-features = ["3d", "default-collider", "bevy_scene"]
157165

158-
[[example]]
159-
name = "collider_constructors"
160-
required-features = ["3d", "default-collider", "bevy_scene"]
161-
162166
[[example]]
163167
name = "diagnostics"
164168
required-features = ["3d", "default-collider", "diagnostic_ui"]
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
//! Demonstrates how to use `CollisionHooks::modify_contacts`
2+
//! and `tangent_velocity` to simulate conveyor belts.
3+
4+
use avian3d::{math::*, prelude::*};
5+
use bevy::{
6+
ecs::system::{lifetimeless::Read, SystemParam},
7+
prelude::*,
8+
};
9+
use examples_common_3d::ExampleCommonPlugin;
10+
11+
fn main() {
12+
App::new()
13+
.add_plugins((
14+
DefaultPlugins,
15+
ExampleCommonPlugin,
16+
// Add our collision hooks to modify contacts for conveyor belts.
17+
PhysicsPlugins::default().with_collision_hooks::<ConveyorHooks>(),
18+
))
19+
.add_systems(Startup, setup)
20+
.run();
21+
}
22+
23+
// Enable contact modification for conveyor belts with the `ActiveCollisionHooks` component.
24+
// Here we use required components, but you could also add it manually.
25+
#[derive(Component)]
26+
#[require(ActiveCollisionHooks(|| ActiveCollisionHooks::MODIFY_CONTACTS))]
27+
struct ConveyorBelt {
28+
local_direction: Vec3,
29+
speed: f32,
30+
}
31+
32+
// Define a custom `SystemParam` for our collision hooks.
33+
// It can have read-only access to queries, resources, and other system parameters.
34+
#[derive(SystemParam)]
35+
struct ConveyorHooks<'w, 's> {
36+
conveyor_query: Query<'w, 's, (Read<ConveyorBelt>, Read<GlobalTransform>)>,
37+
}
38+
39+
// Implement the `CollisionHooks` trait for our custom system parameter.
40+
impl CollisionHooks for ConveyorHooks<'_, '_> {
41+
fn modify_contacts(&self, contacts: &mut Contacts, _commands: &mut Commands) -> bool {
42+
// Get the conveyor belt and its global transform.
43+
// We don't know which entity is the conveyor belt, if any, so we need to check both.
44+
// This also affects the sign used for the conveyor belt's speed to apply it in the correct direction.
45+
let (Ok((conveyor_belt, global_transform)), sign) = self
46+
.conveyor_query
47+
.get(contacts.entity1)
48+
.map_or((self.conveyor_query.get(contacts.entity2), 1.0), |q| {
49+
(Ok(q), -1.0)
50+
})
51+
else {
52+
// If neither entity is a conveyor belt, return `true` early
53+
// to accept the contact pair without any modifications.
54+
return true;
55+
};
56+
57+
// Calculate the conveyor belt's direction in world space.
58+
let direction = global_transform.rotation() * conveyor_belt.local_direction;
59+
60+
// Iterate over all contact surfaces between the conveyor belt and the other collider,
61+
// and apply a relative velocity to simulate the movement of the conveyor belt's surface.
62+
for manifold in contacts.manifolds.iter_mut() {
63+
let tangent_velocity = sign * conveyor_belt.speed * direction;
64+
manifold.tangent_velocity = tangent_velocity.adjust_precision();
65+
}
66+
67+
// Return `true` to accept the contact pair.
68+
true
69+
}
70+
}
71+
72+
fn setup(
73+
mut commands: Commands,
74+
mut materials: ResMut<Assets<StandardMaterial>>,
75+
mut meshes: ResMut<Assets<Mesh>>,
76+
) {
77+
let long_conveyor = Cuboid::new(18.0, 0.1, 6.0);
78+
let short_conveyor = Cuboid::new(14.0, 0.1, 6.0);
79+
80+
let long_conveyor_mesh = meshes.add(long_conveyor);
81+
let short_conveyor_mesh = meshes.add(short_conveyor);
82+
83+
let long_conveyor_material = materials.add(Color::srgb(0.3, 0.3, 0.3));
84+
let short_conveyor_material = materials.add(Color::srgb(0.2, 0.2, 0.2));
85+
86+
// Spawn four conveyor belts.
87+
commands.spawn((
88+
RigidBody::Static,
89+
Collider::from(long_conveyor),
90+
Friction::new(1.0),
91+
ConveyorBelt {
92+
local_direction: Vec3::X,
93+
speed: 6.0,
94+
},
95+
Transform::from_xyz(-3.0, -0.25, 7.0)
96+
.with_rotation(Quat::from_rotation_z(2_f32.to_radians())),
97+
Mesh3d(long_conveyor_mesh.clone()),
98+
MeshMaterial3d(long_conveyor_material.clone()),
99+
));
100+
commands.spawn((
101+
RigidBody::Static,
102+
Collider::from(long_conveyor),
103+
Friction::new(1.0),
104+
ConveyorBelt {
105+
local_direction: Vec3::NEG_X,
106+
speed: 6.0,
107+
},
108+
Transform::from_xyz(3.0, -0.25, -7.0)
109+
.with_rotation(Quat::from_rotation_z(-2_f32.to_radians())),
110+
Mesh3d(long_conveyor_mesh),
111+
MeshMaterial3d(long_conveyor_material.clone()),
112+
));
113+
commands.spawn((
114+
RigidBody::Static,
115+
Collider::from(short_conveyor),
116+
Friction::new(1.0),
117+
ConveyorBelt {
118+
local_direction: Vec3::X,
119+
speed: 3.0,
120+
},
121+
Transform::from_xyz(9.0, -0.25, 3.0).with_rotation(
122+
Quat::from_rotation_y(90_f32.to_radians()) * Quat::from_rotation_z(2_f32.to_radians()),
123+
),
124+
Mesh3d(short_conveyor_mesh.clone()),
125+
MeshMaterial3d(short_conveyor_material.clone()),
126+
));
127+
commands.spawn((
128+
RigidBody::Static,
129+
Collider::from(short_conveyor),
130+
Friction::new(1.0),
131+
ConveyorBelt {
132+
local_direction: Vec3::NEG_X,
133+
speed: 3.0,
134+
},
135+
Transform::from_xyz(-9.0, -0.25, -3.0).with_rotation(
136+
Quat::from_rotation_y(90_f32.to_radians()) * Quat::from_rotation_z(-2_f32.to_radians()),
137+
),
138+
Mesh3d(short_conveyor_mesh),
139+
MeshMaterial3d(short_conveyor_material),
140+
));
141+
142+
// Spawn cube stacks on top of one of the conveyor belts.
143+
let cuboid_mesh = meshes.add(Cuboid::default());
144+
let cuboid_material = materials.add(Color::srgb(0.2, 0.7, 0.9));
145+
for x in -2..2 {
146+
for y in 0..3 {
147+
for z in -2..2 {
148+
let position = Vec3::new(x as f32 + 10.0, y as f32 + 1.0, z as f32);
149+
commands.spawn((
150+
RigidBody::Dynamic,
151+
// This small margin just helps prevent hitting internal edges
152+
// while sliding from one conveyor to another.
153+
CollisionMargin(0.01),
154+
Collider::cuboid(0.98, 0.98, 0.98),
155+
Transform::from_translation(position),
156+
Mesh3d(cuboid_mesh.clone()),
157+
MeshMaterial3d(cuboid_material.clone()),
158+
));
159+
}
160+
}
161+
}
162+
163+
// Directional light
164+
commands.spawn((
165+
DirectionalLight {
166+
illuminance: 5000.0,
167+
shadows_enabled: true,
168+
..default()
169+
},
170+
Transform::default().looking_at(Vec3::new(-1.0, -2.5, -1.5), Vec3::Y),
171+
));
172+
173+
// Camera
174+
commands.spawn((
175+
Camera3d::default(),
176+
Transform::from_translation(Vec3::new(20.0, 10.0, 20.0)).looking_at(Vec3::ZERO, Vec3::Y),
177+
));
178+
}

src/collision/contact_query.rs

Lines changed: 16 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -195,22 +195,15 @@ pub fn contact_manifolds(
195195
return vec![];
196196
}
197197

198-
return vec![ContactManifold {
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(
198+
return vec![ContactManifold::new(
199+
[ContactPoint::new(
208200
contact.point1.into(),
209201
contact.point2.into(),
210202
-contact.dist,
211203
)],
212-
index: 0,
213-
}];
204+
normal,
205+
0,
206+
)];
214207
}
215208
}
216209
}
@@ -234,22 +227,18 @@ pub fn contact_manifolds(
234227
return None;
235228
}
236229

237-
let manifold = ContactManifold {
230+
let manifold = ContactManifold::new(
231+
manifold.contacts().iter().map(|contact| {
232+
ContactPoint::new(
233+
subpos1.transform_point(&contact.local_p1).into(),
234+
subpos2.transform_point(&contact.local_p2).into(),
235+
-contact.dist,
236+
)
237+
.with_feature_ids(contact.fid1.into(), contact.fid2.into())
238+
}),
238239
normal,
239-
points: manifold
240-
.contacts()
241-
.iter()
242-
.map(|contact| {
243-
ContactPoint::new(
244-
subpos1.transform_point(&contact.local_p1).into(),
245-
subpos2.transform_point(&contact.local_p2).into(),
246-
-contact.dist,
247-
)
248-
.with_feature_ids(contact.fid1.into(), contact.fid2.into())
249-
})
250-
.collect(),
251-
index: manifold_index,
252-
};
240+
manifold_index,
241+
);
253242

254243
manifold_index += 1;
255244

src/collision/mod.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,11 +373,56 @@ pub struct ContactManifold {
373373
///
374374
/// The same normal is shared by all `points` in a manifold,
375375
pub normal: Vector,
376+
/// The effective coefficient of dynamic [friction](Friction) used for the contact surface.
377+
pub friction: Scalar,
378+
/// The effective coefficient of [restitution](Restitution) used for the contact surface.
379+
pub restitution: Scalar,
380+
/// The desired relative linear speed of the bodies along the surface,
381+
/// expressed in world space as `tangent_speed2 - tangent_speed1`.
382+
///
383+
/// Defaults to zero. If set to a non-zero value, this can be used to simulate effects
384+
/// such as conveyor belts.
385+
#[cfg(feature = "2d")]
386+
pub tangent_speed: Scalar,
387+
// TODO: Jolt also supports a relative angular surface velocity, which can be used for making
388+
// objects rotate on platforms. Would that be useful enough to warrant the extra memory usage?
389+
/// The desired relative linear velocity of the bodies along the surface,
390+
/// expressed in world space as `tangent_velocity2 - tangent_velocity1`.
391+
///
392+
/// Defaults to zero. If set to a non-zero value, this can be used to simulate effects
393+
/// such as conveyor belts.
394+
#[cfg(feature = "3d")]
395+
pub tangent_velocity: Vector,
376396
/// The index of the manifold in the collision.
377397
pub index: usize,
378398
}
379399

380400
impl ContactManifold {
401+
/// Creates a new [`ContactManifold`] with the given contact points and surface normals,
402+
/// expressed in local space.
403+
///
404+
/// `index` represents the index of the manifold in the collision.
405+
pub fn new(
406+
points: impl IntoIterator<Item = ContactPoint>,
407+
normal: Vector,
408+
index: usize,
409+
) -> Self {
410+
Self {
411+
#[cfg(feature = "2d")]
412+
points: arrayvec::ArrayVec::from_iter(points),
413+
#[cfg(feature = "3d")]
414+
points: points.into_iter().collect(),
415+
normal,
416+
friction: 0.0,
417+
restitution: 0.0,
418+
#[cfg(feature = "2d")]
419+
tangent_speed: 0.0,
420+
#[cfg(feature = "3d")]
421+
tangent_velocity: Vector::ZERO,
422+
index,
423+
}
424+
}
425+
381426
/// The sum of the impulses applied at the contact points in the manifold along the contact normal.
382427
fn total_normal_impulse(&self) -> Scalar {
383428
self.points

0 commit comments

Comments
 (0)