3D physics engine
StableTested and in active use. Breaking changes will be called out. Newton is the only solver that is a MuJoCo port; PGS and CG are our own algorithms. Flex, SDF geoms and several actuator types are not modelled.
noeira.physics3d simulates articulated rigid bodies in generalized
coordinates — joint space rather than world space — following MuJoCo’s design
routine by routine. Models are MJCF XML, the format MuJoCo reads, and the
reference it is checked against is the MuJoCo 3.12.0 runtime pixi installs.
Working in joint space means constraints like “this hinge has one degree of freedom” are structural rather than enforced numerically. A hinge joint is one number.
The pipeline
Section titled “The pipeline”State lives in Data (qpos, qvel, xpos, xquat, sensordata, …); the
model lives in Model. Both are per-field tensors in physics3d/fields/, so
each kernel binds exactly the arrays it reads. The stage order is MuJoCo’s
mj_step: kinematics and position-stage sensors, velocities and velocity-stage
sensors, CRBA and factorization, RNE, passive and actuator forces, collision,
the constraint solve, acceleration-stage sensors, integration.
Package layout
Section titled “Package layout”| Package | Contents |
|---|---|
kinematics/ | forward kinematics, quaternion math, site and camera frames, mocap bodies |
dynamics/ | CRBA, RNE, LDL and LU, tendons and wrapping, actuation, gravity compensation, fluid forces, OSC and site IK |
collision/ | broadphase, analytic pairs, GJK/EPA, multi-contact, hulls, heightfields — see Collision |
constraints/ | contact, limit, friction, equality and tendon rows — see Constraints and tendons |
solver/ | Newton (per-env and blocked), CG, island PGS, elliptic cone, noslip, warm start |
integrator/ | Euler, RK4, implicit / implicitfast |
sensors/ | the <sensor> table and its per-type kernels |
ray/ | ray casting against every geom type, mesh BVH |
parser/ | MJCF reader, <attach> expander, compile-time and runtime model builders |
studio/ | the model inspector’s editing, validation and stepping logic |
Dynamics
Section titled “Dynamics”| Stage | Implementation |
|---|---|
| Mass matrix | CRBA, dense or tree-walk (CRBA_TREEWALK), then LDL |
| Bias forces | RNE — Coriolis, centrifugal, gravity; rne_post fills cacc / cfrc_int / cfrc_ext for the force sensors |
| Passive forces | joint springs and dampers, tendon springs, gravity compensation (<body gravcomp>), fluid forces (inertia-box model, <option wind>) |
| Tendons | fixed tendons, spatial tendons through sites, sphere and cylinder wrapping with side sites (dynamics/tendon.mojo, dynamics/wrap.mojo) |
| Actuation | motor, position (kp, kv, dampratio), velocity, general affine gain/bias, adhesion, the mujoco.pid plugin; ctrlrange / forcerange / actuatorfrcrange; filter / filterexact / integrator activation dynamics |
| Transmissions | joint, fixed tendon, spatial tendon, site (6-vector gear) |
| Operational-space control | robosuite 1.4’s OSC_POSE, the controller LIBERO’s demonstrations were recorded with, one arithmetic for host and device (dynamics/osc_pose_gpu.mojo) |
| Site IK | dm_control’s damped least-squares qpos_from_site_pose (dynamics/ik_site.mojo), used by manipulation resets |
| Velocity derivatives | qderiv — actuator and RNE derivatives for the implicit integrators |
Spatial tendon wrapping and the site / spatial-tendon transmissions are CPU-only. The compile-time model builder refuses an actuator the GPU actuation kernel cannot drive, so a batched model cannot silently lose one.
Integrators
Section titled “Integrators”| Integrator | Notes |
|---|---|
euler | semi-implicit with implicit joint damping, honours <flag eulerdamp>; HalfCheetah, Pusher, Sawyer and the dm_control suite use it |
rk4 | four stages, constraint re-solve per stage; the environment trait’s default, so Hopper, Walker2d, Ant, Swimmer and Humanoid use it |
implicit / implicitfast | M − h·qDeriv, LU-factored; used by the studio for models that ask for it |
The choice is a compile-time constant on the environment config, along with any workspace it requires:
comptime INTEGRATOR: StaticString = "rk4"comptime INTEGRATOR_WS_EXTRA: Int = rk4_extra_workspace_size[NQ, NV]()Constraint solvers
Section titled “Constraint solvers”| Solver | Status |
|---|---|
newton | A port of MuJoCo’s Newton: warm start, the 3-phase line search, incremental Hessian updates, elliptic-cone Hessian, <option iterations tolerance ls_*>. The environment default. |
| Newton, blocked | One environment per GPU block with the matrices in shared memory. Pyramidal and elliptic cones, bit-identical to the per-env kernel on the gated fixtures. NVIDIA only. |
cg | Polak-Ribière nonlinear CG with a fixed 100-iteration cap; not MuJoCo’s algorithm |
pgs, island | acceleration-level projected Gauss-Seidel, and a variant that freezes converged contact islands; not MuJoCo’s algorithms |
| noslip | mj_solNoSlip for both cones, run after Newton when the model sets noslip_iterations |
Contacts use a pyramidal or elliptic friction cone at condim 1, 3, 4
or 6. The cone comes from the model’s <option cone>; the solver is a
compile-time parameter of the integrator (SOLVER), and the environment
facades build Newton. Constraints and tendons covers
which row types each solver handles.
Sensors
Section titled “Sensors”<sensor> is parsed into a sensor table, Data carries sensordata, and
sensors/eval.mojo runs MuJoCo’s three stage passes — position, velocity,
acceleration — at the points mj_forward runs them, with cutoff applied.
Served kinds:
jointpos · jointvel · jointactuatorfrc · actuatorpos · tendonposframepos · framequat · framexaxis · frameyaxis · framezaxisframelinvel · frameangvel · subtreecom · subtreelinvel · subtreeangmomaccelerometer · velocimeter · gyro · force · torque · touchrangefinder · magnetometerA declared kind that is not served still gets its address, so every later
sensor lands at MuJoCo’s offset; its slot reads NaN and looking it up by name
raises. The acceleration-stage sensors that read cacc / cfrc_int need
RNE_POST=True, which only the Euler integrator wires. tendonpos is CPU-only
in a batch.
MJCF loading
Section titled “MJCF loading”Two routes into the same parser:
# Compile time — the model's dimensions become part of its type.from noeira.physics3d.parser import ModelDefFromXML
# Run time — one binary, any file.from noeira.physics3d.parser import ( parse_model_runtime, dims_from_flat, build_model_runtime,)var fmd = parse_model_runtime("path/to/scene.xml")var dims = dims_from_flat(fmd, max_contacts=20)Every shipped environment uses the compile-time route; the runtime route is
what the studio uses to open any file, and it is CPU-only. The parser
covers default-class chains, <include>, <attach> with name prefixing and
unit conversion, <frame pos quat>, the three inertia modes, settotalmass,
meshes (STL and OBJ), heightfields, keyframes and <pair> / <exclude>.
Gymnasium’s MuJoCo files, the dm_control suite and the Menagerie scenes the
validation sweeps use are read as published, not edited copies.
A feature MuJoCo has and this engine does not is counted and printed at load, or refused, rather than dropped. The MuJoCo 3.12 audit tallied those prints over 83 Menagerie scenes and every dm_control asset and got zero — see Validation.
GPU execution
Section titled “GPU execution”The pipeline runs batched on GPU. Most stages run one thread per environment,
with cooperative (PARALLEL_GPU) variants of FK, CRBA, the LDL factor and RNE
that are bit-exact against the serial kernels.
Two stages have block-per-environment kernels on NVIDIA: the blocked Newton
solver, and a block collision kernel that runs a warp’s candidate pairs
on the same routine. Metal keeps the per-environment kernels — the blocked
solver’s threadgroup memory exceeds Metal’s 32 KB limit on most models — so an
Apple run does not exercise the blocked paths, and pixi run -e nvidia test-physics3d-gpu is the gate for them.
Two measured numbers, each only meaningful with its setup:
| Measurement | Result |
|---|---|
Block collision kernel vs the serial kernel, 1024 sprawled Unitree G1 lanes, RTX 5090 (benchmarks/physics3d_gpu/bench_g1_collision.mojo) | 107.7 → 10.4 ms per launch, bit-identical output |
| Single-env CPU step time as a ratio of MuJoCo 3.10.0’s, Apple M1 Pro, interleaved runs (2026-09-06) | humanoid 0.66×, hopper 0.67×, walker2d 0.73×, ant 0.78×, dog 0.82×, humanoid_CMU 0.91×, Sawyer 1.06×, a 3-brick reassembly scene 1.14× |
A ratio below 1 is faster than MuJoCo. The CPU ratios predate the move to the 3.12 runtime and come from the compile-time path; the runtime-loaded path is slower on the same XML.
- Collision — pairs, meshes, heightfields, broadphase
- Constraints and tendons — equality, limits, friction, solver choice
- Validation — how this is checked against MuJoCo
- 2D physics — the batched impulse engine