Physics Backends

Every PhysicsRoot builds an isolated world, and a backend implements it. This page is the reference: which backends exist, how one is chosen, what differs between them, and every physics knob. The task-oriented guide is Physics.

Backend

Package

Notes

BuiltinPhysics (3D)

simvx-core, always present

Pure Python, runs everywhere including the browser. See the built-in 3D solver.

BuiltinPhysics2D

simvx-core, always present

Pure Python, with a real moment of inertia, rotation-aware collision and polygon clipping. It refuses no shape pair.

JoltPhysics

simvx-physics-jolt, opt-in

Native Jolt (cffi on desktop, JoltPhysics.js WASM on the web). 3D only.

PymunkPhysics2D

pymunk, used when installed

Native Chipmunk2D. 2D only.

Installing pymunk makes it the 2D default. Jolt is opt-in: installing it makes it available by name but does not change the 3D default, because a solver swap changes how a game plays.

What the backends promise

  • The same contract everywhere. The same call has the same observable effect: a DYNAMIC body falls, a filter that fails one way rejects the pair, a change wakes what it was holding up, a pair reports one ENTER and one EXIT. Game code never branches on the backend to make a call.

  • Numbers only within a tolerance. Two solvers do not agree on exactly where a crate rests or on which step it settles.

  • Anything that genuinely differs is a Capability you ask world.capabilities() about, and each has a documented way to degrade around it.

A gameplay decision is never derived from a quantity whose precision depends on the backend: a character’s step-up, for example, is decided by the walkable-slope test of the surface it lands on.

Selecting a backend

Precedence, highest first:

  1. PhysicsRoot(backend=...) on the subtree.

  2. The project physics_backend setting, in .simvx/config.json.

  3. "auto", the default.

"auto" picks the best installed backend for each dimension: an installed native backend that opts into auto-discovery (pymunk does, Jolt does not), else the built-in solver.

A named backend fails loudly. Naming one that is not installed, or that cannot serve the scene’s dimension, raises PhysicsBackendUnavailable rather than silently simulating with another solver. So a mixed 2D and 3D project on pymunk uses "auto", not "pymunk". Name "builtin" to pin the reference solver where a native one is installed.

from simvx.core import PhysicsRoot

root = self.add_child(PhysicsRoot(backend="jolt"))   # only this branch simulates on Jolt
root.add_child(my_dynamic_body)

Several roots with different backends can run at once: examples/features/physics/backend_compare.py runs the built-in solver and Jolt side by side.

Project-wide, in .simvx/config.json at the project root:

{ "general": { "physics_backend": "jolt" } }

A scene then raises at startup on a machine without Jolt; use "auto" if the game should run wherever it lands.

What the built-in 3D solver does well

It is always present, runs wherever the engine runs, and is the reference the cross-backend tests are written against. It suits spheres and capsules on flat ground, low-density scenes, prototypes, and anything that does not depend on the exact resting pose of a stack.

Shape pairs and features

Exact

every sphere and capsule pair; sphere, capsule and box against a static triangle mesh; cylinder-sphere and cylinder rays; convex-hull overlap (GJK)

Approximate

cylinder-box (the cylinder’s bounding box); cylinder-cylinder and capsule-cylinder (cylinder treated as a capsule); convex-hull penetration depth and rays; contact response (one impulse at the centre of mass, no inertia tensor); swept queries substep, so a reported distance is a lower bound

Rotation ignored

box-vs-box, hull point clouds, a mesh body’s own orientation

Refused

convex hull against a triangle mesh

The refused pair raises from create_body, set_body_shape, set_body_filter, shapecast and overlap, naming the fix: select Jolt, or give the two bodies layers that never meet. Only a pair the layers say could meet is refused.

It does not model rolling

With no inertia tensor, built-in contact friction is a linear impulse at the centre of mass, so a contact can neither start nor slow a spin: a pushed ball slides and is braked rather than rolling, and an off-centre push(at=...) spins far less than on a full solver. A body with nothing touching it coasts the same distance on every backend. Once friction is involved, the gap depends on the shape and the friction. A 10 m/s push, twenty seconds, built-in against Jolt:

Body

friction 0.02

friction 0.05

friction 0.1

Sliding box

0.04%

0.13%

0.29%

Rolling sphere

6.6%

36%

59%

If rolling props are central to a game, use Jolt for those scenes.

What Jolt adds

  • A production rigid-body solver: warm-started contacts, stable stacking.

  • Oriented collision: a rotating box collider really sweeps, so a spinning paddle herds objects.

  • A real inertia tensor, so an off-centre impulse spins what it hits.

  • Convex hull against a triangle mesh.

  • Exact swept queries.

  • Throughput for high body counts.

Character behaviour is the same on every backend: a CharacterBody3D is a KINEMATIC body moved by one shared collide-and-slide policy over each backend’s sweep.

Jolt keeps a kinematic body active only while its pose is being written (a character writes it every step). A kinematic body created and never moved again deactivates after the sleep timer and stops being reported to areas; give it can_sleep=False if it must stay detectable.

Installing Jolt

pip install simvx includes simvx-physics-jolt on Linux x86_64 and aarch64, Windows x86_64, and macOS x86_64 and arm64, from a prebuilt wheel; only Linux x86_64 is tested so far. On any other platform the engine installs without it and naming "jolt" raises. x86_64 builds need SSE4.2, not AVX2. Building from source is covered in Building from source.

Web export

simvx export web bundles the JoltPhysics.js (WASM) runtime when the game selects Jolt (it scans the sources for backend="jolt" or a physics_backend setting). The web default stays BuiltinPhysics.

simvx export web my_game.py -o my_game.html
  • Jolt and Pyodide have separate WASM heaps and transforms cross between them once per frame, so practical body counts are lower than on desktop: thousands rather than tens of thousands.

  • The web backend mirrors the PhysicsWorld API, and reports its capabilities through the same gate as desktop.

examples/features/physics/jolt_web.py is a minimal web Jolt scene.

Body scale

Every backend honours a per-body scale and agrees on which scales each collider can express (see Scale). A collider that cannot carry a non-uniform scale raises the same error on every backend.

Capability differences

world.capabilities() returns a frozenset[Capability]:

Capability

Built-in 3D / 2D

pymunk

Jolt desktop

Jolt web

Where absent

CONTACT_IMPULSE

yes

yes

no

no

read contact.impulse_estimate, which every backend publishes

CONTINUOUS

yes

no

yes

yes

cap the per-step motion, or sweep it yourself

SLEEP

yes

no

yes

yes

nothing sleeps; every body behaves as an awake one

SENSOR_DETECTS_STATIC

yes

yes

yes*

yes*

make the body KINEMATIC, or poll world.overlap()

DETERMINISTIC

no

no

no

no

replicate state rather than input

VEHICLES

no

no

no

no

wheels as raycasts plus forces

SOFT_BODY

no

no

no

no

a lattice of bodies and springs

* Except a sensor whose collider is a mesh: see below.

Check at setup, not mid-collision:

from simvx.core.physics.capability import Capability

if Capability.CONTACT_IMPULSE in root.world.capabilities():
    ...  # key impact damage off contact.impulse
else:
    ...  # use contact.impulse_estimate

CONTACT_IMPULSE

Neither Jolt build exposes the impulse its solver applied, so there contact.impulse is None, which is never a stand-in for a measured 0.0. Every contact on every backend also carries impulse_estimate: the impulse that would arrest the approach, from the pair’s masses and the difference of the two bodies’ linear velocities. It is the same formula on every backend, so hit sounds and damage keyed off it behave the same everywhere. Where a backend measures impulse, the two agree within a few percent for a simple landing.

The estimate ignores restitution (a bouncy pair exchanges up to (1 + e) times it), is 0.0 for a pair that is not approaching, and ignores spin. It does not use contact.velocity, which is taken at the contact point, because where in a contact patch that point lies is each narrow phase’s own choice, and a spinning crate’s point velocity would vary several-fold between backends.

sound.volume = min(1.0, contact.impulse_estimate / 10.0)   # portable
if contact.impulse is not None:
    damage = contact.impulse * 0.5                          # exact where measured

SENSOR_DETECTS_STATIC

An area’s sensor is a static body, and both native libraries only search pairs from the moving side, so the native adapters hold sensors kinematic internally (the caller’s mode is unchanged and a sensor never starts moving). Sleeping and static bodies are then reported on every backend.

It is not free. A sensor runs narrow phase against the static geometry it overlaps, scaling with sensors times static bodies covered; a world with no sensors pays nothing, and a sensor over one floor costs nothing measurable. A scene with hundreds of trigger volumes over hundreds of static props should budget for it, or turn a hot trigger into a world.overlap() poll.

On Jolt, a sensor whose collider is a mesh cannot be held kinematic (mesh colliders are static only), so it misses static and sleeping bodies. Give a trigger convex geometry, which is what trigger volumes usually are.

A velocity written to a STATIC body is kept and read back on every backend, and becomes live if the body turns movable. On the 2D backends and the built-in 3D solver it also acts as a surface velocity (a conveyor belt); on Jolt it does nothing.

SLEEP

Covers is_sleeping, wake() / sleep(), can_sleep and the wake=False opt-out (see Sleeping). pymunk lacks it, because Chipmunk2D’s sleeping is unsafe to drive from the engine. Without it is_sleeping is always False, sleep() does nothing, and the world’s two sleep thresholds are stored but change nothing.

Where it is present, piles sleep and wake as a unit and a sleeping body is frozen. The built-in solvers wake exactly the touching pile; Jolt wakes everything in the volume the changed body gives up, which can also catch a neighbour standing beside the pile.

CONTINUOUS

pymunk accepts the continuous flag and integrates discretely anyway (Chipmunk2D has no swept collision), reported once per body, so a fast small body can tunnel through a thin collider there.

Joints and constraints

Every backend captures a joint’s anchor into each body’s own frame, so a pin on a spinning platform orbits with it everywhere. What differs is how firm the constraint is:

Built-in 3D / 2D

pymunk

Jolt

Convergence

impulse iterations plus a soft positional correction (position_iterations)

Chipmunk’s iterated solver, no position pass

warm-started, production

Angular response

3D uses mass in place of the inertia tensor; 2D has a real moment

real

real inertia tensor

  • A loaded chain sags slightly on the built-in solvers (about 1% on the worst link of a hanging six-link chain at the default position_iterations; raise it to tighten). pymunk hangs it at rest length but stretches more in a swing. A rope that must not stretch wants a native backend.

  • GrooveJoint2D’s endpoint is a hard stop on pymunk and a soft one on the built-in solver, which lets a slider pushed hard along the rail overrun the end. A groove keeps the anchor on the rail on both, but leaves the slider’s rotation free, and the two solvers turn it differently.

  • No motors, limits or breakable joints on any backend: drive a mechanism with add_torque.

Knob reference

Each knob has one home:

Home

Knobs

Because

Material, per contact

friction, restitution, friction_combine, restitution_combine

they describe a surface many bodies share

Body

mass, linear_damping, angular_damping, gravity_scale, can_sleep, continuous

they describe one object’s dynamics

World

gravity, solver_iterations, position_iterations, sleep_time_threshold, sleep_velocity_threshold, contact_slop

they describe the space everything is in

Knob

Default

Built-in

pymunk

Jolt

linear_damping, angular_damping

0.05

yes

yes (see below)

yes, damped after acceleration

gravity_scale

1.0

yes

yes (see below)

yes

solver_iterations

16

velocity iterations

space.iterations

velocity steps

position_iterations

3

position passes over contacts and joints

stored, not run

stored, not run

sleep_time_threshold

0.5 s

yes

stored, not run

yes

sleep_velocity_threshold

0.05

yes

stored, not run

yes

contact_slop

0.001

penetration deadband

collision_slop

penetrationSlop

  • Damping on pymunk. Chipmunk has no per-body damping or gravity factor. Bodies at the defaults use the space-wide setting for free; a body that deviates gets a per-body Python integrator, a few microseconds per body per step. Give a handful of bodies their own damping freely; a thousand of them is a cost to measure.

  • Damping on Jolt is applied after the step’s acceleration rather than before, so a heavily damped body under sustained acceleration falls slightly slower there (a relative damping * dt; under 0.1% at the default). With nothing accelerating the body the result is identical.

  • solver_iterations steadies tall stacks held awake and costs time only while bodies are awake. Jolt and Chipmunk are largely indifferent to it.

  • position_iterations tightens joint chains and an awake stack’s rest error on the built-in solvers. A scene with no joints pays nothing extra for raising it.

  • contact_slop is the overlap the solver leaves uncorrected. A settled body rests exactly that deep in what it stands on, at any world scale, so lower is better on a backend that sleeps and the default suits pixel-scale games as well as metre-scale ones. It replaces both native libraries’ own defaults (Jolt 0.02, Chipmunk 0.1); set world.contact_slop to one of those to get a library’s own behaviour back. Very large values saturate on the native backends.

examples/features/physics/body_knobs.py shows every knob live.

See also

  • Physics: the physics guide.

  • examples/demos/waterfall.py: a Jolt showcase (glass mill, spinning paddle, flow control).