Scenes
A scene is the 3D model of the space your robot works in. Every session is recorded against one version of one scene, and Session Replay draws the robot's path, its events and its camera and lidar records inside that model. You do not need Unity to make a scene: any glTF model works, and the SDK ships a tool that exports a Gazebo world.
Scene id and scene version
The node records into the scene that two parameters name:
scene_id: the scene's UUID, which Cognitive3D assigns when you first upload the scene. Configure refuses a value that is not a UUID, so a placeholder left in the file stops the node.scene_version: the version number. The first upload is version 1, and every update of the scene creates the next version.
/**:
ros__parameters:
scene_id: "<your-scene-uuid>"
scene_version: "<your-scene-version>"
scene_version is a string of digits, so quote it: scene_version: "3". An unquoted 3 is a YAML
integer and the node does not start, and a value that is not a whole number of 1 or more refuses
configure. Configuration has the parameter reference.
Use the version number, not the version id. The Upload Web App's result lists sceneId,
versionNumber and versionId; scene_version takes versionNumber, and versionId is a
different number. With C3D_ORG_API_KEY set, doctor says when it is given the version id; see
Confirm the scene belongs to your project.
A session stays with the scene version it was recorded against. When you upload a new version of
the scene, set scene_version to it; sessions recorded before stay on the earlier version.
Get a scene
Use whichever of these fits:
- Your project already has the scene, uploaded earlier or from another Cognitive3D SDK. Take its id and version number from the Cognitive3D dashboard.
- You have a 3D model of the site. Export it as glTF with a separate
.binbuffer (not.glb), then prepare the files and upload them. - Your robot runs in Gazebo. Export the world, check the result, and upload it.
Export a Gazebo world
export_world_gltf.sh turns an SDF world into glTF with Gazebo's own exporter and assimp. It needs
Gazebo Garden or later: it runs gz sim, reads GZ_SIM_RESOURCE_PATH, and loads the exporter by
its Gazebo Sim plugin name. It is tested with Gazebo Harmonic, the release paired with Jazzy.
Gazebo Classic and Gazebo Fortress are not supported; see
On Gazebo Classic or Fortress.
Run it where Gazebo already loads your world, such as the machine or container you run the
simulation in, with gz on the PATH and assimp installed:
sudo apt install assimp-utils
ros2 run cognitive3d_ros export_world_gltf.sh my_world /tmp/scene
The first argument is a world name, found as <name>.sdf under GZ_SIM_RESOURCE_PATH and
AMENT_PREFIX_PATH, or the path to an .sdf file. The tool writes scene.gltf and scene.bin
into the output directory, with every texture the world uses copied beside them, ready to upload.
It:
- Runs a copy of the world headless for 20 iterations, with Gazebo's COLLADA world exporter added.
- Repairs the COLLADA up axis. Gazebo's exporter declares no up axis, COLLADA then defaults to
Y-up, and Gazebo is Z-up, so an unrepaired export lies on its side with no error from any
tool.
collada_zup_repair.pyadds the declaration, and copies the file unchanged when one is already there. - Converts the result to glTF with assimp, as
scene.gltfand its bufferscene.bin. - Copies each texture beside
scene.gltfand points the glTF at it by its bare file name, which is all a scene upload keeps. It stops with an error when two textures share a file name, since one would replace the other on the platform. - Checks the result with
gltf_bbox.py, and reports its orientation.
It stops with an error when Gazebo could not resolve some of the world's geometry, and it prints every warning Gazebo logged. Read them: Gazebo's wording varies between versions, so a model that failed to load can be missing from the export without the error firing.
The orientation verdict comes last:
| verdict | meaning |
|---|---|
consistent with UPRIGHT |
Y, glTF's up axis, is the shortest extent, as it is for a building or a floor plan |
SOMETHING TO LOOK AT |
Y is not the shortest extent. Either the up-axis repair did not take, or the world is taller than it is wide. Open the file and look |
INCONCLUSIVE |
The extents are too close to each other for the check to say anything. Check by eye after upload |
The tool exits with status 0 when the export is fit to upload. Status 1 is a real problem that
gltf_bbox.py's report names, such as a texture the world references that exists nowhere: the
scene would upload and render without it.
A third argument of glb writes one self-contained <name>.glb file instead. The platform does
not accept a .glb scene, so keep the default for anything you upload.
To repair a COLLADA file you exported from Gazebo another way, run the repair on its own:
ros2 run cognitive3d_ros collada_zup_repair.py world.dae world_zup.dae
On Gazebo Classic or Fortress
A Humble robot often simulates in Gazebo Classic (gazebo) or Gazebo Fortress (ign gazebo),
and the tool runs with neither. Run the export on a Jazzy machine or container instead, which
needs no ROS graph and none of the robot's packages:
- Install Gazebo Harmonic and assimp, and then the Jazzy packages as Install describes:
sudo apt install ros-jazzy-ros-gz assimp-utils
- Copy the world file there, with the directories of every model it includes, and put those
directories on
GZ_SIM_RESOURCE_PATH. Harmonic resolvesmodel://URIs through it, where Classic usesGAZEBO_MODEL_PATH. - Run
export_world_gltf.shwith the path to the world file.
Harmonic may not load a Classic world exactly as written. The tool stops with an error when some of the world's geometry does not resolve, and prints every warning Gazebo logged: read them, and look at the result before you upload it. When the world will not load, export a 3D model of the site another way, and prepare the files yourself.
Prepare the files
A scene upload is a scene.gltf and its scene.bin, plus any textures it references. The upload
stores the two files under exactly those names and does not edit the glTF, so the glTF's
buffers[].uri must already read scene.bin. A glTF that still references my_world.bin uploads
without an error and the scene is empty in Session Replay.
An export from export_world_gltf.sh already follows these rules; go straight to the check below.
For a glTF from another exporter, rename the files and point the glTF at the new buffer name:
cd /tmp/scene
mv my_world.bin scene.bin
mv my_world.gltf scene.gltf
python3 - <<'EOF'
import json
with open("scene.gltf") as f:
spec = json.load(f)
for buffer in spec["buffers"]:
buffer["uri"] = "scene.bin"
with open("scene.gltf", "w") as f:
json.dump(spec, f)
EOF
This assumes one buffer. Two more rules apply to every scene:
- Every primitive needs a material. A primitive with vertex colors and no material renders
solid pink, with no error anywhere. A flat
baseColorFactoris enough. - Texture references are bare file names, uploaded beside the
.gltf. The upload keeps file names only, so a reference such astextures/wall.pngresolves on your disk and is missing on the platform, and the model renders untextured.
Check the result before you upload it:
ros2 run cognitive3d_ros gltf_bbox.py scene.gltf
It exits with status 0 when the files are fit to upload. It reports INCOMPLETE for a referenced
file that is missing or shorter than the glTF declares, and WILL UPLOAD EMPTY for a reference the
viewer cannot fetch. The upload accepts both kinds of file without an error. It does not check
materials. Given no file, it prints its usage and exits with status 2.
Upload the scene
Upload the scene with the Upload Web App at https://upload.cognitive3d.com, which also comes as a
desktop app. Sign in with your developer key, choose New Scene, or Update Scene to add a
version to an existing scene, and upload scene.gltf, scene.bin and the textures. The result
lists the sceneId and versionNumber to put in your parameter file. The dashboard documentation's
Scene Uploads page describes the app
and its file requirements.
Confirm the scene belongs to your project
[!WARNING] The platform accepts a session whose scene id belongs to another project. It creates an empty scene with that id in your application key's project and records the session there, so Session Replay draws the robot in an empty world. Nothing at run time reports it. Check the scene's project before your first session.
The Upload Web App has no project picker. The developer key you enter decides the project, and the app reuses the last developer key entered in that browser unless you enter another.
doctor checks this for you. With C3D_ORG_API_KEY set, its scene/project check reads the scene
from the platform, and fails when it has no version numbered scene_version, saying so when the
value you gave is the version id. With C3D_PROJECT_ID set as well, doctor checks that your
application key belongs to that project, and scene/project also fails when the scene belongs to
another project, or when no scene with this id is visible to your organization: the id does not
exist, or belongs to another organization. Without C3D_PROJECT_ID, it names the scene's project
and does not compare the project. When the platform does not answer, the check is a note; without
an organization key it does not run. See doctor and verification.
To check by hand, open the project your application key belongs to in the Cognitive3D dashboard and
find the scene in it. Or ask the API, with your organization key. This asks the API host your
environment names (production unless C3D_API_HOST is set), picks the header format from the key
without printing it, and prints the scene's project and version numbers:
(
case "$C3D_ORG_API_KEY" in
orgkey-*) auth="$C3D_ORG_API_KEY" ;;
*) auth="APIKEY:ORGANIZATION $C3D_ORG_API_KEY" ;;
esac
curl -fsS -H "Authorization: $auth" \
"https://${C3D_API_HOST:-api.cognitive3d.com}/v0/scenes/<your-scene-uuid>"
) | python3 -c 'import json, sys; s = json.load(sys.stdin); print("projectId:", s["projectId"]); print("versionNumber:", [v["versionNumber"] for v in s["versions"]])'
projectId must be your project's id, and versionNumber must include your scene_version. The
API answers 403 when no scene with this id is visible to your organization. An organization key
that begins with orgkey- goes in the header as it is; an older key without that prefix goes as
APIKEY:ORGANIZATION <key>.
Choose a pose convention
pose_convention says how the robot's ROS poses map onto the scene's axes. It has no default, and
configure refuses a node without it. For a scene exported with export_world_gltf.sh, or by any
ROS-to-glTF exporter that maps gltf.xyz = (ros.x, ros.z, -ros.y), use gltf_authored. For a
scene authored in Unity, use unity. The two differ by a 90 degree turn, and the platform accepts a
session recorded under the wrong one and draws its path off the floor plan.
Coordinates explains how to choose and how to check.
The robot as a Dynamic Object
The robot's Dynamic Object is off by default (robot_dynamic_object: false). Session Replay draws
the robot from the pose timeline, so a default session registers no object and needs no robot
mesh. Turn it on when you want Session Replay to draw your robot's own model, or when a tool reads
the Dynamic Object stream.
With robot_dynamic_object: true, a robot mesh must be uploaded to the scene version the session
records against, and four more parameters apply:
| parameter | default | what |
|---|---|---|
robot_mesh |
"robot_placeholder" |
The uploaded mesh's name |
robot_object_id |
"robot" |
The object id the session records the robot under |
robot_display_name |
"Robot" |
The object's name in the dashboard |
mesh_yaw_offset_deg |
0.0 |
A turn about the vertical axis, applied to the mesh |
Configuration has the full reference, and Coordinates covers how the robot's rotation is drawn under each pose convention.
Prepare the robot mesh
A robot mesh is a glTF named after the mesh, its buffer, and optionally a thumbnail:
<mesh>.gltf, where<mesh>is the value ofrobot_mesh. Session Replay fetches the model asobjects/<mesh>/<mesh>.gltf, so a glTF with any other name renders nothing.- Its buffer,
<mesh>.bin, named in the glTF'sbuffers[].uriby that bare file name. Unlike a scene, an object keeps the file names you upload. The Upload Web App requires the two names to match; a scripted upload accepts any bare file name for the buffer. - Optionally
cvr_object_thumbnail.png, which the dashboard shows in object lists.
The platform does not accept a .glb mesh. Check the files with the object rules:
ros2 run cognitive3d_ros gltf_bbox.py --object robot_placeholder.gltf
Choose a model whose front and side look different. On a rotationally symmetric model, such as a disc or a cylinder, a robot drawn 90 degrees wrong looks right.
mesh_yaw_offset_deg corrects a mesh whose forward axis is not the robot frame's: 90 turns the
mesh's +X onto the direction of travel, and -90 the other way. It is yaw only. When the robot's
URDF places its visual mesh with a roll or pitch as well, bake that rotation into the mesh before
you upload it. The mesh is already in glTF axes, so express the rotation in glTF axes too: a
rotation R written in ROS axes becomes M R M⁻¹, where M is the ROS-to-glTF axis map your
exporter used. Applied directly, it turns the mesh about the wrong axis and still looks plausible.
Upload the robot mesh
Objects belong to a scene version, and a new scene version starts with none. Upload the robot mesh again after every new version of the scene.
The Upload Web App can upload the robot mesh; the dashboard documentation's
Object Uploads page describes how.
The app assigns the object its own id. Set robot_object_id to that id: doctor's
object id/registered check lists the ids your project has. With a different id, Session Replay
still draws the robot, but the uploaded object's initial position, rotation, scale and thumbnail do
not apply, and analytics that key on the object id report nothing for it.
To script the upload instead, make two requests with your developer key, to the data host your
environment names (production unless C3D_DATA_HOST is set).
[!WARNING] A Dynamic Object upload is two requests. The first creates the object; the second stores its mesh files, and its path ends in the mesh name, not the object id. Both answer with an empty 2xx response whether or not anything was stored. With only the first done, sessions record normally and Session Replay draws no robot.
The first request creates the object, with the id and mesh name your parameters use:
curl -X POST \
-H "Authorization: APIKEY:DEVELOPER $DEVELOPER_KEY" \
-H "Content-Type: application/json" \
-d '{"objects":[{"id":"robot","mesh":"robot_placeholder","name":"Robot","scaleCustom":[1,1,1],"initialPosition":[0,0,0],"initialRotation":[0,0,0,1]}]}' \
"https://${C3D_DATA_HOST:-data.cognitive3d.com}/v0/objects/<your-scene-uuid>?version=<your-scene-version>"
The second stores the files, one file part each, under the mesh name:
curl -X POST \
-H "Authorization: APIKEY:DEVELOPER $DEVELOPER_KEY" \
-F "file=@robot_placeholder.gltf" \
-F "file=@robot_placeholder.bin" \
"https://${C3D_DATA_HOST:-data.cognitive3d.com}/v0/objects/<your-scene-uuid>/robot_placeholder?version=<your-scene-version>"
Neither response says whether the object is complete. A .glb, or a file that is not a mesh at
all, gets the same answer as a good upload. Check with doctor, below.
Mesh files are permanent per mesh name
Once files are stored under a mesh name, they are never replaced. Uploading under the same name
again answers 2xx and keeps serving the original files, and deleting and recreating the object does
not release the name. While you are still changing the model, upload each change under a new mesh
name (robot_v2, robot_v3, and so on) and set robot_mesh to match.
A recorded session keeps asking for the mesh name it was recorded with. To see a corrected mesh, upload it under a new name and record a new session.
Check before you drive
With C3D_ORG_API_KEY and C3D_PROJECT_ID set, doctor checks the Dynamic Object against the
platform: mesh/exists looks for the mesh in your project, object id/registered looks for the
object id, and mesh/files reads the object back from your scene version with its files. Run it
after every upload. With robot_dynamic_object off, the three are reported together as
robot dynamic object (NOT CHECKED). doctor and verification
describes each check.