Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
95 commits
Select commit Hold shift + click to select a range
1750ce8
Add ovphysx and warp engine backends with MJCF-to-PhysX-USD converter
zalo Mar 23, 2026
2fc5443
Rewrite MJCF-to-USD converter to match Isaac Lab's C++ importer
zalo Mar 23, 2026
43e3c57
Add ONNX export for ASE actor model
zalo Mar 23, 2026
7e53fc8
Add browser-based PhysX WASM + three.js + ONNX demo
zalo Mar 23, 2026
82e8c67
Fix self-collision explosion and add debug GUI panel
zalo Mar 23, 2026
56736be
Fix observation pipeline to match Python reference implementation
zalo Mar 23, 2026
8741fff
Fix joint rotation obs to use kinematic model's exp_map convention
zalo Mar 23, 2026
3f58d8d
Fix Y-up to Z-up quaternion conversion (was negating all rotations)
zalo Mar 23, 2026
fa9132b
Refactor to Z-up everywhere, rotate only in Three.js
zalo Mar 23, 2026
33c95b6
Fix ground plane box orientation for Z-up
zalo Mar 23, 2026
e7e2228
Fix ONNX export: match normalizer clip=10 and min_std=1e-4
zalo Mar 23, 2026
03763b6
Pack ONNX model weights internally (fix onnxruntime-web loading)
zalo Mar 23, 2026
62e7579
Add diagnostic tools: test pose button, full obs dump, WS comparison
zalo Mar 23, 2026
af335bc
Apply init_pose on spawn to match Newton/Isaac Lab reset
zalo Mar 23, 2026
fab26d2
Fix key body positions via JS-side forward kinematics
zalo Mar 23, 2026
56f9687
Fix mass/inertia: use per-geom density instead of flat 1000
zalo Mar 23, 2026
a9a8b80
Use exact reference masses from Newton/MuJoCo solver
zalo Mar 23, 2026
35d6323
Fix solver iterations: 4,0 matching Isaac Lab (was 32,1)
zalo Mar 23, 2026
6f0fcc5
Add Run Diagnostic button for principled step-by-step debugging
zalo Mar 23, 2026
b0857eb
Set exact reference mass, inertia tensor, and COM per body
zalo Mar 23, 2026
e88f4c0
Add PhysX parameter matching and action bounds clipping for web demo
zalo Mar 23, 2026
ec31fb0
Fix action bounds clipping: add cache buster for JSON, proper joint l…
zalo Mar 23, 2026
c3b4dc9
Add action smoothing and revert to training-matched solver settings
zalo Mar 23, 2026
03a0495
Add drive velocity targets, reset action smoothing, tune alpha
zalo Mar 23, 2026
592a8a6
Fix ONNX export: use actual agent model instead of manual reconstruction
zalo Mar 23, 2026
65d6979
Use v2 ONNX model in web demo, remove action smoothing
zalo Mar 23, 2026
76f8605
Add root rotation debug logging (upVec, heading, quat)
zalo Mar 23, 2026
5964e39
Add Isaac Lab CPU PhysX comparison and --cpu flag for test script
zalo Mar 23, 2026
8822543
Set joint friction to 0 matching Isaac Lab, add PhysX inspection tools
zalo Mar 23, 2026
072629f
Match all Isaac Lab PhysX parameters from USD inspection
zalo Mar 23, 2026
cb8f02d
Add full observation audit: 155/158 dims match perfectly
zalo Mar 23, 2026
909131a
Add step-by-step physics audit, revert self-collision
zalo Mar 23, 2026
b17c776
Fix armature mismatch: right_hand_y/z 0.003 -> 0.01 matching Isaac Lab
zalo Mar 23, 2026
1a477ec
Add closed-loop audit: sword FK position error 0.75 at init!
zalo Mar 23, 2026
1d09e01
Fix key body positions: use PhysX link poses, NOT JS-side FK!
zalo Mar 24, 2026
c13bdd1
Revert to FK-based key body pos, add pose test cases
zalo Mar 24, 2026
0c06f90
FOUND IT: setRootLinearVelocity/setRootAngularVelocity don't work!
zalo Mar 24, 2026
db2c917
Revert key_pos to FK: PhysX links don't match kin_char_model FK
zalo Mar 24, 2026
71ade4a
Add comprehensive debugging status document for handoff
zalo Mar 24, 2026
711f97c
Use PxArticulationCache API to set ALL state atomically
zalo Mar 24, 2026
dec224a
Fix PhysX WASM spherical joint axis swap — character now stands!
zalo Mar 24, 2026
39d59c9
Clean up UI, simplify main loop, add substep slider
zalo Mar 24, 2026
1b26767
Set substeps=1 default, quadruple solver iterations (16 pos, 4 vel)
zalo Mar 24, 2026
69c88b9
Set velocity iterations to 1
zalo Mar 24, 2026
feb5e52
Set velocity iterations to 4
zalo Mar 24, 2026
0549928
Fix spherical joint axis mapping: consecutive order per joint
zalo Mar 24, 2026
b10d570
Port fixes to AMP humanoid and Go2 quadruped demos
zalo Mar 24, 2026
7281bc9
Remove extra demos, add GitHub Pages deployment for ASE demo
zalo Mar 24, 2026
8544434
Collapse debug controls by default
zalo Mar 24, 2026
2436022
Clean up repo: remove debug traces, capture tools, update gitignore
zalo Mar 24, 2026
e08cf42
Remove 1200 lines of dead debug button handler code
zalo Mar 24, 2026
f554467
Deploy Pages on both main and ovphysx-engine branches
zalo Mar 24, 2026
da148fa
Scope PR to web demo only: revert engine changes, remove non-web files
zalo Mar 24, 2026
27d1f92
Remove mjcf_to_physx_usd converter (unused by web demo)
zalo Mar 24, 2026
d42c19d
Add JS MJCF parser: parse character XML directly in the browser
zalo Mar 24, 2026
d1cb9d5
Runtime MJCF parsing + ONNX metadata baking
zalo Mar 24, 2026
c55d07d
Compute mass/inertia/COM from MJCF geoms, bake ONNX metadata
zalo Mar 24, 2026
8b8fb20
Complete MJCF→browser pipeline: accurate mass/inertia, ONNX metadata
zalo Mar 24, 2026
dcd3837
Remove humanoid_data.json and export_humanoid_json.py
zalo Mar 24, 2026
e9a20d5
Remove duplicate XML; GitHub Action copies MJCF at deploy time
zalo Mar 24, 2026
2ba2dc4
Clean up index.html: remove all dead debug code, add model_config.json
zalo Mar 24, 2026
8226ac7
Single-file ONNX character kit: bake MJCF + config into ONNX
zalo Mar 24, 2026
356f9da
Remove all fallbacks: ONNX is the single source of truth
zalo Mar 24, 2026
935ffa9
Replace custom HTML UI with lil-gui panel
zalo Mar 24, 2026
657ab78
Smooth latent cycling + lil-gui Skills panel, substeps=2
zalo Mar 24, 2026
57e43f4
Log latent vectors to console for manual curation
zalo Mar 25, 2026
970f87e
Fix timing: fixed 30Hz policy rate decoupled from render rate
zalo Mar 25, 2026
142bc62
Spread physics substeps across render frames, add latent paste box
zalo Mar 25, 2026
27c8b6d
Disable auto-cycle by default, log latent on reset and random skill
zalo Mar 25, 2026
b709133
Add HLC export tool and pretrained HLC ONNX models
zalo Mar 25, 2026
8e13cb9
Add mode dropdown for task selection (Free + HLC stubs)
zalo Mar 25, 2026
96eab1c
Named skill presets: encode 79 motion clips to latent vectors
zalo Mar 25, 2026
e149b3f
Peak latent encoding: element-wise signed max across all frames
zalo Mar 25, 2026
e2df967
Test: unnormalized peak latents (raw magnitudes ~2x unit norm)
zalo Mar 25, 2026
30f10bc
Latent trajectory playback: full animation sequences instead of singl…
zalo Mar 25, 2026
041c7db
Fix activeSequence scope, remove lerp from trajectory playback
zalo Mar 25, 2026
4e3c928
Fix sequence playback speed: advance by full control step duration
zalo Mar 25, 2026
0f76b29
Fix self-collision toggle: guard against missing getShape API
zalo Mar 25, 2026
7816020
Add HRL agent for ASE task training (LLC→HLC hierarchy)
zalo Mar 25, 2026
1d89f2a
Fix HRL agent: action space patching, LLC env wrapping, obs slicing
zalo Mar 25, 2026
fe89702
HRL training: debug timing, tqdm progress, config tuning
zalo Mar 25, 2026
e426029
Remove HRL training code — too slow with MimicKit's training loop
zalo Mar 25, 2026
9370128
Remove accidentally committed debug/test files
zalo Mar 25, 2026
b3640c1
Remove HLC export tools — incompatible with web demo
zalo Mar 25, 2026
71ba497
Remove HLC ONNX models
zalo Mar 25, 2026
01d7a14
Inline hand-derived latent presets, remove latent_presets.json
zalo Mar 25, 2026
3f918fe
Revert .gitignore to main, remove debug screenshots and .playwright-mcp
zalo Mar 25, 2026
79bdb89
Inline MJCF parser into index.html, rename export_onnx_v2 → export_onnx
zalo Mar 25, 2026
81f898a
Fix PhysX parity: PGS solver, iteration counts, obs key body position…
zalo Mar 25, 2026
8e6650d
Fix window._mk getters for live references after resetHumanoid
zalo Mar 25, 2026
5ed4015
Use convex mesh for cylinder collision shapes (shield)
zalo Mar 25, 2026
57042fa
Remove recording/playback/debug audit code from web demo
zalo Mar 25, 2026
c99d356
Fix velocity iterations to 0 (matching Isaac Lab exactly)
zalo Mar 25, 2026
25c5c11
UI cleanup and Lock Y Axis via D6 joint
zalo Mar 25, 2026
b92c17b
Address PR review comments: fix pelvis offset, cache cylinder meshes,…
zalo Apr 1, 2026
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
39 changes: 39 additions & 0 deletions .github/workflows/deploy-pages.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: Deploy Web Demo to GitHub Pages

on:
push:
branches: [main, ovphysx-engine]
paths: ['web/**']
workflow_dispatch:

permissions:
contents: read
pages: write
id-token: write

concurrency:
group: pages
cancel-in-progress: true

jobs:
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
lfs: true

- name: Setup Pages
uses: actions/configure-pages@v4

- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: web

- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
310 changes: 310 additions & 0 deletions tools/export_onnx.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,310 @@
"""Export ASE actor to ONNX using the actual agent model (no manual reconstruction).

This loads the full agent with its model, wraps the inference pipeline
(obs_norm → actor → a_norm) into a single nn.Module, and exports it.
This guarantees exact equivalence with PyTorch inference.

Usage:
cd MimicKit
python tools/export_onnx.py \
--arg_file args/ase_humanoid_sword_shield_args.txt \
--model_file data/models/ase_humanoid_sword_shield_model.pt \
--output web/ase_humanoid_sword_shield_actor.onnx
"""
import sys, os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'mimickit'))

# Isaac Gym requires being imported before torch
try:
import isaacgym
except ImportError:
pass

import argparse
import numpy as np
import torch
import torch.nn as nn

parser = argparse.ArgumentParser()
parser.add_argument('--arg_file', default='args/ase_humanoid_sword_shield_args.txt')
parser.add_argument('--model_file', default='data/models/ase_humanoid_sword_shield_model.pt')
parser.add_argument('--output', default='web/ase_humanoid_sword_shield_actor.onnx')
parser.add_argument('--engine', default='data/engines/isaac_lab_engine.yaml',
help='Engine config (only used to build env for model construction)')
args = parser.parse_args()


class ASEActorWrapper(nn.Module):
"""Wraps the full agent inference pipeline into a single exportable module.

Input: raw_obs (B, obs_dim), latent (B, latent_dim)
Output: action (B, act_dim)

Bakes in: obs normalization, actor network, action unnormalization.
"""

def __init__(self, agent):
super().__init__()

# Copy normalizer parameters as buffers
self.register_buffer('obs_mean', agent._obs_norm._mean.data.clone())
self.register_buffer('obs_std', agent._obs_norm._std.data.clone().clamp(min=1e-4))
self.obs_clip = agent._obs_norm._clip

self.register_buffer('a_mean', agent._a_norm._mean.data.clone())
self.register_buffer('a_std', agent._a_norm._std.data.clone().clamp(min=1e-4))

# Reference the actual model layers (no reconstruction!)
self.actor_layers = agent._model._actor_layers
self.mean_net = agent._model._action_dist._mean_net

def forward(self, raw_obs: torch.Tensor, latent: torch.Tensor) -> torch.Tensor:
# Normalize observation
norm_obs = (raw_obs - self.obs_mean) / self.obs_std
norm_obs = torch.clamp(norm_obs, -self.obs_clip, self.obs_clip)

# Concatenate obs + latent (same as ASEModel.eval_actor)
x = torch.cat([norm_obs, latent], dim=-1)

# Actor forward pass
h = self.actor_layers(x)
norm_action = self.mean_net(h) # deterministic (mode) action

# Unnormalize action
action = norm_action * self.a_std + self.a_mean
return action


def main():
# Need to build env + agent to get the actual model with correct architecture.
# We use a dummy engine config that doesn't require a GPU simulator.
# If Isaac Lab is available, use it; otherwise fall back to loading from checkpoint.

try:
# Try building via the full agent pipeline
from util.arg_parser import ArgParser
import util.mp_util as mp_util
import run as mimickit_run

mk_args = ArgParser()
mk_args.load_file(args.arg_file)
mk_args._table['engine_config'] = [args.engine]
mk_args._table['num_envs'] = ['1']
mk_args._table['mode'] = ['test']
mk_args._table['visualize'] = ['false']

device = 'cuda:0' if torch.cuda.is_available() else 'cpu'
mp_util.init(0, 1, device, None)

env = mimickit_run.build_env(mk_args, 1, device, False)
agent = mimickit_run.build_agent(mk_args, env, 'cpu')
agent.load(args.model_file)
agent.eval()

print(f"Agent loaded via full pipeline")
print(f" obs_dim={agent._obs_norm._mean.shape[0]}")
print(f" act_dim={agent._a_norm._mean.shape[0]}")
print(f" latent_dim={agent._model._enc_out.weight.shape[0]}")

except Exception as e:
print(f"Failed to build via full pipeline: {e}")
raise

# Create wrapper
wrapper = ASEActorWrapper(agent)
wrapper.eval()

obs_dim = wrapper.obs_mean.shape[0]
latent_dim = agent._model._enc_out.weight.shape[0]
act_dim = wrapper.a_mean.shape[0]

print(f"\nExporting: raw_obs({obs_dim}) + latent({latent_dim}) → action({act_dim})")

# Dummy inputs
dummy_obs = torch.randn(1, obs_dim)
dummy_z = torch.randn(1, latent_dim)

# PyTorch reference
with torch.no_grad():
pt_action = wrapper(dummy_obs, dummy_z)
print(f"PyTorch output: {pt_action[0, :5].numpy()}")

# Export
torch.onnx.export(
wrapper,
(dummy_obs, dummy_z),
args.output,
input_names=["obs", "latent"],
output_names=["action"],
dynamic_axes={
"obs": {0: "batch"},
"latent": {0: "batch"},
"action": {0: "batch"},
},
opset_version=17,
)
print(f"Exported to {args.output}")

# Verify
import onnxruntime as ort
sess = ort.InferenceSession(args.output)
ort_inputs = {"obs": dummy_obs.numpy(), "latent": dummy_z.numpy()}
ort_action = sess.run(["action"], ort_inputs)[0]

max_diff = np.max(np.abs(pt_action.numpy() - ort_action))
print(f"ONNX output: {ort_action[0, :5]}")
print(f"Max diff: {max_diff:.2e}")
assert max_diff < 1e-3, f"ONNX diverges: {max_diff}"

# Verify with real-ish obs
print("\nVerifying with agent's actual inference...")
test_obs = torch.randn(1, obs_dim)
test_z = torch.randn(1, latent_dim)
test_z = test_z / test_z.norm(dim=-1, keepdim=True)

with torch.no_grad():
# Agent path
norm_obs = agent._obs_norm.normalize(test_obs)
dist = agent._model.eval_actor(norm_obs, test_z)
agent_action = agent._a_norm.unnormalize(dist.mode)

# Wrapper path
wrapper_action = wrapper(test_obs, test_z)

# ONNX path
ort_action2 = sess.run(["action"], {
"obs": test_obs.numpy(),
"latent": test_z.numpy()
})[0]

agent_np = agent_action.numpy()
wrapper_np = wrapper_action.numpy()
print(f"Agent action[:5]: {agent_np[0,:5]}")
print(f"Wrapper action[:5]: {wrapper_np[0,:5]}")
print(f"ONNX action[:5]: {ort_action2[0,:5]}")
print(f"Agent vs Wrapper max_diff: {np.max(np.abs(agent_np - wrapper_np)):.2e}")
print(f"Agent vs ONNX max_diff: {np.max(np.abs(agent_np - ort_action2)):.2e}")
print(f"Wrapper vs ONNX max_diff: {np.max(np.abs(wrapper_np - ort_action2)):.2e}")

# Bake model metadata into ONNX for the web demo.
# This eliminates the need for a separate JSON config file.
import onnx
model = onnx.load(args.output)

# Collect metadata from agent
meta = {}
meta['obs_dim'] = int(obs_dim)
meta['act_dim'] = int(act_dim)
meta['latent_dim'] = int(latent_dim)
meta['obs_mean'] = wrapper.obs_mean.numpy().tolist()
meta['obs_std'] = wrapper.obs_std.numpy().tolist()
meta['a_mean'] = wrapper.a_mean.numpy().tolist()
meta['a_std'] = wrapper.a_std.numpy().tolist()

# Action bounds from agent config
if hasattr(agent, '_action_low') and agent._action_low is not None:
meta['action_low'] = agent._action_low.cpu().numpy().tolist()
meta['action_high'] = agent._action_high.cpu().numpy().tolist()

# Init pose and env config — try env attributes, fall back to existing JSON
json_fallback = {}
json_path = os.path.join(os.path.dirname(args.output), 'humanoid_data.json')
if os.path.isfile(json_path):
import json as _json
json_fallback = _json.load(open(json_path))
print(f" Loaded JSON fallback from {json_path}")

def _try_tensor(obj, attr, idx=0):
"""Extract a list from a tensor attribute, handling 1D and 2D tensors."""
t = getattr(obj, attr, None)
if t is None: return None
t = t.cpu()
if t.dim() > 1: t = t[idx]
return t.numpy().flatten().tolist()

# Init pose
init_dof = _try_tensor(env, '_init_dof_pos') or json_fallback.get('init_dof_pos')
init_root_pos = _try_tensor(env, '_init_root_pos') or json_fallback.get('init_root_pos')
init_root_rot = _try_tensor(env, '_init_root_rot') or json_fallback.get('init_root_rot_quat')
if init_dof and isinstance(init_dof, list) and len(init_dof) > 1:
meta['init_dof_pos'] = init_dof
if init_root_pos and isinstance(init_root_pos, list) and len(init_root_pos) == 3:
meta['init_root_pos'] = init_root_pos
if init_root_rot and isinstance(init_root_rot, list) and len(init_root_rot) == 4:
meta['init_root_rot_quat'] = init_root_rot

# Action bounds
action_low = _try_tensor(agent, '_action_low') or json_fallback.get('action_low')
action_high = _try_tensor(agent, '_action_high') or json_fallback.get('action_high')
if action_low: meta['action_low'] = action_low
if action_high: meta['action_high'] = action_high

# Key body IDs and settings
key_ids = _try_tensor(env, '_key_body_ids') or json_fallback.get('key_body_ids')
if key_ids: meta['key_body_ids'] = [int(x) for x in key_ids]
meta['global_obs'] = bool(getattr(env, '_global_obs', json_fallback.get('global_obs', False)))
meta['pelvis_z'] = float(getattr(env, '_pelvis_z', json_fallback.get('pelvis_z', 0.703)))
meta['tpose_pelvis_z'] = float(getattr(env, '_tpose_pelvis_z', json_fallback.get('tpose_pelvis_z', 0.903)))

# Bake MJCF XML into metadata so the ONNX is a single-file character kit
mjcf_path = getattr(env, '_char_file', None) or json_fallback.get('mjcf_file')
# Also try the arg_file's char_file
if not mjcf_path:
try:
mjcf_path = mk_args.parse_string('char_file')
except: pass
# Try env_config's char_file
if not mjcf_path or not os.path.isfile(mjcf_path):
try:
import yaml
with open(mk_args.parse_string('env_config')) as f:
env_cfg = yaml.safe_load(f)
if 'char_file' in env_cfg and os.path.isfile(env_cfg['char_file']):
mjcf_path = env_cfg['char_file']
except: pass
if mjcf_path and os.path.isfile(mjcf_path):
with open(mjcf_path) as f:
meta['mjcf_xml'] = f.read()
print(f" Baked MJCF from {mjcf_path} ({len(meta['mjcf_xml'])} chars)")

# Write ALL metadata as a single JSON blob under a sentinel key.
# This allows the web demo to find it by scanning the raw ONNX bytes
# (onnxruntime-web doesn't expose metadata via its JS API).
import json
sentinel_key = 'mimickit_config'
config_json = json.dumps(meta, separators=(',', ':')) # compact
entry = onnx.StringStringEntryProto(key=sentinel_key, value=config_json)
model.metadata_props.append(entry)
# Also write individual entries for tools that read metadata normally
for key, value in meta.items():
if key == 'mjcf_xml': continue # already in the blob
entry = onnx.StringStringEntryProto(key=key, value=json.dumps(value))
model.metadata_props.append(entry)

onnx.save(model, args.output, save_as_external_data=False)
print(f"\nBaked {len(meta)} metadata entries into ONNX:")
for k in sorted(meta.keys()):
v = meta[k]
if isinstance(v, list) and len(v) > 5:
print(f" {k}: [{v[0]:.4f}, ... {len(v)} items]")
else:
print(f" {k}: {v}")

# Check file size
size_mb = os.path.getsize(args.output) / (1024 * 1024)
print(f"\nOutput file: {args.output} ({size_mb:.1f} MB)")

if size_mb < 0.1:
print("WARNING: File is very small — weights may be stored externally in .onnx.data file")
print("Re-saving with all weights internal...")
import onnx
model = onnx.load(args.output)
onnx.save(model, args.output, save_as_external_data=False)
size_mb = os.path.getsize(args.output) / (1024 * 1024)
print(f"Re-saved: {args.output} ({size_mb:.1f} MB)")

print("\nPASS: All verifications passed.")


if __name__ == "__main__":
main()
Binary file added web/ase_humanoid_sword_shield_actor.onnx
Binary file not shown.
Loading