Skip to content

Add Unitree robot URDFs and physics simulation CLI#180

Merged
ecto merged 7 commits intomainfrom
claude/vcad-robot-simulation-dO7tE
May 10, 2026
Merged

Add Unitree robot URDFs and physics simulation CLI#180
ecto merged 7 commits intomainfrom
claude/vcad-robot-simulation-dO7tE

Conversation

@ecto
Copy link
Copy Markdown
Owner

@ecto ecto commented May 10, 2026

Summary

This PR adds complete support for simulating Unitree robots (G1 humanoid and Go2 quadruped) by introducing URDF example files, enhancing the URDF importer to preserve authored inertial properties, and adding a new vcad simulate CLI command for running physics simulations.

Key Changes

New Robot Models

  • unitree-g1.urdf: Simplified 23-DOF humanoid (12 leg + 1 waist + 10 arm joints) with primitive geometry and realistic mass distribution
  • unitree-go2.urdf: Simplified 12-DOF quadruped (4 legs × 3 joints) with ~15 kg total mass matching published specs

URDF Importer Enhancements

  • Mesh resolution system: Added UrdfReadOptions struct to handle package://NAME/... URI resolution with configurable package roots, enabling proper mesh loading from ROS-style package layouts
  • Inertial properties preservation: URDF <inertial> blocks (mass, inertia tensor, center-of-mass) now flow through the importer into PartDef::inertial instead of being discarded
  • New read_urdf_with_options() API: Allows callers to specify package roots and URDF directory for mesh resolution

Physics Simulation

  • STL mesh loader (vcad-kernel-physics/src/stl.rs): Loads binary/ASCII STL files with automatic metre→millimetre conversion and non-uniform scale support
  • Inertial-aware physics: PhysicsWorld now prefers authored inertial properties over mesh-derived estimates, enabling accurate simulation of real robot dynamics
  • vcad simulate CLI command: New subcommand that:
    • Accepts .urdf or .vcad input files
    • Configurable simulation parameters (steps, timestep, logging frequency)
    • Supports --package-root for resolving package:// URIs
    • Prints joint states and simulation statistics

IR and Data Model

  • InertialProperties struct: New type in vcad-ir capturing mass (kg), inertia tensor (kg·m²), and center-of-mass (mm)
  • CsgOp::MeshImport: New CSG operation for mesh references with optional scale, replacing ad-hoc mesh handling

Testing

  • Integration tests (unitree.rs): End-to-end validation that both robots load, build physics worlds, and step without panicking
  • STL loader tests (stl_loader.rs): Verify unit conversion and scale application; test package:// URI resolution

Implementation Details

  • URDF inertials are converted from metres to millimetres (matching vcad IR conventions) during import
  • Physics simulation prefers authored mass/inertia over mesh density estimates, critical for accurate dynamics
  • The mesh resolver gracefully logs when package:// URIs cannot be located, allowing fallback to placeholder geometry
  • All <link> elements must precede <joint> elements in URDF files (required by vcad's quick-xml-serde reader)

https://claude.ai/code/session_01HtymVXbgcAvFXrPkPv33MA

claude added 3 commits May 9, 2026 16:57
Phase 1 of an official-URDF effort: ship simplified primitive-geometry
URDFs that exercise the full kinematic and physics pipeline today, plus
a `vcad simulate` CLI command so the demo is one command from the
terminal.

The simplified URDFs use only boxes / cylinders / spheres because the
URDF reader's <mesh> handling is currently a no-op (it routes mesh
references to CsgOp::StepImport, which only loads STEP). Topology, joint
origins, axes, and limits track the real Unitree G1 (23 actuated DOF)
and Go2 (12 actuated DOF). Follow-up phases will pass URDF <inertial>
through the IR, add STL import, and vendor the official URDFs.

Includes integration tests that load both robots and step the physics
world to confirm finite states, plus a changelog entry.

https://claude.ai/code/session_01HtymVXbgcAvFXrPkPv33MA
Phase 2 of the official-URDF effort. Add InertialProperties to
vcad_ir::PartDef so authored mass / inertia tensor / centre-of-mass can
travel from a URDF <inertial> tag all the way to phyz, without being
re-derived from a tessellated mesh + density.

PhysicsWorld::from_document now prefers PartDef.inertial when present,
falling back to the existing bounding-box estimate when it isn't. The
URDF reader populates the field for every link that ships an <inertial>
block; the unit conversion mirrors the rest of the importer (kg + kg·m²
stay SI, COM offset converts metres → millimetres for IR).

Extends the Unitree integration test to assert the G1's authored mass
sums into a sensible whole-robot ballpark, so a future regression that
silently drops <inertial> is loud.

The new field is `Option<InertialProperties>` with `serde(default)`, so
existing on-disk .vcad files round-trip unchanged. Every Rust
construction site now passes `inertial: None` explicitly — no
behavioural change in the editor / loon / mecheval paths.

https://claude.ai/code/session_01HtymVXbgcAvFXrPkPv33MA
Phase 3 of the official-URDF effort. Add CsgOp::MeshImport for triangle-
mesh files and a stl_io-backed loader so URDF <mesh filename="..."> tags
resolve to actual geometry instead of broken StepImport stubs. Includes
package:// URI resolution against caller-supplied package roots.

Pieces:

  - vcad-ir: new CsgOp::MeshImport { path, scale }. Stub handlers added
    in every match site that already handled CsgOp::StepImport (loon,
    vcode, wasm, evaluate, validate, repl).
  - vcad-kernel-physics::stl: load_stl() reads binary or ASCII STL via
    stl_io and converts to a TriangleMesh in vcad's millimetre frame,
    applying URDF <mesh scale="..."> factors.
  - PhysicsWorld::evaluate_part: bypass the BRep-solid path for
    MeshImport — load straight into TriangleMesh and feed to the
    convex-hull collider builder.
  - vcad-kernel-urdf: new UrdfReadOptions { package_roots, urdf_dir }
    plus read_urdf_with_options / read_urdf_from_str_with_options. The
    reader resolves package://NAME/path against each root, falling back
    to a 1cm placeholder cube when a mesh can't be found (so the rest of
    the kinematic + inertial tree still loads).
  - vcad-cli: `vcad simulate --package-root DIR` (repeatable) for ROS
    workspaces.

Tests: a synthetic 1-metre triangle round-trips through the loader at
1000 mm with correct scale handling, and an end-to-end test confirms
package:// URIs resolve to absolute paths via the URDF reader.

https://claude.ai/code/session_01HtymVXbgcAvFXrPkPv33MA
@vercel
Copy link
Copy Markdown

vercel Bot commented May 10, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
mecheval Ready Ready Preview, Comment May 10, 2026 2:19am
vcad Ready Ready Preview, Comment May 10, 2026 2:19am
vcad-docs Ready Ready Preview, Comment May 10, 2026 2:19am
vcad-mcp Ready Ready Preview, Comment May 10, 2026 2:19am

Request Review

Phase 4 of the official-URDF effort, and the first end where you can
point `vcad simulate` at an upstream URDF and get sensible behaviour.

Vendored from `unitreerobotics/unitree_ros`:

  - examples/unitree-g1-official.urdf — 23-DOF humanoid, references STL
    meshes (load by running against the URDF in its native dir or by
    vendoring meshes alongside).
  - examples/unitree-go2-official.urdf — quadruped, references DAE
    meshes; vcad doesn't load DAE yet so links resolve to 1cm
    placeholder cubes, but joint topology and authored <inertial> still
    flow through correctly.
  - examples/UNITREE.md — explains the four URDF fixtures and how to
    enable mesh resolution.

Importer fixes that made loading real URDFs possible:

  - Reorder <robot> children before serde-deserialise so interleaved
    <link>/<joint>/<material> siblings don't trip quick-xml's
    "duplicate field" error. The pre-pass walks the XML with quick-xml
    events, captures byte ranges per top-level element, and rebuilds
    a normalised string with all materials → links → joints → other.
  - Link.visual: Option<Visual> → visuals: Vec<Visual> (and same for
    collisions / Visual.material). Real-world URDFs (Unitree's Go2 in
    particular) attach multiple visuals or multiple <material> children
    to a single <visual>. The first entry is used as the primary.
  - Skip MeshImport for non-STL extensions (.dae, .obj, …) so DAE
    references don't reach the STL parser as garbage. They fall back to
    the placeholder cube path.

Tests: load both vendored official URDFs, assert sensible link / joint
counts (33 / 32 for G1 23-DOF, 42 / 41 for Go2), check authored mass
sums into the right whole-robot ballpark, and step the physics world
without diverging.

https://claude.ai/code/session_01HtymVXbgcAvFXrPkPv33MA
Replace the 0.004166666 magic literal with `1.0 / 240.0` (via
`default_value_t`), and explain in the help text that 240 Hz is the
standard rigid-body timestep used by phyz / Rapier / Bullet.

https://claude.ai/code/session_01HtymVXbgcAvFXrPkPv33MA
Phase 5 of the official-URDF effort: you can now drop a `.urdf` file
onto the viewport at vcad.io and the robot loads as a regular vcad
Document with assembly + joints + authored inertials. The Simulate tab
+ Play button just work from there.

Pieces:

  - vcad-kernel-wasm: new `importUrdfBuffer(data)` export wrapping
    `vcad_kernel_urdf::read_urdf_from_str`. Returns the imported
    Document as JSON (which the existing `Document.fromJson` path
    already knows how to load).
  - @vcad/engine: thin TS wrapper `Engine.importUrdf(buffer)` matching
    the existing `importStep` shape, with a runtime guard for kernels
    built without urdf support.
  - @vcad/core: export `computeNextIds` so callers building a VcadFile
    from a non-`.vcad` Document (here: URDF) get correct
    `nextNodeId` / `nextPartNum`.
  - @vcad/app: handle `.urdf` extension in the drop / file-picker
    pipeline. Toast reports link + joint counts on success. Mesh
    references inside the URDF can't reach the user's filesystem from
    the browser, so they fall back to 1cm placeholder cubes per link
    — joint topology and authored mass / inertia still flow through,
    so simulation behaves like the real robot to first order.
  - Rebuild the checked-in kernel WASM artifact (~13 MB now, was 10 MB
    optimised — the new mirror skips wasm-opt for the local sandbox
    build; CI re-optimises). Disable wasm-opt in the release profile
    metadata so offline rebuilds don't have to download binaryen.

Drag examples/unitree-g1.urdf or examples/unitree-g1-official.urdf onto
the viewport at the Vercel preview from PR #180 to try it out.

https://claude.ai/code/session_01HtymVXbgcAvFXrPkPv33MA
Phase 6: drop-down access to all four Unitree fixtures from the
File → Examples menu (and the command palette / inline onboarding).
No drag-drop required — pick "Unitree G1 (humanoid)" or any sibling,
hit the Simulate ▶ button, watch the robot fall over.

Pieces:

  - examples/index.ts: extend the `Example` type with an optional
    `urdf: { urdfText, name }` source bundle, mutually exclusive with
    the existing `file: ExampleFile`.
  - Four new entries that import the URDFs via Vite `?raw` so the text
    lands in the bundle as a string. Lazy IR materialisation happens
    at click time through `engine.importUrdf` — saves us from baking
    several hundred kB of pre-converted Document JSON into the app.
  - Header / CommandPalette / InlineOnboarding: detect URDF examples
    and dispatch through the `vcad:load-example` event with a `urdf`
    detail variant. App.tsx's onLoadExample handler now branches on
    detail shape and routes URDFs through the same import path the
    drag-drop uses.

The two "official" entries are the Unitree-shipped descriptors: visuals
appear as 1cm placeholder cubes (browser can't reach STL/DAE meshes)
but joint topology and authored inertials are exact, so simulation
behaves like the real robot. The two hand-authored entries ship full
primitive geometry inline.

https://claude.ai/code/session_01HtymVXbgcAvFXrPkPv33MA
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants