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) |
|
Pure Python, runs everywhere including the browser. See the built-in 3D solver. |
BuiltinPhysics2D |
|
Pure Python, with a real moment of inertia, rotation-aware collision and polygon clipping. It refuses no shape pair. |
JoltPhysics |
|
Native Jolt (cffi on desktop, JoltPhysics.js WASM on the web). 3D only. |
PymunkPhysics2D |
|
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
DYNAMICbody falls, a filter that fails one way rejects the pair, a change wakes what it was holding up, a pair reports oneENTERand oneEXIT. 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
Capabilityyou askworld.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:
PhysicsRoot(backend=...)on the subtree.The project
physics_backendsetting, in.simvx/config.json."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
PhysicsWorldAPI, 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 |
|---|---|---|---|---|---|
|
yes |
yes |
no |
no |
read |
|
yes |
no |
yes |
yes |
cap the per-step motion, or sweep it yourself |
|
yes |
no |
yes |
yes |
nothing sleeps; every body behaves as an awake one |
|
yes |
yes |
yes* |
yes* |
make the body |
|
no |
no |
no |
no |
replicate state rather than input |
|
no |
no |
no |
no |
wheels as raycasts plus forces |
|
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 ( |
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 |
|
they describe a surface many bodies share |
Body |
|
they describe one object’s dynamics |
World |
|
they describe the space everything is in |
Knob |
Default |
Built-in |
pymunk |
Jolt |
|---|---|---|---|---|
|
|
yes |
yes (see below) |
yes, damped after acceleration |
|
|
yes |
yes (see below) |
yes |
|
|
velocity iterations |
|
velocity steps |
|
|
position passes over contacts and joints |
stored, not run |
stored, not run |
|
|
yes |
stored, not run |
yes |
|
|
yes |
stored, not run |
yes |
|
|
penetration deadband |
|
|
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_iterationssteadies tall stacks held awake and costs time only while bodies are awake. Jolt and Chipmunk are largely indifferent to it.position_iterationstightens 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_slopis 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 (Jolt0.02, Chipmunk0.1); setworld.contact_slopto 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).