DreamLake

ROS bags & robot models

Drop an .mcap file into the viewer and it opens with a generated layout — no config, no conversion. This page is the short map of what decodes, how a tf tree becomes a moving robot, and where robot models come from.

What decodes with zero config

Channels are dispatched by the writer's own declarations — wire encoding and schema name — never guessed from shapes:

in the fileshows as
Foxglove schemas (protobuf or json): CompressedImage / RawImagecamera panes
foxglove.PointCloudpoint-cloud pane
FrameTransform(s)3D scene (tf tree, below)
PoseInFrame3D pose track (an axis triad following the pose)
SceneUpdate model primitivesmeshes in the 3D scene
ROS 2 bags (cdr + ros2msg — rosbag2's own MCAP output)same as their Foxglove twins ↓
ROS 1 bags (ros1 + ros1msg)same as their Foxglove twins ↓
sensor_msgs/JointStatejoint charts and, with a robot description, a moving 3D robot (below)
any other numeric/string channel (IMU, diagnostics, …)line charts / span tracks

Well-known ROS types decode like their Foxglove twins — the message is normalized, then everything downstream is one code path: tf2_msgs/TFMessage → FrameTransforms · sensor_msgs CompressedImage/Image/PointCloud2 → CompressedImage/RawImage/PointCloud · geometry_msgs/PoseStamped, nav_msgs/Odometry → PoseInFrame. The catalog keeps the honest original name in meta.schema and records the twin in meta.twin.

Still declined, each with a console warning naming why: ros2idl, flatbuffers, Grid, CameraCalibration, compressed video.

The tf tree is the 3D scene

A tf message states where a child frame sits in its parent. Reading a child as: transform3d composes those local transforms down the declared parent_frame_id chain — across /tf and /tf_static — into fixed-frame poses, the same semantics Foxglove renders. In the 3D pane:

  • every tracked frame draws an axis triad following its motion;
  • frames with declared parentage are connected by bones — a tf-only humanoid recording reads as a moving skeleton, no model required;
  • charts are untouched: a tf field bound to a lineChart still plots the schema's own local numbers.

From skeleton to robot: URDF models

recon3d takes external geometry by URL in its models: slot, and a .urdf loads directly — robots stay in their native distribution form:

yaml
- view: recon3d
  up: z
  tracks: [{ field: "/tf::*", as: transform3d }]
  models:
    - https://huggingface.co/datasets/live9080/dreamlake-robots/resolve/main/g1/g1_29dof.urdf

How a URDF behaves:

  • one named node per link — each joins its /tf::<link> track by name;
  • mesh references resolve relative to the .urdf (STL / OBJ / DAE / glTF, plus box/cylinder/sphere primitives). A Collada file's <up_axis> is ignored, as ROS tooling ignores it: a URDF's mesh is in the link frame, so the same part exported as STL has to land identically;
  • links no track claims stand at their zero-angle rest pose — unless a tracked ancestor exists, in which case they ride it;
  • .xacro is refused with a clear error — expand it first.

Plain glb / gltf / obj / stl URLs work in the same slot.

package:// — pointing at a description where it lives

package://<pkg>/meshes/x.dae is a ROS package-manager path. Only a package registry knows where <pkg> is, and a browser has none, so the name is resolved three ways, most explicit first:

yaml
models:
  - url: https://…/droid_panda_robotiq/panda_robotiq_85.urdf
    packages:
      franka_description: https://…/franka_description
      robotiq_2f_85_gripper_visualization: https://…/robotiq_2f_85_gripper_visualization
  1. the packages: map above — always wins;
  2. the URDF's own URL, when one of its path segments is the package name. …/franka_description/robots/panda_arm.urdf referencing package://franka_description/meshes/visual/link0.dae resolves with no config at all — which is the point: upload a description repo unchanged (URDF in robots/, meshes in meshes/) and it works;
  3. the URDF's directory, as a best effort, with a console warning naming the package it could not locate.

Only a description that references a package it does not live inside — a composition of two upstream packages, say — needs the map.

Joint angles are 3D too: forward kinematics

Two recordings of the same robot can look completely different on disk:

the bag carrieswhat it is
/tf per linkposes robot_state_publisher already computed — self-contained geometry
/joint_statesthe joint ANGLES — low-dimensional, and not 3D until a URDF interprets them

The second shape is extremely common: the angles are the robot's state, the poses are derivable from them, so many rigs never write the poses down. Bind the topic with joints: and the viewer does that derivation itself — the same product robot_state_publisher would have published:

yaml
- view: recon3d
  up: z
  models: [ https://…/g1/g1_29dof.urdf ]
  joints: [ /joint_states ]
  tracks: [{ field: "/tf::pelvis", as: transform3d }]   # the floating base
  • angles are matched by joint name, never by index — sensor_msgs/JointState carries a name array precisely because the order is the publisher's business, and each message is aligned on its own names;
  • <mimic> joints follow their master, which is the only reason a gripper opens: one commanded finger, several linkage joints that must track it;
  • travel is clamped to each joint's <limit>, so a mis-scaled topic bends the robot to its stops rather than folding it through itself;
  • /joint_states says nothing about where the robot is. Give the base its own transform3d track (world → pelvis) and the whole solve rides it; with no such track the robot stands at the origin.

Precedence, most authoritative first: a link's own transform3d track (someone recorded that pose) → forward kinematics → riding a tracked ancestor → the rest pose. A published pose always wins over a computed one.

The charts benefit too: a JointState topic's columns are named position.<joint> after the publisher's own joints, so the plot is labelled with joint names instead of position.0 … position.28.

Preset robots, applied by name match

Known robots live in the registry live9080/dreamlake-robots (URDF + meshes, license noted per robot) — currently the Unitree G1, the Franka Panda, and the Panda + Robotiq 2F-85 pairing DROID-style recordings publish. The generated default view applies a preset when a file's published NAMES carry that description's own naming, and there are two fingerprints because there are two kinds of recording:

  • tf frame ids ↔ the description's link names — a bag with per-link tf;
  • JointState names ↔ the description's joint names — a bag that publishes no frames at all, where nothing else could say which robot this is.

Either one firing selects the preset. The match is strict — most of that fingerprint must be present, and never fewer than six exact names — and it scales with the description: a 36-link humanoid needs ~22 matching names, an 8-link arm needs 6. So a G1 recording opens as the G1, a Panda recording opens as a Panda, and "looks like a humanoid" never fires: a Valkyrie bag, which has a pelvis and knees and shoulders under its own naming, keeps the skeleton.

When several presets fit — and they will, since the Panda's links are a subset of the Panda-plus-gripper's — the one that explains more of what the file published wins. That is why a DROID episode opens with the gripper attached rather than as a bare arm.

The applied model appears in the generated .dreamrc as a models: line — delete it to opt out, or point it at a different description.

Adding a robot: upload its urdf + meshes folder to the registry, then add one entry (id, url, link and joint fingerprints, and a packages: map if it references a package it does not live inside) to dataset-viz/formats/robot-presets.ts. viz/scripts/fingerprint.py prints both fingerprints from a URDF.

Pointers