Lucky Robots Blog Open Roles

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.

Module: Hazel/src/Hazel/MuJoCo/ MujocoSceneAsset.h MujocoSceneInstance.h MujocoVFS.h Z-up · (w,x,y,z)
MuJoCo space Z-up   quat (w, x, y, z) +X +Y +Z m_AxisMjToHz (rotate −90° X) m_AxisHzToMj (rotate +90° X) Hazel space Y-up   quat (x, y, z, w) +X +Y +Z
Two coordinate frames, two quaternion layouts. Use 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.

editor + runtime thread-safe

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.

runtime only rebuilt on demand

Coordinate systems — critical

Read this before touching any transform

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.

FrameUp axisQuaternion layoutHow 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.

SimulateWithSubsteps: phases repeat per substep Acquisition scene → MuJoCo sync proxy xforms in Control actuator commands recalc from joint states Physics mj_step() + gravity compensation Export MuJoCo → scene sync body xforms out step start step end
Per-step phase order. Substepping repeats the same four phases inside the step, so control and proxy sync stay aligned with each mj_step().
PhaseWhat MuJoCo does
AcquisitionReads scene-side proxy transforms and writes them into MuJoCo state.
ControlApplies actuator controls; recalculates from pending joint states.
Physicsmj_step() with gravity compensation.
ExportSyncs updated MuJoCo body transforms back out to entities.

Key types

TypeHeaderRole
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

RebuildFromScene is expensive

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.

Never cache mjModel* / mjData* externally

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-body transforms go through qpos[0:7]

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.

Contacts and forces are step-local

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().

C# P/Invoke: no string return type

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.
Keep the asset / instance split clean

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.