simvx.core.physics.world

PhysicsWorld: the interface between the engine and a physics backend.

This module defines PhysicsWorld, the abstract interface every physics backend (builtin, pymunk, Jolt) implements. It is a transport abstraction, not a semantics one: all backends run the same kind of rigid-body simulation, so the contract is about moving body state across the Python<->native boundary efficiently, not about defining solver behaviour.

The load-bearing part of the contract is the bulk-array transfer: per-frame body state is exchanged as a single numpy buffer (one transfer per world per frame), in a fixed body->row order, by filling a caller-preallocated array in place. Per-body crossings are reserved for setup/teardown and are never used on the hot path. See :meth:PhysicsWorld.register_bodies,

meth:

PhysicsWorld.read_transforms, and :meth:PhysicsWorld.read_velocities.

The interface is testable standalone by constructing a concrete backend and calling :meth:step manually; in normal use a PhysicsRoot node owns the world and drives it from the fixed-step loop.

Module Contents

Classes

BodyMode

Motion mode of a body, mirroring the modes every serious solver exposes.

RaycastHit

Result of a successful raycast against the world.

SweepHit

Result of a shape sweep stopping against a body.

ContactPhase

Edge phase of a body-pair contact, diffed by the broadphase each step.

ContactEvent

A node-agnostic body-pair collision event emitted by a physics world.

OverlapEvent

A node-agnostic sensor-overlap event emitted by a physics world.

PhysicsWorld

Abstract backend interface: one isolated simulation world.

Functions

normalise_gravity

Convert a gravity write to exactly components finite floats.

normalise_damping

Validate a damping coefficient (finite, >= 0) and return it as a float.

normalise_gravity_scale

Validate a per-body gravity multiplier (finite) and return it as a float.

is_unit_scale

True when scale is exactly (1, 1, 1) and so changes no geometry.

body_scale_unchanged

True when scale is byte-identical to the scale a body already carries.

normalise_body_scale

Validate a body scale against a shape kind and return it as float32 (3,).

Data

API

simvx.core.physics.world.BodyHandle

None

simvx.core.physics.world.ShapeHandle

None

simvx.core.physics.world.JointHandle

None

simvx.core.physics.world.DEFAULT_LINEAR_DAMPING

0.05

simvx.core.physics.world.DEFAULT_ANGULAR_DAMPING

0.05

simvx.core.physics.world.DEFAULT_GRAVITY_SCALE

1.0

simvx.core.physics.world.DEFAULT_SOLVER_ITERATIONS

8

simvx.core.physics.world.DEFAULT_POSITION_ITERATIONS

3

simvx.core.physics.world.DEFAULT_SLEEP_TIME

0.5

simvx.core.physics.world.DEFAULT_SLEEP_VELOCITY

0.05

simvx.core.physics.world.DEFAULT_CONTACT_SLOP

0.001

simvx.core.physics.world.normalise_gravity(value: Any, components: int) tuple[float, ...][source]

Convert a gravity write to exactly components finite floats.

The vector twin of :func:_knob_float, shared by every backend’s gravity setter so all of them refuse the same values in the same words. Boxing the write straight into a Vec2 / Vec3 does not: those constructors are written for game code and are deliberately forgiving, so a 3-sequence handed to a 2D world loses its last component, a bare number broadcasts across the axes, and a NaN sails through and poisons every body in the world on the next step. A knob is not the place for any of that.

Args: value: The caller’s write: any sequence of exactly components real numbers (a Vec2 / Vec3, a tuple, a list, a numpy row). components: How many axes this world’s gravity has, 2 or 3.

Returns: The axes as plain floats, in order, for the caller to box.

Raises: ValueError: If the write is not a sequence, is the wrong length, or holds anything that is not a finite number. Strings are refused rather than read character by character.

simvx.core.physics.world.normalise_damping(value: float, what: str) float[source]

Validate a damping coefficient (finite, >= 0) and return it as a float.

Damping is a per-second rate applied to the velocity a body ALREADY carries, once per step, before that step’s acceleration is added: v = v * max(0, 1 - damping * dt) + a * dt. 0 coasts forever, 1 sheds roughly 63% of the speed per second. There is no upper bound – a value above 1/dt simply stops the body dead in one step, which is a legitimate way to ask for that.

Damping the carried velocity rather than the sum is load-bearing at rest: the acceleration a resting body is given is exactly what its contact is about to cancel, so damping that too leaves a residue the solver cannot remove and a settled stack jitters on it. The two orders are identical whenever nothing is accelerating the body, which is the case damping exists for.

The pure-Python tiers run exactly this. Both native lanes damp at the stated rate and differ only in where their own integrator applies it, in a way that is bounded rather than cumulative:

  • Jolt damps AFTER adding the step’s acceleration, so a body under sustained acceleration ends up a relative damping * dt slower – 0.08% at the default rate and 60 Hz, 3.3% at a rate of 2. It vanishes the moment nothing is accelerating the body.

  • pymunk integrates position BEFORE damping the velocity, so a body coasting from a push travels one step’s worth of the speed it has shed further than the formula above: a fixed damping * dt fraction of the coast, 0.083% at the default rate and 0.83% at 0.5. The metres grow with the coast; the fraction does not. (That lane also runs Chipmunk’s space-wide exponential damping for a body on the seam default and the exact formula for one that deviates; the two forms differ by ~3e-7 per step at 60 Hz.)

simvx.core.physics.world.normalise_gravity_scale(value: float) float[source]

Validate a per-body gravity multiplier (finite) and return it as a float.

Any usable value is legal, including 0 (a body that ignores gravity, the usual way to float a pickup) and negatives (a body that falls upward, the usual way to make a balloon). NaN and the infinities are refused, and so is any magnitude at or beyond 1e30: a multiplier that large is not a gravity setting a scene meant, it overflows to infinity the moment it is integrated against a float32 velocity, and the bound is what lets one comparison reject NaN and the infinities too.

simvx.core.physics.world.is_unit_scale(scale: numpy.ndarray) bool[source]

True when scale is exactly (1, 1, 1) and so changes no geometry.

A byte comparison, so a backend can skip materialising a scaled shape for the overwhelmingly common unscaled body without a float epsilon deciding it.

simvx.core.physics.world.body_scale_unchanged(scale: object, stored: numpy.ndarray) bool[source]

True when scale is byte-identical to the scale a body already carries.

The pose write is the seam’s hottest per-body crossing, and the node layer states the node’s scale on every one of them, so a scale that has not moved since the last write is the overwhelmingly common case. Deciding it by a byte compare against what the body already holds is some twenty times cheaper than validating the value again, and validating it again would be redundant: a scale is validated when it is first accepted, and the geometry it was accepted for is re-validated independently whenever the collider is swapped.

Conservative by construction: anything that is not a float32 array of exactly the stored bytes answers False and takes the full validating path, so a caller passing a tuple or a float64 array loses only the shortcut, never correctness.

Dimension-agnostic – stored carries the dimension, so the 2D seam uses this function too.

simvx.core.physics.world.normalise_body_scale(scale: object, kind: str) numpy.ndarray[source]

Validate a body scale against a shape kind and return it as float32 (3,).

Scale lives on the BODY, not on the shape resource, because one resource is shared by every body that uses that geometry. Each backend applies it to its own instance of the shape, so this is where the rule that decides which scales a given geometry can express is stated once for all of them.

Non-uniform scale is REJECTED rather than approximated where the geometry cannot carry it: a sphere and a capsule have one radius, so squashing one on a single axis has no representation, and quietly substituting the largest or the mean would give a collider that does not match what is on screen – which is the defect scale exists to fix. A box, a convex hull and a triangle mesh scale componentwise; a cylinder scales freely along Y and uniformly across X/Z.

Negative components MIRROR: they flip a point cloud through the origin and are irrelevant to the analytic kinds, whose parameters are magnitudes. Uniformity is therefore judged on magnitudes, so a sprite-style (-1, 1, 1) flip is a uniform scale.

Args: scale: A Vec3 or any 3-sequence. kind: The backend’s own name for the geometry ("sphere", "box", "capsule", "cylinder", …). Unknown kinds scale componentwise.

Returns: The scale as a float32 (3,) array. Always a fresh array, never a view of the caller’s: backends keep what this returns as the body’s own scale and compare the next write against it, so sharing a buffer with a caller that mutates its Vec3 in place would leave a body whose recorded scale had changed and whose collider had not.

Raises: ValueError: If any component is zero or non-finite, or if the geometry cannot represent the requested non-uniform scale.

class simvx.core.physics.world.BodyMode[source]

Bases: enum.Enum

Motion mode of a body, mirroring the modes every serious solver exposes.

  • STATIC: immovable collider. Never integrated; infinite mass.

  • DYNAMIC: force-simulated; responds to gravity, impulses, contacts.

  • KINEMATIC: code-moved; pushes dynamic bodies, immune to forces.

STATIC

‘static’

DYNAMIC

‘dynamic’

KINEMATIC

‘kinematic’

__new__(value)
__repr__()
__str__()
__dir__()
__format__(format_spec)
__hash__()
__reduce_ex__(proto)
__deepcopy__(memo)
__copy__()
name()
value()
class simvx.core.physics.world.RaycastHit[source]

Result of a successful raycast against the world.

Attributes: body: Handle of the body the ray hit. point: World-space contact point (Vec3). normal: World-space surface normal at the hit (Vec3, unit length). distance: Distance from the ray origin to point along the ray.

body: simvx.core.physics.world.BodyHandle

None

point: simvx.core.math.Vec3

None

normal: simvx.core.math.Vec3

None

distance: float

None

class simvx.core.physics.world.SweepHit[source]

Result of a shape sweep stopping against a body.

Mirrors :class:RaycastHit exactly (a single “other” body, like a query result): the swept body is implicit (the caller). Maps cleanly onto Jolt (body <- hit BodyID, normal <- contact normal, distance <- fraction * |motion|).

Attributes: body: Handle of the OTHER body that was hit. point: World-space contact point (Vec3). normal: World-space surface normal (Vec3, unit), pointing AWAY from the other body toward the moving body, i.e. the direction that separates the mover. A sweep that begins already in contact still reports the surface’s separating normal, never the cast axis, so a caller can classify what it is touching. distance: A distance along motion at which the mover’s shape is guaranteed NOT to penetrate the blocker. A backend with a true time-of-impact sweep reports max(0.0, toi - skin); a backend that substeps reports a bound refined by bisection to |motion| / (substeps * 2**8) and ignores skin, because its own quantum is already larger than any sane skin. It is therefore an under-estimate of the true touch distance whose error is bounded by the backend’s sweep granularity, and it may be exactly 0.0 for a sweep that begins in contact. Callers advance to exactly distance and subtract nothing further.

body: simvx.core.physics.world.BodyHandle

None

point: simvx.core.math.Vec3

None

normal: simvx.core.math.Vec3

None

distance: float

None

class simvx.core.physics.world.ContactPhase[source]

Bases: enum.Enum

Edge phase of a body-pair contact, diffed by the broadphase each step.

Only the two transitions are reported (no per-frame “stay”): a pair fires ENTER the step it begins overlapping and EXIT the step it stops.

ENTER

‘enter’

EXIT

‘exit’

__new__(value)
__repr__()
__str__()
__dir__()
__format__(format_spec)
__hash__()
__reduce_ex__(proto)
__deepcopy__(memo)
__copy__()
name()
value()
class simvx.core.physics.world.ContactEvent[source]

A node-agnostic body-pair collision event emitted by a physics world.

Keyed by body HANDLES only: the world never names a node. The tree maps a / b back to nodes and fires the node-level collided / separated Signals. Both static-dynamic and dynamic-dynamic pairs are reported, and both bodies are notified (the tree reorients per side).

Orientation convention is fixed here as a -> b (mirroring the internal narrow-phase _Contact.normal); the tree negates normal / rel_velocity for the b side so each body sees the separating direction pointing toward itself.

A contact event is emitted only for pairs with at least one DYNAMIC participant. Kinematic-vs-kinematic and kinematic-vs-static pairs cannot be pushed apart, produce no solver work, and produce no ContactEvent on any backend. Sensor overlap is the separate one-directional stream (see

Class:

OverlapEvent) and is unaffected.

Attributes: a: Handle of the first body of the pair (canonical order). b: Handle of the second body of the pair. phase: :class:ContactPhase (ENTER / EXIT). point: World contact point (Vec3). Meaningful on ENTER; degenerate (Vec3(0)) on EXIT (no live manifold). The same degenerate value, for the same reason, on the rare ENTER whose manifold carries no point at all: a narrow phase that reports a touching pair and nowhere it touches has no world point to publish, and every backend answers that one way (:func:~simvx.core.physics.capability.contact_manifold_payload). normal: Unit contact normal oriented a -> b (Vec3). Degenerate (Vec3(0)) on EXIT. impulse: Normal impulse magnitude the solver applied to the pair this step. 0.0 on EXIT, and 0.0 on an ENTER the solver resolved with no impulse (e.g. separating velocity). None iff the backend does not advertise :attr:Capability.CONTACT_IMPULSE: it has no hook into its solver’s applied lambda, so it reports no number rather than a plausible-looking guess. None and 0.0 are different facts and both stay expressible. impulse_estimate: The portable stand-in for impulse: the impulse it would take to arrest the approach, computed the same way on every backend from the pair’s masses and the difference of the two bodies’ LINEAR velocities (:func:~simvx.core.physics.capability.contact_impulse_estimate). Always a number, never None, on every backend and whether or not :attr:~simvx.core.physics.capability.Capability.CONTACT_IMPULSE is advertised. 0.0 on EXIT, and 0.0 for a pair that is not approaching. It is not a measurement: it ignores restitution and the solver’s work entirely, which is what makes one formula serve every backend. Note that it is NOT this event’s own rel_velocity put through that formula: the at-point value below carries each backend’s choice of contact point, and a spinning body’s estimate computed from it would not be portable. Read impulse where it is measured and this everywhere else. rel_velocity: Pre-solve velocity of b w.r.t. a AT THE CONTACT POINT (Vec3), spin included: each body’s v + omega x r for the offset from its centre to :attr:point, differenced. A wheel spinning against the ground reports the speed of its tread, which its centre is not moving at. That meaning is contract on every backend, because the engine owns the error-prone half: the difference of the two bodies’ LINEAR velocities is one line of caller code, while deriving the at-point value needs the contact point and both angular velocities. Where neither body spins it reduces to that linear difference, and so does the value published where :attr:point is degenerate for want of a manifold point: there is nowhere to measure at, so the linear difference is what every backend publishes rather than a spin read at some substituted place. Degenerate (Vec3(0)) on EXIT.

a: simvx.core.physics.world.BodyHandle

None

b: simvx.core.physics.world.BodyHandle

None

phase: simvx.core.physics.world.ContactPhase

None

point: simvx.core.math.Vec3

None

normal: simvx.core.math.Vec3

None

impulse: float | None

None

impulse_estimate: float

None

rel_velocity: simvx.core.math.Vec3

None

class simvx.core.physics.world.OverlapEvent[source]

A node-agnostic sensor-overlap event emitted by a physics world.

A SECOND, independent edge-diffed stream, parallel to :class:ContactEvent but never mixed with it: a sensor pair produces NO collision response and NO manifold, so there is no point / normal / impulse / rel_velocity to carry.

Keyed by body HANDLES only (node-agnostic, like :class:ContactEvent), but DIRECTED sensor -> other rather than a canonical unordered pair: the detection is one-directional (the observing sensor decides via its mask), so a sensor-vs-sensor overlap can fire on one side without the other. Measured with two overlapping sensors built as an Area builds one: both sides report on all five backends, pymunk included since its sensors are held KINEMATIC (a pair of STATIC shapes, which is what they used to be, Chipmunk never forms at all). The tree maps both handles to nodes and routes body_entered vs area_entered by the OTHER node’s type.

Attributes: sensor: Handle of the detecting sensor body (the observer). other: Handle of the detected body (a normal body OR another sensor). phase: :class:ContactPhase (ENTER / EXIT); reused, no second phase enum.

sensor: simvx.core.physics.world.BodyHandle

None

other: simvx.core.physics.world.BodyHandle

None

phase: simvx.core.physics.world.ContactPhase

None

class simvx.core.physics.world.PhysicsWorld(*, gravity: simvx.core.math.Vec3)[source]

Bases: abc.ABC

Abstract backend interface: one isolated simulation world.

A PhysicsWorld owns a set of bodies, advances them as a unit at a fixed timestep via :meth:step, and exchanges per-frame state in bulk. Concrete backends (BuiltinPhysics, later JoltPhysics) implement every method.

What this promises across backends

This ABC and its 2D sibling :class:~simvx.core.physics.world2d.Physics2DWorld are held to one contract, stated here for both.

  • Identical contract behaviour. The same call has the same effect and the same observable consequences on every backend: a body created DYNAMIC falls, a filter that fails in one direction rejects the pair, a mutator wakes what the body it names was holding up, a pair reports exactly one ENTER and one EXIT, a sensor reports the bodies that overlap it. A node never branches on which backend it got in order to make a call. Where a backend cannot deliver one of those clauses the shortfall is written down rather than left silent, and sensing has the two the seam knows of. One is queryable: a STATIC body is reported only where

    attr:

    ~simvx.core.physics.capability.Capability.SENSOR_DETECTS_STATIC is advertised, which is every backend today (measured, one sensor over one box: one ENTER on all five, whatever mode the box is in) with the one exception that member states, a Jolt sensor whose collider is a mesh. The other is part of the event’s own definition: a sensor overlapping ANOTHER sensor is reported per observer, each side firing only where its own mask admits the other – an asymmetric pair fires on one side alone – as

    class:

    OverlapEvent describes.

  • Numbers only within a tolerance, and the tolerance is documented. Two solvers do not agree on where a crate rests or which step it settles on. Numeric equality between the pure-Python solvers and Jolt is explicitly NOT promised, and code that needs a number to be reproducible needs it from one backend. docs/core/physics_backends.md records the differences that are big enough to design around.

  • Backend-dependent features and payload fields are queryable. Anything that genuinely differs is a :class:~simvx.core.physics.capability.Capability the caller can ask :meth:capabilities about, never a silent substitute and never a plausible-looking stand-in for a value the backend cannot measure.

Two rules follow, and they bind new work on this seam as much as they describe it:

  1. No gameplay-visible discrete decision may be derived from a quantity whose precision is backend-dependent. Continuous quantities may differ by a tolerance; a yes/no the player can see must not turn on that tolerance. The character step-up probe is the worked example: deciding “can I step here” from sweep precision alone let a character climb a loose sphere on an exact time-of-impact backend and not on a substepped one, so the decision is made on the walkable-slope test of the surface it lands on, which every backend agrees about. The one place the rule is not yet fully honoured is recorded where it lives, on the step-up helper behind :func:~simvx.core.physics.slide.move_and_slide: a rounded character mounting a ledge and ratcheting up a sphere are the same manoeuvre, and separating them needs a floor check the seam cannot express.

  2. Every Capability ships a documented degradation path. A capability a caller cannot degrade around is a typed docstring rather than a contract. Each member of :class:~simvx.core.physics.capability.Capability says what a game does where it is absent, and CONTACT_IMPULSE – the one whose absence would otherwise leave a caller with nothing – publishes its fallback on the event itself as

    attr:

    ContactEvent.impulse_estimate.

Bulk-array contract (the keystone)

  1. Call :meth:register_bodies once (or whenever membership changes) to fix the body->row order used by the bulk readers.

  2. Each frame, after :meth:step, call :meth:read_transforms and/or

    meth:

    read_velocities, passing a caller-preallocated, C-contiguous float32 numpy array of the documented shape. The backend fills it in place; it must not allocate or return a new array on the hot path.

The array shapes/dtypes/contiguity are part of the contract and MUST be checked by subclasses (see _check_transforms_out / _check_velocities_out, which raise ValueError: the buffer is caller input, so the check is not an assert).

Shape contract (immutable values, owned by the resource that asked)

A shape handle names an IMMUTABLE VALUE, and every create_* factory mints a FRESH one. The factories do not memoise on the geometry: the world has no way of knowing when a caller has finished with a handle, so a world-level cache keyed on geometry would be a table that only ever grows – one entry per distinct size a collider was ever animated through, for the life of the world.

Sharing and lifetime belong one level up, to the Shape resource (:class:~simvx.core.physics.shapes.Shape), which builds its handle once per world and releases it when the resource is collected. One resource used by a thousand bodies is one backend record; a collider rebuilt every frame holds one record at a time. A caller working against this interface directly owns what it creates and releases it with :meth:destroy_shape.

Two rules follow, and both are part of this interface, not an implementation detail of any backend:

  1. There is no mutation API, and there must never be one. A hypothetical set_shape_radius would silently resize every body built on the handle. Any edit-the-geometry surface must BUILD A NEW HANDLE from the new parameters and hand it to :meth:set_body_shape, which is exactly what changing a collider does today.

  2. Destruction is about the handle, never about a body. A handle can be shared by many bodies, so “destroy the shape this body uses” is meaningless as an instruction to the simulation: :meth:destroy_shape releases the world’s reference and leaves every body’s geometry alone. See its docstring.

Per-body SCALE is separate from the shape and belongs to the body, precisely because a shape is shared: :meth:create_body and :meth:set_body_transform take a scale, and the backend applies it to its own instance of the geometry. Not every geometry can express every scale (a sphere has one radius), and the ones that cannot raise rather than approximate – see

Func:

normalise_body_scale.

Initialization

Initialise the world.

Args: gravity: World gravity acceleration vector (Vec3), metres/s^2.

property gravity: simvx.core.math.Vec3[source]

World gravity acceleration vector (Vec3), metres/s^2.

Scaled per body by the gravity_scale :meth:create_body takes, so a balloon or a pickup opts out of this without the world changing.

property solver_iterations: int[source]

Impulse-solver iterations per :meth:step (>= 1).

The convergence / cost dial: more iterations means a tighter stack and a stiffer joint chain for proportionally more time. Every tier has this one dial and honours it (the builtin velocity loop, Jolt’s velocity steps, Chipmunk’s space.iterations). Its partner for the joints alone is

Attr:

position_iterations.

Defaults to :data:DEFAULT_SOLVER_ITERATIONS.

property position_iterations: int[source]

Rigid-joint position passes per :meth:step (>= 1).

The second half of a sequential-impulse solve, and the one a jointed assembly notices: the velocity loop stops a joint’s error growing, and these passes drain the error the loop leaves behind. Raising it tightens a loaded chain for the cost of that many more passes over the joints the scene has, and costs a scene with no joints nothing at all. Contacts are not driven by it – their positional correction is one pass per step by design – so this is a joint dial, not a stacking dial.

Honoured by the two builtin solvers, which is where the seam’s own position pass lives. The three native lanes store the value and read it back without running it: Chipmunk has no position solver at all, and Jolt’s own position-step count, though it sits in the very settings struct the seam does push, is carried by neither lane’s entry point (simvx_jolt_set_world_settings and the shim’s setWorldSettings), so a Jolt world runs the library’s default whatever this says. See docs/core/physics_backends.md.

Defaults to :data:DEFAULT_POSITION_ITERATIONS, so a world that never touches it behaves exactly as it always has.

property sleep_time_threshold: float[source]

Seconds of continuous sub-threshold motion before a body sleeps (> 0).

World-level, with only the can_sleep boolean on the body, because that is the model the recommended backend has: Jolt keeps timeBeforeSleep and pointVelocitySleepThreshold in its world settings struct and exposes allowSleeping per body and nothing else. A per-body threshold would have to be emulated everywhere, for a knob whose real use (“this one must never sleep”) can_sleep already serves.

Defaults to :data:DEFAULT_SLEEP_TIME.

property sleep_velocity_threshold: float[source]

Speed below which a body counts as at rest, m/s (>= 0).

One threshold, not one per axis of motion: the pure-Python tiers test it against linear speed and against angular speed alike (rad/s against the same number), and Jolt against the point velocity at the body’s extremities, which folds spin into the same measure. 0 means a body must be exactly still to sleep.

Defaults to :data:DEFAULT_SLEEP_VELOCITY.

property contact_slop: float[source]

Contact overlap tolerated without correction, world units (>= 0).

The deadband every impulse solver keeps so that a resting contact is not fought over every step: penetration up to this depth is left alone, and only the excess is pushed out.

So it is a floor on how deep a settled body rests, and lower is strictly better on any backend that sleeps. A body at rest sits exactly contact_slop inside what it is standing on, at any world scale: examples/features/physics/body_knobs.py --test prints crates resting at 0.4990 against a geometric 0.5 at the 0.001 default and at 0.4030 at 0.1, and the pile sleeps 4/4 at the default with a slop change waking none of it.

It is a knob because a backend may want its own library’s tolerance back (world.contact_slop = 0.02 for Jolt), not because the value should track the scene’s scale. Raising it at pixel scale is not the recommendation it used to be here, and the text that said so was wrong in both halves: raising it does not buy sleep, because a settled pile already sleeps at the default, and it does not stop a body sinking, it is the thing that makes it sink. Measured on the builtin 2D solver at pixel scale, a lone 20-unit crate sinks 0.0008 / 0.0198 / 0.0998 / 0.4998 / 1.9998 at slop 0.001 / 0.02 / 0.1 / 0.5 / 2.0, and a settled four-high pile shows no post-settle movement at any of them.

What raising it will NOT do is rescue a game-scale stack that is held awake. Measured on the built-in 2D solver, a four-high stack of 20-unit boxes at gravity 980 with can_sleep=False, over 900 and 2000 steps and at drop gaps of 0, 0.02 and 1.0: the worst rest-height error is within 1% of the same number at 0.001, 0.02 and 0.05 in every one of those twelve scenes, and the stack that collapses collapses at all three. That residual belongs to the once-per-step position pass, not to this deadband; the same pile allowed to sleep settles correctly at any of the three, because the drain that runs as an island parks is what reaches the stacked pose.

Defaults to :data:DEFAULT_CONTACT_SLOP.

capabilities() frozenset[simvx.core.physics.capability.Capability][source]

Return the set of :class:Capability features this backend honours.

The rigid-body surface is the parity contract every backend implements with the same methods and the same behaviour; this method is the ONE place backends advertise what genuinely depends on the backend: a whole feature this interface has no methods for (vehicles, soft bodies), a strengthened guarantee about a method every backend has (cross-platform determinism, about :meth:step), or a payload field that carries a value only where the backend can measure it (the contact-event impulse). A node checks Capability.X in world.capabilities() and branches, degrades or refuses explicitly.

The default (this base implementation) is the empty set, so a partially implemented backend claims nothing by accident. Concrete backends override to list exactly what they honour, in either direction: the builtin solver advertises what it measures, and an optional native backend omits what the library it wraps gives it no hook for.

Every member carries a documented degradation path, so a caller that finds one absent has somewhere to go rather than a missing feature: read the member’s own docstring for what to do instead.

abstractmethod create_sphere(radius: float) simvx.core.physics.world.ShapeHandle[source]

Create a sphere collision shape and return an opaque handle.

Args: radius: Sphere radius, world units (> 0).

Returns: An opaque shape handle for use with :meth:create_body.

abstractmethod create_box(half_extents: simvx.core.math.Vec3) simvx.core.physics.world.ShapeHandle[source]

Create an axis-aligned box collision shape (centred at the origin).

Args: half_extents: Half-sizes along x/y/z (Vec3, all > 0).

Returns: An opaque shape handle for use with :meth:create_body.

abstractmethod create_capsule(radius: float, height: float) simvx.core.physics.world.ShapeHandle[source]

Create a Y-axis capsule collision shape and return an opaque handle.

Args: radius: Capsule radius, world units (> 0). height: Total extent along Y including the two hemispherical caps (> 0). The central segment half-length is max(0, height / 2 - radius); when height <= 2 * radius the segment collapses to a point and the capsule behaves as a sphere of radius.

Returns: An opaque shape handle for use with :meth:create_body.

abstractmethod create_cylinder(radius: float, height: float) simvx.core.physics.world.ShapeHandle[source]

Create a Y-axis cylinder collision shape and return an opaque handle.

Args: radius: Cylinder radius, world units (> 0). height: Total extent along Y with flat caps at +-height / 2 (> 0).

Returns: An opaque shape handle for use with :meth:create_body.

abstractmethod create_convex_hull(points: numpy.ndarray) simvx.core.physics.world.ShapeHandle[source]

Create a convex-hull collision shape from a point cloud.

Args: points: (N, 3) float32 array of >= 4 finite points. The backend computes its own internal hull representation from the cloud. Orientation is supported via the body transform like other shapes, EXCEPT the basic BuiltinPhysics backend, which IGNORES hull rotation (the cloud is treated in world axes offset by the body position); the Jolt backend rotates properly.

Returns: An opaque shape handle for use with :meth:create_body. The basic backend’s penetration depth/normal for a hull is an EPA-lite approximation (GJK overlap is exact); see builtin/world.py.

abstractmethod create_mesh(vertices: numpy.ndarray, indices: numpy.ndarray) simvx.core.physics.world.ShapeHandle[source]

Create a STATIC triangle-mesh collision shape (level geometry).

Args: vertices: (N, 3) float32 vertex positions. indices: (3 * T,) int64 flat triangle-list indices (three per triangle), each in [0, N).

Returns: An opaque shape handle for use with :meth:create_body. A mesh shape is a STATIC-ONLY collider: placing it on a non-STATIC body is an error (rejected at :meth:create_body and :meth:set_body_mode). It carries no inertia / mass and cannot be used as a moving query shape (:meth:shapecast / :meth:overlap reject a mesh probe).

abstractmethod destroy_shape(shape: simvx.core.physics.world.ShapeHandle) None[source]

Release the world’s reference to a shape handle.

Drops shape from the backend’s shape table. A backend that owns a native shape object frees it here; a pure-Python backend simply forgets the record. This is what a :class:~simvx.core.physics.shapes.Shape resource calls for each handle it owns when it is collected, and what a caller working against this interface directly calls for the handles it created.

Bodies are not touched. One handle is routinely shared by many bodies (see the shape contract on :class:PhysicsWorld), so destroying it cannot mean “take the collider away from whoever is using it”, and a resource going out of scope must never yank the geometry out from under a live body. Every body created with this shape keeps its geometry and keeps simulating exactly as before. Destroying a shape that is still in use neither raises nor detaches nor changes any body, on every backend.

“Keeps simulating” is the FULL surface, not just the next :meth:step: a body whose shape handle has been released stays editable through every set_body_* setter and remains sweepable through :meth:sweep_body, which is what keeps a character controller working. A backend must therefore reach a body’s geometry through the record that body owns and never by looking the caller’s handle back up in its shape table.

An unknown handle is a silent no-op, the same silent-drop contract

Meth:

remove_joint uses, so destroying twice or destroying after

Meth:

clear is safe in any teardown order.

The HANDLE, however, is invalid afterwards: passing it to

Meth:

create_body, :meth:set_body_shape, :meth:shapecast or

Meth:

overlap raises KeyError, and is a caller error rather than a way to resurrect the geometry. KeyError on every backend and every one of those four calls, matching what a bogus BODY handle already raises, so one except KeyError covers the lot.

Args: shape: A handle previously returned by one of the create_* shape factories.

abstractmethod create_body(shape: simvx.core.physics.world.ShapeHandle, body_type: simvx.core.physics.world.BodyMode, transform: Any, *, mass: float = 1.0, scale: simvx.core.math.Vec3 | None = None, can_sleep: bool = True, linear_damping: float = DEFAULT_LINEAR_DAMPING, angular_damping: float = DEFAULT_ANGULAR_DAMPING, gravity_scale: float = DEFAULT_GRAVITY_SCALE, collision_layer: int = 1, collision_mask: int = 4294967295, is_sensor: bool = False, material: simvx.core.physics.material.PhysicsMaterial | None = None, continuous: bool = False) simvx.core.physics.world.BodyHandle[source]

Create a body in the world and return its handle.

Args: shape: An opaque shape handle from :meth:create_sphere, :meth:create_box, :meth:create_capsule, :meth:create_cylinder, :meth:create_convex_hull, or :meth:create_mesh. A mesh shape on a non-STATIC body is an error (mesh colliders are STATIC-only), rejected here and in :meth:set_body_mode. body_type: One of :class:BodyMode. transform: Initial world transform: a Transform3D, a bare position (identity orientation), or a (position, orientation) pair whose orientation is a Quat. The pair form states a complete pose, so pass Quat() for identity rather than leaving the orientation unset. mass: Body mass in kg, > 0 (a non-positive or NaN mass raises ValueError, whatever the mode). It is consulted while the body is DYNAMIC and RETAINED across :meth:set_body_mode, so a body created STATIC or KINEMATIC carries this mass into a later DYNAMIC flip. STATIC and KINEMATIC bodies integrate as infinite-mass regardless of the value. scale: Per-body scale of shape (Vec3); None (the default) means unscaled. Scale belongs to the body rather than the shape because a shape handle is shared, so one collider resource serves bodies at any number of sizes. The backend applies it to its own instance of the geometry, and it composes with the pose: a body at scale=4 collides as the four-times-larger collider, which is what makes a scaled node’s collider match what is drawn. Not every geometry can express every scale, and the ones that cannot RAISE rather than approximate (see :func:normalise_body_scale). Negative components mirror. Editable afterwards through :meth:set_body_transform. can_sleep: Whether this body is ALLOWED to fall asleep once it settles (see :meth:sleeping). Defaults True. False keeps it permanently simulated, which is what a body that must react the instant something reaches it wants: a trigger platform, a player-driven prop, or anything a game polls the velocity of. Editable afterwards through :meth:set_body_can_sleep. linear_damping: Per-second rate at which the body sheds linear speed with nothing touching it, applied as v = v * max(0, 1 - linear_damping * dt) + a * dt (see :func:normalise_damping, which also records where Jolt’s integrator differs under sustained acceleration). Defaults to :data:DEFAULT_LINEAR_DAMPING. This is a body knob rather than a world one because it stands in for the drag of a specific object’s shape and material, which is why a feather and a cannonball want different values in the same air. Editable afterwards through :meth:set_body_damping. angular_damping: The same rate for spin, applied the same way to the angular velocity. Defaults to :data:DEFAULT_ANGULAR_DAMPING. Independent of linear_damping: a wheel that must keep rolling but stop sliding wants them different. gravity_scale: Multiplier on the world’s gravity for this body alone. 1 (the default) falls normally, 0 ignores gravity entirely (a floating pickup, a hovering drone), and a negative value falls upward (a balloon). Multiplies :attr:gravity, so a world with no gravity has none whatever this says. Editable afterwards through :meth:set_body_gravity_scale. collision_layer: 32-bit layer membership of this body (which layers it lives on). Stored verbatim; defaults to layer 1. collision_mask: 32-bit mask of layers this body scans for collisions. Defaults to all (0xFFFFFFFF) so the bare API collides every pair. A pair (a, b) collides iff (a.mask & b.layer) and (b.mask & a.layer): both bodies must opt in to the other (the Box2D / Rapier convention). is_sensor: When True, this body is a SENSOR (trigger). It is created / destroyed / teleported exactly like a normal body and participates in the broadphase, but is EXCLUDED from collision resolution (it skips the solver, applies no impulse, never appears in the contact-event stream, and never blocks a shape sweep) and instead generates a SEPARATE overlap-event stream (see :meth:drain_overlap_events) using the ONE-DIRECTIONAL filter sensor.mask & other.layer (the observer decides; the other body’s mask is irrelevant), never the AND body-body rule. A sensor is an ordinary body with a flag, not a separate kind of handle. material: The body’s SURFACE: friction, restitution and the two combine modes, as one :class:PhysicsMaterial resource. None (the default) uses :data:~simvx.core.physics.material.DEFAULT_PHYSICS_MATERIAL, which is mu = 0.5 and no bounce. A surface is a shared property of many bodies – ice, rubber, wood – so it is one resource passed by reference rather than four loose numbers repeated per body; the world reads its values and keeps none of it, so the same instance may be handed to any number of bodies and to any number of worlds. The resource is FROZEN, so it cannot be edited afterwards at all: give a live body a different surface through :meth:set_body_material. continuous: When True, this body uses continuous collision detection: each step its centre displacement is swept against STATIC geometry and clamped to the time-of-impact so a fast small body cannot tunnel through thin static colliders. Defaults False (discrete). Basic-tier honesty: a CENTRE ray / shapecast sweep vs STATIC bodies only, no rotational sweep and no dynamic-vs-dynamic CCD; the Jolt backend honours the flag faithfully via EMotionQuality::LinearCast.

Returns: An opaque body handle, stable until :meth:destroy_body.

abstractmethod destroy_body(handle: simvx.core.physics.world.BodyHandle) None[source]

Remove a body from the world.

After destruction the handle is invalid. Callers that use the bulk readers must re-call :meth:register_bodies to re-establish row order.

An unknown handle is a silent no-op, the same contract

Meth:

destroy_shape follows, so destroying twice or destroying after

Meth:

clear is safe in any teardown order.

Destruction ENDS every contact and sensor overlap the body was in, so the event streams report one EXIT per open edge rather than dropping it: a listener is never left believing a pair is still in contact (the same rule

Meth:

set_body_filter follows). The events surface in the first

Meth:

drain_contact_events / :meth:drain_overlap_events after the next

Meth:

step, and never twice. Their payload is the ordinary degenerate EXIT payload, and the destroyed handle they carry is an identity to match against a caller’s own bookkeeping, never an argument for a further world call.

Destruction also WAKES every body that was in contact with this one, so whatever it was holding up falls (see :meth:sleeping).

Args: handle: A handle previously returned by :meth:create_body.

abstractmethod set_body_transform(handle: simvx.core.physics.world.BodyHandle, transform: Any, *, scale: simvx.core.math.Vec3 | None = None, wake: bool = True) None[source]

Teleport a body to a new world transform, optionally rescaling it.

Wakes the body, and a pose that really moves it also wakes the bodies it was in contact with, so a platform driven out from under a sleeping crate drops the crate (see :meth:sleeping). Re-writing the pose a body already holds moves nothing and wakes nothing.

Args: handle: Body handle. transform: New world transform, in the same forms :meth:create_body accepts. A (position, orientation) pair states a complete pose: pass Quat() for identity. scale: New per-body scale of the body’s shape (Vec3), or None (the default) to leave the scale it already has alone. None rather than (1, 1, 1) is load-bearing: a character controller re-writes its body’s pose every step and knows nothing about scale, so a default that reset it would silently unscale every such body once per step. Re-stating the scale a body already has changes no geometry. Rescaling IS a geometry change, so it wakes the body’s neighbours exactly as :meth:set_body_shape does. wake: Whether this write may disturb sleepers (see :meth:sleeping). False re-poses the body without waking it or anything it was holding up: streaming a level in, or repositioning a pooled object, must not re-activate a settled pile.

Raises: ValueError: If the body’s geometry cannot represent scale (see :func:normalise_body_scale).

abstractmethod set_body_velocity(handle: simvx.core.physics.world.BodyHandle, linear: simvx.core.math.Vec3, angular: simvx.core.math.Vec3 | None = None) None[source]

Set a body’s linear and angular velocity directly.

A body the caller made STATIC is never set in motion by this call, on any backend: an immovable body stays where it was put, and the way to move one is :meth:set_body_transform. The write is still remembered and read back, on every backend, and it becomes the body’s live velocity if the body later leaves STATIC. The native adapters hold the value themselves rather than giving it to their library: they hold a STATIC sensor KINEMATIC (see

Attr:

~simvx.core.physics.capability.Capability.SENSOR_DETECTS_STATIC), which the library WOULD integrate, and Jolt keeps no velocity on a static body at all. What such a velocity means beyond the readback is the backend’s: the builtin solver reads it as a surface velocity in the friction solve, which is how a conveyor is expressed, and Jolt does not.

Args: handle: Body handle. linear: Linear velocity (Vec3), world units/s. angular: Angular velocity (Vec3), radians/s about each axis. None (default) means zero angular velocity.

abstractmethod set_body_mode(handle: simvx.core.physics.world.BodyHandle, mode: simvx.core.physics.world.BodyMode, *, wake: bool = True) None[source]

Change a live body’s motion mode in place (no destroy/recreate).

Flips the body between STATIC / KINEMATIC / DYNAMIC, updating its effective (inverse) mass: STATIC and KINEMATIC are infinite-mass (inv_mass 0), DYNAMIC restores the mass the body was created with. Flips are lossless, so freezing a body to STATIC and waking it later gives back exactly its original mass. Maps onto Jolt’s Body::SetMotionType; the builtin backend flips body_type + inverse_mass.

A flip to DYNAMIC wakes the bodies this one was in contact with: it can no longer hold anything up, so what rested on it must fall (see

Meth:

sleeping). Freezing a body to STATIC takes nothing away and leaves a sleeping neighbour asleep, and re-asserting the mode a body already has does nothing at all.

wake governs both halves of that, and the two directions of the flip are not symmetric:

  • LEAVING DYNAMIC ends the body’s own sleep whatever wake says, because an immovable body is never asleep (see :meth:sleeping). That restores the invariant rather than waking anything, so it is not suppressible.

  • ENTERING DYNAMIC with wake=False hands the body to the simulation PARKED: DYNAMIC and asleep, its velocity zeroed, costing nothing until something disturbs it (:meth:wake, a pose write, an impulse, or an awake body arriving at it). That is what streaming a section in wants, its geometry landing settled rather than paying to fall into place. The default hands the body over awake, moving from the next step.

  • A body whose can_sleep is False has no parked state to be handed to, so it is freed AWAKE whichever way wake points: forbidding sleep forbids it by this route too.

  • Entering KINEMATIC parks nothing. A KINEMATIC body never reports as asleep, and it moves only when it is written to, which is itself a disturbance. So it stays in the pair and overlap search whichever way wake points: a platform streamed in and flipped to KINEMATIC where it stands still fires the triggers it is standing in.

Parking needs a backend that advertises :attr:Capability.SLEEP. Without it nothing can be held out of the simulation, so a body freed with wake=False is simulated from the very next step.

Args: handle: Body handle. mode: The new :class:BodyMode. wake: Whether this write may disturb sleepers (see :meth:sleeping), and whether a body entering DYNAMIC is handed over awake.

Raises: ValueError: If the stored mass is not > 0 and mode is DYNAMIC, or if the body’s shape kind forbids mode.

abstractmethod set_body_mass(handle: simvx.core.physics.world.BodyHandle, mass: float, *, wake: bool = True) None[source]

Set a live body’s mass, recomputing its inertia from its current shape.

Velocity is preserved, not momentum: the body keeps moving at the speed it had, and only its response to future impulses, forces and contacts changes. The new mass is RETAINED exactly like the create-time one, so it survives a later :meth:set_body_mode flip; setting it on a STATIC or KINEMATIC body is legal and takes effect the moment that body becomes DYNAMIC.

A re-massed body is also a support whose behaviour has changed under whatever is resting on it, so the wake reaches its neighbours too: a pillar made a thousand times heavier while a crate sleeps on it must not leave that crate reading the old response.

Args: handle: Body handle. mass: New mass in kg, > 0. wake: Whether this write may disturb sleepers (see :meth:sleeping).

Raises: ValueError: If mass is not > 0 (NaN included).

abstractmethod set_body_filter(handle: simvx.core.physics.world.BodyHandle, collision_layer: int, collision_mask: int, *, wake: bool = True) None[source]

Set a live body’s 32-bit layer membership and collision mask together.

One method for the pair because the pair-acceptance rule reads both sides of both bodies ((a.mask & b.layer) and (b.mask & a.layer)), so changing one alone is never the whole answer; a caller changing one passes the current value of the other.

The new filter is honoured from the NEXT :meth:step on, and the event streams follow it: a pair that was touching and no longer matches reports an EXIT rather than vanishing silently, so a listener is never left believing a pair is still in contact, and one that newly matches reports an ENTER like any other new contact. An edit that changes no verdict reports nothing at all: re-stating the filter a pair already matched must not re-announce it.

Args: handle: Body handle. collision_layer: New 32-bit layer membership. collision_mask: New 32-bit mask of layers this body scans. wake: Whether this write may disturb sleepers (see :meth:sleeping).

abstractmethod set_body_material(handle: simvx.core.physics.world.BodyHandle, material: simvx.core.physics.material.PhysicsMaterial | None) None[source]

Replace a live body’s surface material.

The whole surface at once, because that is what a material IS: a caller swapping a body from wood to ice hands over the ice, not four numbers. Honoured from the next :meth:step; contacts already being solved this step keep the coefficients they were solved with.

The world reads the resource’s values and does not retain it. That is deliberate, and safe: a :class:PhysicsMaterial is frozen, so there is no later edit for the world to have missed – a material shared by fifty bodies would otherwise have no way to tell fifty backend records that one of its fields moved. A body’s surface changes only by passing a different one here.

Args: handle: Body handle. material: The new surface, or None for :data:~simvx.core.physics.material.DEFAULT_PHYSICS_MATERIAL.

abstractmethod set_body_damping(handle: simvx.core.physics.world.BodyHandle, linear: float, angular: float) None[source]

Set a live body’s linear and angular damping together.

Both at once for the reason :meth:set_body_filter takes both halves of the filter: they are one description of how a body sheds motion, and a caller changing one passes the current value of the other.

Honoured from the next :meth:step. Raising damping on a body already in flight slows it from there rather than retroactively.

Args: handle: Body handle. linear: Per-second linear damping rate (>= 0, finite). angular: Per-second angular damping rate (>= 0, finite).

Raises: ValueError: If either rate is negative, NaN or infinite.

abstractmethod set_body_gravity_scale(handle: simvx.core.physics.world.BodyHandle, scale: float) None[source]

Set a live body’s gravity multiplier.

Honoured from the next :meth:step, and it WAKES the body: gravity is applied by integration, which skips a sleeper, so a body parked in mid-air at scale = 0 would otherwise stay there when gravity was switched back on for it.

Args: handle: Body handle. scale: New multiplier on the world’s gravity for this body. Any finite value, including 0 and negatives.

Raises: ValueError: If scale is NaN or infinite.

abstractmethod set_body_continuous(handle: simvx.core.physics.world.BodyHandle, enabled: bool) None[source]

Turn continuous collision detection on or off for a live body.

Honoured only where the backend advertises

Attr:

~simvx.core.physics.capability.Capability.CONTINUOUS. A backend that does not (the library it wraps has no CCD at all) accepts the call and does nothing, exactly as it already ignores the continuous argument to :meth:create_body; the capability is how a caller finds that out rather than by backend name.

Args: handle: Body handle. enabled: True for continuous (swept) integration, False for discrete.

abstractmethod set_body_shape(handle: simvx.core.physics.world.BodyHandle, shape: simvx.core.physics.world.ShapeHandle, *, wake: bool = True) None[source]

Swap a live body’s collision shape, keeping everything else.

The body keeps its handle, its pose, its velocity, its mode, its mass, its filter, its sensor flag, its material and its CCD flag; only the geometry changes. The inertia is recomputed from the NEW shape at the body’s retained mass, so a body does not silently keep the rotational response of the shape it no longer has.

A swap is a teleport of geometry, not a move: the new shape can overlap neighbours the old one cleared. Nothing is resolved at swap time, so an overlap introduced this way is pushed apart by the ordinary contact solve over the following steps, and a body swapped inside static geometry is pushed out of it the same way. The body wakes, so that recovery starts on the next step rather than whenever something else happens to disturb it.

The event streams describe the geometry, not the swap: a pair the new shape still touches is NOT announced again (it never stopped touching), a pair it no longer reaches reports an EXIT, and one it newly reaches reports an ENTER. Replacing a collider with an identical one is therefore silent.

The swap also vacates whatever volume the old geometry held, so the wake reaches the body’s neighbours as well: shrinking a platform under a sleeping crate drops the crate rather than leaving it on a ledge that is no longer there.

Args: handle: Body handle. shape: An opaque shape handle from one of the create_* factories. wake: Whether this write may disturb sleepers (see :meth:sleeping).

Raises: ValueError: If the new shape’s kind forbids the body’s current mode (a triangle mesh is a STATIC-only collider).

abstractmethod body_velocity(handle: simvx.core.physics.world.BodyHandle) tuple[simvx.core.math.Vec3, simvx.core.math.Vec3][source]

Read a body’s current (linear, angular) velocity, per-body.

Cold per-body read parallel to :meth:body_transform. The bulk

Meth:

read_velocities stays the hot scatter path; this is the accessor used by PhysicsBody3D.velocity / .spin for a single synchronous read-back (e.g. self.velocity += dv).

Args: handle: Body handle.

Returns: (linear, angular) velocity (Vec3, Vec3); angular in radians/s. Returns zero velocities for an infinite-mass body that was never moved.

abstractmethod body_mass(handle: simvx.core.physics.world.BodyHandle) float[source]

Read a body’s EFFECTIVE mass in kg, the one an impulse divides by.

The counterpart of :meth:set_body_mass, and deliberately not a read-back of what was set: a STATIC or KINEMATIC body is infinite-mass whatever mass it was created with, so it answers math.inf and an impulse applied to it moves nothing. Flipping the same body to DYNAMIC restores its retained mass and this returns that.

Args: handle: Body handle.

Returns: Mass in kg for a DYNAMIC body, math.inf for an immovable one.

abstractmethod sleeping(handle: simvx.core.physics.world.BodyHandle) bool[source]

True if the body is asleep (skipped by integrate + solve until woken).

STATIC / KINEMATIC bodies are never ‘asleep’ (they were never awake): returns False for them, and a body flipped off DYNAMIC stops reporting as asleep at once, whatever wake= the flip was given – an immovable body has no motion to resume, so clearing the flag restores this invariant rather than waking anything. A sleeping body stays a full collider and still reads back its (frozen) transform / velocity through the bulk readers.

A sleeper is woken by anything the solver would otherwise let it miss: a contact, an impulse or force, a write to its own pose, velocity, mode or any of the live-edit setters, AND a change to a body it was in contact with that takes support away. Destroying a body, teleporting it, changing its geometry or its mass, or flipping it to DYNAMIC therefore wakes whatever was resting on it; without that a crate whose support has gone hangs in mid-air for the life of the world, because the ordinary wake-on-contact needs a contact that still exists. Writes that change nothing (re-posing a parked platform where it already is, re-asserting the mode a body already has) wake nothing, so a game may drive both every frame.

Sleep and wake are ISLAND-ATOMIC. A pile parks as a unit: no body commits until every DYNAMIC body it touches, and everything those touch in turn, has come to rest as well. A disturbance that wakes any member of a sleeping island wakes all of it, so nothing a change took support from is left asleep however deep the pile: pull the bottom crate out of a settled stack and the whole stack falls, not just the crate that was on it. What a pile rests ON is in nobody’s island (STATIC and KINEMATIC bodies end the walk), so two piles sharing a floor sleep and wake independently.

Asleep is FROZEN. A sleeping body’s pose is the pose it fell asleep at, every step it stays asleep, so anything that reads a settled scene – a save, a placement check, a golden image – reads the same numbers until something wakes it.

That is an invariant of this interface rather than a courtesy each backend remembers: every mutator that can take support away (:meth:set_body_transform, :meth:set_body_mode, :meth:set_body_mass,

Meth:

set_body_filter, :meth:set_body_shape) wakes the body and its neighbours, and each takes wake=False to suppress it for the cases where a write is bookkeeping rather than a disturbance: streaming a level in, respawning a pooled object, or an editor writing a value the player cannot feel. Suppression is opt-IN because getting it wrong the other way strands a body in mid-air, which no later step can recover.

Meth:

apply_impulse, :meth:apply_force and :meth:apply_torque take no such flag: waking is the point of them.

abstractmethod wake(handle: simvx.core.physics.world.BodyHandle) None[source]

Wake a sleeping body, whatever its sleep timer had reached.

Reaches only the body it names: the island rule belongs to a change that takes support away, and this call takes nothing away. Use it when a game knows something the solver cannot see (a script is about to read the body’s velocity, a scripted force is coming next frame). A body that is already awake, and one that is STATIC or KINEMATIC and so was never asleep, are both a no-op.

Args: handle: Body handle.

abstractmethod sleep(handle: simvx.core.physics.world.BodyHandle) None[source]

Put a body to sleep now, without waiting for it to settle.

Freezes the body where it is: it is skipped by integration and the contact velocity solve until something wakes it, while staying a full collider that still reads back its (frozen) pose. Use it to park a pile a game knows is finished (a completed level section, a body a cutscene has taken over) rather than paying for it to come to rest first.

A no-op on a body that is STATIC or KINEMATIC (never awake), and on one whose can_sleep is False: forbidding sleep means forbidding it, so a body that must stay simulated cannot be put to sleep by hand either.

Args: handle: Body handle.

abstractmethod set_body_can_sleep(handle: simvx.core.physics.world.BodyHandle, enabled: bool) None[source]

Allow or forbid this body ever falling asleep.

The create-time can_sleep argument, live. Forbidding it wakes the body if it was asleep, so the effect is immediate rather than starting at the next settle; allowing it again lets the body settle from the current step, with no credit for time already spent at rest.

A body that may not sleep is simulated every step for the life of the world. That is the point of it, and it is also the cost: it will show up in a profile as a body that never leaves the integrate and solve sets, which is correct rather than a defect.

Args: handle: Body handle. enabled: True to allow sleeping (the default), False to forbid it.

abstractmethod apply_impulse(handle: simvx.core.physics.world.BodyHandle, impulse: simvx.core.math.Vec3, *, at: simvx.core.math.Vec3 | None = None, angular: simvx.core.math.Vec3 | None = None) None[source]

Apply an instantaneous velocity change to a body NOW.

Unlike :meth:apply_force this takes effect immediately (it mutates velocity, not an accumulator) and is NOT cleared by :meth:step. Inert on non-DYNAMIC bodies (inverse mass 0).

Args: handle: Body handle. impulse: Linear impulse (Vec3), N*s. Adds impulse * inv_mass to the linear velocity. at: Optional world-space application point. When given, the offset r = at - position contributes an angular impulse cross(r, impulse) (basic tier scales it by inv_mass as a stand-in for the inverse inertia tensor, which the basic backend does not model). None applies the impulse purely through the centre of mass (no torque). angular: Optional explicit angular impulse (Vec3), for spin_up. Adds angular * inv_mass (basic-tier inverse inertia stand-in) to the angular velocity, independent of at.

abstractmethod apply_force(handle: simvx.core.physics.world.BodyHandle, force: simvx.core.math.Vec3, *, at: simvx.core.math.Vec3 | None = None) None[source]

Accumulate a continuous force, applied during the NEXT :meth:step.

The force is integrated as acceleration (force * inv_mass) before position integration, then auto-cleared at the end of the step. To sustain a force the caller must re-add it every fixed step; a single call affects exactly one step. Inert on non-DYNAMIC.

Args: handle: Body handle. force: Linear force (Vec3), N. at: Optional world-space application point. When given, the offset r = at - position adds a torque cross(r, force) to the torque accumulator. None applies the force through the COM.

abstractmethod apply_torque(handle: simvx.core.physics.world.BodyHandle, torque: simvx.core.math.Vec3) None[source]

Accumulate a continuous torque, applied during the NEXT :meth:step.

Auto-cleared after the step like :meth:apply_force: re-add each fixed step to sustain it. Inert on non-DYNAMIC. Basic tier applies it as torque * inv_mass (inverse inertia stand-in).

Args: handle: Body handle. torque: Torque (Vec3), N*m.

abstractmethod create_fixed_joint(a: simvx.core.physics.world.BodyHandle, b: simvx.core.physics.world.BodyHandle) simvx.core.physics.world.JointHandle[source]

Weld two bodies: lock their full relative transform.

Captures the CURRENT relative pose of b in a’s frame at create time (relative position AND relative orientation) and holds it in that frame: the two bodies thereafter move as one rigid assembly, and the whole assembly swings round when a turns.

Built-in backend caveat: it has NO inertia tensor, so the angular lock uses inverse_mass as the inverse-inertia scalar; a long thin body or an off-centre weld will rotate too easily. Convergence is a few sequential-impulse iterations, so a long weld chain sags slightly. Use the Jolt backend for precise articulated mechanisms.

Args: a: First body handle (the reference frame). b: Second body handle (welded into a’s frame).

Returns: An opaque :data:JointHandle, valid until :meth:remove_joint (or until either body is destroyed, which silently drops the joint).

abstractmethod create_pin_joint(a: simvx.core.physics.world.BodyHandle, b: simvx.core.physics.world.BodyHandle, anchor: simvx.core.math.Vec3) simvx.core.physics.world.JointHandle[source]

Pin two bodies at a single world-space point (ball / point-to-point).

Constrains the two bodies so the world point anchor stays coincident on both (they cannot separate there) while leaving all three rotational DOF free. The anchor is captured at create as a point in EACH body’s own frame, and turned back into world axes by that body’s current orientation every solver pass – so a pin on a spinning body orbits with it, which is what the shape of the joint promises.

Basic-tier honesty on the built-in solvers: angular cross-coupling uses inverse_mass as the inverse-inertia scalar (there is no inertia tensor), and convergence is a few iterations, so a long chain sags slightly. The anchoring itself is exact; the narrowphase is the part that still treats most colliders as unrotated.

Args: a: First body handle. b: Second body handle. anchor: World-space pivot point shared by both bodies (Vec3).

Returns: An opaque :data:JointHandle.

abstractmethod create_hinge_joint(a: simvx.core.physics.world.BodyHandle, b: simvx.core.physics.world.BodyHandle, anchor: simvx.core.math.Vec3, axis: simvx.core.math.Vec3) simvx.core.physics.world.JointHandle[source]

Hinge two bodies: pin at anchor + one free rotational DOF about axis.

A point constraint at anchor (like :meth:create_pin_joint) PLUS an angular constraint that locks the two off-axis rotational DOF, leaving free rotation only about axis. The axis is given in world space, normalised, and captured into each body’s own frame at create, so it turns with the bodies: a door on a post that is itself turning keeps swinging about the post. There are no motors and no angular limits.

Built-in backend caveat: the same inverse_mass inverse-inertia-scalar stand-in as :meth:create_pin_joint, and only a few solver iterations.

Args: a: First body handle. b: Second body handle. anchor: World-space hinge pivot point (Vec3). axis: World-space hinge axis (Vec3, normalised at create).

Returns: An opaque :data:JointHandle.

abstractmethod create_spring_joint(a: simvx.core.physics.world.BodyHandle, b: simvx.core.physics.world.BodyHandle, rest_length: float, stiffness: float, damping: float) simvx.core.physics.world.JointHandle[source]

Soft distance-spring between the two body centres (compliant, not rigid).

A soft constraint that pulls the two body centres of mass toward rest_length apart with spring constant stiffness (N/m) and damping damping (N*s/m). Unlike the rigid joints it is intentionally compliant: it applies a soft velocity impulse with a k*x bias and a c*v damping term, and is NEVER position-corrected.

Basic-tier honesty: the builtin backend uses the two COMs, NOT per-body anchors (Pin / Hinge use anchors, Spring uses centres for simplicity). The explicit soft-impulse form can oscillate or overshoot when stiffness is large relative to the fixed dt; a stiff spring needs a smaller dt or the Jolt backend. Nothing is silently clamped.

Args: a: First body handle. b: Second body handle. rest_length: Target centre-to-centre separation (world units, >= 0). stiffness: Spring constant k (N/m). damping: Damping coefficient c (N*s/m).

Returns: An opaque :data:JointHandle.

abstractmethod remove_joint(handle: simvx.core.physics.world.JointHandle) None[source]

Remove a constraint; the handle is invalid afterwards.

A no-op if handle is unknown (already removed, or silently dropped because one of its bodies was destroyed): this is the SAME silent-drop contract :meth:destroy_body already uses for touching / overlap pairs, not an error-swallowing shim. A joint whose body was freed is the expected case, so removing it twice (once by the body-purge, once by the joint node’s own teardown) must be safe in either teardown order.

Args: handle: A handle previously returned by a create_*_joint call.

abstract property body_count: int[source]

Number of bodies currently in the world.

Read-only. Used by SceneTree.physics_tick to skip stepping empty worlds for zero overhead, mirroring PhysicsServer.body_count.

abstractmethod clear() None[source]

Remove every body and joint, emptying the world.

A level-teardown / restart-the-scene primitive that returns the world to an empty state (body_count == 0) WITHOUT discarding the world object, its configured :attr:gravity, or its backend. Per-step edge-diff buffers (contacts / overlaps) and any warm-start cache are reset so the next step starts from a clean broadphase. Cached shape handles stay valid (shapes are reusable resources), and handle counters keep advancing so a freed handle is never re-issued to a new body. After :meth:clear, callers using the bulk readers must re-:meth:register_bodies.

abstractmethod step(dt: float) None[source]

Advance the whole world once by a fixed timestep.

This integrates every body, resolves collisions, and updates internal state for the entire world as a unit. It is designed to be driven by a fixed-step accumulator (SceneTree.physics_tick).

Args: dt: Fixed timestep in seconds. Callers must pass a constant value.

abstractmethod drain_contact_events() list[simvx.core.physics.world.ContactEvent][source]

Return and CLEAR this step’s buffered enter/exit contact events.

Edge-only and broadphase-driven: the backend diffs the touching-pair set each :meth:step and buffers a :class:ContactEvent for every pair that began (ENTER) or stopped (EXIT) overlapping. Returns [] when nothing changed. Node-agnostic: events are keyed by body handles only; the tree maps handles to nodes and fires the Signals. Both static-dynamic AND dynamic-dynamic pairs are reported, filtered by the same layer/mask rule the simulation uses.

abstractmethod drain_overlap_events() list[simvx.core.physics.world.OverlapEvent][source]

Return and CLEAR this step’s buffered sensor-overlap enter/exit events.

Edge-only, broadphase-driven, DIRECTED (keyed sensor -> other), filtered by the one-directional sensor rule (sensor.mask & other.layer), node-agnostic. Distinct from

Meth:

drain_contact_events: a sensor pair produces NO collision response and NO manifold, so the event carries only the two handles plus the :class:ContactPhase. Returns [] when nothing changed.

abstractmethod register_bodies(handles: list[simvx.core.physics.world.BodyHandle]) None[source]

Fix the body->row order used by the bulk readers.

Establishes the mapping from each body handle to a row index. After this call, :meth:read_transforms / :meth:read_velocities fill row i with the state of handles[i]. Call again whenever membership or desired ordering changes.

Args: handles: Ordered list of body handles. len(handles) is the row count N expected by the bulk readers.

abstractmethod read_transforms(out: numpy.ndarray) None[source]

Fill out with current body transforms, in place.

Bulk hot-path read. The backend writes into the caller-owned buffer and allocates nothing.

Args: out: Pre-allocated array of shape (N, 7), dtype float32, C-contiguous, where N matches the most recent :meth:register_bodies. Each row is [px, py, pz, qx, qy, qz, qw]: position xyz followed by an orientation quaternion in xyzw order (scalar-last). Row i corresponds to handles[i].

abstractmethod read_velocities(out: numpy.ndarray) None[source]

Fill out with current body velocities, in place.

Bulk hot-path read. The backend writes into the caller-owned buffer and allocates nothing.

Args: out: Pre-allocated array of shape (N, 6), dtype float32, C-contiguous, where N matches the most recent :meth:register_bodies. Each row is [lx, ly, lz, ax, ay, az]: linear velocity xyz followed by angular velocity xyz (radians/s). Row i corresponds to handles[i].

abstractmethod raycast(origin: simvx.core.math.Vec3, direction: simvx.core.math.Vec3, max_dist: float, *, mask: int = 4294967295) simvx.core.physics.world.RaycastHit | None[source]

Cast a ray and return the nearest hit, or None.

Args: origin: Ray origin in world space (Vec3). direction: Ray direction (Vec3); need not be normalised. max_dist: Maximum distance along direction to test. mask: Query layer mask. Only bodies whose collision_layer & mask is non-zero are considered. Defaults to all layers. This is the single query-mask convention (one query mask vs each body’s layer), distinct from the bidirectional body-pair rule used by the simulation.

Returns: A :class:RaycastHit for the closest body intersected within max_dist whose layer matches mask, or None if the ray hits nothing.

abstractmethod raycast_all(origin: simvx.core.math.Vec3, direction: simvx.core.math.Vec3, max_dist: float, *, mask: int = 4294967295) list[simvx.core.physics.world.RaycastHit][source]

Cast a ray and return EVERY hit within max_dist, sorted by distance.

Like :meth:raycast but collects all intersected bodies (whose collision_layer & mask is set) instead of only the nearest, returned ascending by :attr:RaycastHit.distance. Empty list on no hit. Backs PhysicsQuery.raycast_all and the exclude= filter path of raycast (which must skip excluded nearer hits).

Args: origin: Ray origin in world space (Vec3). direction: Ray direction (Vec3); need not be normalised. max_dist: Maximum distance along direction to test. mask: Query layer mask (single query-mask vs body-layer convention).

Returns: All :class:RaycastHit\ s within max_dist, sorted ascending by distance (empty if the ray hits nothing).

abstractmethod shapecast(shape: simvx.core.physics.world.ShapeHandle, origin: simvx.core.math.Vec3, direction: simvx.core.math.Vec3, max_dist: float, *, mask: int = 4294967295) simvx.core.physics.world.SweepHit | None[source]

Sweep a shape along a ray and return the earliest-TOI contact, or None.

Sweeps shape from origin along direction (need not be normalised) up to max_dist against world bodies, returning the earliest time-of-impact contact among bodies whose collision_layer & mask is set, else None. Reuses

Class:

SweepHit: the swept shape is the implicit caller, body is the hit body, normal is the separating normal pointing back toward the cast origin, and distance is the TOI distance along direction.

Basic-tier honesty: on the builtin backend this is a substepped sweep, not a true continuous cast, so fast casts vs very thin colliders can tunnel and box orientation is ignored (AABB), matching :meth:sweep_body.

Args: shape: An opaque shape handle from :meth:create_sphere, :meth:create_box, :meth:create_capsule, or :meth:create_cylinder. origin: Cast origin in world space (Vec3). direction: Cast direction (Vec3); need not be normalised. max_dist: Maximum sweep distance along direction. mask: Query layer mask (single query-mask vs body-layer convention).

Returns: The earliest-TOI :class:SweepHit, or None if nothing was hit.

abstractmethod overlap(shape: simvx.core.physics.world.ShapeHandle, transform: Any, *, mask: int = 4294967295) list[simvx.core.physics.world.BodyHandle][source]

Return all bodies a static shape overlaps at transform.

Places shape at transform (same flexible forms as

Meth:

create_body) and returns the handles of every body it overlaps whose collision_layer & mask is set, sorted by handle for determinism. Basic-tier honesty: AABB-ish narrowphase, box orientation ignored, like :meth:sweep_body. Every body in the table is visible here, including a KINEMATIC character body.

Args: shape: An opaque shape handle from :meth:create_sphere, :meth:create_box, :meth:create_capsule, or :meth:create_cylinder. transform: World pose to place the shape at (same flexible forms as :meth:create_body). mask: Query layer mask (single query-mask vs body-layer convention).

Returns: Sorted list of overlapping body handles (empty if none).

abstractmethod sweep_body(handle: simvx.core.physics.world.BodyHandle, motion: simvx.core.math.Vec3, *, from_transform: tuple[simvx.core.math.Vec3, simvx.core.math.Quat] | None = None, skin: float = 0.0) simvx.core.physics.world.SweepHit | None[source]

Cast a body’s shape along motion and report the first blocker.

The one non-mutating sweep primitive: it does NOT move handle and does not mutate any other body, so a caller can run a whole collide-and-slide loop against it while tracking the pose itself.

A body blocks the cast only when ALL of the following hold: it is not the mover itself; it is not a sensor (a sensor is excluded from collision resolution, so it never blocks a sweep); the pair passes the canonical AND rule (mover.mask & other.layer) and (other.mask & mover.layer); in 2D the one-way filter does not reject it; and the contact OPPOSES the cast direction (dot(motion_dir, separating_normal) < -1e-4), so a surface the mover already rests on does not halt a tangential cast.

A contact reported at distance zero must still carry a true surface normal, never the cast axis: a caller classifies floor / wall / ceiling from that normal, and an exact-sweep backend whose narrowphase degenerates at zero separation has to adjudicate the case rather than pass the axis on.

Args: handle: Body handle of the mover. motion: World-space displacement to sweep along (Vec3); the sweep length is |motion|. from_transform: Optional (position, orientation) pose to cast from instead of the body’s own stored pose. A complete pose, not a bare position, so a caller tracking a pose in Python never needs a second read-back to recover the orientation. skin: Contact clearance to leave at the blocker, in world units. A backend with an exact time-of-impact sweep subtracts it; a substepped backend ignores it (see :attr:SweepHit.distance).

Returns: The nearest blocking :class:SweepHit, or None when the path is clear.

move_and_collide(handle: simvx.core.physics.world.BodyHandle, motion: simvx.core.math.Vec3) simvx.core.physics.world.SweepHit | None[source]

Move a kinematic body by motion, stopping at the first contact.

Concrete composition of :meth:sweep_body and :meth:set_body_transform, identical on every backend: sweep, then advance to exactly SweepHit.distance along motion (the full motion when the path is clear). Does NOT slide and does NOT integrate gravity: one sweep, stop at the first blocker. The collide-and-slide policy for a character body lives in simvx.core.physics.slide, not here.

Args: handle: A body created with :attr:BodyMode.KINEMATIC. motion: World-space displacement (Vec3), already velocity * dt (this call takes a displacement, so dt lives in the caller).

Returns: A :class:SweepHit (other body, world point, separating normal, guaranteed-clear distance) if the sweep was blocked, else None after moving the full motion.

abstractmethod body_transform(handle: simvx.core.physics.world.BodyHandle) tuple[simvx.core.math.Vec3, simvx.core.math.Quat][source]

Read a body’s current pose as (position, orientation).

The cold per-body pose read, parallel to :meth:body_velocity: returns plain Vec3 / Quat so callers get a clean synchronous read-back after a user-driven :meth:move_and_collide without the bulk path.

Args: handle: Body handle.

Returns: (position, orientation).

__slots__

()

simvx.core.physics.world.__all__

[‘BodyMode’, ‘Capability’, ‘CombineMode’, ‘PhysicsMaterial’, ‘DEFAULT_LINEAR_DAMPING’, ‘DEFAULT_ANGU…