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.
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
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])
stl for the .stl files, collada for the .dae files, and wavefront for the .obj files.urdf feature of bevy_rapier3d also enables the loading of the .stl, .dae, and .obj meshes referenced by an
URDF file.robotics feature also enables the loading of the .stl, .dae, and .obj meshes referenced by an URDF file..stl, .dae, and .obj meshes referenced by an URDF file.
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 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. stl, collada, and wavefront. Each mesh of the file becomes one shape, converted as selected by the MeshConverter:.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))