Scene and obstacles¶
The Scene is the cell. Everything in it shares three
conventions: lengths are meters, the world is Z-up, and orientations
are quaternions in (x, y, z, w) order. Wherever a method takes
quaternion=None, identity is assumed; wherever it takes robot=None, the
scene's first robot is meant.
Primitives¶
scene.add_box("table", size=(0.6, 0.6, 0.05), position=(0.4, 0.0, 0.0))
scene.add_sphere("dome", radius=0.1, position=(0.0, 0.5, 0.3))
scene.add_cylinder("post", radius=0.04, length=0.8, position=(0.5, 0.5, 0.4))
Box size is full extents; cylinders follow the URDF convention (axis along
local +z). Every add_* returns the final name — a taken name is uniquified
rather than rejected, so use the return value if you generate names in a loop.
Meshes¶
scene.add_mesh("fixture", "fixture.stl", position=(0.5, 0.0, 0.0),
scale=(0.001, 0.001, 0.001)) # a mm-unit STL
STL and OBJ. The studio renders the original mesh; the collision shape is a VHACD convex decomposition, computed on first load (about a second per mesh) and cached on disk — subsequent runs are instant. See Collision checking for why.
Posing, recoloring, removing¶
scene.set_obstacle_pose("table", (0.5, 0.0, 0.0))
scene.set_obstacle_color("table", (0.8, 0.2, 0.2)) # linear RGB, display only
scene.obstacle_pose("table") # ((x,y,z), (x,y,z,w))
scene.obstacle_names
scene.remove_obstacle("table")
Colour says what a surface is; a material says how it takes light, and the two are separate — bare steel and a painted panel can share a grey and still look nothing alike:
scene.set_obstacle_material("panel", metalness=0.85, roughness=0.42) # bare steel
scene.set_obstacle_material("cabinet", metalness=0.15, roughness=0.55) # paint
scene.set_obstacle_material("panel") # back to the viewer's choice
scene.obstacle_material("panel") # (metalness, roughness) | None
Both knobs are the 0–1 pair that glTF, USD Preview Surface and three.js all speak, so they mean the same thing wherever the scene ends up. Metal is what makes an unpainted body read as metal rather than as grey plastic: it reflects its surroundings instead of carrying a diffuse colour of its own. Appearance never touches collision or planning.
Two switches are worth knowing:
scene.set_obstacle_enabled("cleat_3", False) # out of collision, still rendered
scene.set_obstacle_visible("proxy_7", False) # still collides, not rendered
The two are independent, and the pair is what lets a workpiece carry both a
display mesh and its own collision shape. Convex decomposition fills a body
shell's door and window apertures — and a welding gun works through those
— so a catalog workpiece ships a display shell alongside a set of authored
convex pieces that keep the openings open. Load the shell with collision off
and the pieces with rendering off, and the scene both looks right and
collides right:
scene.add_mesh("body/shell", "…/visual/biw.obj", (0, 0, 0.78))
scene.set_obstacle_enabled("body/shell", False) # looks right
for piece in pieces: # …collides right
scene.add_mesh(f"body/{piece}", f"…/collision/{piece}.stl", (0, 0, 0.78))
scene.set_obstacle_visible(f"body/{piece}", False)
Disabled obstacles keep rendering and keep riding conveyors — they are scenery that happens to move. The dual-arm demo's belt cleats work exactly this way.
z = 0 is the floor
The studio draws the shop floor at z = 0, so geometry below it is
behind the floor and never appears — a cell laid out around a robot
base at the origin looks half-missing. Build upward instead: floor at
zero, the robot's mounting plane on top of its pedestal
(bt.Scene(robot, base_position=(0, 0, 0.74))), everything else
measured from there.
Seating a workpiece on a fixture wants the mesh's own dimensions, not a number
measured off it once. obstacle_bounds
returns the world-frame (min, max) of anything already in the scene, so a
cell can ask where the underside is and lift it onto the pallet:
low, high = scene.obstacle_bounds("body/floor_pan")
scene.set_obstacle_pose("body/floor_pan", (0, 0, PALLET_TOP - low[2]))
That keeps the cell correct when the asset is rebuilt — the trap being that a hard-coded lift is silently wrong the day the mesh's origin moves, and a workpiece a centimetre into its fixture reads as a permanent collision.
Named frames¶
A frame is a named pose — a mount point, a teach point, a fixture datum:
scene.add_frame("fixture_datum", (0.5, 0.2, 0.1))
scene.frames # {name: ((x,y,z), (x,y,z,w))}
scene.frame("fixture_datum") # one pose, unpackable:
scene.set_robot_base_pose(*scene.frame("/World/MountFrame"))
Frames mostly arrive from USD import — every leaf Xform in the stage becomes one — which is what lets a layout file carry its own mount and teach points.
The scene is live¶
Everything above is mirrored to any connected studio immediately, and edits
made in the studio land back in this object. There is one scene; Python and
the browser are two views of it. bt.studio(scene, block=False) keeps your
prompt while you work from both sides.
Where the rest lives¶
| Cell ingredient | Guide |
|---|---|
| USD stages as environments | USD import |
| Collision queries and the ACM | Collision checking |
| Motions and constraints | Motion planning |
| Sensors, conveyors, sources, axes | Sensors and devices |
| Grasping and conveyor tracking | Attach and tracking |
| Saving the whole cell | Projects |