Rigid collision: contacts and forces#
Every simulation step, Genesis World finds which rigid bodies touch, generates a contact manifold for each touching pair, and hands those contacts to the constraint solver. This page explains how detection works conceptually and how to read the resulting contacts and contact forces back into Python. Force resolution (how contacts turn into accelerations) is covered in Rigid constraint model.
The detection code lives under genesis/engine/solvers/rigid/collider/, driven by the collider’s detection() method.
How contacts are detected#
Detection runs in two phases each step, from a cheap approximate cull to an exact contact manifold.
Broad phase. Each geometry gets a world-space axis-aligned bounding box (AABB), recomputed every step because rigid bodies move. A Sweep-and-Prune pass then reports only the geometry pairs whose boxes overlap, turning an all-pairs comparison into a near-linear one. The same pass drops pairs that cannot physically collide:
Adjacency: two geometries on the same link, or on directly connected links.
Collision masks: pairs excluded by their
contype/conaffinitybitmasks.Static pairs: two geometries both fixed relative to the world.
Hibernation: contacts between sleeping bodies, unless one side is awake.
Narrow phase. Each surviving pair is resolved to an exact contact manifold: a contact normal, a penetration depth, and one or more contact points. The algorithm depends on the geometry pair:
Geometry pair |
Path |
|---|---|
General convex–convex, including plane–convex |
Minkowski Portal Refinement (MPR) by default, or GJK with EPA when |
Plane–box and box–box |
Analytic special case, ported from MuJoCo for stability when a box lies flush on a plane. |
Any geometry against height-field terrain |
Terrain routine that can emit several contact points per supporting cell. |
Non-convex meshes |
Signed-distance-field sampling: vertices first (vertex–face), then edges (edge–edge), keeping the deepest penetration. |
MPR and GJK both operate through a support function (“which vertex lies farthest along a given direction?”) so they run branch-free on the GPU without face-adjacency caches. GJK additionally reports a separation distance when the geometries are apart and is the differentiable path (it is selected automatically when the scene requires gradients). Both accelerate support queries with a precomputed support field; see Support field for how that structure is built and used. To capture flush faces rather than a single point, Genesis perturbs the pose slightly around the first contact normal and gathers the extra contacts that result.
The number of candidate pairs the broad phase may emit is bounded by the max_collision_pairs option on RigidOptions. Exceeding it at runtime halts the simulation, so raise it for scenes with dense contact.
Reading contacts#
Read the contacts from the most recent scene.step() with get_contacts() on any rigid entity. It returns a dict of parallel arrays, one entry per contact that involves the entity.
import genesis as gs
gs.init(backend=gs.gpu)
scene = gs.Scene()
plane = scene.add_entity(gs.morphs.Plane())
ball = scene.add_entity(gs.morphs.Sphere(radius=0.2, pos=(0.0, 0.0, 0.5)))
scene.build()
for _ in range(200):
scene.step()
contacts = ball.get_contacts() # all contacts involving the ball
positions = contacts["position"] # world-frame contact points, shape ([n_envs,] n_contacts, 3)
forces = contacts["force_a"] # force on geom A, shape ([n_envs,] n_contacts, 3), N
Each entry shares a leading contact axis. Index and scalar fields have shape ([n_envs,] n_contacts); vector fields have shape ([n_envs,] n_contacts, 3). The fields are:
geom_a,geom_b: global geometry indices of the two geometries in the pair. Recover a geometry withscene.rigid_solver.geoms[idx].link_a,link_b: global link indices of the links owning those geometries, recoverable viascene.rigid_solver.links[idx].position: contact point in the world frame, in meters.normal: contact normal, a world-space unit vector.penetration: penetration depth, positive when the geometries overlap.force_a,force_b: contact force on geometry A and on geometry B. They are equal and opposite, in newtons.valid_mask: present only when the scene is parallelized. See the note below.
To restrict the result to contacts against one other entity, pass with_entity. Passing the entity itself returns self-collisions only, and exclude_self_contact=True drops them:
contacts = ball.get_contacts(with_entity=plane) # only ball–plane contacts
Note
With multiple environments, every field carries a leading n_envs axis and is padded to the largest contact count across environments, so the same array is rectangular. valid_mask (shape (n_envs, n_contacts)) marks which rows are real; filter with it before using the data. A single-environment scene returns the fields already trimmed, with no valid_mask.
Net contact force per link#
When you only need the total external contact force on each link rather than the individual contact points, use get_links_net_contact_force(). It sums the contact forces the solver applied to every link of the entity:
net = ball.get_links_net_contact_force() # world frame, shape ([n_envs,] n_links, 3), N
This is the aggregate the constraint solver accumulated, so it reflects the resolved contact forces rather than the raw manifold. For a link resting on the ground, it balances gravity.
When to use a contact sensor instead#
get_contacts() and get_links_net_contact_force() pull the whole contact set on demand, which is convenient for scripting and debugging. For a per-link signal you sample every step in a control or training loop (with history, noise, and delay handled for you), attach a contact sensor instead. ContactForce reports the net force on a link in its own frame, and the tactile probes estimate dense per-taxel forces. See Contact and force sensors.
See also#
Rigid constraint model: how contacts, joint limits, and equality constraints are solved for the resulting motion.
Support field: the acceleration structure behind MPR and GJK support queries.
Contact and force sensors: contact and tactile sensors for per-step readings.