Introduction
Swift is a light-weight browser-based animation visualiser which provides robotics-specific functionality for rapid prototyping of algorithms, research, and education. Built using Python and Javascript, Swift is cross-platform (Linux, macOS, and Windows).
Swift provides:
visualisation of mesh objects (Collada, STL, OBJ, glTF/GLB, PLY, VRML/WRL, and PCD files) and primitive shapes;
robot visualisation and simulation;
interactive UI controls (sliders, buttons, and more) for driving a scene from the browser;
recording and saving a video of the simulation;
source code which can be read for learning and teaching.
Swift is the primary visualisation engine for the Robotics Toolbox for Python. Through the Robotics Toolbox, Swift can visualise over 150 robot models – contemporary robots from Franka-Emika, Kinova, Universal Robotics, Rethink, as well as classical robots such as the Puma 560 and the Stanford arm.
Installation
pip install swift-sim
Swift is normally installed as a dependency of roboticstoolbox-python rather than used standalone:
pip install roboticstoolbox-python
Swift requires Python 3.10 or later.
Quick start
Displaying a shape
Let’s start with a simple example that creates a blue cube in a Swift environment. The following code snippet can be run in a Python interpreter and it’s included as examples/box1.py
1import spatialgeometry as sg
2from swift import Swift
3
4env = Swift()
5env.launch(ground_opacity=0.5)
6
7box = sg.Cuboid([0.2, 0.2, 0.2], color="blue")
8env.add_shape(box)
9
10env.hold() # keep the browser tab open
Line 4 creates a Swift environment into which objects can be placed. Line 5 launches the visualisation of the environment, and it opens a browser tab to show it. Line 7 adds a blue box (cube) in the scene. The cube’s reference point is its centre, and by default it is positioned at the origin of the world frame. Color can be specified as a string (e.g. “blue”) or as an RGB or RGBA list of floats in the range 0-1.
The graphical objects are defined by the spatialgeometry module, and the box is a cuboid (a rectangular prism) that is 0.2m in each dimension.
The ground plane is rendered with 50% opacity (line 5) so that the box appears to be half underwater – you can
even look under the ground plane to see the box from below.
Line 8 adds the box to the scene, and line 10 blocks the script from exiting, so that the browser tab remains open.
You can navigate around the scene using your mouse, see Viewpoint control for details.
You can close the browser tab and the script will exit automatically.
You can also press ^C in the terminal to exit the script and this will close the browser tab.
Let’s try something a little more interesting, this is examples/box2.py
1import spatialgeometry as sg
2from spatialmath import SE3
3from swift import Swift
4
5env = Swift()
6env.launch(ground_pattern="@tile")
7
8box = sg.Cuboid([0.2, 0.2, 0.2], pose=SE3(0, 0, 0.2)*SE3.RPY(45, 45, 0, unit="deg"), color="blue")
9env.add_shape(box)
10
11env.hold() # keep the browser tab open
This time we have set the ground plane to be opaque (default) and to have a tiled pattern. The ground pattern can be selected as a grid (“@grid”) or it can be the path to a texture image. The pose of the cube has been explicitly set, rotated the box 45 degrees about the x-axis and 45 degrees about the y-axis, and then raised above the ground plane.
A much more complex example is examples/busy_scene.py which shows a scene with many objects including a number of new shapes such as cylinders, spheres, meshes, and a path. The big disk has a low opacity and you change your view point to look through it. It also has a gravel textured ground plane.
Under the hood, Swift’s env.add() calls the shape’s to_dict() method and sends it over
a websocket to the browser which runs Swift’s JavaScript code to render the scene.
Next, we will look at how to animate shapes in Swift.
Animating shapes
To animate a shape, we simply change its pose and call step() to update the
scene. The following example, examples/box_orbit1.py, animates a box moving in an orbit around the origin:
1import spatialgeometry as sg
2import spatialmath as sm
3import numpy as np
4from spatialmath import SE3
5from swift import Swift
6
7env = Swift()
8env.launch(realtime=True, ground_opacity=0.5)
9
10W = 0.1 # size of the box
11box = sg.Cuboid([W, W, W], color=[0.2, 0.4, 1.0, 1.0])
12env.add_shape(box)
13
14# animate
15dt = 0.02 # time step, 50 fps
16for t in np.arange(0, 20, dt): # run for 5 seconds
17 print(f"t = {t:.2f}")
18 box.T = sm.SE3.Rx(t / 10) * sm.SE3.Rz(t) * sm.SE3.Tx(3 * W)
19 env.step(dt)
20env.hold() # keep the browser tab open
The realtime argument to launch() (line 8) means that the simulation will run in real (clock) time, and the time step is set to 0.02 seconds (50 fps).
The box’s pose is updated
at line 18 by setting its T attribute to a new pose – this can be an SE3 object
or a \(4 \times 4\) homogeneous transformation matrix. In this case pose is computed as a function
of time, and the box moves in a circular orbit on a plane that tilts about the x-axis
slowly over time. The scene is updated at line 19 by calling step() which
waits for dt seconds before continuing to the next iteration of the loop.
If there were multiple animated shapes in the scene, we would update all their poses first, and
call step() once at the end of the loop to update the scene.
The clock is shown at top right of the Swift window, and the speed of the animation is controlled
by the realtime parameter passed to launch and keyboard commands, described in Playback controls.
An alternative, and more concise, way to achieve this is using callbacks. The following example, examples/box_orbit2.py, shows how to use a callback function to compute the pose of the box as a function of time:
1import spatialgeometry as sg
2import spatialmath as sm
3from spatialmath import SE3
4from swift import Swift
5
6env = Swift()
7env.launch(realtime=True, ground_opacity=0.5)
8
9W = 0.1 # size of the box
10box = sg.Cuboid([W, W, W], color="blue")
11
12def orbit(t, values):
13 return sm.SE3.Rx(t / 10) * sm.SE3.Rz(t) * sm.SE3.Tx(3 * W)
14
15env.add_shape(box, callback=orbit)
16
17# animate
18env.run(20, dt=0.02) # run for 5 seconds, 50 fps
In line 15, when we add the shape to the Swift environment we specify a callback function (lines 12-13) that computes the pose of the box as a function of time. Line 18 runs a simulation loop for 20 seconds with a time step of 0.02 seconds (50 fps), calls any registered callback functions, and updates object poses.
Swift also provides a simple way to add interactive sliders to the scene, and the following example, examples/box_sliders.py, shows how to use sliders to control the position of a box in 3D space:
1import spatialgeometry as sg
2from spatialmath import SE3
3from swift import Swift, Slider
4
5env = Swift()
6env.launch(realtime=True, ground_opacity=0.5)
7
8W = 0.2
9box = sg.Cuboid([W, W, W], color=[0.2, 0.4, 1.0, 1.0])
10
11def box_pose(t, values):
12 # z is height above the floor, not the box centre
13 return SE3(values["x"], values["y"], values["z"] + W / 2)
14
15env.add_shape(box, callback=box_pose)
16
17env.add_ui(Slider(min=-0.5, max=0.5, step=0.01, value=0.0, label="Box X", unit="m"), name="x")
18env.add_ui(Slider(min=-0.5, max=0.5, step=0.01, value=0.0, label="Box Y", unit="m"), name="y")
19env.add_ui(Slider(min=0.0, max=0.6, step=0.01, value=0.0, label="Box Z", unit="m"), name="z")
20
21# animate
22env.run(dt=0.02) # run forever at 50 fps
Lines 17-19 add slider elements to the Swift environment. Each slider has a minimum and maximum value, a step size, an initial value, a label, and a unit. The sliders are named “x”, “y”, and “z” and their values are passed to all callback functions as the second argument – it is a dictionary keyed on this name. In this example, the callback function (lines 11-13) computes the pose of the box based on the slider values.
The slider for the z-axis controls the height of the box above the ground plane, so we add half the box height to the slider value to compute the box’s pose.
UI elements appear in a panel on the left, not in the 3D scene where shapes are rendered. Slider elements can also have explicit user defined callback functions that are called when the slider value changes, and these can be used to control the pose of a shape or any other parameter in the scene.
Collision checking
The Spatial Geometry package provides a simple way to check for collisions between shapes. The following example, examples/collision.py, shows this working for a blue box at the origin and a green sphere controlled by a slider. The distance between the two shapes is displayed in a label, and when the sphere gets too close to the box, both shapes change color to red.
1import spatialgeometry as sg
2from spatialmath import SE3
3from swift import Swift
4from swift.Elements import Slider, Label
5
6env = Swift()
7env.launch(realtime=True, ground_opacity=0.1)
8
9box = sg.Cuboid([0.2, 0.2, 0.2], pose=SE3(0, 0, 0), color="blue") # blue box at origin
10env.add_shape(box)
11
12def sphere_pose(t, values):
13 return SE3(values["x"], 0, 0)
14
15sphere = sg.Sphere(0.1, pose=SE3(0.5, 0, 0.1), color="green") # green sphere
16env.add_shape(sphere, callback=sphere_pose)
17
18env.add_ui(Slider(lambda v: None, min=-2, max=2, step=0.01, value=0.5, label="Sphere X", unit="m"), name="x")
19env.add_ui((distance := Label("")), name="label")
20
21while True:
22 env.step(0.05)
23 d, p1, p2 = box.closest_point(sphere)
24 distance.label = f"Distance: {d:.3f}"
25 if d < 0.1:
26 box.color = "red" # change box color to red
27 sphere.color = "red" # change sphere color to red
28 else:
29 box.color = "blue" # change box color back to blue
30 sphere.color = "green" # change sphere color back to green
This example draws on concepts that are familiar from the previous examples. The blue box is created at the origin, and a green sphere is created at x=0.5. The sphere’s pose is controlled by a slider, and the callback function (lines 12-13) computes the pose of the sphere based on the slider value.
The distance between the two shapes is computed in line 23
using the closest_point() method of the box, which returns the distance and the
closest points on each shape. The distance is displayed in a label, and when the
distance is less than 0.1, both shapes change color to red.
Note that this line could just as easily have been
23d, p1, p2 = box.closest_point(sphere)
Assemblies
In robotics we often have a number of rigid bodies connected together by joints, and we can represent this as an assembly. The following example, examples/two_link_arm.py, shows a simple two link arm with two revolute joints. The joint angles are controlled by sliders.
1import numpy as np
2import spatialgeometry as sg
3from spatialmath import SE3
4from swift import Swift, Slider
5
6env = Swift()
7env.launch(realtime=True)
8
9class TwoLinkArm:
10 """A pure kinematic model: two links, two revolute joints about z."""
11
12 def __init__(self, L1=0.3, L2=0.25, thickness=0.03):
13 self.L1 = L1
14 self.L2 = L2
15 self.link1 = sg.Cuboid([L1, thickness, thickness], color="red")
16 self.link2 = sg.Cuboid([L2, thickness, thickness], color="blue")
17
18 def part_poses(self, q) -> list[SE3]:
19 # World pose of each link, purely as a function of q. Each cuboid's local origin
20 # sits at its own proximal (joint) end, so Tx(length / 2) places its centre correctly
21 frame1 = SE3.Rz(q[0])
22 frame2 = frame1 * SE3.Tx(self.L1) * SE3.Rz(q[1])
23 return [frame1 * SE3.Tx(self.L1 / 2), frame2 * SE3.Tx(self.L2 / 2)]
24
25arm = TwoLinkArm()
26
27handle = env.add_assembly(
28 arm.part_poses,
29 [arm.link1, arm.link2],
30 q0=[0.0, 0.0],
31 callback=lambda t, values: [values["q1"], values["q2"]],
32)
33
34env.add_ui(Slider(min=-np.pi, max=np.pi, step=0.01, value=0.0, label="Joint 1", unit="rad"), name="q1")
35env.add_ui(Slider(min=-np.pi, max=np.pi, step=0.01, value=0.0, label="Joint 2", unit="rad"), name="q2")
36
37env.show()
38
39env.run(dt=0.02) # run forever at 50 fps
Again, there are many familiar concepts in this example. Lines 9-23 define a class which constructs a simple two-link robot. The links
are rectangular prisms, the first link is red, the second is blue.
The part_poses() method is the forward kinematics and computes the world pose of each link frame as a function of the joint angle array q. The
link frames are at the proximal end of each link, so in line 23 the frames are translated by half the length to account for the shape’s origin being
in the centre not at one end. The method maps the configuration q to the world of each constituent shape.
The add_assembly() method is new. Its parameters are the method to compute shape poses from configuration, the list of shapes, the initial configuration,
and a UI callback function. The callback extracts the relevant configuration values from the values dictionary and returns a configuration array.
It adds the assembly to the Swift environment, and registers its own callback.
The run() method drives the animation loop. For the assembly, at each time step:
the callback at line 31 is called to obtain the configuration array
arm.part_poses()is invoked with the configuration array, which returns a list of the world pose of each constituent shapethose poses are then sent to the browser to update the scene.
We also introduce a new method env.show() which displays what’s in the scene, and in this case displays:
Swift backend, t = 0.0, scene:
[0] AssemblyHandle
UI[2] Slider "q1"
UI[3] Slider "q2"
Scene graphs
The concept of a scene graph is fundamental to 3D graphics. A scene graph is a tree of
nodes, where each node has a pose relative to its parent node. The root node is the
world frame, and all other nodes are defined relative to their parent. The world pose
of a node is computed by multiplying the poses along the path from the root to the node.
Scene graphs are a spatialgeometry concept, not a Swift one – see spatialgeometry’s own
Scene graphs
section for a fuller treatment.
1import numpy as np
2import spatialgeometry as sg
3from spatialmath import SE3
4from swift import Swift, Slider
5
6env = Swift()
7env.launch(realtime=True)
8
9L1, L2, thickness = 0.3, 0.25, 0.03
10link1 = sg.Cuboid([L1, thickness, thickness], color="red")
11link2 = sg.Cuboid([L2, thickness, thickness], color="blue")
12
13link2.scene_parent = link1
14
15env.add_shape(link1, callback=lambda t, values: SE3.Rz(values["q1"]) * SE3.Tx(L1/2))
16env.add_shape(link2, callback=lambda t, values: SE3.Tx(L1/2) * SE3.Rz(values["q2"]) * SE3.Tx(L2/2))
17
18env.add_ui(Slider(min=-np.pi, max=np.pi, step=0.01, value=0.0, label="Joint 1", unit="rad"), name="q1")
19env.add_ui(Slider(min=-np.pi, max=np.pi, step=0.01, value=0.0, label="Joint 2", unit="rad"), name="q2")
20
21print(link1.tree())
22env.show()
23
24env.run(dt=0.05)
Lines 10-11 define the two colored rectangular prisms which represent the links of the robot. Line 13 declares that the second link is a child of the first link, so its pose is defined relative to the first link’s frame.
Lines 15-16 add the two links to the Swift environment, and specify a callback function for each link which computes its world pose as a function of the joint angles. The first link’s pose is computed as:
a rotation about the z-axis by the first joint angle, then
a translation along the x-axis by half its length, which is where the centre of its cuboid is located.
The second link’s pose is relative to the first link and is computed as:
a translation along the x-axis by the remaining half-length of the first link, so the frame is now at the distal end of that link, then
a rotation about the z-axis by the second joint angle, and then
a translation along the x-axis by half its length, which is where the centre of its cuboid is located.
Line 21 displays the scene graph that contains link1, from its root.
The alternative tree_children() shows only the part of the scene graph
containing this node and its children.
The output of link1.tree() is:
Cuboid(scale=[0.3, 0.03, 0.03], color=(1.0, 0.0, 0.0), pose='t = 0, 0, 0; rpy/zyx = 0°, 0°, 0°') <==
Cuboid(scale=[0.25, 0.03, 0.03], color=(0.0, 0.0, 1.0), pose='t = 0, 0, 0; rpy/zyx = 0°, 0°, 0°')
Indentation is used to show the hierarchical structure of the scene graph, the blue link is the child of the red link, and the current link (self) indicated by <==.
The Swift environment is displayed at line 22, and the output shows the two links and the two sliders:
Swift backend, t = 0.0, scene:
[0] Cuboid(scale=[0.3, 0.03, 0.03], color=(1.0, 0.0, 0.0), pose='t = 0, 0, 0; rpy/zyx = 0°, 0°, 0°')
[1] Cuboid(scale=[0.25, 0.03, 0.03], color=(0.0, 0.0, 1.0), pose='t = 0, 0, 0; rpy/zyx = 0°, 0°, 0°')
UI[2] Slider "q1"
UI[3] Slider "q2"