2.6 · Physics (MuJoCo)
Robotics-grade physics for articulated robots, contact-rich manipulation, and actuator-driven control. MuJoCo lives alongside Jolt and Box2D — you opt into it per scene, not globally.
m_AxisMjToHz /
m_AxisHzToMj and the Hz/Mj-suffixed methods. Never swizzle by hand.
Overview
MuJoCo is the engine’s robotics workhorse: articulated bodies, joint limits, tendons, actuators, and detailed contact dynamics. It runs as a per-scene subsystem, layered behind two main types — a thread-safe asset that holds the compiled model, and an instance that owns the live simulation state.
MujocoSceneAsset
The on-disk model. Loads MJCF / URDF through MujocoVFS, which packs the model file
plus referenced assets (meshes, textures, includes) into a single TOC + buffer. Thread-safe mesh
caching via GetOrCreateCachedMeshHandle() (mutex + condvar). Retains a
read-only compiled mjModel for editor-time analysis — for example,
LimbIK chain validation in the motion-graph editor.
MujocoSceneInstance
The live simulation. Owns the per-scene mjModel* and mjData*. Runtime-only.
Built per scene activation via RebuildFromScene, which walks the ECS for proxy
colliders and bakes them into a fresh MJCF before recompiling.
Coordinate systems — critical
MuJoCo and Hazel use different up axes and different quaternion layouts. Every transform that crosses the boundary must be converted with the engine’s helpers — never by hand-swizzling components.
| Frame | Up axis | Quaternion layout | How to convert |
|---|---|---|---|
| MuJoCo | Z-up |
(w, x, y, z) |
To Hazel: m_AxisMjToHz (rotate −90° about X) |
| Hazel | Y-up |
(x, y, z, w) |
To MuJoCo: m_AxisHzToMj (rotate +90° about X) |
Methods on MujocoSceneInstance are explicitly suffixed Hz or Mj
to advertise which space their inputs and outputs live in. Mixing them silently is the easiest way to
produce a robot that looks “lying on its side” in the viewport.
Phased stepping
MuJoCo doesn’t bolt onto Scene::OnUpdate directly — it registers four
TimeManager phase callbacks. Each step runs the phases in order, and the same ordering is
preserved at every substep boundary when SimulateWithSubsteps is in use.
mj_step().
| Phase | What MuJoCo does |
|---|---|
| Acquisition | Reads scene-side proxy transforms and writes them into MuJoCo state. |
| Control | Applies actuator controls; recalculates from pending joint states. |
| Physics | mj_step() with gravity compensation. |
| Export | Syncs updated MuJoCo body transforms back out to entities. |
Key types
| Type | Header | Role |
|---|---|---|
MujocoSceneAsset |
Hazel/src/Hazel/MuJoCo/MujocoSceneAsset.h |
On-disk model. Packs MJCF/URDF + assets via MujocoVFS. Thread-safe mesh cache. Read-only compiled mjModel for editor analysis. |
MujocoSceneInstance |
Hazel/src/Hazel/MuJoCo/MujocoSceneInstance.h |
Live simulation. Owns mjModel* / mjData*. Registers the four phase callbacks. Provides Hz/Mj query methods. |
MujocoVFS |
Hazel/src/Hazel/MuJoCo/MujocoVFS.h |
Virtual file system wrapping MJCF + referenced assets in a packed TOC + buffer. |
MujocoActuator |
Hazel/src/Hazel/MuJoCo/MujocoActuator.h |
Per-actuator metadata; populated by MujocoSceneAsset::BuildActuatorList. |
MujocoConvexDecomposer |
Hazel/src/Hazel/MuJoCo/MujocoConvexDecomposer.h |
CoACD-driven convex decomposition for collision geometry. |
Ownership & lifecycle
MujocoSceneAsset is created and owned by the asset system; many scenes can share one asset.
MujocoSceneInstance is created per scene activation and torn down when the scene is
deactivated. RebuildFromScene recompiles the MuJoCo model from the current ECS state —
it produces a brand new mjModel* and mjData*, so any pointers previously held
against the old pair become dangling immediately.
Collision geometry
Collision shapes come from a separate convex decomposition pass via MujocoConvexDecomposer
(backed by CoACD). Visual meshes are not used directly as collision shapes — the
visual mesh and the collision mesh are distinct, and only the decomposed convex hulls participate in
contact resolution.
Pitfalls
It recompiles the entire MuJoCo model. Call it after structural changes — adding proxy
colliders, swapping in a new asset, scene activation — not per frame.
Per-frame mutation belongs in qpos / ctrl, not in a rebuild.
The instance owns them. Any rebuild invalidates them. If you stash a raw pointer in another subsystem and the scene rebuilds, the next access is a use-after-free.
Free-floating bodies are positioned by writing qpos[0:3] (position) and
qpos[3:7] (quaternion, MuJoCo’s w,x,y,z). body.xpos is a
read-only output computed by MuJoCo and writing to it does nothing.
Contact and force readings are only valid immediately after a step. Don’t cache them
across frames; they’ll silently go stale on the next mj_step().
Returning string directly from a MuJoCo P/Invoke leaks or crashes. Use
nint for the return and convert with Marshal.PtrToStringUTF8() on the C# side.
Extending
Common extension recipes, all rooted in the same handful of files:
| You want to add… | Where to change |
|---|---|
| A new body / geom / site query | Add a method on MujocoSceneInstance that reads from m_Model /
m_Data using the MuJoCo C API. Suffix with Hz or Mj. |
| A new actuator type | Extend MujocoActuator and
MujocoSceneAsset::BuildActuatorList so the actuator is discovered at asset load time. |
| A new proxy collider component | Add the ECS component, then handle it in RebuildFromScene where proxies are
baked into the rebuilt MJCF. |
| A new gRPC query (body / site / geom) |
1. Add a MujocoSceneComponent_* ScriptGlue function in
Hazel/src/Hazel/Script/ScriptGlue/MujocoSceneComponent.cpp.
2. Run scripts/ScriptGen.bat.
3. Add the C# wrapper in Hazel-ScriptCore/.../Components.cs.
4. Implement the RPC in MujocoSceneServiceImpl.
|
If a piece of state can be reused across scenes (mesh handles, the compiled model used for editor
analysis, packed VFS bytes), it belongs on the asset. If it only exists while the simulation is
live (mjData, runtime caches, frame-local contact data), it belongs on the instance.