Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions models/mujoco/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Generated by scripts/bootstrap_mujoco_models.py
unitree_g1
unitree_a1
boston_dynamics_spot
32 changes: 32 additions & 0 deletions models/mujoco/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# MuJoCo Models (Bootstrap, Not Checked In)

This folder is intentionally kept lightweight. Large MuJoCo model assets are **not** committed to this repository.

## Bootstrap

Run:

```bash
python scripts/bootstrap_mujoco_models.py
```

The script will sparse-clone MuJoCo Menagerie at a pinned commit and create local symlinks under this folder.

## Source models

Pinned source repository:
- https://github.com/google-deepmind/mujoco_menagerie

Pinned commit:
- `a03e87bf13502b0b48ebbf2808928fd96ebf9cf3`

Models fetched:
- `unitree_g1`
- `unitree_a1`
- `boston_dynamics_spot`

## Notes

- No mesh/XML transforms are applied. Models are used as-is from menagerie.
- `webdemo/public/examples/scenes/<model>` symlinks are also created by the bootstrap script.
- `webdemo/public/examples/scenes/files.json` is regenerated by the script based on fetched assets.
105 changes: 105 additions & 0 deletions scripts/bootstrap_mujoco_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
#!/usr/bin/env python3
"""Bootstrap MuJoCo Menagerie models for GTDynamics webdemo.

What it does:
1. Sparse-clones/pulls google-deepmind/mujoco_menagerie at a pinned commit.
2. Materializes only required model folders:
- unitree_g1
- unitree_a1
- boston_dynamics_spot
3. Creates symlinks:
- models/mujoco/<model>
- webdemo/public/examples/scenes/<model>
4. Regenerates webdemo/public/examples/scenes/files.json from on-disk assets.

This keeps large binary model files out of this git repository.
"""

from __future__ import annotations

import json
import os
import shutil
import subprocess
from pathlib import Path

MENAGERIE_URL = "https://github.com/google-deepmind/mujoco_menagerie.git"
MENAGERIE_COMMIT = "a03e87bf13502b0b48ebbf2808928fd96ebf9cf3"
MODELS = ["unitree_g1", "unitree_a1", "boston_dynamics_spot"]
ASSET_EXTS = {".xml", ".obj", ".stl", ".png", ".jpg", ".jpeg", ".mtl", ".skn"}


def run(cmd: list[str], cwd: Path | None = None) -> None:
subprocess.run(cmd, cwd=str(cwd) if cwd else None, check=True)


def ensure_symlink(link_path: Path, target_path: Path) -> None:
if link_path.is_symlink():
if link_path.resolve() == target_path.resolve():
return
link_path.unlink()
elif link_path.exists():
if link_path.is_dir():
shutil.rmtree(link_path)
else:
link_path.unlink()

link_path.parent.mkdir(parents=True, exist_ok=True)
rel_target = os.path.relpath(target_path, link_path.parent)
link_path.symlink_to(rel_target)


def generate_files_json(scenes_dir: Path) -> list[str]:
files: list[str] = []
for model in MODELS:
model_dir = scenes_dir / model
if not model_dir.exists():
continue
for path in sorted(model_dir.rglob("*")):
if not path.is_file():
continue
if path.suffix.lower() not in ASSET_EXTS:
continue
files.append(path.relative_to(scenes_dir).as_posix())
return files


def main() -> int:
repo_root = Path(__file__).resolve().parents[1]
cache_root = Path.home() / ".cache" / "gtdynamics" / "mujoco_menagerie"
checkout_root = cache_root / MENAGERIE_COMMIT
menagerie_repo = checkout_root / "repo"

checkout_root.mkdir(parents=True, exist_ok=True)

if not menagerie_repo.exists():
run([
"git", "clone", "--filter=blob:none", "--sparse", MENAGERIE_URL, str(menagerie_repo)
])

run(["git", "fetch", "--depth", "1", "origin", MENAGERIE_COMMIT], cwd=menagerie_repo)
run(["git", "checkout", "--detach", MENAGERIE_COMMIT], cwd=menagerie_repo)
run(["git", "sparse-checkout", "set", *MODELS], cwd=menagerie_repo)

models_dir = repo_root / "models" / "mujoco"
scenes_dir = repo_root / "webdemo" / "public" / "examples" / "scenes"

for model in MODELS:
source = menagerie_repo / model
ensure_symlink(models_dir / model, source)
ensure_symlink(scenes_dir / model, models_dir / model)

files = generate_files_json(scenes_dir)
files_json_path = scenes_dir / "files.json"
files_json_path.write_text(json.dumps(files, indent=2) + "\n", encoding="utf-8")

print("Bootstrap complete.")
print(f"Menagerie commit: {MENAGERIE_COMMIT}")
print(f"Model symlinks under: {models_dir}")
print(f"Scene symlinks under: {scenes_dir}")
print(f"Regenerated: {files_json_path}")
return 0


if __name__ == "__main__":
raise SystemExit(main())
4 changes: 4 additions & 0 deletions webdemo/.browserslistrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
> 1%
last 2 versions
not dead
not ie 11
5 changes: 5 additions & 0 deletions webdemo/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[*.{js,jsx,ts,tsx,vue}]
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true
97 changes: 97 additions & 0 deletions webdemo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# GT Dynamics MuJoCo Web Demo

Local-only Vite + Vue demo for MuJoCo trajectory playback.

## Prerequisites

- Node.js 18+
- Git (for bootstrap script)

## First-time setup

From repo root:

```bash
python scripts/bootstrap_mujoco_models.py
```

This downloads model assets on demand (sparse checkout from MuJoCo Menagerie), creates symlinks, and regenerates:
- `webdemo/public/examples/scenes/files.json`

Model provenance is documented in:
- `models/mujoco/README.md`

## Run locally

```bash
cd webdemo
npm install
npm run dev
```

## Control model

This demo runs MuJoCo dynamics; it is not pure kinematic teleporting.

- Default behavior in this GTD fork: `control_mode: "position"`
- Meaning: `ctrl` is sent as desired joint positions for MuJoCo `<position>` actuators
- MuJoCo still resolves forces/torques, contacts, and friction physically

Optional fallback for custom models:

- `control_mode: "torque_pd"` uses explicit torque-like PD in JS before writing `ctrl`

Per-robot controller mode is configured in:
- `webdemo/public/examples/configs/<robot>.json`

Note:
- If a clip includes `root_pos` / `root_quat`, the root can be replayed directly from the clip.
- For fully dynamic locomotion evaluation, use clips/controllers that do not hard-impose root motion every tick.

## File layout

- Scene assets (symlinked by bootstrap script):
- `webdemo/public/examples/scenes/<model>/...`
- Scene preload list:
- `webdemo/public/examples/scenes/files.json`
- Robot configs:
- `webdemo/public/examples/configs/<robot>.json`
- Motion assets:
- `webdemo/public/examples/motions/<robot>/motions.json`
- `webdemo/public/examples/motions/<robot>/motions/<clip>.json`

## Add a motion clip

1. Put clip JSON files in:
- `webdemo/public/examples/motions/<robot>/motions/`
2. Register them in:
- `webdemo/public/examples/motions/<robot>/motions.json`

Supported clip keys:
- `joint_pos` or `jointPos`
- `root_pos` or `rootPos` (optional)
- `root_quat` or `rootQuat` (optional)

## Export from GT Dynamics

Use the exporter in the main repo:

```bash
python scripts/export_motion_clip.py \
--input /path/to/trajectory.json \
--output webdemo/public/examples/motions/g1/motions/my_clip.json \
--joint-names left_hip_pitch_joint right_hip_pitch_joint ... \
--fps 60
```

Then add `my_clip.json` to `webdemo/public/examples/motions/g1/motions.json`.

## Attribution

- Direct code structure origin: [Axellwppr/humanoid-policy-viewer](https://github.com/Axellwppr/humanoid-policy-viewer)
Qingzhou Lu (Axellwppr on Github) said: "The framework code will be MIT-licensed (I’ll update the repo shortly). Mjlab and any motion datasets remain under their respective licenses."
- Runtime dependencies:
- [mujoco-js](https://www.npmjs.com/package/mujoco-js) (MuJoCo WASM)
- [three.js](https://threejs.org/)
- [Vue 3](https://github.com/vuejs/core)
- [Vite](https://github.com/vitejs/vite)
12 changes: 12 additions & 0 deletions webdemo/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>GT Dynamics MuJoCo Clip Demo</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
18 changes: 18 additions & 0 deletions webdemo/jsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"compilerOptions": {
"allowJs": true,
"target": "ES2020",
"module": "ESNext",
"baseUrl": ".",
"moduleResolution": "Bundler",
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"ESNext",
"DOM"
]
}
}
20 changes: 20 additions & 0 deletions webdemo/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "gtdynamics-webdemo",
"private": true,
"type": "module",
"version": "0.1.0",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"mujoco-js": "^0.0.7",
"three": "0.151.3",
"vue": "^3.5.13"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.3",
"vite": "^6.2.2"
}
}
70 changes: 70 additions & 0 deletions webdemo/public/examples/configs/a1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
{
"scene": "unitree_a1/scene.xml",
"clip_joint_names": [
"FR_hip_joint",
"FR_thigh_joint",
"FR_calf_joint",
"FL_hip_joint",
"FL_thigh_joint",
"FL_calf_joint",
"RR_hip_joint",
"RR_thigh_joint",
"RR_calf_joint",
"RL_hip_joint",
"RL_thigh_joint",
"RL_calf_joint"
],
"stiffness": [
100.0,
100.0,
100.0,
100.0,
100.0,
100.0,
100.0,
100.0,
100.0,
100.0,
100.0,
100.0
],
"damping": [
2.0,
2.0,
2.0,
2.0,
2.0,
2.0,
2.0,
2.0,
2.0,
2.0,
2.0,
2.0
],
"default_joint_pos": [
0.0,
0.9,
-1.8,
0.0,
0.9,
-1.8,
0.0,
0.9,
-1.8,
0.0,
0.9,
-1.8
],
"default_root_pos": [
0.0,
0.0,
0.27
],
"default_root_quat": [
1.0,
0.0,
0.0,
0.0
]
}
Loading
Loading