Skip to content

BulletLab Cookbook

This cookbook provides short, copy-pasteable snippets for common tasks in BulletLab.

1. Instant Model Deployment (quickLaunch)

Deploy any robot model (URDF/MJCF or Arsenal package) into a full GUI simulation with interactive joint controls (Position / Velocity / Torque modes), dynamic camera tracking, live telemetry, and an interactive console in just one line of code:

import bulletlab

# Basic 1-line deployment
bulletlab.quickLaunch("r2d2.urdf")

# Deploy directly from BulletLab Arsenal (assets pre-cached before window opens)
bulletlab.quickLaunch("arsenal:reference_bot")

# Deploy with custom camera, gravity, and spawn parameters
bulletlab.quickLaunch(
    "kuka_iiwa/model.urdf",
    gravity=(0, 0, -9.81),
    camera_distance=3.0,
    camera_pitch=-25.0,
    camera_mode="smooth",       # "smooth", "chase", or "snap"
    spawn_position=(0, 0, 0.1),
    ground_plane=True,
)

2. Minimal Simulation Loop

The most basic setup to get a custom physics world running with a plane and a robot.

from bulletlab import Simulation, Robot
from bulletlab.core.world import World

sim = Simulation(mode="gui").start()
World(sim).load_plane()
robot = Robot.load("r2d2.urdf", sim=sim, position=(0, 0, 0.5))

while sim.is_connected:
    sim.step()

2. Moving a Robot (Velocity Control)

Use the velocity property on a Joint object to command a target speed.

# Drive two wheels forward at 10 rad/s
robot.joints['left_wheel_joint'].velocity = 10.0
robot.joints['right_wheel_joint'].velocity = 10.0

3. Moving a Robot (Position Control)

Use the position property to command a joint to a specific angle (in radians).

import math
# Move a robotic arm joint to 90 degrees
robot.joints['shoulder_pan_joint'].position = math.pi / 2

4. Reading Robot State

Access the robot's base or specific links to read live physical states.

# Base position (x, y, z)
x, y, z = robot.base_position

# Base orientation (quaternion: x, y, z, w)
qx, qy, qz, qw = robot.base_orientation

# Get the world position of a specific link (e.g. end effector)
end_effector_pos = robot.links['gripper_link'].position

5. Setting up Telemetry and Live Plots

Use TelemetryManager to track variables, and LivePlot to graph them dynamically.

from bulletlab.telemetry import TelemetryManager
from bulletlab.plotting import LivePlot

telemetry = TelemetryManager()
# Watch the robot's speed (magnitude of base velocity)
telemetry.watch("Speed", lambda: robot.speed, unit="m/s")

# Plot it
plot = LivePlot(title="Rover Telemetry")
plot.watch("Speed", lambda: robot.speed, color="#00ff88")
plot.start()

# Inside your loop:
# telemetry.update(t=sim.elapsed_time)
# plot.update()

6. Changing Physics Properties

You can alter physics parameters on the fly via the sim or link objects.

# Change gravity to Moon gravity
sim.gravity = (0, 0, -1.62)

# Make a specific link heavier
robot.links['base_link'].mass = 50.0

# Change lateral friction
robot.links['wheel_link'].lateral_friction = 1.2

7. Attaching the BulletLab UI

Add a complete Dear ImGui dashboard with an Explorer, Properties panel, and Console with just two lines of code.

from bulletlab.ui import BulletLabUI

ui = BulletLabUI(sim=sim, robots=[robot], telemetry=telemetry).start()

# Inside your loop:
# ui.step()

8. Virtual Joystick Control

Add an interactive 2D joystick to your custom panel for intuitive robot control. The joystick calls your callbacks every frame, so wheels keep spinning while you hold it.

from bulletlab import Simulation, Robot
from bulletlab.core.world import World
from bulletlab.ui import BulletLabUI
from bulletlab.ui import widgets as ui

sim = Simulation(mode="gui").start()
World(sim).load_plane()
robot = Robot.load("husky/husky.urdf", sim=sim)

app = BulletLabUI(sim=sim, robots=[robot])

@app.custom_panel("Drive")
def drive_panel():
    # Single joystick: Y=forward/back, X=turn
    ui.joystick(
        "Rover Drive",
        on_y=lambda v: [
            setattr(robot.joints["front_left_wheel"],  "velocity", v * 10),
            setattr(robot.joints["front_right_wheel"], "velocity", v * 10),
            setattr(robot.joints["rear_left_wheel"],   "velocity", v * 10),
            setattr(robot.joints["rear_right_wheel"],  "velocity", v * 10),
        ],
        on_x=lambda v: [
            setattr(robot.joints["front_left_wheel"],  "velocity", -v * 5),
            setattr(robot.joints["front_right_wheel"], "velocity",  v * 5),
            setattr(robot.joints["rear_left_wheel"],   "velocity", -v * 5),
            setattr(robot.joints["rear_right_wheel"],  "velocity",  v * 5),
        ],
        snap=True,                        # Release = stop
        size=80,                          # Larger joystick circle
        handle_color=(0.2, 0.8, 0.4, 1.0),
    )

app.start()
while sim.is_connected:
    sim.step()
    app.step()
    if app.should_close:
        break
app.stop()
sim.stop()

Tip: Set snap=False for a latching mode where the robot keeps driving at the last commanded speed after you release. Set it back to snap=True if you want the robot to stop immediately when you let go.

9. Loading a Robot at a Custom Tilt Angle

Use the tilt parameter on Robot.load() to set an initial orientation using an intuitive axis + angle shorthand instead of computing quaternions by hand.

from bulletlab import Simulation, Robot
from bulletlab.core.world import World

sim = Simulation(mode="gui").start()
World(sim).load_plane()

# Tilt 30° around the Y axis (nose-down / forward lean)
robot = Robot.load("laikago/laikago.urdf", sim=sim,
                   position=(0, 0, 0.5),
                   tilt=((0, 1, 0), 30))

# Tilt 45° around the X axis (lean left / roll)
robot = Robot.load("laikago/laikago.urdf", sim=sim,
                   position=(2, 0, 0.5),
                   tilt=((1, 0, 0), 45))

# Diagonal axis — vector is normalised automatically
robot = Robot.load("laikago/laikago.urdf", sim=sim,
                   position=(4, 0, 0.5),
                   tilt=((1, 1, 0), 60))

# Combine with an explicit orientation (heading) + tilt
# The tilt is applied on top of the base orientation
import pybullet as p, math
heading = p.getQuaternionFromEuler([0, 0, math.radians(90)])  # face East
robot = Robot.load("laikago/laikago.urdf", sim=sim,
                   position=(6, 0, 0.5),
                   orientation=heading,
                   tilt=((0, 1, 0), 15))   # also pitch forward 15°

while sim.is_connected:
    sim.step()

Axis reference:

Axis Effect
(1, 0, 0) Roll — lean left / right
(0, 1, 0) Pitch — nose up / down
(0, 0, 1) Yaw — spin on the spot
(1, 1, 0) Diagonal — normalised automatically

The angle is always in degrees. The axis does not need to be a unit vector.

10. Custom Terrain & Obstacles

No raw PyBullet needed — the World class handles everything.

import numpy as np, math
from bulletlab import Simulation
from bulletlab.core.world import World

sim = Simulation(mode="gui").start()
world = World(sim)

# ── Flat ground (standard) ───────────────────────────────────────────
world.load_plane()

# ── Primitive obstacles ──────────────────────────────────────────────
world.create_box((1.0, 0.5, 0.3), position=(2, 0, 0.15), color=(0.8, 0.4, 0.1, 1))
world.create_sphere(radius=0.25, position=(0, 2, 0.25), color=(0.2, 0.8, 0.2, 1))
world.create_capsule(radius=0.1, height=0.6, position=(-1, 1, 0.5))

# ── Heightfield terrain ──────────────────────────────────────────────
n = 128
xs = np.linspace(0, 4 * math.pi, n)
heights = np.outer(np.sin(xs), np.cos(xs))   # 2-D sine wave hills
world.load_heightfield(heights, xy_scale=0.08, z_scale=0.3,
                       color=(0.55, 0.45, 0.35, 1.0))

# ── Scatter 20 rock-like boxes across a 20×20 m area ────────────────
world.scatter_obstacles(20, kind="box", size_range=(0.2, 0.5),
                        region=(-10, -10, 10, 10), seed=42)

# ── Remove one body, clear all ───────────────────────────────────────
rock = world.create_box((0.3, 0.3, 0.3), position=(5, 0, 0.15))
world.remove_body(rock)
# world.clear()  ← removes everything this World created

while sim.is_connected:
    sim.step()

11. Applying Forces, Torques & Runtime Dynamics

from bulletlab import Simulation, Robot
from bulletlab.core.world import World

sim = Simulation(mode="gui").start()
World(sim).load_plane()
robot = Robot.load("laikago/laikago.urdf", sim=sim, position=(0, 0, 0.6))

# ── External forces (must be called every step) ──────────────────────
while sim.is_connected:
    # Continuous upward thrust (like a drone rotor)
    robot.apply_force((0, 0, 15.0))

    # Air drag proportional to speed
    vx, vy, vz = robot.base_velocity
    robot.apply_force((-0.2 * vx, -0.2 * vy, -0.2 * vz))

    # Apply force to a specific link (in the link's own frame)
    robot.apply_force((1.0, 0, 0), link="trunk", frame="local")

    # Spin torque around Z axis
    robot.apply_torque((0, 0, 2.0))

    sim.step()

# ── Runtime physics parameters (call once, takes effect immediately) ─
robot.set_dynamics(mass=20.0)                        # change body mass
robot.set_dynamics("FR_hip", lateral_friction=2.0)  # grippier foot
robot.set_dynamics(linear_damping=0.0, angular_damping=0.0)  # slippery

# Bump restitution on all links
for name in robot.links:
    robot.set_dynamics(name, restitution=0.5)

Key rule: apply_force() and apply_torque() are single-step — PyBullet clears them after every sim.step(). Call them inside your loop for continuous effects. set_dynamics() is persistent — call it once to change a property permanently.

12. Loading from BulletLab Arsenal

BulletLab Arsenal is the official robot asset registry. No manual download, no path setup.

from bulletlab import Simulation, Robot, ArsenalError
from bulletlab.core.world import World

# ── Option A: Install permanently, then load from local path ─────────────────
urdf = Robot.install("reference_bot")           # → ~/.bulletlab/packages/…
urdf = Robot.install("reference_bot/BLem1")     # specific model
urdf = Robot.install("reference_bot", path="robots/")  # custom directory

# Load the installed URDF as a normal local file
sim = Simulation(mode="gui").start()
World(sim).load_plane()
robot = Robot.load(str(urdf), sim=sim, position=(0, 0, 0.3))

# ── Option B: Load directly via session cache (no permanent files) ────────────
robot = Robot.load("arsenal:reference_bot", sim=sim)
robot = Robot.load("arsenal:reference_bot/BLem1", sim=sim, position=(0, 0, 0.5))

# All standard Robot.load() parameters work with Arsenal URIs:
robot = Robot.load(
    "arsenal:reference_bot",
    sim=sim,
    position=(0, 0, 0.5),
    fixed_base=False,
    tilt=((0, 1, 0), 10),   # tilt works too
)

# ── Error handling ────────────────────────────────────────────────────────────
try:
    robot = Robot.load("arsenal:no_such_package", sim=sim)
except ArsenalError as e:
    print(f"Arsenal error: {e}")

while sim.is_connected:
    sim.step()

Install vs. direct load:

Robot.install() Robot.load("arsenal:...")
Files after exit ✅ Permanent ❌ Cleaned up automatically
Offline use ✅ Yes ❌ Network required each session
Best for Reproducible setups Quick demos and experiments