Skip to main content

Scene loaders

Rapier provides companion crates converting some popular robotics formats into the rigid-bodies, the colliders, and the joints they contain. These crates are included in the Python package, so there is nothing more to install: they are exposed by the urdf, mjcf, and mesh submodules of the rapier3d.loaders module (e.g., from rapier3d.loaders import urdf). The loaders insert the rigid-bodies, colliders, and joints of the loaded models into the sets of a physics world (world.rigid_bodies, world.colliders, etc.), and return their handles.

URDF​

The rapier3d-urdf crate loads the Unified Robot Description Format used by the ROS community. A robot is read from a URDF file (or from a string), and is then inserted into the world either with impulse joints or with multibody joints.

info

Robotics generally care a lot about joint violation. Then inserting the model using multibody joints is strongly recommended since they guarantee the absence of joint violation by encoding the locked degrees of freedom into the equations of motion directly instead of relying on the constraints solver (which might not converge).

The robot is read as an UrdfRobot by urdf.UrdfRobot.from_file (which also returns the parsed URDF description), then inserted into the world with UrdfRobot.insert_using_impulse_joints or UrdfRobot.insert_using_multibody_joints, given the sets it is inserted into. Each link becomes a rigid-body (with its colliders), and each joint an impulse joint or a multibody joint. The returned UrdfRobotHandles gives the handles of the rigid-body and of the colliders created for each link (UrdfRobotHandles.links), and the handles of the joint created for each URDF joint, together with the handles of the two rigid-bodies it attaches (UrdfRobotHandles.joints):

world = rp.PhysicsWorld()

# Read the robot (`path` is the path of the URDF file), then insert its links and joints into the world.
robot, _ = urdf.UrdfRobot.from_file(path)
handles = robot.insert_using_multibody_joints(world.rigid_bodies, world.colliders, world.multibody_joints)
print(f"The robot has {len(handles.links)} links.")

The UrdfLoaderOptions control the conversion of the robot. On top of the options shown below, scale scales the whole robot, mesh_converter selects how its meshes are converted into shapes (see meshes), collider_blueprint is the collider every collider of the robot is created from, and squeeze_empty_fixed_links (enabled by default) removes the links without any geometry or inertia that are attached by fixed joints. The insertion consumes the UrdfRobot: to insert the same robot once more, read it again, or rebuild it from its parsed description with UrdfRobot.from_robot. It can be moved before its insertion with UrdfRobot.append_transform. When inserting with multibody joints, the last argument is a combination of UrdfMultibodyOptions flags: UrdfMultibodyOptions.JOINTS_ARE_KINEMATIC makes the multibody joints kinematic (they are then entirely controlled by the application, e.g., through inverse kinematics), and UrdfMultibodyOptions.DISABLE_SELF_CONTACTS ignores the contacts between the links of the robot:

options = urdf.UrdfLoaderOptions(
# Whether colliders are created from the collision shapes of the links.
# Default: True
create_colliders_from_collision_shapes=True,
# Whether colliders are created from the visual shapes of the links.
# Default: False
create_colliders_from_visual_shapes=False,
# Whether the mass properties declared by the links are applied to their rigid-bodies.
# Default: True
apply_imported_mass_props=True,
# Whether the colliders of two links attached by a joint can collide.
# Default: False
enable_joint_collisions=False,
# Whether the root links are fixed rigid-bodies.
# Default: False
make_roots_fixed=True,
# The pose applied to the whole robot, e.g., to convert its Z-up convention to Y-up.
# Default: the identity pose.
shift=rp.Isometry3(rotation=rp.Rotation3.from_axis_angle((1.0, 0.0, 0.0), math.pi / 2.0)),
# The rigid-body every link is created from (before the URDF data is applied).
# Default: None (a dynamic rigid-body).
rigid_body_blueprint=rp.RigidBody.dynamic(),
)
robot, _ = urdf.UrdfRobot.from_file(path, options)
# Insert the robot with impulse joints. The insertion consumes `robot`.
impulse_robot = robot.insert_using_impulse_joints(world.rigid_bodies, world.colliders, world.impulse_joints)

# Read the robot again to insert it 10 units further, with multibody joints.
robot, _ = urdf.UrdfRobot.from_file(path, options)
robot.append_transform(rp.Isometry3(translation=(10.0, 0.0, 0.0)))
multibody_robot = robot.insert_using_multibody_joints(
world.rigid_bodies,
world.colliders,
world.multibody_joints,
urdf.UrdfMultibodyOptions.DISABLE_SELF_CONTACTS,
)

A robot can also be read from a string containing its URDF description with UrdfRobot.from_str, in which case the directory used to resolve the relative paths of its meshes is given by its mesh_dir argument (the current directory by default). The parsed URDF description gives the name of each link and joint of the robot:

# Read the robot from a string containing its URDF description. The relative paths of its meshes are resolved
# from `mesh_dir` (the current directory by default).
robot, description = urdf.UrdfRobot.from_str(urdf_xml, mesh_dir=mesh_dir)
# The parsed URDF description lists the names of the links and joints of the robot.
print("Links:", [link.name for link in description.links])
print("Joints:", [joint.name for joint in description.joints])
warning

The Python package also loads the .stl, .dae, and .obj meshes referenced by an URDF file. Note that a joint inserted as a multibody joint is reset to its neutral position, i.e., all of its coordinates are zero.

MJCF​

The rapier3d-mjcf crate loads the MJCF XML format of MuJoCo. We recommend browsing the MuJoCo Menagerie repository which contains many MJCF models.

The model is read as an MjcfRobot by mjcf.MjcfRobot.from_file (or from a string with MjcfRobot.from_str, in which case the directory used to resolve its includes and its meshes is given by its base_dir argument), then inserted into the world with MjcfRobot.insert_using_impulse_joints or MjcfRobot.insert_using_multibody_joints (which consume the MjcfRobot). The returned MjcfRobotHandles gives the handles of the rigid-body and colliders created for each body of the model (MjcfRobotHandles.bodies, with None for the MJCF bodies that don't have a rigid-body, e.g., the world body), and the handles of the joints created for its joints and for its <equality> constraints (MjcfRobotHandles.joints and MjcfRobotHandles.equality_joints):

world = rp.PhysicsWorld()

# Read the model (`path` is the path of the MJCF file), then insert its bodies and joints into the world.
robot, _model = mjcf.MjcfRobot.from_file(path)
handles = robot.insert_using_impulse_joints(world.rigid_bodies, world.colliders, world.impulse_joints)

On top of the options shared with UrdfLoaderOptions, the MjcfLoaderOptions can skip the plane geometries of the model (skip_plane_geoms, enabled by default), disable the motors of its joints (disable_joint_motors), and select how the contype and conaffinity attributes of its geometries are converted into collision groups (contact_filter_mode). The collision groups of the colliders of the model can also be modified after its insertion, like for any other collider.

The MjcfMultibodyOptions flags given to MjcfRobot.insert_using_multibody_joints are the same as for URDF, as well as MjcfMultibodyOptions.SKIP_LOOP_CLOSURES (the <equality> constraints closing loops aren't inserted as impulse joints), MjcfMultibodyOptions.SKIP_JOINT_MOTORS, MjcfMultibodyOptions.SKIP_JOINT_LIMITS, and MjcfMultibodyOptions.SKIP_JOINT_SPRINGS. Note that the gravity declared by the model isn't applied automatically: it is given by MjcfRobot.gravity. Each actuator of the model drives the motor of its joint (the actuators are listed by MjcfRobotHandles.actuators, each giving its name and the handle of the joint it drives): MjcfRobotHandles.apply_controls sets the control inputs of all the actuators at once (one per actuator), given the joint set the model was inserted into. MjcfRobotHandles.apply_keyframe resets the robot to one of the keyframes of the model (listed by MjcfRobotHandles.keyframe_names), given by its index or its name, and MjcfRobotHandles.keyframe_controls gives the control inputs that hold the robot in the pose of a keyframe (instead of pulling its position actuators back to zero):

# Rotate the model to convert its Z-up convention to Y-up.
options = mjcf.MjcfLoaderOptions(
shift=rp.Isometry3(rotation=rp.Rotation3.from_axis_angle((1.0, 0.0, 0.0), -math.pi / 2.0))
)
robot, _ = mjcf.MjcfRobot.from_file(path, options)
# The gravity declared by the model isn't applied automatically. It is expressed in the frame of the
# model file, so we rotate it like `options.shift` rotated the model.
world.gravity = options.shift.rotation.transform_vector(robot.gravity)
# Unlike the URDF loader, joints are generally inserted as multibody joints (like in MuJoCo).
handles = robot.insert_using_multibody_joints(
world.rigid_bodies,
world.colliders,
world.multibody_joints,
world.impulse_joints,
mjcf.MjcfMultibodyOptions.SKIP_LOOP_CLOSURES | mjcf.MjcfMultibodyOptions.DISABLE_SELF_CONTACTS,
)

# Drive the actuators of the model: one control input per actuator.
print("Actuators:", [actuator.name for actuator in handles.actuators])
controls = [0.0] * len(handles.actuators)
controls[0] = 0.5
handles.apply_controls(world.rigid_bodies, world.multibody_joints, controls, gain_scale=1.0)

# Reset the robot to the first keyframe declared by the model (if any), given by its index or its name.
if handles.keyframe_names:
handles.apply_keyframe(world.rigid_bodies, world.multibody_joints, 0)
# The control inputs holding the robot in the pose of this keyframe.
controls = handles.keyframe_controls(0)
handles.apply_controls(world.rigid_bodies, world.multibody_joints, controls)

The contact rules of the model (<contact><exclude>, and the friction of <contact><pair>) are applied by physics hooks created by MjcfRobotHandles.contact_hooks, which must be assigned to PhysicsWorld.physics_hooks (where they run natively, without calling back into Python). If you have your own physics hooks, call the filter_contact_pair and modify_solver_contacts methods of these MjcfContactHooks from them instead:

# The contact rules of the model, applied by physics hooks.
world.physics_hooks = handles.contact_hooks()
world.step()
note

Note that MJCF sometimes describes a whole simulation and not only a scene, therefore some of its elements have no equivalent in Rapier, and some others are approximated.

Meshes​

The rapier3d-meshloader crate builds shapes from the usual mesh files, which is what both the MJCF and URDF loaders use internally. It is useful on its own whenever the collision geometry of a scene comes from an asset file rather than from primitive shapes. It reads the .stl, .dae, and .obj files.

A mesh file is read by mesh.load_from_path, which returns a list with one LoadedShape per mesh of the file, giving its shape and its pose (as well as the vertices and indices of the original mesh, as NumPy arrays). A mesh that failed to be converted is given as a MeshConversionError instead, while a file that can't be read raises a MeshLoaderError. The conversion is selected by the converter argument, one of the MeshConverter values (MeshConverter.TRIMESH by default, MeshConverter.trimesh_with_flags applying TriMeshFlags to the triangle meshes), and the meshes can be scaled by the scale argument. A mesh that is already in memory can be converted the same way with mesh.load_from_raw_mesh:

world = rp.PhysicsWorld()

# Every mesh of the file becomes one shape, converted here into its convex hull.
shapes = mesh.load_from_path(path, converter=rp.MeshConverter.CONVEX_HULL, scale=1.0)
for shape in shapes:
# The meshes that failed to be converted are given as exceptions instead.
if isinstance(shape, Exception):
print("Mesh conversion failed:", shape)
continue
world.add_collider(rp.Collider.new(shape.shape).position(shape.pose))