Skip to content

2D physics engine

StableTested and in active use. Breaking changes will be called out.

noeira.physics2d is a purpose-built impulse-based engine for LunarLander, BipedalWalker and CarRacing. It is not a general 2D physics library — it implements exactly what those three need, which is why it can be fully batched on GPU.

LunarLandercontact + terrain
BipedalWalkerrevolute joints + LIDAR
CarRacingtire slip model

The design decision that shapes everything: state is stored strided across the batch, as [BATCH, STATE_SIZE].

One slab, one launchOne row per environment, STATE_SIZE wide. Every offset into a row is a compile-time constant.BATCH ↓bodiescontactsjoints⋮env 0env 1env 2env N−1one step_gpu() — one launch advances every environmentA row is one environment's entire state: bodies, contacts and joints contiguous, not threearrays. PhysicsLayout fixes every offset at compile time — no runtime address arithmetic.
one environmentthe rest of the batch
noeira/physics2d/
├── layout.mojo PhysicsLayout — compile-time offsets for bodies, contacts, joints
├── state.mojo the state slab
├── config.mojo compile-time configuration
├── constants.mojo
└── env_helpers.mojo

PhysicsLayout computes every field offset at compile time, so a kernel indexes directly into the slab with no runtime arithmetic and no per-body object to chase. One kernel launch advances every environment. CarRacing goes furthest: CarPhysicsKernel fuses its whole physics step into a single step_gpu() call.

PackageContents
integrators/semi-implicit Euler
collision/flat terrain, edge terrain, circle against polygon
solvers/the impulse contact solver
joints/revolute joints with limits, motors and springs
car/slip-based tire friction, tile collision, the fused car kernel
lidar/distance sensing
traits/the Integrator, CollisionSystem and ConstraintSolver traits

Contacts are resolved by applying impulses that correct velocity directly, rather than by integrating penalty forces. For the shallow contact stacks these environments produce, that is both cheaper and more stable than a full constraint formulation — and it parallelises cleanly across the batch, since each environment’s contacts are independent.

BipedalWalker’s legs are a connected multi-body system, not free rigid bodies. RevoluteJointSolver in joints/ solves the revolute constraints along the chain — anchor coincidence, plus optional angle limits, motors and springs — which is what keeps the limbs attached under load.

The solver works on any set of revolute joints in the layout, so a new jointed 2D robot does not need new solver code.

CarRacing’s distinguishing feature. Rather than assuming a friction coefficient, the model computes lateral and longitudinal slip at each wheel and derives the friction force from it.

The consequence is that the car behaves like a car: it can lose traction under hard cornering, oversteer, and spin. Throttle and steering interact.

This makes CarRacing meaningfully harder than the Gymnasium original, and policies do not transfer between them.

BipedalWalker’s 10 rangefinder readings come from lidar/, casting rays against the terrain each step. They are what let the agent see upcoming ground rather than react to contact alone.

The environments are the intended interface, but the engine is usable on its own:

from noeira.physics2d import (
PhysicsLayout, PhysicsState, SemiImplicitEuler, ImpulseSolver,
RevoluteJointSolver,
)

Define a layout for your bodies, contacts and joints, then compose an integrator, a collision system and the solvers over the batched state.

Two engines exist because the requirements genuinely differ.

physics2dphysics3d
Coordinatesmaximal (world-space bodies)generalized (joint space)
Contactsimpulseconstraint solve (Newton/CG/PGS)
Batchingdesigned in from the startadded over the field layout
Model sourcecodeMJCF XML
Scopethree environmentsany articulated rigid body

physics2d trades generality for throughput. For LunarLander it is the right call — the dynamics are simple and the win is running 256 of them at once.