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.
An appearance over a box¶
scene.add_box("part", (0.06, 0.06, 0.04), (0.4, 0.2, 0.9))
scene.set_obstacle_visual_asset("part", "shapes/workpiece.usda", "/Shapes/workpiece",
(0.06, 0, 0, 0, 0, 0.06, 0, 0, 0, 0, 0.04, 0, 0, 0, 0, 1))
An obstacle can be drawn from a USD prim while it collides as the
primitive it was added as: the box is what the planner checks and what a
grasp was taught against; the prim is the picture. The column-major
transform maps the prim's coordinates into the obstacle's frame — a scale
for a unit-box shape, a unit conversion for a millimetre-authored asset.
The prim's own materials are used unless color_override=True, which
tints it with the obstacle's colour; set_obstacle_material still applies
on top. The appearance moves, attaches, saves into a project (the layer
travels in the .botrail), exports with a USD recording and round-trips
through generate_python with its resident. bt.parts.appearance does
this for the shape library;
prims imported with load_usd carry theirs from the stage.
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.0, roughness=0.55) # paint
scene.set_obstacle_material("window", metalness=0.0, roughness=0.16, opacity=0.24)
scene.set_obstacle_material("panel") # back to the viewer's choice
scene.obstacle_material("panel") # (metalness, roughness) | None
scene.set_obstacle_material("top", finish="wood") # a pattern over the colour
scene.obstacle_finish("top") # "wood" | "checker_plate" | "plastic" | None
finish names a pattern the studio draws over the colour — timber grain
along a board's length, the raised bars of tread plate, the pebble of a
moulded bin — tiled at its real pitch on any box, cylinder or sphere. On its
own it also sets the two knobs the pattern reads as (tread plate is bright
metal); a knob you give wins. It is presentation only: the pictures a
rollout renders and a USD export carry the colour and the knobs and never
the pattern.
opacity (0 transparent … 1 opaque) makes a window or a guard pane
see-through; it is appearance only — collision and sensors see the solid —
and obstacle_opacity(name) reads it back. 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: the studio applies them to primitives and
meshes alike (a colour change or a collision highlight leaves a mesh's
textures in place), a project and its generated Python carry them, and USD
export writes them as UsdPreviewSurface inputs. 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 |