simvx.core.physics.builtin.world¶
What it is adequate for¶
Spheres and capsules on flat ground, low-density scenes, prototypes, and anything
whose gameplay does not turn on the exact resting pose of a stack. For oriented
boxes, hulls, meshes under load and dense stacking, install simvx-physics-jolt:
installing it makes Jolt the default 3D backend, and PhysicsRoot(backend= "builtin") pins this solver back.
Mechanism: semi-implicit (symplectic) Euler integration, a sweep-and-prune broad phase feeding an analytic narrow phase, and a sequential normal-impulse solver with Baumgarte positional correction and a small penetration slop. The GEOMETRY is written for clarity over raw throughput; what is optimised is how OFTEN it runs. The broad phase proposes only plausible pairs and a pair whose bodies have not moved replays its last answer, so a settled scene costs almost nothing however many bodies are in it.
Shape set: sphere, box, capsule, cylinder, convex hull and static triangle mesh.
EXACT¶
Every sphere and capsule pairing: capsule contacts reduce to a sphere test at the closest point(s) of the segments, reusing the sphere depth/normal maths.
Sphere / capsule / box against a static triangle mesh: closest-point-on-triangle (Ericson RTCD 5.1.5) per candidate triangle, with the box reporting its OWN support along the contact direction, so a box rests on a mesh floor at the height it rests on a box floor. Mesh raycast is Moller-Trumbore.
Cylinder-sphere and every ray query against a cylinder: analytic finite-cylinder maths.
Convex hull boolean overlap: GJK.
All queries (raycast, shapecast, overlap, sweep_body) as to WHETHER they hit, subject to the substepping note below.
APPROXIMATE, and named at the call site¶
None of these silently fakes a result; each carries a comment where it is computed.
Cylinder-box: AABB of the cylinder.
Cylinder-cylinder and capsule-cylinder: the cylinder is treated as a capsule, so the rims round off. Cylinders are the honest weak shape here.
Convex hull penetration depth and normal: EPA-lite, a bounded-iteration expanding polytope with a search-direction fallback.
Convex hull raycast: the slab test against the AABB of the point cloud.
Contact response: one linear impulse at the centre of mass. Rotation is integrated for round-tripping, but there is no inertia tensor, so an off-centre impulse never spins a body. The 2D solver DOES carry a real moment of inertia; see
world2d.py.Swept queries substep rather than solving a true time of impact, so a reported
distanceis a lower bound and a fast enough body can tunnel.
ROTATION IGNORED¶
_box_box, _hull_world_points and a mesh body’s own orientation in
_shape_vs_mesh: each treats its geometry in world axes offset by the body
position. An oriented box against another box, a rotated hull, and a rotated mesh
level are therefore all wrong here.
REFUSED¶
Convex hull against a triangle mesh. There is no routine for it, and what the pair
returned before it was refused was NO CONTACT, so a hull fell through a mesh floor
in silence. It raises at create_body, set_body_shape, set_body_filter
and at the two query entry points, naming simvx-physics-jolt. Box-vs-mesh is
NOT refused: it is exact (see above).
BuiltinPhysics world: the dependency-free 3D reference solver.
Concrete :class:~simvx.core.physics.world.PhysicsWorld implementation, pure
Python (numpy only). It is always present, it needs nothing installed, it runs
everywhere the engine runs including the browser, and it is the behavioural parity
target the seam’s cross-backend tests are written against.
Module Contents¶
Classes¶
Pure-Python default backend (basic tier). |
Data¶
API¶
- class simvx.core.physics.builtin.world.BuiltinPhysics(*, gravity: simvx.core.math.Vec3)[source]¶
Bases:
simvx.core.physics.world.PhysicsWorldPure-Python default backend (basic tier).
See :class:
~simvx.core.physics.world.PhysicsWorldfor the full contract.Initialization
Initialise the world.
Args: gravity: World gravity acceleration vector (
Vec3), metres/s^2.- capabilities() frozenset[simvx.core.physics.capability.Capability][source]¶
Advertise the measured contact impulse and continuous collision.
This backend owns its sequential-impulse solver, so it reads the converged normal impulse straight off the last velocity iteration and puts it in the contact-event payload:
CONTACT_IMPULSEis a real measurement here, not an estimate.CONTINUOUSis honoured by_integrate, which sweeps a flagged body’s centre displacement against STATIC geometry and clamps it to the time of impact. It advertises no cross-platform determinism (its float maths is reproducible on one machine only), no vehicles and no soft bodies.SLEEPis honoured by_update_sleeping, which parks a settled DYNAMIC body and skips it in integrate and solve until something disturbs it.SENSOR_DETECTS_STATICis honoured because_collide_sensorssweeps a sensor against every body its mask admits rather than against a moving set, so a trigger over level geometry reports it. Explicit (not inherited) so the claim is a deliberate, tested promise, not an accident of the default.
- clear() None[source]¶
Remove every body and joint, emptying the world.
See :meth:
~simvx.core.physics.world.PhysicsWorld.clear. Resets the body / joint tables and the per-step edge-diff + warm-start caches so a re-populated world starts from a clean broadphase state. Gravity, shapes, and the handle counters are intentionally NOT reset (shapes are reusable resources; monotonic handles keep freed handles from aliasing a live one).
- create_sphere(radius: float) simvx.core.physics.world.ShapeHandle[source]¶
- create_box(half_extents: simvx.core.math.Vec3) simvx.core.physics.world.ShapeHandle[source]¶
- create_capsule(radius: float, height: float) simvx.core.physics.world.ShapeHandle[source]¶
- create_cylinder(radius: float, height: float) simvx.core.physics.world.ShapeHandle[source]¶
- create_convex_hull(points: numpy.ndarray) simvx.core.physics.world.ShapeHandle[source]¶
- create_mesh(vertices: numpy.ndarray, indices: numpy.ndarray) simvx.core.physics.world.ShapeHandle[source]¶
- destroy_shape(shape: simvx.core.physics.world.ShapeHandle) None[source]¶
Release this world’s record of a shape handle.
See :meth:
~simvx.core.physics.world.PhysicsWorld.destroy_shape. There is nothing native to free here: the record is simply dropped from the shape table. Bodies built from it hold their own_Shaperecord directly and are unaffected. Unknown handles are a silent no-op.
- create_body(shape: simvx.core.physics.world.ShapeHandle, body_type: simvx.core.physics.world.BodyMode, transform: object, *, 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]¶
- destroy_body(handle: simvx.core.physics.world.BodyHandle) None[source]¶
- set_body_transform(handle: simvx.core.physics.world.BodyHandle, transform: object, *, scale: simvx.core.math.Vec3 | None = None, wake: bool = True) None[source]¶
- set_body_velocity(handle: simvx.core.physics.world.BodyHandle, linear: simvx.core.math.Vec3, angular: simvx.core.math.Vec3 | None = None) None[source]¶
- set_body_mode(handle: simvx.core.physics.world.BodyHandle, mode: simvx.core.physics.world.BodyMode, *, wake: bool = True) None[source]¶
- set_body_mass(handle: simvx.core.physics.world.BodyHandle, mass: float, *, wake: bool = True) None[source]¶
- set_body_filter(handle: simvx.core.physics.world.BodyHandle, collision_layer: int, collision_mask: int, *, wake: bool = True) None[source]¶
- set_body_material(handle: simvx.core.physics.world.BodyHandle, material: simvx.core.physics.material.PhysicsMaterial | None) None[source]¶
- set_body_damping(handle: simvx.core.physics.world.BodyHandle, linear: float, angular: float) None[source]¶
- set_body_gravity_scale(handle: simvx.core.physics.world.BodyHandle, scale: float) None[source]¶
- set_body_continuous(handle: simvx.core.physics.world.BodyHandle, enabled: bool) None[source]¶
- set_body_shape(handle: simvx.core.physics.world.BodyHandle, shape: simvx.core.physics.world.ShapeHandle, *, wake: bool = True) None[source]¶
- body_velocity(handle: simvx.core.physics.world.BodyHandle) tuple[simvx.core.math.Vec3, simvx.core.math.Vec3][source]¶
- body_mass(handle: simvx.core.physics.world.BodyHandle) float[source]¶
- wake(handle: simvx.core.physics.world.BodyHandle) None[source]¶
- sleep(handle: simvx.core.physics.world.BodyHandle) None[source]¶
- set_body_can_sleep(handle: simvx.core.physics.world.BodyHandle, enabled: bool) None[source]¶
- sleeping(handle: simvx.core.physics.world.BodyHandle) bool[source]¶
- 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_force(handle: simvx.core.physics.world.BodyHandle, force: simvx.core.math.Vec3, *, at: simvx.core.math.Vec3 | None = None) None[source]¶
- apply_torque(handle: simvx.core.physics.world.BodyHandle, torque: simvx.core.math.Vec3) None[source]¶
- create_fixed_joint(a: simvx.core.physics.world.BodyHandle, b: simvx.core.physics.world.BodyHandle) simvx.core.physics.world.JointHandle[source]¶
- 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]¶
- 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]¶
- 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]¶
- remove_joint(handle: simvx.core.physics.world.JointHandle) None[source]¶
- drain_contact_events() list[simvx.core.physics.world.ContactEvent][source]¶
- drain_overlap_events() list[simvx.core.physics.world.OverlapEvent][source]¶
- register_bodies(handles: list[simvx.core.physics.world.BodyHandle]) None[source]¶
- raycast(origin: simvx.core.math.Vec3, direction: simvx.core.math.Vec3, max_dist: float, *, mask: int = 4294967295) simvx.core.physics.world.RaycastHit | None[source]¶
- 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]¶
- 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]¶
Substepped, non-mutating sweep of a body’s shape (basic tier).
Conservative advancement is out of tier scope (the narrow phase is analytic AABB-ish overlap, not a true continuous TOI shape-cast), so this substeps along
motionwith a step count sized from the mover’s smallest feature, capped at 64. The first substep that penetrates a blocking body brackets the contact between the last fraction proved clear and that one, and the bracket is then BISECTED against that blocker alone (_SWEEP_REFINE_STEPShalvings), so the reporteddistanceis a bound good to|motion| / (substeps * 2**8)rather than to one whole substep. It is still a lower bound and may be exactly0.0for a sweep that begins in contact. Fast movers vs very thin colliders can still tunnel BETWEEN substeps, because the bisect refines a bracket the scan found rather than finding brackets the scan missed.Structurally non-mutating: the mover’s shape and pose are wrapped in a TRANSIENT
_Bodyprobe, so nothing in the body table is touched andfrom_transformcosts nothing.skinis accepted and IGNORED: the substep quantum is already larger than any sane skin (see- Attr:
~simvx.core.physics.world.SweepHit.distance).
A body blocks only when it is not the mover, is not a sensor, passes the canonical AND layer/mask rule, and presents a contact that OPPOSES the sweep. Sensors take no part in collision resolution, so one never blocks a sweep and (because the ground and step-up probes route through here) never counts as ground or as a step surface. A non-opposing touch, e.g. the floor a character already rests on while it walks, is a touch and not a blocker: reporting it would halt every slide at distance
0and the mover could never move. Fraction0is never sampled (the substep loop starts ats = 1), so this backend cannot report the cast axis as a normal.
- 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]¶
Substepped shape sweep, earliest-TOI contact (basic tier).
Mirrors :meth:
sweep_body’s substepped scan but with a transient probe shape (not a registered body) and the single query-mask convention (mask & body.layer). Earliest TOI wins: substeps are the outer loop, bodies the inner, so the first penetrating substep is the earliest hit.
- overlap(shape: simvx.core.physics.world.ShapeHandle, transform: object, *, mask: int = 4294967295) list[simvx.core.physics.world.BodyHandle][source]¶
Static shape-vs-body overlap test, all matches (basic tier).
Places a transient probe shape at
transformand returns the handles of every body it overlaps whoselayer & maskis set, sorted by handle for determinism. Single query-mask convention.
- body_transform(handle: simvx.core.physics.world.BodyHandle) tuple[simvx.core.math.Vec3, simvx.core.math.Quat][source]¶
- property gravity: simvx.core.math.Vec3¶
- property solver_iterations: int¶
- property position_iterations: int¶
- property sleep_time_threshold: float¶
- property sleep_velocity_threshold: float¶
- property contact_slop: float¶
- move_and_collide(handle: simvx.core.physics.world.BodyHandle, motion: simvx.core.math.Vec3) simvx.core.physics.world.SweepHit | None¶
- __slots__¶
()
- simvx.core.physics.builtin.world.__all__¶
[‘BuiltinPhysics’]