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¶
Motion mode of a body, mirroring the modes every serious solver exposes. |
|
Result of a successful raycast against the world. |
|
Result of a shape sweep stopping against a body. |
|
Edge phase of a body-pair contact, diffed by the broadphase each step. |
|
A node-agnostic body-pair collision event emitted by a physics world. |
|
A node-agnostic sensor-overlap event emitted by a physics world. |
|
Abstract backend interface: one isolated simulation world. |
Functions¶
Convert a gravity write to exactly |
|
Validate a damping coefficient (finite, |
|
Validate a per-body gravity multiplier (finite) and return it as a float. |
|
True when |
|
True when |
|
Validate a body scale against a shape kind and return it as float32 |
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
componentsfinite floats.The vector twin of :func:
_knob_float, shared by every backend’sgravitysetter so all of them refuse the same values in the same words. Boxing the write straight into aVec2/Vec3does 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
componentsreal numbers (aVec2/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.0coasts forever,1sheds roughly 63% of the speed per second. There is no upper bound – a value above1/dtsimply 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 * dtslower – 0.08% at the default rate and 60 Hz, 3.3% at a rate of2. 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 * dtfraction of the coast, 0.083% at the default rate and 0.83% at0.5. The metres grow with the coast; the fraction does not. (That lane also runs Chipmunk’s space-wide exponentialdampingfor 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 beyond1e30: 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
scaleis 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
scaleis 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 –
storedcarries 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
Vec3or 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 itsVec3in 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.EnumMotion 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 topointalong 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:
RaycastHitexactly (a single “other” body, like a query result): the swept body is implicit (the caller). Maps cleanly onto Jolt (body<- hitBodyID,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 alongmotionat which the mover’s shape is guaranteed NOT to penetrate the blocker. A backend with a true time-of-impact sweep reportsmax(0.0, toi - skin); a backend that substeps reports a bound refined by bisection to|motion| / (substeps * 2**8)and ignoresskin, 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 exactly0.0for a sweep that begins in contact. Callers advance to exactlydistanceand 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.EnumEdge 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
ENTERthe step it begins overlapping andEXITthe 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/bback to nodes and fires the node-levelcollided/separatedSignals. 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 negatesnormal/rel_velocityfor thebside 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
ContactEventon 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 onENTER; degenerate (Vec3(0)) onEXIT(no live manifold). The same degenerate value, for the same reason, on the rareENTERwhose 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 orienteda -> b(Vec3). Degenerate (Vec3(0)) onEXIT. impulse: Normal impulse magnitude the solver applied to the pair this step.0.0onEXIT, and0.0on anENTERthe solver resolved with no impulse (e.g. separating velocity).Noneiff 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.Noneand0.0are different facts and both stay expressible. impulse_estimate: The portable stand-in forimpulse: 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, neverNone, on every backend and whether or not :attr:~simvx.core.physics.capability.Capability.CONTACT_IMPULSEis advertised.0.0onEXIT, and0.0for 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 ownrel_velocityput 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. Readimpulsewhere it is measured and this everywhere else. rel_velocity: Pre-solve velocity ofbw.r.t.aAT THE CONTACT POINT (Vec3), spin included: each body’sv + omega x rfor 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:pointis 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)) onEXIT.- 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:
ContactEventbut 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 DIRECTEDsensor -> otherrather 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 anAreabuilds one: both sides report on all five backends, pymunk included since its sensors are heldKINEMATIC(a pair ofSTATICshapes, which is what they used to be, Chipmunk never forms at all). The tree maps both handles to nodes and routesbody_enteredvsarea_enteredby 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.ABCAbstract backend interface: one isolated simulation world.
A
PhysicsWorldowns 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, laterJoltPhysics) implement every method.What this promises across backends
This ABC and its 2D sibling :class:
~simvx.core.physics.world2d.Physics2DWorldare 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
DYNAMICfalls, 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 oneENTERand oneEXIT, 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: aSTATICbody is reported only where- attr:
~simvx.core.physics.capability.Capability.SENSOR_DETECTS_STATICis advertised, which is every backend today (measured, one sensor over one box: oneENTERon 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:
OverlapEventdescribes.
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.mdrecords 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.Capabilitythe caller can ask :meth:capabilitiesabout, 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:
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.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.Capabilitysays what a game does where it is absent, andCONTACT_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)
Call :meth:
register_bodiesonce (or whenever membership changes) to fix the body->row order used by the bulk readers.Each frame, after :meth:
step, call :meth:read_transformsand/or- meth:
read_velocities, passing a caller-preallocated, C-contiguousfloat32numpy 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 raiseValueError: 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
Shaperesource (: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:
There is no mutation API, and there must never be one. A hypothetical
set_shape_radiuswould 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.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_shapereleases 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_bodyand :meth:set_body_transformtake ascale, 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_bodytakes, 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_settingsand the shim’ssetWorldSettings), so a Jolt world runs the library’s default whatever this says. Seedocs/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_sleepboolean on the body, because that is the model the recommended backend has: Jolt keepstimeBeforeSleepandpointVelocitySleepThresholdin its world settings struct and exposesallowSleepingper 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_sleepalready 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.
0means 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_slopinside what it is standing on, at any world scale:examples/features/physics/body_knobs.py --testprints crates resting at0.4990against a geometric0.5at the0.001default and at0.4030at0.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.02for 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
gravity980 withcan_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 at0.001,0.02and0.05in 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:
Capabilityfeatures 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 checksCapability.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); whenheight <= 2 * radiusthe segment collapses to a point and the capsule behaves as a sphere ofradius.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 basicBuiltinPhysicsbackend, 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); seebuiltin/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_bodyand :meth:set_body_mode). It carries no inertia / mass and cannot be used as a moving query shape (:meth:shapecast/ :meth:overlapreject a mesh probe).
- abstractmethod destroy_shape(shape: simvx.core.physics.world.ShapeHandle) None[source]¶
Release the world’s reference to a shape handle.
Drops
shapefrom 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.Shaperesource 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 everyset_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_jointuses, so destroying twice or destroying after- Meth:
clearis safe in any teardown order.
The HANDLE, however, is invalid afterwards: passing it to
- Meth:
create_body, :meth:set_body_shape, :meth:shapecastor- Meth:
overlapraisesKeyError, and is a caller error rather than a way to resurrect the geometry.KeyErroron every backend and every one of those four calls, matching what a bogus BODY handle already raises, so oneexcept KeyErrorcovers 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: aTransform3D, a bare position (identity orientation), or a(position, orientation)pair whose orientation is aQuat. The pair form states a complete pose, so passQuat()for identity rather than leaving the orientation unset. mass: Body mass in kg,> 0(a non-positive or NaN mass raisesValueError, 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 ofshape(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 atscale=4collides 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 asv = 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 oflinear_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,0ignores 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 filtersensor.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:PhysicsMaterialresource.None(the default) uses :data:~simvx.core.physics.material.DEFAULT_PHYSICS_MATERIAL, which ismu = 0.5and 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 viaEMotionQuality::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_bodiesto re-establish row order.An unknown handle is a silent no-op, the same contract
- Meth:
destroy_shapefollows, so destroying twice or destroying after- Meth:
clearis safe in any teardown order.
Destruction ENDS every contact and sensor overlap the body was in, so the event streams report one
EXITper open edge rather than dropping it: a listener is never left believing a pair is still in contact (the same rule- Meth:
set_body_filterfollows). The events surface in the first- Meth:
drain_contact_events/ :meth:drain_overlap_eventsafter the next- Meth:
step, and never twice. Their payload is the ordinary degenerateEXITpayload, 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_bodyaccepts. A(position, orientation)pair states a complete pose: passQuat()for identity. scale: New per-body scale of the body’s shape (Vec3), orNone(the default) to leave the scale it already has alone.Nonerather 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_shapedoes. wake: Whether this write may disturb sleepers (see :meth:sleeping).Falsere-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
STATICis 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 leavesSTATIC. 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.
wakegoverns both halves of that, and the two directions of the flip are not symmetric:LEAVING DYNAMIC ends the body’s own sleep whatever
wakesays, 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=Falsehands 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_sleepis False has no parked state to be handed to, so it is freed AWAKE whichever waywakepoints: 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
wakepoints: 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 withwake=Falseis 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
> 0andmodeis DYNAMIC, or if the body’s shape kind forbidsmode.
- 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_modeflip; 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
massis 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:
stepon, 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:
PhysicsMaterialis 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
Nonefor :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_filtertakes 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 atscale = 0would 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
0and negatives.Raises: ValueError: If
scaleis 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 thecontinuousargument 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_velocitiesstays the hot scatter path; this is the accessor used byPhysicsBody3D.velocity/.spinfor 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 answersmath.infand 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.inffor 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 takeswake=Falseto 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_forceand :meth:apply_torquetake 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_sleepis 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_sleepargument, 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_forcethis 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. Addsimpulse * inv_massto the linear velocity. at: Optional world-space application point. When given, the offsetr = at - positioncontributes an angular impulsecross(r, impulse)(basic tier scales it byinv_massas a stand-in for the inverse inertia tensor, which the basic backend does not model).Noneapplies the impulse purely through the centre of mass (no torque). angular: Optional explicit angular impulse (Vec3), forspin_up. Addsangular * inv_mass(basic-tier inverse inertia stand-in) to the angular velocity, independent ofat.
- 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 offsetr = at - positionadds a torquecross(r, force)to the torque accumulator.Noneapplies 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 astorque * 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
bina’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 whenaturns.Built-in backend caveat: it has NO inertia tensor, so the angular lock uses
inverse_massas 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
anchorstays 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_massas 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 aboutaxis.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 aboutaxis. 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_massinverse-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_lengthapart with spring constantstiffness(N/m) and dampingdamping(N*s/m). Unlike the rigid joints it is intentionally compliant: it applies a soft velocity impulse with ak*xbias and ac*vdamping 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
stiffnessis large relative to the fixeddt; a stiff spring needs a smallerdtor 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
handleis unknown (already removed, or silently dropped because one of its bodies was destroyed): this is the SAME silent-drop contract :meth:destroy_bodyalready 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_*_jointcall.
- abstract property body_count: int[source]¶
Number of bodies currently in the world.
Read-only. Used by
SceneTree.physics_tickto skip stepping empty worlds for zero overhead, mirroringPhysicsServer.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:
stepand buffers a :class:ContactEventfor 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_velocitiesfill rowiwith the state ofhandles[i]. Call again whenever membership or desired ordering changes.Args: handles: Ordered list of body handles.
len(handles)is the row countNexpected by the bulk readers.
- abstractmethod read_transforms(out: numpy.ndarray) None[source]¶
Fill
outwith 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), dtypefloat32, C-contiguous, whereNmatches 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). Rowicorresponds tohandles[i].
- abstractmethod read_velocities(out: numpy.ndarray) None[source]¶
Fill
outwith 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), dtypefloat32, C-contiguous, whereNmatches 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). Rowicorresponds tohandles[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 alongdirectionto test. mask: Query layer mask. Only bodies whosecollision_layer & maskis 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:
RaycastHitfor the closest body intersected withinmax_distwhose layer matchesmask, orNoneif 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:
raycastbut collects all intersected bodies (whosecollision_layer & maskis set) instead of only the nearest, returned ascending by :attr:RaycastHit.distance. Empty list on no hit. BacksPhysicsQuery.raycast_alland theexclude=filter path ofraycast(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 alongdirectionto test. mask: Query layer mask (single query-mask vs body-layer convention).Returns: All :class:
RaycastHit\ s withinmax_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
shapefromoriginalongdirection(need not be normalised) up tomax_distagainst world bodies, returning the earliest time-of-impact contact among bodies whosecollision_layer & maskis set, elseNone. Reuses- Class:
SweepHit: the swept shape is the implicit caller,bodyis the hit body,normalis the separating normal pointing back toward the cast origin, anddistanceis the TOI distance alongdirection.
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 alongdirection. mask: Query layer mask (single query-mask vs body-layer convention).Returns: The earliest-TOI :class:
SweepHit, orNoneif 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
shapeattransform(same flexible forms as- Meth:
create_body) and returns the handles of every body it overlaps whosecollision_layer & maskis 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
motionand report the first blocker.The one non-mutating sweep primitive: it does NOT move
handleand 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, orNonewhen 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_bodyand :meth:set_body_transform, identical on every backend: sweep, then advance to exactlySweepHit.distancealongmotion(the fullmotionwhen 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 insimvx.core.physics.slide, not here.Args: handle: A body created with :attr:
BodyMode.KINEMATIC. motion: World-space displacement (Vec3), alreadyvelocity * dt(this call takes a displacement, sodtlives in the caller).Returns: A :class:
SweepHit(other body, world point, separating normal, guaranteed-clear distance) if the sweep was blocked, elseNoneafter moving the fullmotion.
- 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 plainVec3/Quatso callers get a clean synchronous read-back after a user-driven :meth:move_and_collidewithout 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…