Skip to content

Collision checking

Collision is checked between the robot and itself, the robot and every enabled obstacle, and — with several robots — robot against robot. The same geometry serves the live queries here, the planner, and the sequence rollout's tick checks.

The queries

scene.in_collision()             # bool, the fast yes/no
scene.check_collisions()         # who: [(('link', 'forearm_link'), ('obstacle', 'wall')), ...]
scene.min_obstacle_distance()    # tightest robot-obstacle distance in meters
                                 # 0 when colliding, None with no obstacles
scene.min_obstacle_distance(to=["guard"])   # ... to the obstacles under guard/ only

check_collisions names pairs as (kind, name) tuples with kind "link" or "obstacle" — the same names the studio uses to highlight the offending geometry in red.

Over a whole baked cycle, the equivalent of min_obstacle_distance is SequenceTimeline.min_clearance — see Timeline assertions.

What the shapes are

Primitives collide as themselves. Meshes collide as VHACD convex decompositions: triangle-mesh-vs-triangle-mesh testing misses containment and mis-reports distance, so every mesh is decomposed into convex pieces (about a second per mesh on first load, cached on disk afterwards). The studio still renders the original mesh — only the collision layer sees the decomposition. The pieces hug the mesh surface rather than the corners of the voxels they were grown from, so a small gap keeps its clearance instead of gaining a resolution-dependent skin — and two faces in exact zero-gap contact count as touching. Concavities are still approximated, and a mesh with holes in it can lose its solid interior.

Anything that could not be prepared for collision is listed, not silently dropped:

scene.collision_warnings         # link shapes skipped for collision checking

Self-collision, and the inter-robot ACM

A robot's own allowed-contact matrix (adjacent links, permanently-touching pairs) is generated automatically by sampling. Inter-robot pairs are never inferred: whether two arms may touch depends on where their bases stand, which no sampler can know — so it is the author's call:

scene.allow_inter_robot_collision("near", "/panda/panda_link0",
                                  "far",  "/panda/panda_link0")

the escape hatch for arms that share a mount plate or are meant to touch.

The sampler only finds pairs that touch in nearly every pose (95 % of 256 samples). A whole-body machine has pairs that touch by design and fall short of that — torso segments a vendor checks with overlapping capsules (one RB-Y1 pair touches in 94 % of poses), a drive wheel steering inside its housing — and a plan that includes the torso then starts "in self-collision". Those are declared, the way a MoveIt SRDF does:

robot = robot.allow_collisions([("link_torso_2", "link_torso_4"),
                                ("base_link", "left_wheel_drive_link")])

It returns a new robot; a name that is no link, or a pair of a link with itself, is refused by name. A catalog package declares its own (self_collision.allowed_pairs, with the basis it was taken from) and Robot.from_catalog applies them, so a cell rarely writes this. A declaration rides a welded tool or mount and is saved with the project. The generated script re-authors the pairs a cell declared by hand; a package's own come back with from_catalog and are not written out again.

Switching obstacles out

scene.set_obstacle_enabled("cleats", False)

Disabled obstacles are excluded from every query above but keep rendering — moving scenery, dress geometry, or a fixture you want visible but not planned around.

Grasped objects

An attached obstacle collides as part of the robot: it is checked against the environment, excluded against its touch_links (the gripper pads that are supposed to touch it), and carried through planning. Detach it and it goes back to being an obstacle where it stands.