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. Note that these crates are 3D only, and that they are released separately from rapier3d itself.

These crates are integrated into bevy_rapier3d behind the urdf, mjcf, and meshloader cargo features (see the bevy_rapier3d::loaders module). Instead of inserting anything into the physics context directly, the loaders spawn regular entities with rigid-body, collider, and joint components. Therefore the spawned objects can be modified, queried, or despawned like any other entity.

info

The scene loaders are not available in bevy_rapier2d.

The URDF and MJCF loaders are included in the C library when it is built with the robotics feature (see building the C bindings), which is only available for the 3D library with 32-bits floats. Their types and functions are only declared if RAPIER_ROBOTICS is defined (which the Rapier::rapier CMake target does automatically). The loaded models are owned by the application: they must be freed with their dedicated Free function once they are no longer needed, and they can be inserted as many times as needed before that.

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 setsspawned as entitiesinserted into the worldinserted 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).

let mut world = PhysicsWorld::new();

// Read the robot, then insert its links and joints into the world.
let (robot, _) = UrdfRobot::from_file("robot.urdf", UrdfLoaderOptions::default(), None)?;
let handles = robot.insert_using_multibody_joints(
&mut world.bodies,
&mut world.colliders,
&mut world.multibody_joints,
Default::default(),
);
println!("The robot has {} links.", handles.links.len());

The robot is read as an UrdfModel, and spawned with spawn_urdf_robot. The UrdfSpawnOptions select the kind of joints (impulse joints by default), the transform of the root entity every link is a child of, and the physics context the robot is added to. Each link becomes a rigid-body entity (with an UrdfLinkId component), each of its shapes a collider entity child of the link, and each joint an ImpulseJoint or a MultibodyJoint component on the entity of its child link. The returned SpawnedUrdfRobot lists all these entities, and finds them by their URDF name:

// Read the robot, then spawn its links and joints as entities.
let model = UrdfModel::from_file("robot.urdf", UrdfLoaderOptions::default(), None)?;
let robot = spawn_urdf_robot(
&mut commands,
&model,
&UrdfSpawnOptions {
multibody: true,
multibody_options: UrdfMultibodyOptions::DISABLE_SELF_CONTACTS,
// URDF files are generally Z-up, whereas Bevy is Y-up.
root_transform: Transform::from_rotation(Quat::from_rotation_x(-FRAC_PI_2)),
..default()
},
);
println!("The robot has {} links.", robot.links.len());
let _elbow = robot.joints_by_name["elbow"];

The visual elements of the links are not rendered: they are given by the UrdfLinkVisuals component so you can attach your own meshes to the link entities. On top of what rapier3d-urdf converts, the <dynamics> (damping and friction) and <mimic> elements of the joints are applied to multibody joints (as MultibodyJointDamping, MultibodyJointFriction, and MultibodyJointCouplings components), but they are ignored by impulse joints.

The robot is read as an R3UrdfRobot by r3UrdfRobotFromFile, then inserted into the world with r3UrdfRobot_InsertUsingImpulseJoints or r3UrdfRobot_InsertUsingMultibodyJoints. Each link becomes a rigid-body (with its colliders), and each joint an impulse joint or a multibody joint. The returned R3UrdfRobotHandles gives the handles of the rigid-bodies created for the links with r3UrdfRobotHandles_Bodies, in the order of the links of the URDF file:

R3World *world = r3NewWorld();

// Read the robot (`path` is the path of the URDF file), then insert its links and joints into the world.
R3UrdfLoaderOptions options = r3DefaultUrdfLoaderOptions();
R3UrdfRobot *robot = r3UrdfRobotFromFile(path, &options);
R3UrdfRobotHandles *handles = r3UrdfRobot_InsertUsingMultibodyJoints(world, robot, 0);
printf("The robot has %zu links.\n", r3UrdfRobotHandles_Bodies(handles, NULL, 0));

// The loaded robot and the handles are owned by the application.
r3FreeUrdfRobotHandles(handles);
r3FreeUrdfRobot(robot);

The R3UrdfLoaderOptions (initialized by r3DefaultUrdfLoaderOptions) control the conversion of the robot. The insertion of the same loaded robot can be repeated, e.g., after moving it with r3UrdfRobot_AppendTransform. When inserting with multibody joints, the last argument is a bitmask of options: R3_MULTIBODY_JOINTS_ARE_KINEMATIC makes the multibody joints kinematic (they are then entirely controlled by the application, e.g., through inverse kinematics), and R3_MULTIBODY_DISABLE_SELF_CONTACTS ignores the contacts between the links of the robot:

R3UrdfLoaderOptions options = r3DefaultUrdfLoaderOptions();
// Whether colliders are created from the collision shapes of the links.
// Default: 1
options.createCollidersFromCollisionShapes = 1;
// Whether colliders are created from the visual shapes of the links.
// Default: 0
options.createCollidersFromVisualShapes = 0;
// Whether the mass properties declared by the links are applied to their rigid-bodies.
// Default: 1
options.applyImportedMassProps = 1;
// Whether the colliders of two links attached by a joint can collide.
// Default: 0
options.enableJointCollisions = 0;
// Whether the root links are fixed rigid-bodies.
// Default: 0
options.makeRootsFixed = 1;
// The pose applied to the whole robot, e.g., to convert its Z-up convention to Y-up.
// Default: the identity pose.
options.shift = r3Pose(r3Vector(0.0, 0.0, 0.0), r3RotationFromAxisAngle(r3Vector(1.0, 0.0, 0.0), R3_PI / 2.0));
// The description every rigid-body is created from (before the URDF data is applied).
// Default: a dynamic rigid-body.
options.rigidBodyBlueprint = r3DynamicRigidBodyDesc();
R3UrdfRobot *robot = r3UrdfRobotFromFile(path, &options);

// Insert the same robot twice: once with impulse joints, then 10 units further with multibody joints.
R3UrdfRobotHandles *impulse_robot = r3UrdfRobot_InsertUsingImpulseJoints(world, robot);
r3UrdfRobot_AppendTransform(robot, r3TranslationPose(r3Vector(10.0, 0.0, 0.0)));
R3UrdfRobotHandles *multibody_robot =
r3UrdfRobot_InsertUsingMultibodyJoints(world, robot, R3_MULTIBODY_DISABLE_SELF_CONTACTS);

A robot can also be read from a string containing its URDF description with r3UrdfRobotFromString, in which case the relative paths of its meshes are resolved from the directory given as its second argument (or from the current directory if it is NULL):

// Read the robot from a string containing its URDF description. The relative paths of its meshes are resolved
// from `mesh_dir` (or from the current directory if it is NULL).
R3UrdfLoaderOptions options = r3DefaultUrdfLoaderOptions();
R3UrdfRobot *robot = r3UrdfRobotFromString(urdf_xml, mesh_dir, &options);

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 meshes referenced by an URDF file are only loaded if the corresponding cargo feature of the crate is enabled: stl for the .stl files, collada for the .dae files, and wavefront for the .obj files.The urdf feature of bevy_rapier3d also enables the loading of the .stl, .dae, and .obj meshes referenced by an URDF file.The robotics feature also enables the loading of the .stl, .dae, and .obj meshes referenced by an URDF file.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.

let mut world = PhysicsWorld::new();

// Read the model, then insert its bodies and joints into the world.
let (robot, _model) = MjcfRobot::from_file("robot.xml", MjcfLoaderOptions::default())?;
robot.insert_using_impulse_joints(
&mut world.bodies,
&mut world.colliders,
&mut world.impulse_joints,
);

The model is read as an MjcfRobot, and spawned with spawn_mjcf_model. Unlike the URDF loader, its joints are spawned as multibody joints by default (like in MuJoCo), which can be changed with MjcfSpawnOptions::multibody. The returned SpawnedMjcfModel lists the spawned entities, and finds them by their MJCF name. Note that the gravity declared by the model isn't applied automatically:

// Read the model, then spawn its bodies, joints, and actuators as entities.
let (robot, _model) = MjcfRobot::from_file("robot.xml", MjcfLoaderOptions::default())?;
let model = spawn_mjcf_model(
&mut commands,
&robot,
&MjcfSpawnOptions {
// MJCF files are Z-up, whereas Bevy is Y-up.
root_transform: Transform::from_rotation(Quat::from_rotation_x(-FRAC_PI_2)),
..default()
},
);
// The gravity of the model isn't applied automatically.
configurations.single_mut()?.gravity = model.gravity;

Two more elements of MJCF need your app to be set up accordingly. The contact rules of the model (<contact><exclude>, and the friction of <contact><pair>) are registered in the MjcfContactFilters resource and applied by the MjcfPhysicsHooks physics hooks (if you have your own hooks, call MjcfContactFilters::filter_contact_pair and MjcfContactFilters::modify_solver_contacts from them instead). Each actuator of the model is spawned as an entity with an MjcfActuator component, which drives the motor of its joint once the MjcfPlugin is added:

App::new()
.add_plugins((
DefaultPlugins,
// The hooks apply the `<contact>` rules of the MJCF models.
RapierPhysicsPlugin::<MjcfPhysicsHooks>::default(),
// Applies the controls of the actuators to the joints they drive.
MjcfPlugin::default(),
))

The actuators are then controlled by setting their MjcfActuator::ctrl input:

fn drive_actuators(time: Res<Time>, mut actuators: Query<(&Name, &mut MjcfActuator)>) {
for (name, mut actuator) in actuators.iter_mut() {
if name.as_str() == "cart_motor" {
actuator.ctrl = time.elapsed_secs().sin();
}
}
}

The model is read as an R3MjcfRobot by r3MjcfRobotFromFile (with the R3MjcfLoaderOptions initialized by r3DefaultMjcfLoaderOptions), then inserted into the world with r3MjcfRobot_InsertUsingImpulseJoints or r3MjcfRobot_InsertUsingMultibodyJoints. The returned R3MjcfRobotHandles gives the handles of the rigid-bodies created for the bodies of the model with r3MjcfRobotHandles_Bodies (in the order of the model file, with an invalid handle for the MJCF bodies that don't have a rigid-body):

R3World *world = r3NewWorld();

// Read the model (`path` is the path of the MJCF file), then insert its bodies and joints into the world.
R3MjcfLoaderOptions options = r3DefaultMjcfLoaderOptions();
R3MjcfRobot *robot = r3MjcfRobotFromFile(path, &options);
R3MjcfRobotHandles *handles = r3MjcfRobot_InsertUsingImpulseJoints(world, robot);

// The loaded model and the handles are owned by the application.
r3FreeMjcfRobotHandles(handles);
r3FreeMjcfRobot(robot);

On top of the fields shared with R3UrdfLoaderOptions, the R3MjcfLoaderOptions can skip the plane geometries of the model (skipPlaneGeoms), and disable the motors of its joints (disableJointMotors). The collision groups of the colliders of the loaded model can also be modified before its insertion with r3MjcfRobot_SetBodyColliderCollisionGroups.

The bitmask of options given to r3MjcfRobot_InsertUsingMultibodyJoints accepts the same flags as for URDF, as well as R3_MULTIBODY_SKIP_LOOP_CLOSURES (the <equality> constraints closing loops aren't inserted as impulse joints), R3_MULTIBODY_SKIP_JOINT_MOTORS, R3_MULTIBODY_SKIP_JOINT_LIMITS, and R3_MULTIBODY_SKIP_JOINT_SPRINGS. Note that the gravity declared by the model isn't applied automatically: it is given by r3MjcfRobot_Gravity. Each actuator of the model drives the motor of its joint: r3MjcfRobotHandles_ApplyControlsScaled sets the control inputs of all the actuators at once, and r3MjcfRobotHandles_ApplyKeyframe resets the robot to one of the keyframes of the model:

// Unlike the URDF loader, joints are generally inserted as multibody joints (like in MuJoCo).
R3MjcfRobotHandles *handles = r3MjcfRobot_InsertUsingMultibodyJoints(
world, robot, R3_MULTIBODY_SKIP_LOOP_CLOSURES | R3_MULTIBODY_DISABLE_SELF_CONTACTS);
// 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.
R3Vector gravity = r3MjcfRobot_Gravity(robot);
r3SetGravity(world, r3RotationTransformVector(options.shift.rotation, gravity));

// Drive the actuators of the model: one control input per actuator.
size_t actuator_count = r3MjcfRobotHandles_ActuatorCount(handles);
R3Real *controls = calloc(actuator_count, sizeof(R3Real));
controls[0] = 0.5;
r3MjcfRobotHandles_ApplyControlsScaled(handles, controls, actuator_count, 1.0);
free(controls);

// Reset the robot to the first keyframe declared by the model (if any).
if (r3MjcfRobot_KeyframeCount(robot) > 0) {
r3MjcfRobotHandles_ApplyKeyframe(handles, robot, 0);
}

The contact rules of the model (<contact><exclude>, and the friction of <contact><pair>) are applied by physics hooks created by r3MjcfRobotHandles_ContactHooks, which must be given to each simulation step (the inserted colliders already enable the required hooks). These hooks are owned by the application, and must outlive every step using them:

// The contact rules of the model, applied by physics hooks given to every step.
R3MjcfContactHooks *contact_hooks = r3MjcfRobotHandles_ContactHooks(handles, robot);
R3PhysicsHooks hooks = r3MjcfContactHooks_PhysicsHooks(contact_hooks);
r3Step(world, &hooks, NULL);
// The hooks must be freed once they are no longer used by the steps.
r3FreeMjcfContactHooks(contact_hooks);

Finally, the visual meshes of each body of the model can be read with r3MjcfRobot_BodyVisualCount and r3MjcfRobot_BodyVisual, in order to render the model with your own engine.

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. The formats it reads are selected by its cargo features: stl, collada, and wavefront. Each mesh of the file becomes one shape, converted as selected by the MeshConverter:It reads the .stl, .dae, and .obj files.

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.

let mut world = PhysicsWorld::new();

// Every mesh of the file becomes one shape, converted here into its convex hull.
let shapes = load_from_path("asset.obj", &MeshConverter::ConvexHull, Vector::splat(1.0))?;
for shape in shapes.into_iter().flatten() {
world.insert_collider(ColliderBuilder::new(shape.shape).position(shape.pose), None);
}
// All the meshes of the file are combined into a single collider, each of them being
// converted here into its convex hull.
let collider = Collider::from_mesh_file("asset.obj", &MeshConverter::ConvexHull, Vec3::ONE)?;
commands.spawn((Transform::default(), RigidBody::Dynamic, collider));

Collider::from_mesh_file combines all the meshes of the file into a single collider (with a compound shape if there are several of them). Use load_mesh_file_colliders instead to get one collider per mesh, together with its pose, the mesh itself, and its material (which can be converted into a Bevy mesh for rendering with raw_mesh_to_bevy_mesh if the to-bevy-mesh feature is enabled):

// Every mesh of the file becomes its own collider.
let parts = load_mesh_file_colliders("asset.obj", &MeshConverter::TriMesh, Vec3::ONE)?;
for part in parts {
commands.spawn((part.transform, part.collider));
}

The mesh loader is included in the C library by the robotics feature too. A mesh file is read by r3LoadedMeshesFromFile into an R3LoadedMeshes owned by the application, which gives the shape and the pose of each of its meshes (for an .obj file, each group and each change of material starts a new mesh). The conversion is selected by one of the R3_MESH_CONVERTER_* constants (along with the R3_TRIMESH_* flags applied to the triangle meshes, which are only accepted when converting with R3_MESH_CONVERTER_TRIMESH). A mesh that failed to convert doesn't fail the whole load: r3LoadedMeshes_CloneShape then reports an R3_INVALID_ARGUMENT error for that mesh only:

R3World *world = r3NewWorld();

// Every mesh of the file (`path`) becomes one shape, converted here into its convex hull.
R3LoadedMeshes *meshes = r3LoadedMeshesFromFile(path, R3_MESH_CONVERTER_CONVEX_HULL, 0, r3Vector(1.0, 1.0, 1.0));
for (size_t i = 0; i < r3LoadedMeshes_Count(meshes); i++) {
R3SharedShape *shape = r3LoadedMeshes_CloneShape(meshes, i);
R3ColliderDesc collider = r3DefaultColliderDesc();
collider.shape.kind = R3_SHAPE_DESC_SHARED;
collider.shape.sharedShape = shape;
collider.position = r3LoadedMeshes_Pose(meshes, i);
r3InsertColliderWithoutParent(world, &collider);
// The collider keeps its own reference to the shape.
r3FreeSharedShape(shape);
}
r3FreeLoadedMeshes(meshes);

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))