Colliders
A collider is the shape Rapier uses for contact detection. It is separate from the visual mesh, and the two should usually disagree. Visual meshes have thousands of triangles. Colliders are the simplest shape that approximates the visual closely enough.
Collider Shapes
Ball (Sphere)
#![allow(unused)] fn main() { world.core.set_collider(entity, ColliderComponent::new_ball(0.5)); }
Cheapest shape. A single radius and a position. Use it for projectiles, simple props, and anything that can roll without looking wrong.
Cuboid (Box)
#![allow(unused)] fn main() { world.core.set_collider(entity, ColliderComponent::new_cuboid(1.0, 0.5, 1.0)); }
The three arguments are half-extents, not full widths. The cuboid above is 2 by 1 by 2 meters.
Capsule
A capsule is a cylinder with hemispheres on both ends. The hemispheres prevent the character from snagging on small steps and crease lines, which a flat-bottomed cylinder is prone to do.
#![allow(unused)] fn main() { world.core.set_collider(entity, ColliderComponent::new_capsule(1.0, 0.3)); }
Cylinder
#![allow(unused)] fn main() { world.core.set_collider(entity, ColliderComponent { shape: ColliderShape::Cylinder { half_height: 1.0, radius: 0.5, }, ..Default::default() }); }
Cylinders make good barrels and pillars. They are more expensive than boxes at contact time.
Cone
#![allow(unused)] fn main() { world.core.set_collider(entity, ColliderComponent { shape: ColliderShape::Cone { half_height: 1.0, radius: 0.5, }, ..Default::default() }); }
Triangle Mesh
Triangle mesh is the escape hatch for static geometry that does not fit a primitive shape.
#![allow(unused)] fn main() { let vertices: Vec<[f32; 3]> = mesh.vertices.iter() .map(|v| v.position) .collect(); let indices: Vec<[u32; 3]> = mesh.indices .chunks(3) .map(|c| [c[0], c[1], c[2]]) .collect(); world.core.set_collider(entity, ColliderComponent { shape: ColliderShape::TriMesh { vertices, indices }, ..Default::default() }); }
Rapier builds a BVH over the triangles on insert, so the construction cost is real but one-time. Use trimesh on static bodies only. Dynamic bodies with trimesh colliders run into the limitation that Rapier cannot compute mass properties from a non-closed mesh, and contact resolution between two trimesh dynamic bodies is not well-defined.
Heightfield
A heightfield is a grid of height samples. It is the right shape for terrain.
#![allow(unused)] fn main() { let heights: Vec<f32> = generate_height_grid(64, 64); world.core.set_collider(entity, ColliderComponent { shape: ColliderShape::HeightField { nrows: 64, ncols: 64, heights, scale: [100.0, 50.0, 100.0], }, ..Default::default() }); }
The scale vector is [x, height, z]. The grid above covers a 100 by 100 meter area with vertical scale 50.
Compound
Compound colliders are multiple shapes attached to one body. Cheaper than parenting multiple bodies and keeps the mass properties coherent.
#![allow(unused)] fn main() { let body_collider = ColliderComponent::new_cuboid(0.5, 0.1, 0.5); let head_collider = ColliderComponent::new_ball(0.3); }
Physics Materials
Friction, restitution, and density are fields on ColliderComponent itself.
#![allow(unused)] fn main() { world.core.set_collider(entity, ColliderComponent::new_cuboid(0.5, 0.5, 0.5) .with_friction(0.5) .with_restitution(0.3) .with_density(1.0)); }
Friction is the sliding-resistance coefficient. 0 is frictionless, 1 is sticky. Restitution is the bounce coefficient. 0 absorbs impact, 1 returns it perfectly. Density is mass per unit volume. Rapier computes the body's mass from collider density and shape volume at insertion.
Material Examples
#![allow(unused)] fn main() { let ice = ColliderComponent::new_cuboid(0.5, 0.5, 0.5) .with_friction(0.05) .with_restitution(0.1) .with_density(0.9); let rubber = ColliderComponent::new_ball(0.5) .with_friction(0.9) .with_restitution(0.8) .with_density(1.1); let metal = ColliderComponent::new_cuboid(0.5, 0.5, 0.5) .with_friction(0.4) .with_restitution(0.2) .with_density(7.8); }
Density 7.8 for metal is realistic (steel). Density 1.0 is water. These values matter for the mass ratio between bodies in contact, which controls how much each one moves on collision.
Collision Groups
Collision groups filter which pairs of colliders are even considered for contact testing. A player should not collide with the projectiles it fired. Enemy AI should not interpenetrate.
#![allow(unused)] fn main() { use rapier3d::prelude::*; const GROUP_PLAYER: Group = Group::GROUP_1; const GROUP_ENEMY: Group = Group::GROUP_2; const GROUP_PROJECTILE: Group = Group::GROUP_3; const GROUP_WORLD: Group = Group::GROUP_4; let player_filter = CollisionGroups::new( GROUP_PLAYER, GROUP_ENEMY | GROUP_WORLD, ); }
The first arg is the group this collider belongs to. The second is the bitmask of groups it collides with. A pair collides only if each side's membership intersects the other side's filter.
Sensor Colliders
A sensor reports overlap without applying contact response. Trigger volumes, damage zones, pickup proximity.
#![allow(unused)] fn main() { if let Some(collider) = world.resources.physics.collider_set.get_mut(handle.into()) { collider.set_sensor(true); } }
Sensor pairs come through the collision event stream alongside regular contacts, distinguished by the is_sensor flag.
Collision Events
Rapier queues collision events each step. The engine surfaces them on world.resources.physics.collision_events(), returning a &[CollisionEvent] slice.
#![allow(unused)] fn main() { fn run_systems(&mut self, world: &mut World) { for event in world.resources.physics.collision_events() { match event.kind { CollisionEventKind::Started => { handle_collision_start(event.entity_a, event.entity_b); } CollisionEventKind::Stopped => { handle_collision_end(event.entity_a, event.entity_b); } } } } }
Each event carries the two entities, the kind (Started or Stopped), and the sensor flag.
#![allow(unused)] fn main() { pub struct CollisionEvent { pub entity_a: Entity, pub entity_b: Entity, pub kind: CollisionEventKind, pub is_sensor: bool, } }
Convex Decomposition
For dynamic bodies that need to approximate a concave shape, convex decomposition breaks the mesh into a set of convex pieces and attaches them as a compound collider.
#![allow(unused)] fn main() { use rapier3d::prelude::*; let decomposed = SharedShape::convex_decomposition(&vertices, &indices); }
The result is more expensive than a single primitive but works as a dynamic collider, which a trimesh cannot.
Performance Tips
| Shape | Performance | Use Case |
|---|---|---|
| Ball | Fastest | Rolling objects |
| Cuboid | Fast | Crates, buildings |
| Capsule | Fast | Characters |
| Cylinder | Medium | Barrels, pillars |
| Convex | Medium | Simple props |
| Trimesh | Slow | Static terrain only |
| Compound | Varies | Complex dynamic objects |
Primitives are always cheaper than meshes. Trimesh is for static collision only. Compound on one body beats many small entities connected by joints. The collision mesh almost always wants to be simpler than the visual mesh.