pub struct PhysicsWorld {Show 13 fields
pub config: SolverConfig,
pub bodies: Vec<RigidBody>,
pub distance_constraints: Vec<DistanceConstraint>,
pub contact_constraints: Vec<ContactConstraint>,
pub sdf_colliders: Vec<SdfCollider>,
pub sdf_collision_radius: Fix128,
pub contact_cache: ContactCache,
pub material_table: MaterialTable,
pub body_materials: Vec<MaterialId>,
pub joints: Vec<Joint>,
pub force_fields: Vec<ForceFieldInstance>,
pub events: EventCollector,
pub islands: IslandManager,
/* private fields */
}Expand description
XPBD physics world with batched constraint solving
PhysicsWorld integrates all physics subsystems:
- Rigid body dynamics with XPBD solver
- Automatic collision detection (BVH broad-phase + sphere narrow-phase)
- Joint constraints (Ball, Hinge, Fixed, Slider, Spring, D6,
ConeTwist) - Force fields (wind, gravity wells, buoyancy, vortex, drag)
- Collision filtering (layer/mask bitmasks)
- Contact events (begin/persist/end)
- Sleeping/island management
Fields§
§config: SolverConfigSolver configuration
bodies: Vec<RigidBody>All rigid bodies in the world
distance_constraints: Vec<DistanceConstraint>Distance constraints between bodies
contact_constraints: Vec<ContactConstraint>Contact constraints from collision detection
sdf_colliders: Vec<SdfCollider>SDF colliders for implicit collision detection
sdf_collision_radius: Fix128Default collision radius for body-vs-SDF queries
contact_cache: ContactCacheContact manifold cache for warm starting
material_table: MaterialTableMaterial pair lookup table
body_materials: Vec<MaterialId>Per-body material IDs
joints: Vec<Joint>Joint constraints (solved each substep alongside distance/contact)
Self::add_joint checks that both body indices are in range. A joint
pushed here directly with an index past the last body makes the next
step panic in the joint solve (index out of bounds), except a joint whose
two indices are equal: the solve skips it, and so do the island rebuilds.
force_fields: Vec<ForceFieldInstance>Force fields applied at the start of each step
events: EventCollectorContact and trigger event collector
islands: IslandManagerIsland manager for sleeping and connectivity tracking
Implementations§
Source§impl PhysicsWorld
impl PhysicsWorld
Sourcepub fn emit_contact_audio(&self, generator: &mut AudioGenerator)
pub fn emit_contact_audio(&self, generator: &mut AudioGenerator)
Feed every contact of the last step to generator
(AudioGenerator::process_contact), in the order of
contact_events, which is deterministic.
One call per touching pair (a pair is reported once per step): a
Begin event is a new contact, a
Persist event a continuing one,
and an End event is skipped. The
relative velocity passed is the event’s normal speed
(ContactEvent::relative_velocity,
measured when the contact was detected, before the solver removed it) along
the event normal, plus the tangential part of the bodies’ current linear
velocity difference v_a − v_b. The contact passed has the event’s point as
point_a and point_a + normal·depth as point_b.
The generator’s events are appended to; call
AudioGenerator::begin_frame first to start a new frame.
Source§impl PhysicsWorld
impl PhysicsWorld
Sourcepub fn time_of_impact(
&self,
body: usize,
velocity: Vec3Fix,
dt: Fix128,
filter: &RayFilter,
) -> Option<TOI>
pub fn time_of_impact( &self, body: usize, velocity: Vec3Fix, dt: Fix128, filter: &RayFilter, ) -> Option<TOI>
The first time in a step at which body body, translating with
velocity for dt, touches any other geometry the world collides with
(body shapes, compound children, static colliders and, with std, SDF
colliders), as a TOI.
- Shape. The body is swept as the sphere of its collision radius about
its position (
crate::solver::PhysicsWorld::cast_sphere). For a body with only a collision radius that is its exact geometry. For a body with acrate::shape::Shapeor a compound collider the radius is the bounding radius about its centre of mass, so the reported time is never later than the true shape’s first contact (it can be earlier). A body with no collision radius collides with nothing:None. - Time.
TOI::tis the fraction of the step,0at the start and1atvelocity · dt: the distance travelled to the contact divided by|velocity · dt|. - Point and normal. The contact point on the other geometry and its
surface normal there, toward the moving body: the convention of
sphere_plane_toi, and ofsphere_sphere_toiwith the obstacle as sphere A and the moving body as sphere B (moving byvelocity · dt). A body that already overlaps something reportst = 0, its own centre as the point and−velocity(normalized) as the normal (seecrate::world_shape_query). - What is seen.
filterdecides, except that the body itself (and any SDF collider attached to it) is always excluded: the filter’sRayFilter::exclude_bodyis replaced bybody. Everything else is taken at its current pose and treated as still; pass a relative velocity for a moving target.
An index out of range, dt ≤ 0 or a zero displacement gives None.
Source§impl PhysicsWorld
impl PhysicsWorld
Sourcepub fn debug_draw(&self, flags: &DebugDrawFlags, data: &mut DebugDrawData)
pub fn debug_draw(&self, flags: &DebugDrawFlags, data: &mut DebugDrawData)
Draw this world into data (debug_draw_world); data is cleared first.
Source§impl PhysicsWorld
impl PhysicsWorld
Sourcepub fn stress_heatmap(&self, dt: Fix128, config: &HeatmapConfig) -> Heatmap
pub fn stress_heatmap(&self, dt: Fix128, config: &HeatmapConfig) -> Heatmap
The stress heatmap of the last step’s contacts: generate_stress_heatmap
over the body positions and contact_forces(dt),
where dt is the frame step passed to step (a dt of zero
carries no force, so the map is all zero).
Source§impl PhysicsWorld
impl PhysicsWorld
Sourcepub fn law_id(&self, semantics_id: &[u8; 32]) -> [u8; 32]
pub fn law_id(&self, semantics_id: &[u8; 32]) -> [u8; 32]
Identifier of the law this world steps by, hashed with semantics_id
(pass PHYSICS_SEMANTICS_ID
for this build).
§What it covers
The rules a step reads, not the state it acts on: the solver config,
the contact cache settings, sdf_collision_radius, continuous
collision, the sleep thresholds, the material table, the force fields
and the registered participants. The encoding below lists every field.
Not covered:
- the state: bodies, constraints and joints, including joint motors (their gains and targets live on the joint, so they are state);
- the broadphase kind and the sleep skip, since every choice gives the same bits;
- the caller-supplied code a world can hold: pre-solve hooks, contact modifiers, SDF colliders and an installed GPU solver bridge.
config.warm_start_factor is covered although the CPU step does not
read it: it is passed to an installed GPU solver bridge, whose result
may depend on it.
§Guarantee, and its limit
Two worlds with the same identifier, the same state and the same
inputs step to the same bits, provided the hooks, modifiers, SDF
colliders and bridge they hold also behave the same, since those are
outside the identifier. A participant contributes only its kind, step
rule and ports: two participants of one kind that differ in their
coefficients get the same identifier until participants expose their
coefficients. The converse does not hold either: worlds whose laws step
identically may still differ here (for example two force fields
declared in the other order), so this is not a deduplication key.
One case of this: config.solver_backend is covered, but
step_parallel, try_step_parallel and the GPU bridge
paths run XPBD whatever it says, so on those paths two worlds that
differ only in the backend have different identifiers and step to the
same bits.
§Encoding
SHA-256 over the concatenation below, which an independent
implementation can reproduce byte for byte (the layout of the law
identifiers of alice-zip). Primitive encodings:
| Value | Bytes |
|---|---|
length, count, usize | u64 big-endian |
u32 | 4 bytes big-endian |
MaterialId (u16) | widened to u64, big-endian |
bool | one byte, 0 or 1 |
Fix128 | raw high word (i64) then low word (u64), big-endian |
Vec3Fix | x, y, z as Fix128 |
| byte string, enum value | length, then the bytes; an enum value is written as the tag given in the table |
| list | count, then the items |
Option<T> | bool (1 for Some), then T when present |
Fields, in order:
| # | Field | Encoding |
|---|---|---|
| 1 | LAW_ID_DOMAIN | byte string |
| 2 | WORLD_LAW_KIND | byte string |
| 3 | semantics_id | the 32 bytes, no length |
| 4 | config.substeps, config.iterations | u64, u64 |
| 5 | config.gravity, config.damping | Vec3Fix, Fix128 |
| 6 | config.solver_backend | tag xpbd or tgs |
| 7 | config.warm_start_factor | Fix128 |
| 8 | contact cache warm_start_factor, max_stale_frames | Fix128, u32 |
| 9 | sdf_collision_radius | Fix128 |
| 10 | continuous collision: enabled, motion threshold | bool, Fix128 |
| 11 | sleep linear_threshold, angular_threshold, frames_to_sleep | Fix128, Fix128, u32 |
| 12 | default friction and restitution combine rules | tag average, min, max or multiply, twice |
| 13 | materials, in id order | list of: id (MaterialId), static_friction, dynamic_friction, restitution (Fix128 each), friction combine, restitution combine (tags) |
| 14 | pair overrides, sorted by (mat_a, mat_b) with mat_a <= mat_b | list of: mat_a, mat_b (MaterialId), friction, restitution (Fix128) |
| 15 | force fields, in the order they were added | list of: enabled (bool), the bodies it is limited to (Option of a list of u64, sorted ascending without duplicates), then the field |
| 16 | participants, in registration order | list of: kind (u32), step rule, ports |
A field is its tag followed by its parameters in declaration order:
directional (direction Vec3Fix, strength), point (center
Vec3Fix, strength, repulsive bool, max_force), drag
(coefficient), buoyancy (surface_y, density, drag), vortex (center
Vec3Fix, axis Vec3Fix, strength, falloff_radius), explosion
(center Vec3Fix, strength, radius, falloff_power), magnetic
(position Vec3Fix, moment Vec3Fix, strength); every parameter not
marked otherwise is a Fix128. A step rule is the tag
follow-substep, fixed followed by its dt (Fix128), or
subcycle. A port is its id (u32) followed by the tag read,
write or read-committed.
Sorting the pair overrides and the body lists keeps the identifier independent of the order they were registered in, which the step does not depend on either. Force fields and participants keep their order, since the step applies them in that order.
Source§impl PhysicsWorld
impl PhysicsWorld
Sourcepub fn sdf_ccd_hits(
&self,
dt: Fix128,
config: &SdfCcdConfig,
) -> Vec<(usize, usize, TOI)>
pub fn sdf_ccd_hits( &self, dt: Fix128, config: &SdfCcdConfig, ) -> Vec<(usize, usize, TOI)>
Sweep the world’s moving bodies against its SDF colliders.
A body is swept as a sphere of PhysicsWorld::sdf_collision_radius
(the radius PhysicsWorld::sdf_contacts gives a body without a shape)
along velocity * dt; this is the continuous counterpart of the
discrete push-out in PhysicsWorld::step, to be asked before a step to
learn which fast bodies would tunnel through a thin SDF. Static bodies,
sensors and bodies slower than config.velocity_threshold are skipped,
and an SDF attached to the body itself is never tested against it. Hits
come back as in batch_sphere_trace_sdf, earliest time of impact first.
An SDF attached to a body is swept at the pose it was given when it was
added or at the end of the last step (see
PhysicsWorld::sdf_contacts).
dt is the frame step.
Source§impl PhysicsWorld
impl PhysicsWorld
Sourcepub fn sdf_field(&self) -> WorldSdfField<'_>
pub fn sdf_field(&self) -> WorldSdfField<'_>
The world’s SDF colliders as one field (WorldSdfField), for any
SdfCharacter query: SdfCharacter::move_and_slide,
SdfCharacter::step_on_sphere, SdfCharacter::ground_contact, ….
Sourcepub fn move_sdf_character(
&self,
ch: &mut SdfCharacter,
dt: f32,
control: [f32; 3],
) -> MoveOutcome
pub fn move_sdf_character( &self, ch: &mut SdfCharacter, dt: f32, control: [f32; 3], ) -> MoveOutcome
Advance ch one frame against the world’s SDF colliders:
SdfCharacter::step on PhysicsWorld::sdf_field,
with the same arguments and result. dt is passed through unchanged, as
SdfCharacter::step takes it (dt = 0 moves by control only).
The character sees only the SDF colliders, not bodies or static
colliders; for a capsule against everything the world collides with see
PhysicsWorld::move_character.
Source§impl PhysicsWorld
The sensors read through the world: each method passes the world to the
sensor’s own method and returns what it returns, advancing the sensor’s state
(noise stream, IMU reference velocity) the same way.
impl PhysicsWorld
The sensors read through the world: each method passes the world to the sensor’s own method and returns what it returns, advancing the sensor’s state (noise stream, IMU reference velocity) the same way.
Sourcepub fn lidar_scan(&self, lidar: &mut Lidar) -> LidarScan
pub fn lidar_scan(&self, lidar: &mut Lidar) -> LidarScan
One scan of lidar from its pose in the world (Lidar::scan).
Sourcepub fn lidar_scan_from_body(
&self,
lidar: &mut Lidar,
body: usize,
) -> Option<LidarScan>
pub fn lidar_scan_from_body( &self, lidar: &mut Lidar, body: usize, ) -> Option<LidarScan>
One scan of lidar mounted on body, not seeing that body
(Lidar::scan_from_body). None for an index that is not a body.
Sourcepub fn read_contact_sensor(
&self,
sensor: &mut ContactSensor,
dt: Fix128,
) -> ContactReading
pub fn read_contact_sensor( &self, sensor: &mut ContactSensor, dt: Fix128, ) -> ContactReading
The contact-sensor reading after a step of dt (ContactSensor::read).
Sourcepub fn sample_imu(&self, imu: &mut Imu, dt: Fix128) -> Option<ImuReading>
pub fn sample_imu(&self, imu: &mut Imu, dt: Fix128) -> Option<ImuReading>
The IMU reading after a step of dt (Imu::sample): None when the
body is gone or dt ≤ 0.
Source§impl PhysicsWorld
impl PhysicsWorld
Sourcepub fn ray_caster(&self, filter: RayFilter) -> WorldRayCaster<'_>
pub fn ray_caster(&self, filter: RayFilter) -> WorldRayCaster<'_>
A ray caster over this world’s current state with filter; the body BVH is
built here, once.
Sourcepub fn cast_ray(
&self,
origin: Vec3Fix,
direction: Vec3Fix,
max_t: Fix128,
filter: &RayFilter,
) -> Option<WorldRayHit>
pub fn cast_ray( &self, origin: Vec3Fix, direction: Vec3Fix, max_t: Fix128, filter: &RayFilter, ) -> Option<WorldRayHit>
The nearest hit of a ray with the world’s geometry (see crate::shape_raycast).
Sourcepub fn cast_ray_all(
&self,
origin: Vec3Fix,
direction: Vec3Fix,
max_t: Fix128,
filter: &RayFilter,
) -> Vec<WorldRayHit>
pub fn cast_ray_all( &self, origin: Vec3Fix, direction: Vec3Fix, max_t: Fix128, filter: &RayFilter, ) -> Vec<WorldRayHit>
Every collider the ray hits, one hit each, nearest first.
Source§impl PhysicsWorld
impl PhysicsWorld
Sourcepub fn move_character(
&self,
ctrl: &mut CharacterController,
displacement: Vec3Fix,
) -> MoveResult
pub fn move_character( &self, ctrl: &mut CharacterController, displacement: Vec3Fix, ) -> MoveResult
Move ctrl by displacement, sliding along everything the world collides
with (see crate::world_character), with the default RayFilter.
Updates the controller’s position, grounded state, ground body and platform
velocity as CharacterController::move_and_slide does, and returns them.
Sourcepub fn move_character_with_filter(
&self,
ctrl: &mut CharacterController,
displacement: Vec3Fix,
filter: &RayFilter,
) -> MoveResult
pub fn move_character_with_filter( &self, ctrl: &mut CharacterController, displacement: Vec3Fix, filter: &RayFilter, ) -> MoveResult
PhysicsWorld::move_character seeing only what filter sees (for
instance RayFilter::excluding_body for the character’s own body).
Source§impl PhysicsWorld
impl PhysicsWorld
Sourcepub fn cast_sphere(
&self,
center: Vec3Fix,
radius: Fix128,
direction: Vec3Fix,
max_t: Fix128,
filter: &RayFilter,
) -> Option<WorldShapeHit>
pub fn cast_sphere( &self, center: Vec3Fix, radius: Fix128, direction: Vec3Fix, max_t: Fix128, filter: &RayFilter, ) -> Option<WorldShapeHit>
The nearest collider a sphere of radius touches when its centre moves
from center along direction for at most max_t (see
crate::world_shape_query for the conventions). Radius 0 is
PhysicsWorld::cast_ray; a negative radius, a zero direction or
max_t ≤ 0 gives None.
Sourcepub fn cast_capsule(
&self,
a: Vec3Fix,
b: Vec3Fix,
radius: Fix128,
direction: Vec3Fix,
max_t: Fix128,
filter: &RayFilter,
) -> Option<WorldShapeHit>
pub fn cast_capsule( &self, a: Vec3Fix, b: Vec3Fix, radius: Fix128, direction: Vec3Fix, max_t: Fix128, filter: &RayFilter, ) -> Option<WorldShapeHit>
The nearest collider a capsule (segment a–b grown by radius) touches
when it moves along direction for at most max_t. Coinciding ends are
PhysicsWorld::cast_sphere; a negative radius, a zero direction or
max_t ≤ 0 gives None.
Sourcepub fn overlap_sphere(
&self,
center: Vec3Fix,
radius: Fix128,
filter: &RayFilter,
) -> Vec<RayTarget>
pub fn overlap_sphere( &self, center: Vec3Fix, radius: Fix128, filter: &RayFilter, ) -> Vec<RayTarget>
Every collider a sphere of radius about center overlaps, sorted by
target. A negative radius gives an empty list; radius 0 finds the solids
center is strictly inside.
Sourcepub fn overlap_aabb(&self, aabb: &AABB, filter: &RayFilter) -> Vec<RayTarget>
pub fn overlap_aabb(&self, aabb: &AABB, filter: &RayFilter) -> Vec<RayTarget>
Every collider aabb overlaps, sorted by target (see
crate::world_shape_query). A box with min > max on any axis gives an
empty list.
Source§impl PhysicsWorld
impl PhysicsWorld
Sourcepub fn add_participant(
&mut self,
participant: Box<dyn Participant>,
) -> Result<usize, RegisterError>
pub fn add_participant( &mut self, participant: Box<dyn Participant>, ) -> Result<usize, RegisterError>
Register participant; it runs in every substep from the next step on.
Returns its registration index.
The participant’s ports (Participant::ports) are read once, here,
and checked with the ports of the participants already registered and
the world’s fields (ParticipantPlan::new). A
StepRule::Fixed step is checked against the substep at every step
(StepError::Rule), because the substep is only known then.
§Errors
RegisterError::NonPositiveStep for StepRule::Fixed(Δt ≤ 0),
RegisterError::Order when the ports would form a loop,
RegisterError::Field when they do not fit the fields. The world is
unchanged.
Sourcepub fn participant_count(&self) -> usize
pub fn participant_count(&self) -> usize
Number of registered participants.
Sourcepub fn participant_kinds(&self) -> Vec<ParticipantKind>
pub fn participant_kinds(&self) -> Vec<ParticipantKind>
The kinds of the registered participants, in registration order.
Sourcepub fn participant_state(&self, index: usize) -> Option<Vec<u8>>
pub fn participant_state(&self, index: usize) -> Option<Vec<u8>>
The state of participant index as its Participant::write_state
writes it (the payload a snapshot holds), or None for an index past
the last participant.
Sourcepub fn observe_participant(
&self,
index: usize,
) -> Option<Observed<ObservationSink>>
pub fn observe_participant( &self, index: usize, ) -> Option<Observed<ObservationSink>>
What participant index reports (Participant::observe):
Observed::Undecided while a fault is recorded, None for an index
past the last participant.
Sourcepub const fn fault(&self) -> Option<WorldFault>
pub const fn fault(&self) -> Option<WorldFault>
The first fault recorded, if any. While one is recorded,
Self::try_step refuses to run (StepError::Faulted) and the
checked observations are undecided. It is part of the snapshot.
Sourcepub fn clear_fault(&mut self)
pub fn clear_fault(&mut self)
Forget the recorded fault so that steps run again. The world’s
overflow flag (Self::overflow_detected) is not touched.
Sourcepub fn observe_body_checked(
&self,
body_idx: usize,
) -> Option<Observed<BodyObservation>>
pub fn observe_body_checked( &self, body_idx: usize, ) -> Option<Observed<BodyObservation>>
Self::observe_body, or Observed::Undecided while a fault is
recorded or the overflow flag is up (the values may have left the
range of Fix128); None for an index past the last body.
Sourcepub const fn fields(&self) -> &FieldBoard
pub const fn fields(&self) -> &FieldBoard
The world’s shared fields.
Sourcepub fn declare_field(
&mut self,
id: PortId,
layout: FieldLayout,
mode: FieldMode,
) -> Result<(), DeclareFieldError>
pub fn declare_field( &mut self, id: PortId, layout: FieldLayout, mode: FieldMode, ) -> Result<(), DeclareFieldError>
Declare shared field id, all samples zero. Declare a field before
the participants that use it.
§Errors
DeclareFieldError::Field when the board refuses it,
DeclareFieldError::Ports when the registered participants’ ports
no longer fit with it. The world is unchanged.
Sourcepub fn set_field(
&mut self,
id: PortId,
values: &[Fix128],
) -> Result<(), FieldError>
pub fn set_field( &mut self, id: PortId, values: &[Fix128], ) -> Result<(), FieldError>
Set the committed value of field id (an initial condition, between
steps).
§Errors
FieldError::Unknown or FieldError::Length; the field is
unchanged.
Sourcepub fn try_step(&mut self, dt: Fix128) -> Result<(), StepError>
pub fn try_step(&mut self, dt: Fix128) -> Result<(), StepError>
One step of dt, refusing to run when it cannot run normally.
In order: while a fault is recorded the step is refused
(StepError::Faulted) and the world is unchanged; a non-positive
dt changes nothing and is Ok; every participant’s step rule is
checked against the substep width h the participants are handed
(dt / substeps, or the TGS width under
super::SolverBackend::Tgs, see the module documentation and
try_step_parallel (feature parallel); StepError::Rule) and every
FieldLayout::PerBody field against the body count
(StepError::BodyCount), refused unchanged. Then the step runs (see
the module documentation of crate::world_participant and
Self::step); when a fault was recorded during it the step still
ran to the end and StepError::FaultRaised reports it.
With no participant registered this is Self::step as it always
was, bit for bit.
§Errors
See above.
Sourcepub fn try_step_parallel(&mut self, dt: Fix128) -> Result<(), StepError>
pub fn try_step_parallel(&mut self, dt: Fix128) -> Result<(), StepError>
Self::try_step on the batched (parallel) path of
Self::step_parallel. Participants are called one after the other,
as in Self::try_step. This path runs the XPBD substep loop whatever
super::SolverBackend is configured (with super::SolverBackend::Tgs
it gives the same bits as with Xpbd; a batched TGS path does not exist
yet), so the participants are handed
h = dt / substeps and step rules are checked against that width, also
under super::SolverBackend::Tgs.
§Errors
As Self::try_step.
Source§impl PhysicsWorld
impl PhysicsWorld
Sourcepub fn set_continuous_collision(&mut self, config: WorldCcdConfig)
pub fn set_continuous_collision(&mut self, config: WorldCcdConfig)
Set continuous collision for the world step. Off by default.
Sourcepub fn continuous_collision(&self) -> WorldCcdConfig
pub fn continuous_collision(&self) -> WorldCcdConfig
The continuous collision setting of the world step.
Source§impl PhysicsWorld
impl PhysicsWorld
Sourcepub const WORLD_SNAPSHOT_MAGIC: [u8; 4]
pub const WORLD_SNAPSHOT_MAGIC: [u8; 4]
Magic of a Self::snapshot_world blob (b"APWS", distinct from
Self::STATE_MAGIC of the rollback blob).
Sourcepub const WORLD_SNAPSHOT_VERSION: u16 = 4
pub const WORLD_SNAPSHOT_VERSION: u16 = 4
Format version of a Self::snapshot_world blob. Version 1 blobs
(no participants, fault or fields section) are still read, as a
world without participants, fault or fields, version 1 and 2 blobs
(no continuous_collision section) as a world with continuous
collision off, and version 1 to 3 blobs (no semantics_id / law_id
in the header) without the semantics check, reported as
LawCheck::Unpinned by Self::restore_world_checked; a blob of
any other version is rejected with
WorldSnapshotError::UnsupportedVersion.
Sourcepub fn snapshot_world(&self) -> Vec<u8> ⓘ
pub fn snapshot_world(&self) -> Vec<u8> ⓘ
Write every piece of state Self::step reads into one versioned blob
with a checksum.
Restore it with Self::from_world_snapshot (into a new world) or
Self::restore_world (into an existing one, keeping its SDF fields
and callbacks). Every later step of the restored world is
bit-identical to the original’s.
§Examples
use alice_physics::{Fix128, PhysicsConfig, PhysicsWorld, RigidBody, Vec3Fix};
let mut world = PhysicsWorld::new(PhysicsConfig::default());
world.add_body(RigidBody::new_dynamic(Vec3Fix::from_int(0, 5, 0), Fix128::ONE));
let dt = Fix128::from_ratio(1, 60);
world.step_n(10, dt);
let blob = world.snapshot_world();
let mut branch = PhysicsWorld::from_world_snapshot(&blob).unwrap();
world.step_n(30, dt);
branch.step_n(30, dt);
assert_eq!(world.snapshot_world(), branch.snapshot_world());§Field coverage
Every field of PhysicsWorld, classified. “Saved” fields are written
byte-exact; “rebuilt” fields are derived on restore from saved ones;
“not covered” fields cannot be written as data, and restore instead
requires the target world to already hold the same number of them.
| field | class | how / why |
|---|---|---|
config | saved | all of SolverConfig |
bodies | saved | every RigidBody field, including prev_*, damping, kinematic_target |
distance_constraints | saved | including cached_lambda |
contact_constraints | saved | including cached_lambda |
sdf_colliders (pose) | saved | position / rotation / scale / body_index and the cached inv_rotation / scale_f32 / inv_scale_f32 as stored (not recomputed) |
sdf_colliders (field) | not covered | Box<dyn SdfField> is code; the target world keeps its own fields, and the count must match (WorldSnapshotError::SdfFieldCountMismatch) |
sdf_collision_radius | saved | |
static_colliders | saved | plane / height field / triangle mesh including its BVH nodes as stored |
constraint_batches / batches_dirty / batch_static_bodies | saved | as stored, so a stale-but-clean batch set is reproduced rather than recomputed |
contact_cache (manifolds, max_stale_frames, warm_start_factor) | saved | |
contact_cache (pair_index, std) | rebuilt | from the manifolds in order, the same way ContactCache::end_frame rebuilds it |
material_table / body_materials | saved | materials, pair overrides, default combine rules |
pre_solve_hooks / contact_modifiers (std) | not covered | closures / trait objects; the count is recorded and must match (WorldSnapshotError::CallbackCountMismatch) |
gpu_solver_bridge (feature gpu-solver-bridge) | not covered | trait object; “installed or not” is recorded and must match |
joints | saved | all seven kinds, every field |
joint_motors / joint_motors_3d | not covered | the restore target keeps its own motors (add them with PhysicsWorld::add_joint_motor / PhysicsWorld::add_joint_motor_3d before restoring), as the format predates them; a motor’s gains and targets that change between frames are saved next to the snapshot |
force_fields | saved | field, affected_bodies, enabled |
events | saved | this frame’s events and the previous / current pair and trigger sets that decide begin / persist / end |
islands (sleep_data, config) | saved | |
islands (union-find parent / rank) | rebuilt | IslandManager::new plus a union of every joint’s bodies, the same as the start of every step |
body_collision_radii / body_filters | saved | as stored, including their own lengths |
broadphase / broadphase_tree / broadphase_proxies | saved | the persistent tree node by node (pair order follows the tree layout) |
broadphase_hybrid | rebuilt | empty; its pairs are a pure function of the bodies staged each substep, so a fresh one gives the same pairs |
body_colliders | saved | shape or compound, including the compound’s cached AABB and dirty flag |
overflow_detected | saved | sticky flag |
kinematic_substeps_left | rebuilt | always 0 outside step (reset at the end of every step), and a snapshot is only taken between steps |
tgs_impulse_cache (std) | saved | entries sorted by id, hit / miss counters; the live set is rebuilt empty (the sweep at the end of every step empties it) |
participants (std) | saved / not covered | each participant’s kind and its crate::world_participant::Participant::write_state payload are saved; the participant itself is code, so the target world must hold the same kinds in the same order (WorldSnapshotError::ParticipantMismatch), and every payload is checked before any is read (WorldSnapshotError::ParticipantState) |
participant_plan (std) | not covered | derived from the target world’s own participants and fields |
fault | saved | the recorded fault, so a restored branch is still faulted |
fields | saved | the committed values once (crate::world_participant::FieldBoard::write_values); the target world must declare the same fields (WorldSnapshotError::FieldState) |
ccd | saved | PhysicsWorld::continuous_collision (version 3) |
Not in the table because they are not PhysicsWorld fields:
motors the caller applies itself with crate::motor::apply_motors
and crate::character::CharacterController are owned by the caller and
are applied from outside step, and cloth / fluid / FEM / vehicle state
lives in its own types. Save them next to the world snapshot.
Known difference: the union-find of islands is rebuilt from the joints. After a
remove_joint between two steps, the original still carries that union
until the next step resets it, while the restored world does not; the
two differ only for PhysicsWorld::wake_body called before that next
step. The structure is private to crate::sleeping and has no
non-mutating accessor, so it cannot be copied from &self.
§Format (version 4)
| range | content |
|---|---|
[0..4) | magic PhysicsWorld::WORLD_SNAPSHOT_MAGIC (b"APWS") |
[4..6) | version u16 = PhysicsWorld::WORLD_SNAPSHOT_VERSION |
[6..8) | reserved u16 = 0 |
[8..16) | payload length u64 |
[16..48) | semantics_id: crate::PHYSICS_SEMANTICS_ID of the writing binary |
[48..80) | law_id: PhysicsWorld::law_id of this world under that semantics_id |
[80..80+len) | payload (the sections in the table above, little endian) |
| last 8 | FNV-1a 64 of every preceding byte (header included) |
Version 1 to 3 blobs have the 16-byte header only (no semantics_id
or law_id) and their payload starts at byte 16; the payload itself
is the same in version 3 and 4. Version 4 grows the header rather than
using the reserved bytes, which stay 0.
The header law_id is the rule of the world when it was written. It
is not guaranteed to equal PhysicsWorld::law_id of a world after
restore: the restored world keeps what the blob does not hold (for
example its own participants), so whatever PhysicsWorld::law_id
reads from those comes from the target world. The reader does not
recompute it; Self::restore_world_checked compares the header
value with the one the caller expects.
Version 2 appends three sections to the version 1 payload, after the TGS cache, in this order:
| section | content |
|---|---|
participants | count: u64, then per participant kind: u32, payload_len: u64, payload |
fault | u8 code: 0 none, 1 participant (index: u64, kind: u32, fault tag u8), 2 rigid overflow, 3 force out of range (body: u64), 4 field out of range (field: u32, index: u64) |
fields | crate::world_participant::FieldBoard::write_values |
Version 3 appends one section after fields:
| section | content |
|---|---|
continuous_collision | enabled: u8 (0 / 1), motion_threshold (PhysicsWorld::continuous_collision) |
The version is raised for it because the reader rejects trailing bytes: a section appended to a version 2 blob could not be told apart from a corrupt one.
Sourcepub fn from_world_snapshot(data: &[u8]) -> Result<Self, WorldSnapshotError>
pub fn from_world_snapshot(data: &[u8]) -> Result<Self, WorldSnapshotError>
Build a new world from a Self::snapshot_world blob.
The new world holds no SDF fields, hooks, contact modifiers or GPU
bridge, so a snapshot taken with any of those is rejected
(WorldSnapshotError::SdfFieldCountMismatch /
WorldSnapshotError::CallbackCountMismatch); use
Self::restore_world on a world that holds them instead.
Rotations are restored as stored, as in Self::restore_world.
§Errors
See WorldSnapshotError for which input gives which error.
Sourcepub fn restore_world(&mut self, data: &[u8]) -> Result<(), WorldSnapshotError>
pub fn restore_world(&mut self, data: &[u8]) -> Result<(), WorldSnapshotError>
Replace this world’s state with a Self::snapshot_world blob.
Everything in the blob replaces the current state, including the
SolverConfig and the body population. What is not data stays:
the SDF fields of self.sdf_colliders (paired with the blob’s poses in
order), the pre-solve hooks, contact modifiers and the installed GPU
bridge; their counts must equal the snapshot’s.
Body and SDF collider rotations are restored as stored and brought to
unit length by the next step, so a non-unit rotation in data scales
what it rotates by |q|^2 in the queries made before that step (see
RigidBody::rotation). The next step also rebuilds each collider’s
cached inverse rotation and scale from its restored fields
(crate::sdf_collider::SdfCollider::update_cache).
§Errors
See WorldSnapshotError for which input gives which error. On error self is unchanged.
Sourcepub fn restore_world_checked(
&mut self,
data: &[u8],
expected_law_id: Option<&[u8; 32]>,
) -> Result<LawCheck, WorldSnapshotError>
pub fn restore_world_checked( &mut self, data: &[u8], expected_law_id: Option<&[u8; 32]>, ) -> Result<LawCheck, WorldSnapshotError>
Self::restore_world, also comparing the blob’s rule identifier
with expected_law_id and reporting what could be verified.
| blob | expected_law_id | result |
|---|---|---|
version 4, header semantics_id is not crate::PHYSICS_SEMANTICS_ID | any | WorldSnapshotError::SemanticsMismatch |
| version 4 | Some(id), header law_id == id | LawCheck::Verified |
| version 4 | Some(id), header law_id != id | WorldSnapshotError::LawIdMismatch |
| version 4 | None | LawCheck::SemanticsVerified |
| version 1 to 3 | any (not compared) | LawCheck::Unpinned |
expected_law_id is compared with the header as written, not with
Self::law_id of the restored world (see the format notes on
Self::snapshot_world). To accept only verified blobs, refuse
anything but LawCheck::Verified.
§Intended use
Pass the target world’s own self.law_id(&PHYSICS_SEMANTICS_ID),
computed before the restore, as expected_law_id: the blob is then
accepted only if it was written under the rule this world already
runs. Passing the blob header’s own law_id compares the blob with
itself and always gives LawCheck::Verified, which checks nothing.
The target’s participants are kept by the restore (they are code, not
data), so a target whose participant has the same kind but another
step rule is refused this way, while a plain Self::restore_world
accepts it.
LawCheck::Verified then means that the target was already built
with the same rules as the blob before the restore. The restore
overwrites the configuration, but the target’s own law_id is taken
before that and covers the whole configuration, so a target that
differs only in configuration (gravity, materials, force fields, and
so on) gets WorldSnapshotError::LawIdMismatch: the check is
strict and never accepts a blob written under other rules.
§Examples
use alice_physics::{
Fix128, LawCheck, PhysicsConfig, PhysicsWorld, RigidBody, Vec3Fix,
WorldSnapshotError, PHYSICS_SEMANTICS_ID,
};
let mut world = PhysicsWorld::new(PhysicsConfig::default());
world.add_body(RigidBody::new_dynamic(Vec3Fix::from_int(0, 5, 0), Fix128::ONE));
let blob = world.snapshot_world();
// A target running the same rule: its own law id, taken before the restore
let mut same = PhysicsWorld::new(PhysicsConfig::default());
let expected = same.law_id(&PHYSICS_SEMANTICS_ID);
assert_eq!(same.restore_world_checked(&blob, Some(&expected)), Ok(LawCheck::Verified));
// A target running another rule refuses the blob and keeps its state
let mut other = PhysicsWorld::new(PhysicsConfig {
gravity: Vec3Fix::from_int(0, -3, 0),
..PhysicsConfig::default()
});
let expected = other.law_id(&PHYSICS_SEMANTICS_ID);
let before = other.snapshot_world();
assert!(matches!(
other.restore_world_checked(&blob, Some(&expected)),
Err(WorldSnapshotError::LawIdMismatch { .. })
));
assert_eq!(other.snapshot_world(), before);
// Without an expected id only the stepping semantics is checked
assert_eq!(other.restore_world_checked(&blob, None), Ok(LawCheck::SemanticsVerified));§Errors
Every error of Self::restore_world, and
WorldSnapshotError::LawIdMismatch. On error self is unchanged.
Source§impl PhysicsWorld
impl PhysicsWorld
Sourcepub const STATE_MAGIC: [u8; 4]
pub const STATE_MAGIC: [u8; 4]
Self::serialize_state blob の magic
⚠️ 旧 format は先頭が body 数だったので、magic を検査しないと 新実装が旧 blob を誤って解釈する
Sourcepub const STATE_VERSION: u16 = 3
pub const STATE_VERSION: u16 = 3
Self::serialize_state blob の format version
v1 = header 12 + body ごと 208 + body ごと sleep 5
v2 = v1 + world ごと overflow flag 1 byte (doctrine B-12)
v3 = v2 + world ごと population fingerprint 8 byte
(Self::population_fingerprint、rollback の body 数不一致検査強化)
被覆を足す時はここを上げ、Self::deserialize_state で分岐する
(不一致を silent に読み替えない — 旧 version の blob は false で拒否、
v1 / v2 の blob は v3 実装に対して version 不一致で拒否される、
既存の v1→v2 bump と同じ方針)
Sourcepub fn new(config: SolverConfig) -> Self
pub fn new(config: SolverConfig) -> Self
Create a new, empty physics world with the given solver configuration.
§Examples
use alice_physics::{Fix128, PhysicsConfig, PhysicsWorld, RigidBody, Vec3Fix};
let config = PhysicsConfig::default();
let mut world = PhysicsWorld::new(config);
// World starts with no bodies
assert_eq!(world.bodies.len(), 0);
// Add a body and verify it is tracked
let body = RigidBody::new_dynamic(Vec3Fix::from_int(0, 5, 0), Fix128::ONE);
let id = world.add_body(body);
assert_eq!(id, 0);
assert_eq!(world.bodies.len(), 1);Sourcepub fn reset_world(&mut self)
pub fn reset_world(&mut self)
Reset the world to the same empty state Self::new produces,
keeping the current SolverConfig.
§World Auditor の reset() 契約 (WM-07)
「同じ初期状態から N step を 2 回実行すると bit 一致する」を成立させる
ための入口 *self = Self::new(self.config) と等価 (config は
Copy) — bodies / joints / force_fields / islands /
overflow_detected を含む 全 field が new と同じ既定値に戻る
(installed hook / GPU bridge も含む、呼び出し側が明示的に残したい
状態があれば reset_world の前に退避すること)
Self::deserialize_state による rollback とは異なる経路 — rollback
は「population を caller が replay で合わせてから特定 frame の連続状態を
復元する」契約だが、reset_world は「population ごと空にする」契約
両者は 「population を目標状態まで合わせてから連続状態を復元する」
という同じ形の特殊例 (reset_world は目標 population が空の場合)
§Examples
use alice_physics::{Fix128, PhysicsConfig, PhysicsWorld, RigidBody, Vec3Fix};
let config = PhysicsConfig::default();
let mut world = PhysicsWorld::new(config);
world.add_body(RigidBody::new_dynamic(Vec3Fix::from_int(0, 5, 0), Fix128::ONE));
assert_eq!(world.bodies.len(), 1);
world.reset_world();
assert_eq!(world.bodies.len(), 0);
assert!(!world.overflow_detected());Sourcepub fn set_gpu_solver_bridge(
&mut self,
bridge: Option<Box<dyn GpuSolverBridge + Send + Sync>>,
)
pub fn set_gpu_solver_bridge( &mut self, bridge: Option<Box<dyn GpuSolverBridge + Send + Sync>>, )
v0.11.0: install a GPU solver bridge for automatic contact-solve
routing. Subsequent calls to Self::step / the substep loop
route contact-solve through this bridge instead of the CPU
solver. Pass Some(bridge) to install; use
Self::take_gpu_solver_bridge to remove.
The bridge is stored as Box<dyn GpuSolverBridge + Send + Sync>,
so implementers must satisfy Send + Sync. For ALICE-TRT v3.0.0+
this is guaranteed because TrtSolverAdapter uses
Arc<GpuDevice> and holds only Send + Sync fields.
Callers who want explicit control without installing the bridge
on the world (game-engine wrapper hosts, hot-swap harnesses)
can continue to use Self::step_with_bridge /
Self::substep_with_bridge /
Self::solve_contact_constraints_with_bridge with an
externally-owned bridge — those methods ignore the installed
bridge and take a &mut B parameter.
Sourcepub fn take_gpu_solver_bridge(
&mut self,
) -> Option<Box<dyn GpuSolverBridge + Send + Sync>>
pub fn take_gpu_solver_bridge( &mut self, ) -> Option<Box<dyn GpuSolverBridge + Send + Sync>>
v0.11.0: remove and return the currently installed GPU solver
bridge, if any. Subsequent calls to Self::step /
the substep loop revert to the CPU contact solver.
Sourcepub const fn gpu_solver_bridge_installed(&self) -> bool
pub const fn gpu_solver_bridge_installed(&self) -> bool
v0.11.0: whether a GPU solver bridge is currently installed on this world. Cheap; does not touch the bridge.
Sourcepub fn add_body(&mut self, body: RigidBody) -> usize
pub fn add_body(&mut self, body: RigidBody) -> usize
Add rigid body, returns index
The body’s rotations are stored as unit quaternions (see
RigidBody::set_rotation).
Sourcepub fn add_body_with_radius(&mut self, body: RigidBody, radius: Fix128) -> usize
pub fn add_body_with_radius(&mut self, body: RigidBody, radius: Fix128) -> usize
Add rigid body with a collision sphere radius for automatic detection.
Bodies with a collision radius participate in BVH broad-phase and
sphere-sphere narrow-phase collision detection during step().
The body’s rotations are stored as unit quaternions, as in
Self::add_body.
Sourcepub fn add_shaped_body(
&mut self,
shape: &Shape,
density: Fix128,
position: Vec3Fix,
) -> Result<usize, ShapeError>
pub fn add_shaped_body( &mut self, shape: &Shape, density: Fix128, position: Vec3Fix, ) -> Result<usize, ShapeError>
Add a rigid body shaped like shape, made of material of density, with
its centre of mass at position.
The body gets the shape’s mass (density × volume), the shape’s principal
inertia about the centre of mass (not the unit-sphere inertia
RigidBody::new assigns), and the shape’s bounding sphere about the
centre of mass as its collision radius, so it takes part in the sphere
broad- and narrow-phase like any body added with
Self::add_body_with_radius. See crate::shape for the frames.
§Errors
ShapeError when the density is not positive,
a dimension is not, or the mass or inertia does not fit Fix128. Nothing is
added in that case.
Sourcepub fn set_body_shape(&mut self, body_idx: usize, shape: &Shape) -> bool
pub fn set_body_shape(&mut self, body_idx: usize, shape: &Shape) -> bool
Give an existing body a convex shape as its collider: its collision radius
becomes the shape’s bounding radius about the centre of mass, and a pair of
bodies of which either carries a shape is decided by the shape
(crate::collider::contact; against a body without one, by its collision
sphere) instead of by the bounding spheres. Returns false for an out-of-range index.
The body’s position is taken as the shape’s centre of mass (see
crate::shape); its mass and inertia are not changed — use
Self::add_shaped_body for a body whose mass properties come from the shape.
Sourcepub fn colliders_overlap(&self, a: usize, b: usize) -> bool
pub fn colliders_overlap(&self, a: usize, b: usize) -> bool
Whether the colliders of two bodies overlap, the same way the contact
generation decides it: GJK on their shapes when both carry one
(Self::set_body_shape, Self::add_shaped_body), GJK on the shape and
the other body’s collision sphere when one does, otherwise the test of
their collision spheres. false for an index that is not a body or a body
without a collider. Touching exactly counts as overlapping when a shape is
involved (GJK’s convention) and as clear for two spheres (the contact
generation’s).
Sourcepub fn add_compound_body(
&mut self,
compound: &CompoundShape,
density: Fix128,
position: Vec3Fix,
) -> Result<usize, ShapeError>
pub fn add_compound_body( &mut self, compound: &CompoundShape, density: Fix128, position: Vec3Fix, ) -> Result<usize, ShapeError>
Add a rigid body made of a CompoundShape:
material of density, the compound’s centre of mass at position.
The mass is the children’s total and the inertia is their summed tensor about
the common centre of mass, which for an asymmetric compound has products of
inertia; the body stores the principal moments, so its local frame is the
principal frame, and its initial rotation is the rotation from that frame to
the compound’s as authored — the compound appears in the world exactly as it
was built, translated so its centre of mass is at position. The body
collides as its children (body_collider, internal): a gap between two
children is not part of the body, and its collision radius is the sphere about
the centre of mass that contains them all.
§Errors
ShapeError: NonPositiveDensity;
DegenerateShape for an empty compound or one with no volume;
MassNotRepresentable when the mass or a principal moment is not positive or
the sizes are too large for Fix128. Nothing is added in those cases.
Sourcepub fn remove_body(&mut self, idx: usize) -> Option<RigidBody>
pub fn remove_body(&mut self, idx: usize) -> Option<RigidBody>
Remove a body by index (swap-remove).
The last body is moved to fill the gap. All constraints and joints
referencing the old last index are remapped. Returns the removed body,
or None if the index is out of bounds.
Sourcepub fn body_count(&self) -> usize
pub fn body_count(&self) -> usize
Number of bodies in the world
Sourcepub fn active_body_count(&self) -> usize
pub fn active_body_count(&self) -> usize
Number of active (non-sleeping) bodies
Sourcepub fn set_body_material(&mut self, body_idx: usize, material_id: MaterialId)
pub fn set_body_material(&mut self, body_idx: usize, material_id: MaterialId)
Set a body’s material ID
Sourcepub fn add_pre_solve_hook(&mut self, hook: PreSolveHook)
pub fn add_pre_solve_hook(&mut self, hook: PreSolveHook)
Add a pre-solve contact hook
The hook is called with (body_a_index, body_b_index, &contact).
Return false to skip the contact (e.g., for one-way platforms).
Sourcepub fn clear_pre_solve_hooks(&mut self)
pub fn clear_pre_solve_hooks(&mut self)
Clear all pre-solve hooks
Sourcepub fn add_contact_modifier(&mut self, modifier: Box<dyn ContactModifier>)
pub fn add_contact_modifier(&mut self, modifier: Box<dyn ContactModifier>)
Add a contact modifier
Contact modifiers can mutate contact properties (normal, depth, friction,
restitution) before solving. Return false from modify_contact to discard.
Sourcepub fn clear_contact_modifiers(&mut self)
pub fn clear_contact_modifiers(&mut self)
Clear all contact modifiers
Sourcepub fn begin_frame(&mut self)
pub fn begin_frame(&mut self)
Begin a new simulation frame (updates contact cache lifecycle)
Sourcepub fn add_joint(&mut self, joint: Joint) -> usize
pub fn add_joint(&mut self, joint: Joint) -> usize
Add a joint constraint, returns joint index.
§Panics
Panics if either body index in the joint is out of bounds.
Sourcepub fn remove_joint(&mut self, idx: usize) -> Option<Joint>
pub fn remove_joint(&mut self, idx: usize) -> Option<Joint>
Remove a joint by index (swap-remove), returns the removed joint
Motors follow their joint: a motor on the removed joint is removed with
it, and a motor on the last joint (moved into idx) is re-pointed to
idx.
Sourcepub fn add_joint_motor(
&mut self,
joint_index: usize,
controller: PdController,
) -> usize
pub fn add_joint_motor( &mut self, joint_index: usize, controller: PdController, ) -> usize
Attach a one-axis PD motor to joint joint_index, returns the motor
index.
The motor is applied by Self::step every substep (see
crate::motor::apply_motors for the generalised coordinate per joint
type: the twist angle for a hinge, the centre distance otherwise). The
controller is used as given, so a controller still in
crate::motor::MotorMode::Off does nothing until a target is set
(Self::set_joint_motor_velocity_target / Self::joint_motor_mut).
Motors follow their joint through Self::remove_joint and
Self::remove_body; a motor whose index no longer names a joint
(after editing the public joints field directly) does nothing.
Sourcepub fn joint_motor_mut(&mut self, motor: usize) -> Option<&mut JointMotor>
pub fn joint_motor_mut(&mut self, motor: usize) -> Option<&mut JointMotor>
The motor added as motor by Self::add_joint_motor, for changing its
gains or targets between steps.
Sourcepub fn set_joint_motor_velocity_target(
&mut self,
motor: usize,
target: Fix128,
) -> bool
pub fn set_joint_motor_velocity_target( &mut self, motor: usize, target: Fix128, ) -> bool
Switch motor motor to velocity mode with target (rad/s for a hinge,
m/s along the centre line otherwise). Returns false if there is no
such motor.
Sourcepub fn disable_joint_motor(&mut self, motor: usize) -> bool
pub fn disable_joint_motor(&mut self, motor: usize) -> bool
Turn motor motor off (it applies no force until a target is set
again). Returns false if there is no such motor.
Sourcepub fn add_joint_motor_3d(
&mut self,
joint_index: usize,
kp: Vec3Fix,
kd: Vec3Fix,
max_torque: Fix128,
) -> usize
pub fn add_joint_motor_3d( &mut self, joint_index: usize, kp: Vec3Fix, kd: Vec3Fix, max_torque: Fix128, ) -> usize
Attach a three-axis rotation motor with per-axis gains kp / kd and
torque cap max_torque to joint joint_index, returns its index.
The motor starts Off; Self::set_joint_motor_3d_rotation_target
drives the joint’s relative rotation rotation_b * rotation_a⁻¹ to a
target (see crate::motor::PdController3D::compute_torque). It
follows its joint through removals like Self::add_joint_motor.
Sourcepub fn joint_motor_3d_mut(
&mut self,
motor: usize,
) -> Option<&mut PdController3D>
pub fn joint_motor_3d_mut( &mut self, motor: usize, ) -> Option<&mut PdController3D>
The controller of rotation motor motor (Self::add_joint_motor_3d).
Sourcepub fn set_joint_motor_3d_rotation_target(
&mut self,
motor: usize,
target: QuatFix,
) -> bool
pub fn set_joint_motor_3d_rotation_target( &mut self, motor: usize, target: QuatFix, ) -> bool
Drive rotation motor motor to the relative rotation target
(position mode). Returns false if there is no such motor.
Sourcepub fn joint_count(&self) -> usize
pub fn joint_count(&self) -> usize
Number of joints
Sourcepub fn add_force_field(&mut self, field: ForceFieldInstance) -> usize
pub fn add_force_field(&mut self, field: ForceFieldInstance) -> usize
Add a force field, returns field index
Sourcepub fn remove_force_field(&mut self, idx: usize) -> Option<ForceFieldInstance>
pub fn remove_force_field(&mut self, idx: usize) -> Option<ForceFieldInstance>
Remove a force field by index
Sourcepub fn set_body_collision_radius(&mut self, body_idx: usize, radius: Fix128)
pub fn set_body_collision_radius(&mut self, body_idx: usize, radius: Fix128)
Set collision radius for a body (enables automatic sphere collision detection)
Sourcepub fn clear_body_collision_radius(&mut self, body_idx: usize)
pub fn clear_body_collision_radius(&mut self, body_idx: usize)
Remove collision radius (disable automatic collision detection for this body)
Sourcepub fn set_body_filter(&mut self, body_idx: usize, filter: CollisionFilter)
pub fn set_body_filter(&mut self, body_idx: usize, filter: CollisionFilter)
Set collision filter for a body
Sourcepub fn body_filter(&self, body_idx: usize) -> CollisionFilter
pub fn body_filter(&self, body_idx: usize) -> CollisionFilter
Get collision filter for a body
Sourcepub fn is_sleeping(&self, body_idx: usize) -> bool
pub fn is_sleeping(&self, body_idx: usize) -> bool
Check if a body is sleeping
Sourcepub fn wake_body(&mut self, body_idx: usize)
pub fn wake_body(&mut self, body_idx: usize)
Wake up a body and all connected bodies in its island
Sourcepub fn set_sleep_config(&mut self, config: SleepConfig)
pub fn set_sleep_config(&mut self, config: SleepConfig)
Set the sleep configuration
Sourcepub fn set_sleep_skip(&mut self, enabled: bool)
pub fn set_sleep_skip(&mut self, enabled: bool)
Turn the sleep skip of Self::step on (the default) or off.
With it on, a sleeping body at rest that no joint or distance constraint
references is left out of every stage of the step, and its contacts are
found through a persistent tree of such bodies instead of rebuilding the
broad-phase over it (see StageWork). The simulation is bit-identical
either way: the skip only drops work whose result is already known (a body
at rest stays where it is, with the velocity it already has). Turning it off
drops the tree; it is rebuilt on the next step with the skip on.
Only step with the XPBD backend and Broadphase::Bvh skips; the other
entry points and broad-phases (Broadphase::DynamicTree,
Broadphase::Hybrid) visit every body as before.
Sourcepub const fn sleep_skip(&self) -> bool
pub const fn sleep_skip(&self) -> bool
Whether the sleep skip of Self::step is on (see Self::set_sleep_skip).
Sourcepub const fn stage_work(&self) -> StageWork
pub const fn stage_work(&self) -> StageWork
Work counters of the last Self::step (see StageWork).
Sourcepub fn observe_body(&self, body_idx: usize) -> Option<BodyObservation>
pub fn observe_body(&self, body_idx: usize) -> Option<BodyObservation>
Observe a single body’s law-relevant state.
Returns None if body_idx is out of range (there is no body at
that index — note that Self::remove_body uses swap_remove, so
body_idx is only stable within the frame it was observed).
⚠️ grounded は意図的に持たない — 一般 RigidBody に地面接触の
定義は存在せず (character.rs の CharacterController::detect_ground
は特化アルゴリズムで、全 body に適用できる基準ではない) Law / goal
述語が「接地」を必要とする場合は in_contact + 接触法線
(EventCollector::contact_events の crate::event::ContactEvent::normal) から
自前で構成する
Sourcepub fn observe_bodies(&self) -> Vec<BodyObservation>
pub fn observe_bodies(&self) -> Vec<BodyObservation>
Observe every body in the world, in body index order.
Sourcepub fn contact_events(&self) -> &[ContactEvent]
pub fn contact_events(&self) -> &[ContactEvent]
Get contact events from the last step
Sourcepub fn trigger_events(&self) -> &[TriggerEvent]
pub fn trigger_events(&self) -> &[TriggerEvent]
Get trigger events from the last step
Sourcepub fn drain_contact_events(&mut self) -> Vec<ContactEvent>
pub fn drain_contact_events(&mut self) -> Vec<ContactEvent>
Drain (consume) contact events from the last step
Sourcepub fn drain_trigger_events(&mut self) -> Vec<TriggerEvent>
pub fn drain_trigger_events(&mut self) -> Vec<TriggerEvent>
Drain (consume) trigger events from the last step
Sourcepub fn raycast(
&self,
origin: Vec3Fix,
direction: Vec3Fix,
max_distance: Fix128,
) -> Option<(usize, Fix128)>
pub fn raycast( &self, origin: Vec3Fix, direction: Vec3Fix, max_distance: Fix128, ) -> Option<(usize, Fix128)>
Cast a ray against all body collision spheres, returns (body_index, distance).
Uses a BVH broad-phase to cull bodies outside the ray’s bounding box,
then performs exact ray-sphere intersection on candidates.
Returns None if direction is zero or no body is hit.
Every body is tested as its bounding sphere (its collision radius), even
when it carries a shape or a compound, and static colliders and SDF
colliders are not tested. This differs from the contacts, which a shaped
body makes with its shape: a ray can hit the bounding sphere of a body
that nothing touches there. Self::cast_ray tests the actual geometry
(see crate::shape_raycast).
Sourcepub fn combined_material(
&self,
body_a: usize,
body_b: usize,
) -> CombinedMaterial
pub fn combined_material( &self, body_a: usize, body_b: usize, ) -> CombinedMaterial
Get combined material properties for a body pair
§Claims
- A body whose material is
crate::material::DEFAULT_MATERIALcontributes its ownRigidBody::frictionandRigidBody::restitution(the default material is “no material assigned”); a body with any other material contributes that material’sdynamic_friction/restitutionand its own fields are not read. - Bodies built with the defaults (friction 0.3, restitution 0.5) give the same result as before, since the default material holds the same values.
- The two contributions are combined with the materials’ combine rules; a pair override registered for the two materials takes precedence over both.
- An index outside the body list counts as the default material with the material’s own values.
Sourcepub fn add_distance_constraint(&mut self, constraint: DistanceConstraint)
pub fn add_distance_constraint(&mut self, constraint: DistanceConstraint)
Add distance constraint
Sourcepub fn add_contact(&mut self, contact: ContactConstraint)
pub fn add_contact(&mut self, contact: ContactConstraint)
Add contact constraint
Sourcepub fn add_contact_with_material(
&mut self,
body_a: usize,
body_b: usize,
contact: Contact,
)
pub fn add_contact_with_material( &mut self, body_a: usize, body_b: usize, contact: Contact, )
Add contact constraint with material lookup
Automatically looks up friction/restitution from the material table.
Sourcepub fn clear_contacts(&mut self)
pub fn clear_contacts(&mut self)
Clear all contact constraints. step() does this at the start of every
substep (1.2.0) before detect_collisions, so contacts added manually
between frames are not seen by step(); drive the substep API yourself
if you generate contacts outside the built-in sphere / SDF detection.
Sourcepub fn rebuild_batches(&mut self)
pub fn rebuild_batches(&mut self)
Rebuild constraint batches using greedy graph coloring
Each batch contains constraints that share no bodies, allowing safe parallel modification.
Sourcepub fn num_batches(&self) -> usize
pub fn num_batches(&self) -> usize
Get number of constraint batches (colors used)
Sourcepub fn step(&mut self, dt: Fix128)
pub fn step(&mut self, dt: Fix128)
Step simulation by dt (in fixed-point seconds)
Integrated pipeline:
- Begin event frame
- Apply force fields to body velocities
- Detect collisions (BVH broad-phase + sphere narrow-phase)
- Substep loop (joint motors, integrate, solve constraints + joints, update velocities)
- Update sleeping states
- End event frame (generates end-of-contact events)
Deterministic: same inputs always produce same outputs.
§Limitation: joints
stepsolves the joints once per substep, after and outside theiterationsloop that solves distance and contact constraints.- Joint compliance is therefore exact XPBD with respect to substeps:
each substep’s single solve starts from
λ = 0, so a compliant joint under a constant loadFsettles at the stretchF · compliancewhateveriterationsis. - Convergence of a rigid joint chain depends on
substepsonly, not oniterations.
§Participants
step is Self::try_step with the result dropped: while a fault is
recorded (Self::fault) it leaves the world unchanged. Without
registered participants no fault is ever recorded and step runs as
it always has.
Sourcepub fn step_n(&mut self, n: usize, dt: Fix128)
pub fn step_n(&mut self, n: usize, dt: Fix128)
Run Self::step n times with the same dt.
The native counterpart of the Python step_n and WASM stepN
bindings, which call this: the result is bit-identical to calling
step(dt) n times in a loop, n = 0 leaves the world unchanged, and
a non-positive dt leaves it unchanged for every n (as step does).
§Examples
use alice_physics::{Fix128, PhysicsConfig, PhysicsWorld, RigidBody, Vec3Fix};
let mut a = PhysicsWorld::new(PhysicsConfig::default());
a.add_body(RigidBody::new_dynamic(Vec3Fix::from_int(0, 5, 0), Fix128::ONE));
let mut b = PhysicsWorld::from_world_snapshot(&a.snapshot_world()).unwrap();
let dt = Fix128::from_ratio(1, 60);
a.step_n(3, dt);
for _ in 0..3 {
b.step(dt);
}
assert_eq!(a.bodies[0].position, b.bodies[0].position);Sourcepub fn tgs_cache_stats(&self) -> TgsCacheStats
pub fn tgs_cache_stats(&self) -> TgsCacheStats
Warm-start hit-rate diagnostics for the SolverBackend::Tgs path’s
per-contact impulse cache (see Self::reset_tgs_cache_stats to
isolate a specific measurement window, e.g. after warm-up frames).
Always hits: 0, misses: 0 while config.solver_backend is
Xpbd — the internal step_tgs (the only writer of this cache)
is never called on that path.
Sourcepub fn reset_tgs_cache_stats(&mut self)
pub fn reset_tgs_cache_stats(&mut self)
Clears the Self::tgs_cache_stats hit/miss counters without
touching the cached impulses themselves — the same contract as the
internal ImpulseCache::reset_stats. Useful when a host wants to
measure hit rate over a specific window instead of the cache’s
entire lifetime (e.g. after a scene’s warm-up frames).
Sourcepub fn step_parallel(&mut self, dt: Fix128)
pub fn step_parallel(&mut self, dt: Fix128)
Step with batched constraint solving
When parallel feature is enabled, processes independent constraint
batches in parallel using Rayon. Includes the same integrated pipeline
as step(): force fields, collision detection, joints, events, sleeping.
§Determinism contract
step_parallel is deterministic — the same world stepped twice, on any
number of threads and on every platform, produces the same bits (graph
colouring is deterministic and constraints inside a batch touch
disjoint bodies). It is not bit-identical to Self::step once two
constraints share a body: the batches are a different Gauss–Seidel
ordering than constraint index order, and Gauss–Seidel is order
dependent. Lockstep / rollback peers must therefore all use the same
path (fuzz/fuzz_targets/fuzz_step_parity.rs checks both properties).
§Solver backend
config.solver_backend is not consulted: this path always runs the
XPBD substep loop, so with SolverBackend::Tgs it gives the same
bits as with SolverBackend::Xpbd. A batched TGS path does not exist
yet; use Self::step for TGS.
§Participants
step_parallel is Self::try_step_parallel with the result dropped.
Sourcepub fn solve_contact_constraints_with_bridge<B: GpuSolverBridge + ?Sized>(
&mut self,
bridge: &mut B,
)
pub fn solve_contact_constraints_with_bridge<B: GpuSolverBridge + ?Sized>( &mut self, bridge: &mut B, )
v0.10.0 opt-in: run one PGS contact-solve iteration via a
caller-supplied GpuSolverBridge instead of the CPU-side
solve_contact_constraints hot loop.
§Claims
- Each call runs the hooks and modifiers once, writes the result back to the constraints, then runs one bridge pass over the survivors.
substep_with_bridgeruns the hooks and modifiers once per substep and then only the pass per iteration, so a relative modifier does not compound withiterations.
§Byte-exact CPU parity
The bridge-routed path applies Stage A filters (sensor skip,
pre-solve hooks, contact modifiers) on the CPU side upfront
exactly as solve_contact_constraints does inline. Because
the closures the hooks and modifiers evaluate depend only on
their function inputs — never on live body state that
changes during the loop — a batched upfront application is
byte-exact equivalent to the CPU’s per-constraint
application. Stage B (depth ≤ 0 skip, w_sum < ε skip,
warm-start biased lambda, cached_lambda write, position
correction) is routed through the bridge, which runs the
same sequential Gauss-Seidel semantics because bridge
backends dispatch a single-workgroup single-thread compute
kernel.
§Constraint contract
- The bridge receives a filtered
Vec<ContactConstraint>containing only the constraints that survived Stage A (sensor-free, hook-approved, modifier-approved). The contact / friction / restitution fields may be mutated from the original slot values if a contact modifier ran. - After readback the method writes each updated
cached_lambdaback intoself.contact_constraintsvia an index map, mirroring the CPU write that lands atself.contact_constraints[i].cached_lambda = lambdaat the corresponding slot. - Body positions are written back to
self.bodies[i].position.
§Panics
Panics if the bridge implementation does not override the
v0.9.0 contact-solve methods (send_contact_constraints,
send_body_state, dispatch_contact_solve_iteration,
recv_contact_constraints, recv_body_positions).
Sourcepub fn step_with_bridge<B: GpuSolverBridge + ?Sized>(
&mut self,
bridge: &mut B,
dt: Fix128,
)
pub fn step_with_bridge<B: GpuSolverBridge + ?Sized>( &mut self, bridge: &mut B, dt: Fix128, )
v0.10.0 opt-in: run one full simulation step with the
contact-solve stage routed through a caller-supplied
GpuSolverBridge. Semantically equivalent to
Self::step with substep(_) replaced by
Self::substep_with_bridge inside the substep loop.
§Byte-exact CPU parity
When the bridge implementation is byte-exact vs the CPU
solve_contact_constraints, a full step_with_bridge
produces self.bodies and self.contact_constraints state
byte-identical to what a CPU-only step(dt) call produces
from the same initial state and configuration.
§Solver backend
config.solver_backend is not consulted: the bridge replaces the
contact solve of the XPBD substep, so with SolverBackend::Tgs this
gives the same bits as with SolverBackend::Xpbd. A bridge path for
TGS does not exist yet; use Self::step for TGS.
Sourcepub fn substep_with_bridge<B: GpuSolverBridge + ?Sized>(
&mut self,
bridge: &mut B,
dt: Fix128,
)
pub fn substep_with_bridge<B: GpuSolverBridge + ?Sized>( &mut self, bridge: &mut B, dt: Fix128, )
v0.10.0 opt-in: run one substep with the contact-solve stage
routed through a caller-supplied GpuSolverBridge. The
integrate + distance-projection + joint + velocity-update
stages stay on the CPU; only the inner PGS contact-solve
iterations are dispatched through the bridge.
§Byte-exact CPU parity
When the bridge implementation is byte-exact vs the CPU
solve_contact_constraints (which every ALICE-TRT
TrtSolverAdapter release since v2.6.0 asserts on the
3-platform CI matrix), a full substep_with_bridge sequence
produces self.bodies and self.contact_constraints state
byte-identical to what a CPU-only substep(dt) call produces
from the same initial state and configuration.
Called on its own, this runs no participant: the participants of
crate::world_participant are called by the step loops
(Self::step_with_bridge, Self::try_step) around each substep.
§Solver backend
config.solver_backend is not consulted: the bridge replaces the
contact solve of the XPBD substep, so with SolverBackend::Tgs this
gives the same bits as with SolverBackend::Xpbd. A bridge path for
TGS does not exist yet; use Self::step for TGS.
Sourcepub fn solve_joints_with_bridge<B: GpuSolverBridge + ?Sized>(
&mut self,
bridge: &mut B,
dt: Fix128,
)
pub fn solve_joints_with_bridge<B: GpuSolverBridge + ?Sized>( &mut self, bridge: &mut B, dt: Fix128, )
v0.12.0: run one joint-solve pass with the joint stage routed
through a caller-supplied GpuSolverBridge. Extracts body
positions, rotations and inverse masses into the shape the
bridge expects, uploads them via send_joints, send_body_state
and send_body_rotations, dispatches
dispatch_joint_solve_iteration(dt), reads back the
post-solve positions via recv_body_positions, and writes
them back into self.bodies[i].position.
§Contract
- The bridge must implement the v0.12.0 joint-solve methods
(
send_joints,send_body_rotations,dispatch_joint_solve_iteration); the defaultpanic!implementations surface backends that do not. dtis the substep length used to computecompliance / (dt * dt)inside the kernel.- A joint whose two ends are the same body is not uploaded (the CPU solve skips it too, the body stays free); the remaining joints keep their order.
- When no joint remains (
self.jointsis empty or holds only such joints), this method is a no-op and returns without touching the bridge — useful for callers that unconditionally route through a bridge and let this method decide whether there’s work to do.
§Byte-exact CPU parity
The CPU-side solve_joints runs each joint independently in
index order; the bridge is required to reproduce that exact
order and per-joint arithmetic. ALICE-TRT v3.1.0 satisfies
this by dispatching a single-workgroup single-thread kernel
that walks the uploaded joint list top to bottom.
Sourcepub fn add_sdf_collider(&mut self, collider: SdfCollider) -> usize
pub fn add_sdf_collider(&mut self, collider: SdfCollider) -> usize
Add an SDF collider to the world
A collider attached to a body (SdfCollider::new_dynamic) is placed
at that body’s current pose when the body exists, and follows the body
from then on: every step copies the body’s pose into it before SDF
overlap is resolved and again at the end of the step.
The collider’s cached values are recomputed from its fields
(SdfCollider::update_cache): a rotation or scale written to the
collider directly before the call takes effect, and the rotation is
stored as a unit quaternion.
Sourcepub fn last_step_sdf_contacts(&self) -> &[SdfContact]
pub fn last_step_sdf_contacts(&self) -> &[SdfContact]
The SDF contacts resolved by the last step, every substep of it, in the order they were applied: by substep, then by body index, then by collider index.
Each entry is one push-out of a body out of an SDF collider
(SdfContact): where, along which
normal, how deep, and how fast the body was approaching, and the
collider’s frame at that substep (a collider attached to a body moves
with it during the step, so convert point with the recorded frame,
not with the collider’s pose after the step). The record is
emptied at the head of each step (Self::step, Self::try_step,
step_parallel, step_with_bridge); a step that does not run (a
recorded fault, a non-positive dt) leaves it as it was.
substep_with_bridge called on its own appends to it. Recording
changes nothing in the step: a world whose record is never read steps
to the same bits. The buffer is reused from step to step, so it holds
its largest size so far (one entry per body per collider at most).
Self::sdf_contacts is a different query: what the bodies overlap
now, between steps, without moving anything.
The participants of crate::world_participant read the contacts
recorded so far in the current step through
SubstepCtx::sdf_contacts.
Sourcepub fn remove_sdf_collider(&mut self, idx: usize) -> Option<SdfCollider>
pub fn remove_sdf_collider(&mut self, idx: usize) -> Option<SdfCollider>
Remove an SDF collider by index
Sourcepub fn set_sdf_collision_radius(&mut self, radius: Fix128)
pub fn set_sdf_collision_radius(&mut self, radius: Fix128)
Set the collision radius for body-vs-SDF queries
Sourcepub fn sdf_contacts(&self) -> Vec<(usize, Contact)>
pub fn sdf_contacts(&self) -> Vec<(usize, Contact)>
The contacts of the bodies with the SDF colliders, as (body index, contact):
what step would push each body out of, without moving
anything. A body with a shape or a compound is tested as that collider, any
other body as a sphere of sdf_collision_radius;
static bodies, sensors and an SDF attached to the body itself are skipped.
An SDF attached to a body is queried at the pose it was given when it was
added or at the end of the last step; after moving a body by hand, call
crate::sdf_collider::sync_dynamic_sdf_colliders on
sdf_colliders first.
Sourcepub fn contact_forces(&self, dt: Fix128) -> Vec<(Vec3Fix, Vec3Fix, Fix128)>
pub fn contact_forces(&self, dt: Fix128) -> Vec<(Vec3Fix, Vec3Fix, Fix128)>
The normal force each contact of the last step carries, as
(contact point, normal, force) — the input crate::contact_viz draws.
dt is the frame step passed to step. The solver applies a
separation λ (cached_lambda) along the normal in the last substep, of
length h = dt / substeps, which for bodies of inverse masses wₐ, w_b is
the displacement of a force F over h: λ = (wₐ + w_b)·F·h², so
F = λ / ((wₐ + w_b)·h²). A body resting under gravity then carries its
weight: m·g. The normal points from B to A and the force acts on A. Contacts
between two immovable bodies carry none and are left out. A body that has gone
to sleep (about a minute of rest) is not solved, so its contacts are not there
to report.
Sourcepub fn contact_arrows(&self, dt: Fix128) -> Vec<ContactArrow>
pub fn contact_arrows(&self, dt: Fix128) -> Vec<ContactArrow>
Arrows for the normal forces of the last step’s contacts
(contact_forces through
crate::contact_viz::generate_contact_arrows).
Sourcepub fn contact_friction_arrows(&self, dt: Fix128) -> Vec<ContactArrow>
pub fn contact_friction_arrows(&self, dt: Fix128) -> Vec<ContactArrow>
Two tangential arrows of magnitude μ·F for each contact of the last step,
with the friction coefficient of that contact (they differ between material
pairs).
Sourcepub fn contact_friction_cones(&self, dt: Fix128) -> Vec<FrictionCone>
pub fn contact_friction_cones(&self, dt: Fix128) -> Vec<FrictionCone>
A friction cone (half-angle atan μ, height the normal force) for each
contact of the last step, with that contact’s friction coefficient.
Sourcepub fn add_static_collider(&mut self, collider: StaticCollider) -> usize
pub fn add_static_collider(&mut self, collider: StaticCollider) -> usize
Add an immovable collision surface — a plane, a height field or a triangle
mesh — and return its index. See crate::static_collider.
Sourcepub fn remove_static_collider(&mut self, idx: usize) -> Option<StaticCollider>
pub fn remove_static_collider(&mut self, idx: usize) -> Option<StaticCollider>
Remove a static collider by index, returning it, or None when the index
is out of range. Later colliders shift down by one.
Sourcepub fn static_collider_count(&self) -> usize
pub fn static_collider_count(&self) -> usize
Number of static colliders.
Sourcepub fn set_broadphase(&mut self, kind: Broadphase)
pub fn set_broadphase(&mut self, kind: Broadphase)
Select the broad-phase. Every kind gives bit-identical simulations (see
Broadphase); switching drops the persistent tree and the
Broadphase::Hybrid layers, which are rebuilt on the next step if
that kind is chosen.
Sourcepub fn broadphase(&self) -> Broadphase
pub fn broadphase(&self) -> Broadphase
The broad-phase in use.
Sourcepub fn broadphase_stats(&self) -> BroadphaseStats
pub fn broadphase_stats(&self) -> BroadphaseStats
What the Broadphase::DynamicTree tree holds: how many bodies have a
proxy and how tall the tree is. Both are 0 until a step has run with that
broad-phase.
Sourcepub fn broadphase_proxy_aabb(&self, body_idx: usize) -> Option<AABB>
pub fn broadphase_proxy_aabb(&self, body_idx: usize) -> Option<AABB>
The fattened box the Broadphase::DynamicTree tree keeps for a body, or
None when the body has no proxy (no collision radius, or no step yet).
Sourcepub fn get_body_mut(&mut self, idx: usize) -> Option<&mut RigidBody>
pub fn get_body_mut(&mut self, idx: usize) -> Option<&mut RigidBody>
Get mutable body by index
A rotation written through the returned reference is not normalized
until the next step: a non-unit quaternion q scales what it rotates
by |q|^2 in the queries made before then (see RigidBody::rotation).
Sourcepub const fn overflow_detected(&self) -> bool
pub const fn overflow_detected(&self) -> bool
範囲外の積が起きた step があったか (sticky、WM-01 / doctrine B-12)
true は この world の軌道が信用できない ことを意味する
doctrine §3 の 3 値で言えば undecided — 探索では「この枝は未決定」
§なぜ値ベースの検出器では足りないか
Fix128::mul の積が範囲外で 2 の冪だと hi が厳密に 0 になり
(2⁴⁰ × 2⁴⁰)、position += velocity*dt が 0 加算 →
update_velocities が速度も 0 に上書き →
「原点で完全に静止した自己整合な状態」 に落ちる
⚠️ 静止は物理的に妥当なのでどの不変条件でも red にならない
§sticky である理由
1 度立ったら step を重ねても落ちない 落ちると探索が
「未決定だった枝」を後の step で決定済と誤認する
⚠️ flag は状態の一部なので Self::serialize_state の被覆に
入れる必要がある (入れないと巻き戻した先で undecided が消える、
doctrine B-12 の指摘) — format v2 で収録済 (world ごと 1 byte、
Self::STATE_VERSION 参照)
Sourcepub fn serialize_state(&self) -> Vec<u8> ⓘ
pub fn serialize_state(&self) -> Vec<u8> ⓘ
Serialize world state (for rollback netcode).
§被覆 (v1、2026-09-30)
| 範囲 | 内容 |
|---|---|
[0..4) | magic Self::STATE_MAGIC (b"APHY") |
[4..6) | version u16 = Self::STATE_VERSION |
[6..8) | reserved u16 = 0 (将来の flag 用、8 byte 境界揃え) |
[8..12) | body 数 u32 |
[12..) | body ごとに 208 byte = position 48 + velocity 48 + rotation 64 + angular_velocity 48 |
| 続き | body ごとに 5 byte = SleepState u8 + idle_frames u32 |
| 続き | world ごと overflow flag 1 byte (v2) |
| 続き | world ごと population fingerprint u64 (v3、Self::population_fingerprint) |
⚠️ SleepState + idle_frames を v1 で被覆に入れた (WM-08)
旧 format は body の運動状態だけを持ち、deserialize_state 末尾の
IslandManager::new が sleep 状態を 0 に戻していたため、
巻き戻した先で眠るタイミングがずれて軌道が割れた
(tests/wm08_state_coverage.rs が対照実験付きで red を実測してから実装)
⚠️ magic と version を入れた理由: 旧 blob は先頭が body 数なので、
検査しないと新実装が旧 blob を誤って解釈する magic 不一致 /
version 不一致は deserialize_state が false を返して明示的に拒否する
依然として保存 (= 復元) しないもの: constraints / joints / force fields / collision radii / filters / materials (rollback netcode では game state から毎 frame 再構築される前提) ⚠️ v3 の population fingerprint はこれらの値を検査にのみ使う (復元はしない) ので 「保存しない」契約自体は変わらない
⚠️ crate::netcode::SimulationChecksum は 本 blob の全 byte から
導出される (被覆の単一源化、B-13) ので、ここに state を足すと
checksum も自動で追従する
Sourcepub fn population_fingerprint(&self) -> u64
pub fn population_fingerprint(&self) -> u64
FNV-1a 64-bit hash of per-body quantities Self::serialize_state
does not restore (shape/mass/inertia/collider/filter/material),
in body index order.
§なぜこれが要るか (rollback body 数不一致、gap #3)
Self::deserialize_state は body 数の一致しか見ていない
remove_body は swap_remove なので、count が元に戻っても
index↔body の対応 (= population) が変わっていることがある —
その場合 count 一致だけでは検出できず、別の body に誤った状態を
書き込んで silent に通る (fail-fast になっていない)
⚠️ 位置 / 速度 / 回転 / sleep を含めない — それらは blob 自身が 復元する量なので、ここに混ぜると「自分で組んだ量で自分を検査する」 循環になり population 不一致を検出できなくなる
Sourcepub fn deserialize_state(&mut self, data: &[u8]) -> bool
pub fn deserialize_state(&mut self, data: &[u8]) -> bool
Deserialize world state (for rollback netcode).
Restores per-body transforms. The rotations are restored as stored
and brought to unit length by the next step, so a non-unit rotation in
data scales what it rotates by |q|^2 in the queries made before
that step (see RigidBody::rotation). Parallel arrays (collision radii,
filters, materials, island manager) are resized to match the body
count, preserving existing entries and zero-filling new ones.
§Rollback の契約 (gap #3、2026-10-02)
body 数が一致していても population (どの body がどんな
inv_mass / inv_inertia / collider / filter / material を持つか)
が一致していなければ拒否する (Self::population_fingerprint、v3)
caller は body の生成・破棄 (add_body / remove_body) と joints /
force fields / filters / collider radii / materials の変更を、目標
frame まで決定論的に replay してから本関数を呼ぶこと ⚠️
joints の再構築は本関数の呼び出し前に行う — 本関数の末尾で
island を joints から作り直すため、順序が逆だと sleep の島が
異なる形になる
不一致 (false) の場合、self は一切変更されない (すべての検査は
body 状態を書き込む前に完了する)
Trait Implementations§
Auto Trait Implementations§
impl !Freeze for PhysicsWorld
impl !RefUnwindSafe for PhysicsWorld
impl !UnwindSafe for PhysicsWorld
impl Send for PhysicsWorld
impl Sync for PhysicsWorld
impl Unpin for PhysicsWorld
impl UnsafeUnpin for PhysicsWorld
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read more