#![warn(missing_docs)]
use na::RealField;
use rapier3d::{
dynamics::{
GenericJoint, GenericJointBuilder, ImpulseJointHandle, ImpulseJointSet, JointAxesMask,
JointAxis, MassProperties, MultibodyJointHandle, MultibodyJointSet, RigidBody,
RigidBodyBuilder, RigidBodyHandle, RigidBodySet, RigidBodyType,
},
geometry::{
Collider, ColliderBuilder, ColliderHandle, ColliderSet, MeshConverter, SharedShape,
TriMeshFlags,
},
glamx::EulerRot,
math::{Pose, Real, Rotation, Vector},
na,
};
use std::collections::{HashMap, HashSet};
use std::path::Path;
use urdf_rs::{Geometry, Inertial, Joint, Pose as UrdfPose, Robot};
pub use urdf_rs;
#[cfg(doc)]
use rapier3d::dynamics::Multibody;
bitflags::bitflags! {
#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
pub struct UrdfMultibodyOptions: u8 {
const JOINTS_ARE_KINEMATIC = 0b0001;
const DISABLE_SELF_CONTACTS = 0b0010;
}
}
pub type LinkId = usize;
#[derive(Clone, Debug)]
pub struct UrdfLoaderOptions {
pub create_colliders_from_collision_shapes: bool,
pub create_colliders_from_visual_shapes: bool,
pub apply_imported_mass_props: bool,
pub enable_joint_collisions: bool,
pub make_roots_fixed: bool,
pub trimesh_flags: TriMeshFlags,
pub mesh_converter: Option<MeshConverter>,
pub shift: Pose,
pub scale: Real,
pub collider_blueprint: ColliderBuilder,
pub rigid_body_blueprint: RigidBodyBuilder,
pub squeeze_empty_fixed_links: bool,
}
impl Default for UrdfLoaderOptions {
fn default() -> Self {
Self {
create_colliders_from_collision_shapes: true,
create_colliders_from_visual_shapes: false,
apply_imported_mass_props: true,
enable_joint_collisions: false,
make_roots_fixed: false,
trimesh_flags: TriMeshFlags::all(),
mesh_converter: None,
shift: Pose::IDENTITY,
scale: 1.0,
collider_blueprint: ColliderBuilder::default().density(0.0),
rigid_body_blueprint: RigidBodyBuilder::dynamic(),
squeeze_empty_fixed_links: true,
}
}
}
#[derive(Clone, Debug)]
pub struct UrdfVisual {
pub shape: SharedShape,
pub local_pose: Pose,
}
#[derive(Clone, Debug)]
pub struct UrdfCollider {
pub collider: Collider,
pub visual: Option<UrdfVisual>,
}
#[derive(Clone, Debug)]
pub struct UrdfLink {
pub body: RigidBody,
pub colliders: Vec<UrdfCollider>,
}
#[derive(Clone, Debug)]
pub struct UrdfJoint {
pub joint: GenericJoint,
pub link1: LinkId,
pub link2: LinkId,
}
#[derive(Clone, Debug)]
pub struct UrdfRobot {
pub links: Vec<UrdfLink>,
pub joints: Vec<UrdfJoint>,
}
pub struct UrdfJointHandle<JointHandle> {
pub joint: JointHandle,
pub link1: RigidBodyHandle,
pub link2: RigidBodyHandle,
}
pub struct UrdfColliderHandle {
pub handle: ColliderHandle,
pub visual: Option<UrdfVisual>,
}
pub struct UrdfLinkHandle {
pub body: RigidBodyHandle,
pub colliders: Vec<UrdfColliderHandle>,
}
pub struct UrdfRobotHandles<JointHandle> {
pub links: Vec<UrdfLinkHandle>,
pub joints: Vec<UrdfJointHandle<JointHandle>>,
}
impl UrdfRobot {
pub fn from_file(
path: impl AsRef<Path>,
options: UrdfLoaderOptions,
mesh_dir: Option<&Path>,
) -> anyhow::Result<(Self, Robot)> {
let path = path.as_ref().canonicalize()?;
let mesh_dir = mesh_dir
.or_else(|| path.parent())
.unwrap_or_else(|| Path::new("./"));
let raw = std::fs::read_to_string(&path)?;
let sanitized = sanitize_urdf_str(&raw);
let robot = urdf_rs::read_from_string(&sanitized)?;
Ok((Self::from_robot(&robot, options, mesh_dir), robot))
}
pub fn from_str(
str: &str,
options: UrdfLoaderOptions,
mesh_dir: &Path,
) -> anyhow::Result<(Self, Robot)> {
let sanitized = sanitize_urdf_str(str);
let robot = urdf_rs::read_from_string(&sanitized)?;
Ok((Self::from_robot(&robot, options, mesh_dir), robot))
}
pub fn from_robot(robot: &Robot, options: UrdfLoaderOptions, mesh_dir: &Path) -> Self {
let (robot_owned, force_fixed_links): (std::borrow::Cow<Robot>, HashSet<String>) =
if options.squeeze_empty_fixed_links {
let mut clone = robot.clone();
let force_fixed = squeeze_empty_fixed_links(&mut clone);
(std::borrow::Cow::Owned(clone), force_fixed)
} else {
(std::borrow::Cow::Borrowed(robot), HashSet::new())
};
let robot = robot_owned.as_ref();
let mut name_to_link_id = HashMap::new();
let mut link_is_root = vec![true; robot.links.len()];
let mut links: Vec<_> = robot
.links
.iter()
.enumerate()
.map(|(id, link)| {
name_to_link_id.insert(&link.name, id);
let mut colliders = vec![];
if options.create_colliders_from_collision_shapes {
colliders.extend(link.collision.iter().flat_map(|co| {
urdf_to_colliders(&options, mesh_dir, &co.geometry, &co.origin)
}))
}
if options.create_colliders_from_visual_shapes {
colliders.extend(link.visual.iter().flat_map(|vis| {
urdf_to_colliders(&options, mesh_dir, &vis.geometry, &vis.origin)
}))
}
let mut body = urdf_to_rigid_body(&options, &link.inertial);
let new_pos = options.shift * body.position();
body.set_position(new_pos, false);
UrdfLink { body, colliders }
})
.collect();
let joints: Vec<_> = robot
.joints
.iter()
.map(|joint| {
let link1 = name_to_link_id[&joint.parent.link];
let link2 = name_to_link_id[&joint.child.link];
let pose1 = *links[link1].body.position();
let rb2 = &mut links[link2].body;
let joint = urdf_to_joint(&options, joint, &pose1, rb2);
link_is_root[link2] = false;
UrdfJoint {
joint,
link1,
link2,
}
})
.collect();
if options.make_roots_fixed {
for (link, is_root) in links.iter_mut().zip(link_is_root.iter().copied()) {
if is_root {
link.body.set_body_type(RigidBodyType::Fixed, false)
}
}
}
if !force_fixed_links.is_empty() {
for (id, link) in robot.links.iter().enumerate() {
if force_fixed_links.contains(&link.name) {
links[id].body.set_body_type(RigidBodyType::Fixed, false);
}
}
}
Self { links, joints }
}
pub fn insert_using_impulse_joints(
self,
rigid_body_set: &mut RigidBodySet,
collider_set: &mut ColliderSet,
joint_set: &mut ImpulseJointSet,
) -> UrdfRobotHandles<ImpulseJointHandle> {
let links: Vec<_> = self
.links
.into_iter()
.map(|link| {
let body = rigid_body_set.insert(link.body);
let colliders = link
.colliders
.into_iter()
.map(|co| {
let handle =
collider_set.insert_with_parent(co.collider, body, rigid_body_set);
UrdfColliderHandle {
handle,
visual: co.visual,
}
})
.collect();
UrdfLinkHandle { body, colliders }
})
.collect();
let joints: Vec<_> = self
.joints
.into_iter()
.map(|joint| {
let link1 = links[joint.link1].body;
let link2 = links[joint.link2].body;
let joint = joint_set.insert(link1, link2, joint.joint, false);
UrdfJointHandle {
joint,
link1,
link2,
}
})
.collect();
UrdfRobotHandles { links, joints }
}
pub fn insert_using_multibody_joints(
self,
rigid_body_set: &mut RigidBodySet,
collider_set: &mut ColliderSet,
joint_set: &mut MultibodyJointSet,
multibody_options: UrdfMultibodyOptions,
) -> UrdfRobotHandles<Option<MultibodyJointHandle>> {
let links: Vec<_> = self
.links
.into_iter()
.map(|link| {
let body = rigid_body_set.insert(link.body);
let colliders = link
.colliders
.into_iter()
.map(|co| {
let handle =
collider_set.insert_with_parent(co.collider, body, rigid_body_set);
UrdfColliderHandle {
handle,
visual: co.visual,
}
})
.collect();
UrdfLinkHandle { body, colliders }
})
.collect();
let joints: Vec<_> = self
.joints
.into_iter()
.map(|joint| {
let link1 = links[joint.link1].body;
let link2 = links[joint.link2].body;
let joint =
if multibody_options.contains(UrdfMultibodyOptions::JOINTS_ARE_KINEMATIC) {
joint_set.insert_kinematic(link1, link2, joint.joint, false)
} else {
joint_set.insert(link1, link2, joint.joint, false)
};
if let Some(joint) = joint {
let (multibody, _) = joint_set.get_mut(joint).unwrap_or_else(|| unreachable!());
multibody.set_self_contacts_enabled(
!multibody_options.contains(UrdfMultibodyOptions::DISABLE_SELF_CONTACTS),
);
}
UrdfJointHandle {
joint,
link1,
link2,
}
})
.collect();
UrdfRobotHandles { links, joints }
}
pub fn append_transform(&mut self, transform: &Pose) {
for link in &mut self.links {
let new_pos = transform * link.body.position();
link.body.set_position(new_pos, true);
}
}
}
#[rustfmt::skip]
fn urdf_to_rigid_body(options: &UrdfLoaderOptions, inertial: &Inertial) -> RigidBody {
let mut origin = urdf_to_pose(&inertial.origin);
origin.translation *= options.scale;
let mut builder = options.rigid_body_blueprint.clone();
builder.body_type = RigidBodyType::Dynamic;
if options.apply_imported_mass_props {
builder = builder.additional_mass_properties(MassProperties::with_inertia_matrix(
origin.translation,
inertial.mass.value as Real,
na::Matrix3::new(
inertial.inertia.ixx as Real, inertial.inertia.ixy as Real, inertial.inertia.ixz as Real,
inertial.inertia.ixy as Real, inertial.inertia.iyy as Real, inertial.inertia.iyz as Real,
inertial.inertia.ixz as Real, inertial.inertia.iyz as Real,inertial.inertia.izz as Real,
).into(),
))
}
builder.build()
}
fn urdf_to_colliders(
options: &UrdfLoaderOptions,
_mesh_dir: &Path, geometry: &Geometry,
origin: &UrdfPose,
) -> Vec<UrdfCollider> {
let s = options.scale;
let mut shapes: Vec<(SharedShape, Pose, Option<UrdfVisual>)> = Vec::new();
match &geometry {
Geometry::Box { size } => {
if size[0] == 0.0 && size[1] == 0.0 && size[2] == 0.0 {
return Vec::new();
}
shapes.push((
SharedShape::cuboid(
size[0] as Real * s / 2.0,
size[1] as Real * s / 2.0,
size[2] as Real * s / 2.0,
),
Pose::IDENTITY,
None,
));
}
Geometry::Cylinder { radius, length } => {
shapes.push((
SharedShape::cylinder(*length as Real * s / 2.0, *radius as Real * s),
Pose::rotation(Vector::X * Real::frac_pi_2()),
None,
));
}
Geometry::Capsule { radius, length } => {
shapes.push((
SharedShape::capsule_z(*length as Real * s / 2.0, *radius as Real * s),
Pose::IDENTITY,
None,
));
}
Geometry::Sphere { radius } => {
shapes.push((SharedShape::ball(*radius as Real * s), Pose::IDENTITY, None));
}
#[cfg(not(feature = "__meshloader_is_enabled"))]
Geometry::Mesh { .. } => {
log::error!(
"Mesh loading is disabled by default. Enable one of the format features (`stl`, `collada`, `wavefront`) of `rapier3d-urdf` for mesh support."
);
}
#[cfg(feature = "__meshloader_is_enabled")]
Geometry::Mesh { filename, scale } => {
let full_path = _mesh_dir.join(filename);
let mesh_scale = scale
.map(|sc| Vector::new(sc[0] as Real, sc[1] as Real, sc[2] as Real))
.unwrap_or_else(|| Vector::splat(1.0))
* s;
let converter = options
.mesh_converter
.unwrap_or(MeshConverter::TriMeshWithFlags(options.trimesh_flags));
let needs_visual = !matches!(
converter,
MeshConverter::TriMesh | MeshConverter::TriMeshWithFlags(_)
);
let visual_converter = MeshConverter::TriMeshWithFlags(options.trimesh_flags);
let Ok(loaded_mesh) =
rapier3d_meshloader::load_from_path(full_path, &converter, mesh_scale)
else {
return Vec::new();
};
for loaded in loaded_mesh.into_iter().filter_map(|x| x.ok()) {
let visual = if needs_visual {
rapier3d_meshloader::load_from_raw_mesh(
&loaded.raw_mesh,
&visual_converter,
mesh_scale,
)
.ok()
.map(|(visual_shape, visual_pose)| UrdfVisual {
shape: visual_shape,
local_pose: loaded.pose.inverse() * visual_pose,
})
} else {
None
};
shapes.push((loaded.shape, loaded.pose, visual));
}
}
}
let mut local_pose = urdf_to_pose(origin);
local_pose.translation *= s;
shapes
.into_iter()
.map(move |(shape, shape_transform, visual)| {
let mut builder = options.collider_blueprint.clone();
builder.shape = shape;
let collider = builder.position(local_pose * shape_transform).build();
UrdfCollider { collider, visual }
})
.collect()
}
fn urdf_to_pose(pose: &UrdfPose) -> Pose {
Pose::from_parts(
Vector::new(
pose.xyz[0] as Real,
pose.xyz[1] as Real,
pose.xyz[2] as Real,
),
Rotation::from_euler(
EulerRot::XYZ,
pose.rpy[0] as Real,
pose.rpy[1] as Real,
pose.rpy[2] as Real,
),
)
}
fn urdf_to_joint(
options: &UrdfLoaderOptions,
joint: &Joint,
pose1: &Pose,
link2: &mut RigidBody,
) -> GenericJoint {
let locked_axes = match joint.joint_type {
urdf_rs::JointType::Fixed => JointAxesMask::LOCKED_FIXED_AXES,
urdf_rs::JointType::Continuous | urdf_rs::JointType::Revolute => {
JointAxesMask::LOCKED_REVOLUTE_AXES
}
urdf_rs::JointType::Floating => JointAxesMask::empty(),
urdf_rs::JointType::Planar => JointAxesMask::ANG_AXES | JointAxesMask::LIN_X,
urdf_rs::JointType::Prismatic => JointAxesMask::LOCKED_PRISMATIC_AXES,
urdf_rs::JointType::Spherical => JointAxesMask::LOCKED_SPHERICAL_AXES,
};
let mut joint_to_parent = urdf_to_pose(&joint.origin);
joint_to_parent.translation *= options.scale;
let joint_axis = Vector::new(
joint.axis.xyz[0] as Real,
joint.axis.xyz[1] as Real,
joint.axis.xyz[2] as Real,
)
.normalize_or_zero();
link2.set_position(pose1 * joint_to_parent, false);
let mut builder =
GenericJointBuilder::new(locked_axes).contacts_enabled(options.enable_joint_collisions);
if joint_axis != Vector::ZERO {
let basis = GenericJoint::complete_ang_frame(joint_axis);
let frame2 = Pose::from_rotation(basis);
let frame1 = joint_to_parent * frame2;
builder = builder.local_frame1(frame1).local_frame2(frame2);
} else {
builder = builder.local_frame1(joint_to_parent);
}
match joint.joint_type {
urdf_rs::JointType::Prismatic => {
builder = builder.limits(
JointAxis::LinX,
[
joint.limit.lower as Real * options.scale,
joint.limit.upper as Real * options.scale,
],
)
}
urdf_rs::JointType::Revolute => {
builder = builder.limits(
JointAxis::AngX,
[joint.limit.lower as Real, joint.limit.upper as Real],
)
}
_ => {}
}
builder.build()
}
fn sanitize_urdf_str(input: &str) -> String {
const PLACEHOLDER: &str = "<box size=\"0 0 0\"/>";
let mut out = String::with_capacity(input.len());
let mut rem = input;
while let Some(open_rel) = rem.find("<geometry") {
out.push_str(&rem[..open_rel]);
let after_open_tag = &rem[open_rel..];
let Some(open_end_rel) = after_open_tag.find('>') else {
out.push_str(after_open_tag);
return out;
};
let open_tag = &after_open_tag[..=open_end_rel];
let body_start = open_end_rel + 1;
if open_tag.ends_with("/>") {
out.push_str(open_tag);
rem = &after_open_tag[body_start..];
continue;
}
let body_and_after = &after_open_tag[body_start..];
let Some(close_rel) = body_and_after.find("</geometry>") else {
out.push_str(after_open_tag);
return out;
};
let body = &body_and_after[..close_rel];
let close_tag = "</geometry>";
out.push_str(open_tag);
if body.trim().is_empty() {
out.push_str(PLACEHOLDER);
} else {
out.push_str(body);
}
out.push_str(close_tag);
rem = &body_and_after[close_rel + close_tag.len()..];
}
out.push_str(rem);
out
}
fn is_link_empty(link: &urdf_rs::Link) -> bool {
if !link.visual.is_empty() || !link.collision.is_empty() {
return false;
}
let inertia = &link.inertial.inertia;
link.inertial.mass.value == 0.0
&& inertia.ixx == 0.0
&& inertia.ixy == 0.0
&& inertia.ixz == 0.0
&& inertia.iyy == 0.0
&& inertia.iyz == 0.0
&& inertia.izz == 0.0
}
fn pose_to_urdf_pose(pose: &Pose) -> UrdfPose {
let (rx, ry, rz) = pose.rotation.to_euler(EulerRot::XYZ);
UrdfPose {
xyz: urdf_rs::Vec3([
pose.translation.x as f64,
pose.translation.y as f64,
pose.translation.z as f64,
]),
rpy: urdf_rs::Vec3([rx as f64, ry as f64, rz as f64]),
}
}
fn compose_urdf_pose(parent: &UrdfPose, child: &UrdfPose) -> UrdfPose {
pose_to_urdf_pose(&(urdf_to_pose(parent) * urdf_to_pose(child)))
}
fn rotate_axis_into_child_frame(axis: &urdf_rs::Vec3, child_origin: &UrdfPose) -> urdf_rs::Vec3 {
let pose = urdf_to_pose(child_origin);
let v = Vector::new(axis[0] as Real, axis[1] as Real, axis[2] as Real);
let r = pose.rotation.inverse() * v;
urdf_rs::Vec3([r.x as f64, r.y as f64, r.z as f64])
}
fn squeeze_empty_fixed_links(robot: &mut Robot) -> HashSet<String> {
let mut force_fixed: HashSet<String> = HashSet::new();
loop {
let empty_names: Vec<String> = robot
.links
.iter()
.filter(|l| is_link_empty(l))
.map(|l| l.name.clone())
.collect();
if empty_names.is_empty() {
break;
}
let mut progressed = false;
for empty_name in &empty_names {
let parent_joint_idx = robot
.joints
.iter()
.position(|j| j.child.link == *empty_name);
let child_joint_indices: Vec<usize> = robot
.joints
.iter()
.enumerate()
.filter_map(|(i, j)| (j.parent.link == *empty_name).then_some(i))
.collect();
match parent_joint_idx {
Some(pj_idx) => {
let parent_joint = robot.joints[pj_idx].clone();
let mut spliced_any = false;
for &cj_idx in &child_joint_indices {
if robot.joints[cj_idx].joint_type != urdf_rs::JointType::Fixed {
continue;
}
let child_origin = robot.joints[cj_idx].origin.clone();
let new_origin = compose_urdf_pose(&parent_joint.origin, &child_origin);
let new_axis =
rotate_axis_into_child_frame(&parent_joint.axis.xyz, &child_origin);
let cj = &mut robot.joints[cj_idx];
cj.parent = parent_joint.parent.clone();
cj.origin = new_origin;
cj.joint_type = parent_joint.joint_type.clone();
cj.axis.xyz = new_axis;
cj.limit = parent_joint.limit.clone();
cj.dynamics = parent_joint.dynamics.clone();
cj.mimic = parent_joint.mimic.clone();
cj.safety_controller = parent_joint.safety_controller.clone();
cj.calibration = parent_joint.calibration.clone();
spliced_any = true;
}
let still_has_child = robot
.joints
.iter()
.enumerate()
.any(|(i, j)| i != pj_idx && j.parent.link == *empty_name);
if !still_has_child {
robot.joints.remove(pj_idx);
if let Some(li) = robot.links.iter().position(|l| &l.name == empty_name) {
robot.links.remove(li);
}
progressed = true;
break;
} else if spliced_any {
progressed = true;
break;
}
}
None => {
let mut spliced_any = false;
for &cj_idx in child_joint_indices.iter().rev() {
if robot.joints[cj_idx].joint_type == urdf_rs::JointType::Fixed {
force_fixed.insert(robot.joints[cj_idx].child.link.clone());
robot.joints.remove(cj_idx);
spliced_any = true;
}
}
let still_has_child = robot.joints.iter().any(|j| j.parent.link == *empty_name);
if !still_has_child {
if let Some(li) = robot.links.iter().position(|l| &l.name == empty_name) {
robot.links.remove(li);
}
progressed = true;
break;
} else if spliced_any {
progressed = true;
break;
}
}
}
}
if !progressed {
break;
}
}
force_fixed
}