Skip to main content

Load URDF with React Three Fiber

URDF is the abbreviation of Unified Robot Description Format, which is an XML format for representing a robot model in ROS, we can display a model like above in browser with react three fiber1 through urdf-loaders.

In URDF, the location of a mesh file is specified under the filename attribute of a mesh element with the format: package://<package_name>/path/to/<mesh_name>, for example:

my_pkg/urdf/foo.urdf
...
<geometry>
<mesh filename="package://my_pkg/meshes/bar.dae"/>
</geometry>
...

However, URDF Loader doesn't know where to fetch the mesh files from the given path, we have two options:

  • edit the URDF file by replacing all the package://<package_name> part with the relative path
  • use the packages option2 of URDF Loader to resolve the paths

Setup​

Consider the following ROS package structure

my_pkg/
├── launch/
├── meshes/
│ ├── bar.dae
│ └── baz.dae
├── urdf/
│ └── foo.urdf
├── CMakeLists.txt
└── package.xml

The mesh/URDF files should be copied into the public (static assets) folder of web server, assume no prefix for static files url

app/
├── src/
├── public/
│ ├── models/
│ │ └── foo/
│ │ ├── meshes/
│ │ │ ├── bar.dae
│ │ │ └── baz.dae
│ │ └── urdf/
│ │ └── foo.urdf
│ └── index.html
└── package.json

Option 1​

Simply change all the occurrence of package://my_pkg to .. in URDF file, then use the useLoader hook from r3f

import { useLoader } from '@react-three/fiber'
import URDFLoader, { URDFRobot } from 'urdf-loader'
const FOO_URDF_URL = './models/foo/urdf/foo.urdf'
const Model = () => {
const robot: URDFRobot = useLoader(URDFLoader as any, FOO_URDF_URL)
// do whatever setup with the robot...
return <primitive object={robot} />
}

Option 2​

We need to initialize URDFLoader ourselves in order to remap the packages, basically create a dumb version of useLoader, it doesn't have suspense / cache support so is less convenient

import { useEffect, useState } from 'react'
import URDFLoader, { URDFRobot } from 'urdf-loader'
const Model = () => {
const [robot, setRobot] = useState<URDFRobot>()
useEffect(() => {
const loader = new URDFLoader()
loader.packages = {
my_pkg: './models/foo',
}
loader.load(`./models/foo/urdf/foo.urdf`, (robot) => {
// do whatever setup with the robot...
setRobot(robot)
})
}, [])
return robot && <primitive object={robot} />
}

Make the Model Move​

URDFRobot extends Object3D, it also provides the setJointValue function that would take joint type into account.

Here's an example that changes the robot position and wheel joint values

const maxVel = 0.6
const wheelRadius = 0.04
const trackRadius = 0.5
const Model = () => {
// load robot as above ...
useFrame(({ clock: { elapsedTime } }) => {
if (robot) {
// the wheels should really rotate at different speed to make the
// robot turn, but my eyes don't tell the difference anyway
const wheelAngle = elapsedTime * (maxVel / wheelRadius)
const theta = elapsedTime * (maxVel / trackRadius)
robot.joints['base_link_to_left_wheel'].setJointValue(wheelAngle)
robot.joints['base_link_to_right_wheel'].setJointValue(wheelAngle)
robot.position.set(trackRadius * Math.sin(theta), 0, trackRadius * Math.cos(theta))
robot.rotation.set(-Math.PI / 2, 0, theta)
}
})
return robot && <primitive object={robot} />
}

Notes​

  • When doubt about the path, just open developer tools and switch to the network tab to see if the request url is correct
  • The default coordinate system in three.js have y-axis pointing up instead of z-axis like in most conventions, just rotate the model in x-axis by -Math.PI / 2

Footnotes​

  1. A react renderer for three.js ↩

  2. The packages option is documented here ↩