Skip to content

Web demo: ASE humanoid runs in the browser via PhysX WASM + ONNX Runtime#95

Draft
zalo wants to merge 94 commits intoxbpeng:mainfrom
zalo:ovphysx-engine
Draft

Web demo: ASE humanoid runs in the browser via PhysX WASM + ONNX Runtime#95
zalo wants to merge 94 commits intoxbpeng:mainfrom
zalo:ovphysx-engine

Conversation

@zalo
Copy link
Copy Markdown

@zalo zalo commented Mar 24, 2026

Live Demo

https://zalo.github.io/MimicKit/

ASE humanoid sword-shield character performing combat motions in real-time, running entirely in the browser — no server required.

Summary

  • PhysX WASM (physx-js-webidl@2.7.2) for physics simulation
  • ONNX Runtime Web (1.21.0) for neural network policy inference
  • three.js (0.170.0) for 3D rendering
  • Single-file character kit: one ONNX file contains the trained model, MJCF character XML, normalizers, init pose, and action bounds — everything needed to run a character

Single-file ONNX character kit

The ONNX model file is a complete, self-contained character package:

Baked into ONNX Description
Neural network Actor weights (obs + latent → action)
MJCF XML Full character definition (parsed at runtime by `parseMJCF.js`)
Normalizers obs_mean/std, action_mean/std from training
Init pose Default DOF positions, root position/rotation
Action bounds Clipping ranges for action output
Env config Key body IDs, latent dim, obs dim, pelvis height

The web demo extracts this metadata by scanning the ONNX protobuf binary for a sentinel key (`mimickit_config`), since onnxruntime-web doesn't expose `metadata_props` via its JS API. The baked MJCF is parsed at runtime by `parseMJCF.js` (DOMParser), which computes body mass, inertia tensors, and center-of-mass analytically from geom shapes.

To add a new character: train in Isaac Lab, run `export_onnx_v2.py`, drop the ONNX file into `web/`.

Key Technical Finding

PhysX WASM's spherical joints require DOF values mapped to consecutive axis order `(TWIST=0, SWING1=1, SWING2=2)` matching the expmap input. Isaac Lab's DOF ordering can have non-consecutive assignments (e.g. hips use `[0,2,1]`). Fixed by per-joint axis remapping at load time.

Files

File Description
`web/index.html` Complete web demo (~1500 lines)
`web/parseMJCF.js` Runtime MJCF parser with analytical mass/inertia
`web/ase_humanoid_sword_shield_actor.onnx` Single-file character kit (7MB)
`web/model_config.json` Fallback config (8KB, not needed if ONNX has metadata)
`tools/export_onnx_v2.py` ONNX exporter: bakes model + MJCF + config
`.github/workflows/deploy-pages.yml` GitHub Pages deployment

Local Development

python -m http.server 8080  # from repo root
# open http://localhost:8080/web/index.html

TODO

  • Port remaining demos — AMP humanoid and Go2 quadruped
  • Skill selection UI — Pick trained skills instead of random latent
  • Mobile support — Touch controls, responsive layout

🤖 Generated with Claude Code

zalo and others added 30 commits March 23, 2026 01:11
- tools/mjcf_to_physx_usd: Converts MuJoCo MJCF XML to USDA with PhysX
  physics schemas (ArticulationRootAPI, PhysicsJoint, drives, limits).
  Flat USD hierarchy with world-space body positions for PhysX compatibility.

- mimickit/engines/ovphysx_engine.py: Standalone PhysX 5 engine using the
  ovphysx pip package for physics and Newton ViewerGL for visualization.
  Includes body ordering mapping between ovphysx link indices and Newton
  DFS body order, ground grid rendering, and DOF target clamping.

- mimickit/engines/warp_engine.py: Newton/Warp engine using SolverXPBD
  with explicit PD torque computation matching Isaac Lab's force-based PD.

- data/engines/ovphysx_engine.yaml, warp_engine.yaml: Engine configs.

- Modified engine_builder.py to register both new backends.

- Modified isaac_lab_engine.py to suppress carb performance warnings and
  disable expensive RTX rendering features for better framerate.

- CLAUDE.md: Project guidance for Claude Code.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Key changes to match isaacsim.asset.importer.mjcf behavior:
- Joint frame rotation: compute quaternion aligning MJCF axis with PhysX
  X-axis via GetRotationQuat, matching computeJointFrame() exactly
- Single hinge joints use PhysicsRevoluteJoint with axis="X" + rotated
  localRot0/localRot1 (not identity rotation with mapped axis names)
- Multi-hinge D6 joints use axisMap with dot-product Y/Z comparison
  for proper axis ordering, matching Isaac Lab's 2/3-joint logic
- Added maxForce from actuatorfrcrange on all drives
- Added PhysxSchemaPhysxLimitAPI stiffness/damping on joints
- Added PhysxSchemaPhysxJointAPI armature values
- Translation axes locked (low=1, high=-1) on D6 joints
- Drive type = "force" (not "acceleration"), matching Isaac Lab

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- tools/export_onnx.py: Exports the ASE actor (obs+latent → action) to
  ONNX with baked-in observation normalizer and action unnormalizer.
  The exported graph takes raw obs (158) + latent (64) and outputs
  action (31). Verified PyTorch vs ONNX Runtime: rel_diff < 1e-6.

- data/models/ase_humanoid_sword_shield_actor.onnx: 3.2KB exported model
  ready for onnxruntime-web / browser inference.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Complete in-browser demo running the ASE sword-shield character with:
- physx-js-webidl (PhysX 5 compiled to WASM) for articulation physics
- three.js for 3D rendering with shadows and orbit controls
- onnxruntime-web for neural network policy inference
- All running client-side with no server required

tools/export_humanoid_json.py: Generalized MJCF-to-JSON exporter that works
with any MimicKit character. Accepts --mjcf, --model, --output, --pelvis_z,
--fixed_bodies args. Auto-detects fixed-attachment bodies when not specified.

web/index.html: Single-page demo (863 lines) with PhysX articulation setup,
three.js visualization, ONNX inference loop, ground grid, orbit camera.

web/humanoid_data.json: Exported humanoid description (17 bodies, 31 DOFs)
web/ase_humanoid_sword_shield_actor.onnx: Exported policy model (7MB)

Deployed to: https://mimickit-demo.pages.dev

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Disable self-collision between articulation links via
  eDISABLE_SELF_COLLISION flag + collision group filtering
  (articulation shapes only collide with ground, not each other)
- Add debug panel with: Active Drives toggle (go limp), NN Policy
  toggle, Self-Collision toggle, Gravity toggle, Drive Stiffness/Damping
  scale sliders, Sim Substeps slider, Time Scale slider
- Live debug readout: root_z, root_speed, max_dof_vel, link/DOF count
- Wrap all PhysX joint queries in try-catch to prevent WASM memory
  access crashes from locked/fixed joint axis queries

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three critical bugs causing policy-driven spasming:

1. Double observation normalization: ONNX model already contains the
   normalizer (baked in by export_onnx.py). JS was normalizing AGAIN
   before passing to ONNX, producing nonsensical outputs (~1000x scale).
   Fix: removed JS-side normalization.

2. Wrong observation format: JS was building ~88-dim obs stuffed into
   wrong slots of 158-dim vector. Python uses heading-removed quaternion
   as 6D tan_norm (not raw 4D quat), joint rotations as 96-dim tan_norm
   (not 31-dim raw DOF positions), 6 key bodies (not 5), and all values
   in heading-relative frame. Fix: rewrote buildObservation() with full
   quaternion math pipeline matching char_env.py exactly.

3. Action clamping too restrictive: [-1.5, 1.5] was clamping all 31
   DOFs when legitimate actions reach ±6.7. Fix: per-DOF bounds from
   a_mean ± 3*a_std using normalizer stats from humanoid_data.json.

Also verified: ONNX model is purely feedforward (no recurrent state).
ASE latent vector is the only inter-frame state (already handled).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Python kinematic model treats 3-hinge joints as SPHERICAL and
uses exp_map_to_quat() to convert DOF positions to quaternions. The
JS was composing individual PhysX axis rotations instead.

- Added kinematicJoints array to humanoid_data.json with exact joint
  types (SPHERICAL/HINGE/FIXED), DOF indices, and axes from MJCFCharModel
- Rewrote joint rotation section of buildObservation() to:
  - SPHERICAL: read 3 DOFs as exp_map, convert via expMapToQuat
  - HINGE: read 1 DOF, convert via axisAngleToQuat with Z-up axis
  - FIXED: use identity quaternion
- Iterate in kinematic model body order (not PhysX DOF order)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
quatYupToZup was [-qx, -qz, -qy, qw] which negates all rotation
components, causing the policy to see every rotation as reversed.
The character tried to "stand up sideways" because it perceived left
as right, forward as backward, etc.

Correct formula is [qx, qz, qy, qw] — swap Y↔Z without negation.
Verified against all test cases: identity, heading rotation, forward
tilt, and sideways lean all produce correct Z-up quaternions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Eliminated all Y-up↔Z-up coordinate conversions that were causing
rotation bugs (character trying to stand sideways/upside-down).

- PhysX gravity now (0,0,-9.81) matching MJCF Z-up convention
- Body positions, joint frames, geom poses all used directly in Z-up
- Observation reads PhysX state as Z-up natively (no conversion)
- Deleted zup(), yupToZup3(), quatYupToZup(), quatZupToYup()
- Single Three.js worldGroup with rotation.x=-PI/2 for rendering
- Camera tracking converts Z-up root pos to Y-up for OrbitControls

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PxBoxGeometry(50, 0.5, 50) had the thin dimension on Y, creating a
100x100 wall in the Z direction that the character spawned inside of.
Changed to PxBoxGeometry(50, 50, 0.5) so Z is the thin axis — correct
for Z-up where gravity is (0, 0, -9.81).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The ONNX model was using clamp(-5,5) and min_std=1e-5, but the
actual MimicKit agent uses clip=10.0 (from base_agent.py) and
min_std=1e-4 (from Normalizer). This caused 34 observation dims to
be clamped differently, producing actions off by up to 1700x.

Fixed export_onnx.py to use clamp(-10,10) and min_std=1e-4.
PyTorch vs ONNX now match within 2.4e-6 (verified via WS server).

Also added:
- tools/ws_inference_server.py: WebSocket server for side-by-side
  PyTorch vs ONNX inference comparison
- WS Inference checkbox in web debug panel for live testing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
onnxruntime-web cannot load external .data files via MountedFiles.
Re-saved the model with all tensor data embedded in the single .onnx
file (7MB, no external .data companion file needed).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Verified: JS ONNX, Python ONNX, and Python PyTorch all produce
identical actions for the same first-frame observation (diff=0).
The flailing occurs on subsequent frames as the character moves and
observations diverge from the training distribution.

- Test Pose button: raises both arms with hardcoded drive targets
  (verifies drives and coordinate system without the policy)
- Full observation JSON dump on first frame for Python comparison
- WS server min_std fixed to 1e-4 matching Normalizer

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The env resets the character to init_pose (a combat-ready stance from
the env config), not T-pose. This was causing 85/158 observation dims
to mismatch on the first frame.

- Added init_root_pos, init_root_rot_quat, init_dof_pos to JSON
- applyInitPose() sets root pose via setRootGlobalPose and joint
  positions via setJointPosition, then drive targets to match
- Step physics once after init to propagate joint→body FK
- pelvis_z corrected from 0.903 to 0.703 (matching env config)
- First frame obs now matches Newton within 0.94 max diff (only
  key_body_pos differs due to FK propagation timing)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The last 18 observation dims (key body positions) were wrong because
PhysX link positions don't update from setJointPosition until a full
sim step. Instead of relying on PhysX, compute FK in JS matching the
Python kinematic model exactly:
- Exported fk_parent_indices, fk_local_translations, fk_local_rotations
  from MJCFCharModel to humanoid_data.json
- JS buildObservation() runs FK using these, same as Python's
  forward_kinematics(): body_pos = parent_pos + quat_rotate(parent_rot,
  local_trans), body_rot = quat_mul(parent_rot, quat_mul(local_rot, j_rot))
- When policy is OFF, drives target init_pose (not T-pose zeros)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
updateMassAndInertia was called with density=1000 for ALL links,
ignoring MJCF per-geom densities (250-2226). This gave every body
the wrong mass, making drives push with incorrect force-to-acceleration
ratios. Now computes volume-weighted average density per body from
its geom densities, matching how Isaac Lab/MuJoCo compute mass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replaced flat density=1000 and volume-based density approximation
with exact per-body masses from the Newton solver (total 53.001 kg).
Mass ranges from 0.5 kg (hand) to 12.0 kg (torso), matching the
MJCF geom densities (250-2226) applied to the actual geometry volumes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
articulation.setSolverIterationCounts(32, 1) was using 8x more position
solver iterations than Isaac Lab's max_position_iteration_count=4. This
made constraint resolution too stiff, amplifying force mismatches and
causing instability. Changed to (4, 0) matching Isaac Lab exactly.

Also explains why "decreasing substeps makes it calmer" — fewer substeps
meant fewer total solver iterations per frame.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Diagnostic sequence:
1. Reset to init_pose
2. Read raw PhysX DOF positions, compare with init_dof_pos
3. Build observation, log it
4. Run ONNX with zero latent, log action
5. Apply action to drives
6. Step physics ONCE (1/120s, not 4 substeps)
7. Read resulting state: root pos/vel, DOF positions, DOF deltas

This isolates each pipeline stage to find where divergence occurs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
setMassAndUpdateInertia computes inertia from collision shapes which
don't exactly match the MJCF geometry. Now directly setting:
- mass from Newton reference
- diagonal inertia tensor (Ixx, Iyy, Izz) from Newton reference
- center of mass from Newton reference

This eliminates inertia mismatch as a possible cause of the 9x faster
fall rate observed in the Newton trace replay test.

Also added Newton trace replay button and newton_trace.json for
direct step-by-step physics comparison.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Set armature per joint (0.01-0.02) matching MJCF values
- Set angularDamping, maxDepenetrationVelocity, maxAngularVelocity
- Enable TGS solver matching Isaac Lab GPU PhysX
- Set bounceThresholdVelocity=0.2 matching Isaac Lab
- Add per-DOF action bounds from training env (joint limits * 1.2/1.4)
- Clip actions in applyActions to prevent extreme drive targets
- Set exact initial state from Isaac Lab trace in replay diagnostic
- Add detailed per-DOF comparison logging in replay
- Add Isaac Lab trace for comparison (10 steps with zero latent)
- Add ovphysx CPU PhysX replay script for debugging

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…imit bounds

The ONNX model outputs unbounded actions that the training env clips to
joint-limit-based bounds. Without clipping, extreme actions (±11 rad)
caused violent joint oscillations and character explosions.

Key fixes:
- Add cache buster to humanoid_data.json fetch (was serving stale cached version)
- Compute per-DOF action bounds: 1.2x joint limits for spherical, 1.4x for revolute
- Clip in applyActions to these bounds (matching Python env._apply_action)
- Remove old a_mean±3*a_std clamp (too loose, allowed 3x beyond training bounds)
- Increase substeps to 8 (240Hz physics) and solver iterations to 32 for CPU stability
- Log raw vs clipped action ranges for debugging

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add exponential moving average smoothing (alpha=0.5) on policy actions
  to reduce oscillations from CPU/GPU PhysX dynamics mismatch
- Revert solver iterations to 4,1 (PhysX default, matching Isaac Lab)
- Revert substeps to 4 at 120Hz (matching training config)
- Reset prevAction on humanoid reset
- Character now stabilizes instead of exploding, but falls and stays down

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Explicitly set driveVelocity to 0 for all axes (was unset/default)
- Reset prevAction on humanoid reset for clean smoothing state
- Tune smoothing alpha to 0.8 (less aggressive than 0.5)
- Use 32 solver iterations for better CPU PhysX drive convergence

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The v1 ONNX export manually reconstructed the actor network from checkpoint
keys, which introduced subtle numerical differences (GPU vs CPU float32
arithmetic in normalization with very small std values). This caused the
ONNX model to produce actions differing by up to 3.0 from PyTorch, making
the character fall in Isaac Lab.

The v2 export wraps the actual agent._model._actor_layers and _mean_net
directly, guaranteeing bit-exact equivalence with PyTorch inference.

Verified: v2 ONNX model maintains balance in Isaac Lab (root_z 0.56-0.86)
while v1 model immediately fell (root_z → 0.30 by step 30).

Added test_onnx_in_isaaclab.py for verifying ONNX models in the actual
training environment.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The v2 ONNX export (from export_onnx_v2.py) uses the actual agent model
layers rather than manual reconstruction. This fixes the 3.0 action error
that caused the character to fall even in Isaac Lab.

With the correct ONNX model:
- Character stabilizes in a crouched pose (root_z ~0.42) in web PhysX
- Actions stay moderate (±1.7) instead of exploding (±15)
- Policy actively maintains equilibrium (constant obs/action each frame)
- Remaining gap to standing is CPU vs GPU PhysX dynamics difference

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Logs the actual up-vector direction from PhysX root quaternion
every 60 frames for diagnosing rotation observation issues.

Verified: rotation observation is correct - the character at 50° tilt
sees obs norm[2]=0.641 (correctly reflecting the tilt). The policy
settles in a fallen state because it was trained with early termination
on fall, so it never learned get-up behavior.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Isaac Lab CPU PhysX also runs the policy successfully (root_z 0.57-0.83),
confirming the ONNX model is correct. The CPU vs WASM PhysX replay shows
8cm divergence over 10 steps - both are CPU PhysX but likely different
SDK versions (Isaac Lab uses Omniverse PhysX, web uses physx-js-webidl).

The character survives 3 frames with correct height in web demo but
eventually falls, suggesting the remaining issue is in scene/articulation
configuration differences between Omniverse's MJCF importer and our
manual PhysX WASM setup.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Isaac Lab USD inspection revealed key parameters:
- Joint friction: 0 (PhysX default was 0.05 in our web demo)
- Articulation solver: 32 pos, 1 vel (already set)
- Rigid body solver: 16 pos, 1 vel (per-body, set by Omniverse)
- cfmScale: 0.025 (we don't set this)
- enableGyroscopicForces: true (we don't set this)
- No contact/rest offset overrides (using defaults)
- Scene: TGS solver, bounceThreshold=0.2, PCM collision system

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Scene-level:
- Friction type: patch
- frictionCorrelationDistance: 0.025
- frictionOffsetThreshold: 0.04

Per-body (rigid body):
- Per-body solver iterations: 16 pos, 1 vel
- Sleep threshold: 5e-5
- Stabilization threshold: 1e-5
- CFM scale: 0.025 (constraint force mixing for stability)
- Enable gyroscopic forces

Articulation-level:
- Sleep threshold: 5e-5
- Stabilization threshold: 1e-5

Per-joint:
- Joint friction: 0 (was 0.05 PhysX default)
- Max joint velocity: 1000000

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
zalo and others added 30 commits March 24, 2026 13:15
ASE latent space has no named skills — it's a continuous unsupervised
manifold on a 64-dim unit hypersphere. Instead of static random latents:

- Auto-cycle: sample new target every 30-150 steps (matching ASE training)
- SLERP interpolation: smooth blend between current and target latent
- Skills folder in GUI: toggle auto-cycle, adjust blend speed, manual trigger

Set default substeps to 2 for better stability.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Prints copy-pasteable JSON array to console each time the latent
cycles to a new target. Users can watch the character, find poses
they like, and save the latent from the console for later use.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The policy was trained at 30Hz (4 substeps × 1/120s = 1/30s per step).
Previously, the web ran one policy step per render frame regardless of
frame rate, causing the simulation to run at different speeds depending
on FPS and substep count.

Now: accumulate real time and step policy+physics at exactly 30Hz
(one step per 1/30s of real time). At most one step per render frame
to prevent spiral-of-death on slow machines. Rendering happens every
frame but physics only advances when enough real time has passed.

substeps=4 (fixed, matching training). Substep slider still available
in GUI for experimentation but 4 is the correct value.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Physics: substeps now run at 120Hz real-time (one per 1/120s of real
time accumulated), with policy stepping every 4th substep (30Hz).
At 60fps this gives ~2 physics steps per render frame for smooth
visuals. At 30fps all 4 substeps run in one frame. Physics never
runs faster than real-time thanks to the time accumulator.

Latent paste: text field in Skills folder accepts a JSON array
from the console log. Pasting a latent auto-disables cycling and
holds that skill. Workflow: watch character → copy LATENT from
console → paste to restore later.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Exports 4 high-level controllers from the ASE repo:
- heading (walk in direction): 258-dim obs → 64-dim latent
- location (walk to point): 255-dim obs → 64-dim latent
- reach (hand to target): 256-dim obs → 64-dim latent
- strike (sword attack): 268-dim obs → 64-dim latent

NOTE: These HLCs expect the original ASE 253-dim LLC obs format,
but MimicKit uses 158-dim obs. They need to be retrained or the
obs gap (95 dims) needs to be bridged before they work in the web
demo. For now they serve as reference for the architecture.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Skills folder now has a Mode dropdown:
- Free: current behavior (random/manual latent control)
- Heading, Location, Strike: stubbed out (Coming Soon)

The HLC modes require retraining the high-level controllers for
MimicKit's 158-dim obs format (the pretrained ASE HLCs expect
253-dim obs from the original ASE codebase).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
tools/encode_motions_to_latents.py runs each motion clip through the
ASE encoder to get the corresponding latent vector, labeled by clip
name (Atk_SlashDown, Shield_BlockLeft, WalkForward, Idle_Battle, etc.)

web/latent_presets.json contains 79 named presets in categories:
  Attacks (26), Counters (5), Blocks (6), Parries (9),
  Locomotion (10), Idles (3), Taunts (3), Dodges (3), Turns (4)

Skills dropdown in lil-gui loads presets by name. Selecting a preset
sets the latent directly and disables auto-cycling. "Random" resamples
a random unit vector.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Instead of averaging latents across the clip (which blurs distinct
phases), take the element-wise signed maximum: for each of the 64
latent dimensions, pick the value with the largest absolute magnitude
across all frames. This captures the peak/most characteristic
activation in each dimension, producing a latent that emphasizes
the clip's most extreme features. Normalized to unit sphere after.

Dense sliding window: one encoder sample per 1/30s across the full
clip duration (e.g., 297 frames for a 10s clip).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Skip L2 normalization after element-wise peak encoding to see if
raw magnitudes produce stronger/more distinct skill behaviors.
Norms are ~1.9-2.4 instead of 1.0.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…e vectors

Each preset is now a timed sequence of latent keyframes (~10fps)
sampled across the entire motion clip via dense sliding window.
The web demo plays them back in a loop, interpolating smoothly
between keyframes. Each keyframe is unit-normalized (from encoder).

Example: Atk_2xCombo01 = 32 keyframes over 3.2s, capturing the
full windup → slash → recovery trajectory through latent space.

79 presets, ~1.9MB total. Added tqdm progress bar to encoder script.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Move activeSequence declaration to module scope (was local to init,
  causing ReferenceError in mainLoop)
- Set latent keyframes directly without interpolation — play at
  native animation speed (accumulator advances by DT per substep)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The sequence accumulator was adding DT (1/120s) but only ran once
per policy step (every 4 substeps). Now adds numSubsteps*DT = 1/30s
per policy step, so keyframes advance at real-time speed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
physx-js-webidl may not expose getShape(index) on articulation links.
Gracefully skip filter data update if the API is unavailable.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Ports the ASE hierarchical RL architecture to MimicKit:

learning/hrl_agent.py (165 lines):
  - HRLAgent extends PPOAgent
  - Overrides action space to output 64-dim latent vectors
  - Overrides _step_env to run frozen LLC for llc_steps per HLC step
  - Combines task reward (from env) + discriminator reward (from LLC)
  - Loads pre-trained ASE model as frozen LLC

Training:
  python mimickit/run.py \
    --arg_file args/hrl_steering_humanoid_sword_shield_args.txt \
    --mode train

The HLC learns to select latent skills for task goals (steering,
location, strike) while the frozen LLC executes them as motor actions.
The trained HLC can be exported to ONNX for the web demo.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Patch env action space to latent_dim (64) for model and normalizer build
- Wrap env with LLCEnvWrapper to hide task obs during LLC construction
- Use _curr_obs from rollout loop for first LLC step
- Add missing action_reg_weight to config
- Handle gym/gymnasium import

Training running: 4096 envs, RTX 4090, ~6GB VRAM, 131K samples/iter.
Estimated ~3 hours for 50M samples.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Added timing instrumentation to _step_env (prints avg ms per call)
- Added tqdm progress bar to _rollout_train for per-step visibility
- Set normalizer_samples=0, test_episodes=0 to skip warmup/eval
- Set llc_steps=5 (matching original ASE)

Known issue: training is very slow (~4 min per iteration with 64 envs)
due to serial LLC env.step() calls through Isaac Lab. Each HLC step
does 5 serial env.step() calls. Needs optimization:
- Use more envs (4096) to amortize per-call overhead
- Or batch LLC steps into a single vectorized call
- Or use Isaac Gym backend which has lower per-call overhead

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
MimicKit's serial Python env.step() loop makes HRL training
impractical (~5 hours per iteration with 4096 envs × 5 LLC steps).

Removed: hrl_agent.py, HRL agent config, args file.
Reverted: agent_builder.py, base_agent.py to main.

Next approach: use original ASE LLC+HLC models directly by
reconstructing the 253-dim ASE obs format in JavaScript
(all body pos/rot/vel in heading frame), avoiding retraining.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
git add -A swept up untracked files that shouldn't be in the PR.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The ASE HLC models use a different obs format (253-dim) and PD gains
(kp=5) than MimicKit's LLC (158-dim, kp=300+). Removed:
- tools/export_hlc_onnx.py
- tools/encode_motions_to_latents.py

Latent presets (from the encoder) remain in web/latent_presets.json.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Bake 17 hand-derived skill presets directly into index.html and remove
the external JSON file and trajectory playback infrastructure.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Reduces web demo to a single HTML file + ONNX model. Removes the
separate parseMJCF.js and drops "v2" from the ONNX export tool name.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…s, trajectory comparison

- Force PGS solver instead of TGS to match Isaac Lab PhysX 5.4 default
- Set articulation solver iterations to 4/1 matching Isaac Lab scene caps (was 16/4)
- Remove redundant per-link solver iteration overrides
- Use PhysX link world positions for key body obs instead of FK (matches training)
- Add trajectory playback/comparison system with ghost visualization and error metrics
- Add tools/record_trajectory.py for recording Isaac Lab reference trajectories

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use getters for links, humanoidData, and articulation so external
tooling (Playwright, devtools) always sees the current module-scoped
variables, even after resetHumanoid() reassigns them.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PhysX has no analytic cylinder primitive. The previous capsule
approximation was terrible for thin discs like the shield
(radius=0.3, halfHeight=0.015 → effectively a sphere r=0.3).

Now uses PxConvexMeshGeometry with a 16-sided polygon disc,
giving proper flat collision geometry. Falls back to capsule
if convex mesh cooking fails.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Strip trajectory comparison GUI, ghost visualization, recording hooks,
buildObservationFromState, window._mk export, and tools/record_trajectory.py.
These remain on the physx-parity-audit branch for future use.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PhysX WASM accepts 0 velocity iterations — no minimum clamping needed.
Isaac Lab sets maxVelocityIterationCount=0 at the scene level, so
the web demo should use 0 as well. Previously set to 1 under the
incorrect assumption that PhysX WASM enforced a minimum of 1.

Also removes unused driveScale variable from joint configuration.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Reset button at top level (no folder wrapper)
- Remove Auto-Cycle and Blend Speed controls
- Latent field auto-populates on preset select and random
- Remove Stats folder (internal only)
- Add Lock Y Axis checkbox: uses a PhysX D6 joint between a static
  anchor and the root link, locking Y translation while freeing all
  other DOFs. Proper physics constraint, no teleportation hacks.
- Post-reset hooks re-apply simulation settings (self-collision,
  gravity, drives, Y constraint) and preserve current latent/preset

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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.

1 participant