This guide builds the scene oxijolt was written for: terrain, a building made of several
parts with their own collision groups, a dropped item, the queries a game runs every frame and a
floating origin, then a character walking on a small planet, then a car on terrain and a ragdoll
in a second world. Everything runs headless; nothing is drawn. The three complete programs run as
tests (cargo test -p oxijolt --doc).
Other topics have guides of their own: constraints, soft bodies, tanks and motorcycles, events and contact listeners, saving and restoring a world, running jobs on your own thread pool, determinism and building.
Units are metres, seconds, kilograms and radians. Jolt is right-handed with Y up.
Every body lives in one object layer, and each object layer is kept in one broad-phase
layer (a tree of the broad phase; static and moving bodies usually get one each). A
CollisionLayers table says which pairs of object layers collide. A group whose members are
whole bodies (terrain, chunks, items, actors) gets its own object layer.
Some groups share one body. A chunk is one static body whose shape is a compound of many parts,
and each part has a group: structures you can stand on, features (foliage, canopies) you walk
through. Store the group as the compound child's user_data. Queries read it back from hits
(compound_child) and filter on it with QueryFilter::child_groups, a bit mask over the user
data values 0 to 31.
QueryFilter has three parts that combine: object_layers selects whole bodies by layer,
child_groups selects compound children by group, exclude_body skips one body (the querying
actor's own). An unset part accepts everything.
Layers decide which bodies the solver collides by class. For pairs within a class, such as a
vehicle that must not collide with its driver or a chain whose neighbouring links overlap, give the
bodies a CollisionGroup at creation; queries and characters ignore those groups
(bodies.md). For per-contact decisions (a one-way platform, a
projectile that passes its shooter), a ContactListener validates contacts
(events.md).
Shape::new_box,new_sphere,new_cylinderandnew_capsule(cylinders and capsules along local Y). Boxes and cylinders have Jolt's convex radius of 0.05 m, which rounds their edges for contacts and shape casts;new_box_with_convex_radius(.., 0.0)gives sharp edges. Rays always see the sharp shape.Shape::new_height_fieldtakesn * nsamples in Jolt's row-major order: sample(x, z)issamples[z * n + x]. A column-major source must be transposed.f32::MAXis a hole. With the default block size 2,n = 33is padded to 34 and covers exactly the 32 x 32 cells given. Heightfields are for static bodies only.Shape::new_plane(normal, constant, half_extent)is static ground: everything behindnormal · p + constant = 0is solid, within a square of2 * half_extentaround the plane andhalf_extentdeep. Nothing collides beyond that square. Only static bodies take a plane.Shape::new_compoundplaces children with their own pose and user data. Child order is part of the shape.MutableCompoundedits a compound's children at run time and publishes new shapes, whichBodyMut::set_shapeinstalls (bodies.md).- A dynamic or kinematic body needs an inertia tensor Jolt can decompose. When the tensor is not
exactly diagonal (a rotated compound child, an offset centre of mass), a slender part is refused
past about 54 times longer than wide for a square box and about 47 times longer than its
diameter for a capsule or cylinder (limits.md). A shape with an
exactly diagonal tensor, such as an unrotated primitive, is not affected. An inertia given with
BodySettings::mass_and_inertiahas rules of its own (bodies).
A Shape holds one Jolt reference and every body holds its own, so a shape can be dropped after
creating bodies and shared between bodies and worlds.
PhysicsWorld::step(dt) runs one collision step. Err means the step was rejected (a dt that is
not finite or outside MIN_DELTA_TIME (1 µs) to MAX_DELTA_TIME (1 s)) and nothing happened. Ok(report) means the world advanced; report.is_complete() is false when
Jolt dropped contacts because a fixed-size buffer was full, and the flag names the WorldSettings
limit to raise. The world stays valid either way.
PhysicsWorld::step_with_collision_steps(dt, n) advances by dt in n collision steps of dt / n
inside one Jolt update, with n from 1 to PhysicsWorld::MAX_COLLISION_STEPS (4); step(dt) is
the same call with n = 1. A count outside that range is StepError::InvalidValue, a collision
step shorter than MIN_DELTA_TIME is StepError::InvalidDeltaTime. The call gives one report and
one event batch. It is not the same as calling step(dt / n) n times: separate calls clear
forces, check sleep and batch events at each call, and let the game change inputs in between
(limits).
use oxijolt::prelude::*;
fn main() -> oxijolt::error::Result<()> {
let mut world = PhysicsWorld::new(WorldSettings::default())?;
// 1/30 s in two collision steps of 1/60 s.
let report = world.step_with_collision_steps(1.0 / 30.0, 2)?;
assert!(report.is_complete());
assert!(world.step_with_collision_steps(1.0 / 30.0, 0).is_err());
Ok(())
}Gravity is per world (WorldSettings::gravity, set_gravity). A game with radial gravity sets it
to zero and adds a force to each body before the step (BodyMut::add_force).
After a step, a game copies body poses into its own entities.
PhysicsWorld::active_body_poses_into fills a buffer with the id, position and rotation of every
body that is awake when it is called, in ascending BodyId order, under one lock; sleeping and
static bodies are absent. The poses are copies, so the world is free again once the call returns.
The awake set is not the set of bodies that moved. A body that falls asleep at the end of a step
still moved in that step and is absent from the readout after it; with
EventSettings::body_activation on, its ActivationEvent::Deactivated from take_events says to
read it once more with world.body(id). A pose written to a sleeping body
(Activation::DontActivate), rebase and restore_state move bodies that stay asleep, and
restore_state changes the awake set without events: after those, sync every body the game
tracks.
use oxijolt::prelude::math::*;
use oxijolt::prelude::*;
fn main() -> oxijolt::error::Result<()> {
let mut world = PhysicsWorld::new(WorldSettings::default())?;
let ball = Shape::new_sphere(0.5)?;
let mut balls = Vec::new();
for x in 0..3 {
let at = RVec3::new(2.0 * x as Real, 5.0, 0.0);
balls.push(world.create_body(&ball, &BodySettings::new_dynamic().position(at))?);
}
let mut poses = Vec::new();
for _ in 0..10 {
assert!(world.step(1.0 / 60.0)?.is_complete());
world.active_body_poses_into(&mut poses);
for pose in &poses {
// Here a game writes `pose.position` and `pose.rotation` to the entity of `pose.id`.
assert!(balls.contains(&pose.id));
}
}
assert_eq!(poses.len(), 3);
Ok(())
}With Bevy, glob-import oxijolt::prelude::* next to bevy::prelude::*: the prelude leaves Vec3,
Quat and Result to Bevy. The rustdoc of oxijolt::prelude has a transform-sync system.
Queries take &PhysicsWorld, so many threads may run them while nobody steps the world. They see
created, moved and removed bodies at once, without a step. Every normal they report is the
outward surface normal of the obstacle: a floor below gives a normal pointing up.
cast_ray: the closest hit along a ray. Convex shapes are solid (a ray that starts inside hits at fraction 0) and triangles are hit from both sides.cast_shape: the first obstacle a moving shape hits, with the distance it can move, the contact point and the obstacle's normal.ShapeCast::target_distance(spheres and capsules) stops the cast short of the obstacle.collide_shape: every obstacle a shape at a pose overlaps, with penetration depth and normal. The hits come in no particular order; sort them when order matters.collide_point: every body whose shape contains a point, sorted by body and sub-shape id. "Contains" is Jolt's rule per shape: solid convex shapes, meshes by the parity of the triangles above the point (the inside of a closed mesh, except where the upward ray passes through an edge or a vertex: the centre of a cube mesh is reported outside), never a heightfield, and strictly behind a plane.Shape::collide_pointasks one shape, in its own frame.
PhysicsWorld::rebase(bodies, rotation, translation) moves the whole world into a new frame with
one rigid change of coordinates: positions, rotations, velocities and gravity. No body wakes up or
falls asleep, and resting bodies keep their contacts, because Jolt caches contacts relative to the
bodies. The list must name every body of the world once, in a stable order of the caller's choice.
Rebase between steps, before adding the tick's forces: forces added since the last step are not
rotated. Queries see the new poses at once; optimize_broad_phase afterwards is optional and only
makes queries faster until the next step.
Awake bodies restart Jolt's sleep timer, as for every pose change, so they may fall asleep later
than without the rebase. New positions are only checked to be finite, not to lie within
limits::MAX_POSITION: a body the simulation carried out of the frame does not block a rebase.
A rotating rebase refuses a world with a body created with fewer than six degrees of freedom
(BodyError::RestrictedDofs), because Jolt locks world axes and a rotation would turn the body out
of them; a translation alone is accepted.
The rest of the world moves along:
- Characters move in id order after the bodies: position and rotation as for bodies, up and
velocity as vectors. Their cached contacts and ground stay in the old frame; after a rotating
rebase call
refresh_character_contactsfor every character before its next update. - Vehicles move with their chassis. A rotation also rotates each vehicle's gravity override, the up of a ray or sphere tester and each motorcycle's target lean. The wheel contacts stay in the old frame until the next step.
- Ragdolls, soft bodies and constraints are stored relative to their bodies and need no change. A soft body's vertices turn with it, which leaves its body rotation non-identity.
- Pulleys are the exception: their fixed points are world points, so a rebase recreates each
pulley in the new frame, in id order after the vehicles. A translation alone recreates pulleys
too. A pulley keeps its id, enabled state, ratio and lengths, and drops its warm start. It also
drops its cached rope directions: Jolt starts them at -Y and keeps the old one for a rope segment
of zero length, so only such a segment notices. Jolt removes the old pulley by moving its last
constraint into the freed place and appends the new pulley at the end, so the order of other
constraints changes too. The same calls give the same order.
A taut rope's length then rounds differently in
f32; a step in which it comes out just under the maximum length leaves the rope slack, and a hanging pair drifts by a few millimetres from where it would be without the rebase before the drift decays; 0.0023 m was measured after a turn of 0.4 rad. In double precision the slack step can come later than the first step after the rebase.
A rebase that moves anything makes earlier WorldStates unrestorable (state.md).
The requirement is that on one machine the same calls in the same order give bit-identical
results whatever the job system. The tests check it with 1 and 4 worker threads and with caller
job systems, on the scenes determinism.md lists. The order in which bodies are
created and removed is part of the state: it decides the BodyIds. Keep hash-map iteration order,
time and thread identity out of the calls that drive the world, and sort collide_shape hits
before acting on them. Equal results across platforms and compilers need the
cross-platform-deterministic feature.
With the debug-renderer feature, PhysicsWorld::debug_lines returns a DebugLines buffer with the
wireframe of the colliders around a point, filtered like a query; debug_lines_into refills one
buffer every frame. It produces line data only and
draws nothing. It has no level of detail, so cap the output with DebugLineSettings::max_lines.
A character is Jolt's CharacterVirtual: a convex shape, usually a capsule, that is not a body.
PhysicsWorld::create_character adds one; each tick the game sets its velocity and calls
update_character, which moves it by collision queries, slides it along what it hits and then
applies ExtendedUpdateSettings: stick to the floor and Jolt's walk stairs. Gravity is an argument
of the update and is not added to the velocity; the caller feeds the vertical speed itself.
- Shape and pose. The character position is where the shape's offset starts. With a capsule of
half height
hand radiusr,shape_offset(0, h + r, 0)puts the capsule's bottom at the position, and Jolt addscharacter_padding(0.02 m) along up, so a character at rest stands that far above the ground.CharacterSettings::humanoid(height, radius)builds such a capsule and keeps it in the settings, so noShapehas to outlive them. - Up. Up and rotation can change before every update (
CharacterMut::set_up,set_rotation), so on a planet up is radial. TheExtendedUpdateSettingsvectors are in world space and default to +Y; with another up, pass them along it. - Ground.
ground_stateisOnGround,OnSteepGround(steeper thanmax_slope_angle),NotSupportedorInAir. Below about 0.81° Jolt turns the slope limit off, so withmax_slope_angle(0.0)no ground is too steep. A new character reportsInAiruntil its first update orrefresh_character_contacts; refresh one that starts on the ground, or stick to floor (which needs support before the update) does nothing on the first tick. - Contacts.
active_contactslists what the last update touched. Normals point toward the character.contact_compound_childgives the touched compound child'suser_data(the game's collision group),contact_object_layerthe touched body's layer. - Filters.
update_characterandrefresh_character_contactstake the sameQueryFilteras queries. A game that keeps a kinematic capsule body per actor excludes the character's own withexclude_body; a character's optional inner body (CharacterSettings::inner_body) is never hit by its own character. - Replays.
save_statereturns the character's persistent state (pose, velocity, up, ground and the contacts with collision);restore_stateinto a world rebuilt the same way continues the run bit for bit. Values the caller carries between ticks, such as its vertical speed, belong in the replay too. Every world numbers its characters from 1 in creation order, so a rebuilt world gives the same ids. - Rebase.
PhysicsWorld::rebasemoves the characters with the bodies; after a rotating rebase, callrefresh_character_contactsfor each character before its next update. - Contact callbacks.
set_character_contact_listenerinstalls aCharacterContactListenerfor moving platforms and conveyors (adjust_body_velocity, read back asground_velocity), contacts a character ignores, whether a contact pushes the character or the character pushes it, and added, persisted and removed contacts (events.md).
Jolt's walk stairs and its steep-slope test judge an obstacle by the surface normal at the contact.
On a box with sharp edges (new_box_with_convex_radius(.., 0.0)) a capsule pressing on a face
touches it at the top edge. There Jolt picks the top face's normal or the side's by float
rounding of the contact point, so walk stairs on sharp steps is unreliable: the same settings
climb a 0.45 m step and refuse a 0.40 m one, and the outcome changes from scene to scene.
The capsule also creeps onto low sharp edges by itself (up to about 0.37 m in the walker tests).
No CharacterSettings value makes this dependable. On Jolt's default rounded boxes walk stairs
works (a 0.25 m step with a 0.4 m step-up is climbed), but the height it climbs is about
step-up + padding + r (1 - cos max_slope_angle), not the step-up itself, and the step's rounding
changes it too, so measure it for your capsule.
A game with sharp structures turns walk stairs off (walk_stairs_step_up(Vec3::ZERO)) and steps
in its own code after the update, with shape casts, which report the geometry's own normal: when a
grounded walker was held back by a steep contact, cast the capsule up by the step height (head
room), forward by the free room the step needs, and down onto the step; if the top is walkable and
not a dynamic body, set the character's position there and refresh its contacts. The repository's
reference controller for VOIDRUN does exactly this in crates/oxijolt/tests/common/walker.rs
(a 0.45 m step with 0.5 m of free room on a planet of radius 99, with underground recovery, the
vertical speed rules, stick to floor as the floor snap and a chained replay). It is test support,
not part of the API: copy and adapt it.
use oxijolt::prelude::math::*;
use oxijolt::prelude::*;
/// The game's collision groups, stored as compound child user data.
const STRUCTURE: u32 = 1;
const FEATURE: u32 = 2;
fn main() -> oxijolt::error::Result<()> {
// Layers: static terrain and chunks, moving items. Items collide with everything.
let mut layers = CollisionLayers::new(2);
let fixed = BroadPhaseLayer::new(0);
let moving = BroadPhaseLayer::new(1);
let terrain = layers.add_object_layer(fixed);
let chunk = layers.add_object_layer(fixed);
let item = layers.add_object_layer(moving);
layers
.enable_collision(item, terrain)
.enable_collision(item, chunk)
.enable_collision(item, item);
let mut world = PhysicsWorld::new(
WorldSettings::default()
.layers(layers)
.worker_threads(2),
)?;
// Terrain: a flat 33 x 33 heightfield at y = 0, one sample per metre, x and z in [-16, 16].
let ground = Shape::new_height_field(
33,
&[0.0; 33 * 33],
&HeightFieldSettings::default().offset(Vec3::new(-16.0, 0.0, -16.0)),
)?;
let terrain_body =
world.create_body(&ground, &BodySettings::new_static().object_layer(terrain))?;
// A chunk at (5, 0, 5): a porch you can stand on (top at y = 1) under a canopy you walk
// through (top at y = 3.5). One static body, two groups.
let porch = Shape::new_box(Vec3::new(2.0, 0.5, 2.0))?;
let canopy = Shape::new_cylinder(0.5, 2.0)?;
let house = Shape::new_compound(&[
CompoundChild {
shape: &porch,
position: Vec3::new(0.0, 0.5, 0.0),
rotation: Quat::IDENTITY,
user_data: STRUCTURE,
},
CompoundChild {
shape: &canopy,
position: Vec3::new(0.0, 3.0, 0.0),
rotation: Quat::IDENTITY,
user_data: FEATURE,
},
])?;
let chunk_body = world.create_body(
&house,
&BodySettings::new_static()
.position(RVec3::new(5.0, 0.0, 5.0))
.object_layer(chunk),
)?;
// An item dropped from 2 m: a 0.5 m crate of 2 kg with continuous collision detection.
let crate_shape = Shape::new_box(Vec3::new(0.25, 0.25, 0.25))?;
let item_body = world.create_body(
&crate_shape,
&BodySettings::new_dynamic()
.position(RVec3::new(-5.0, 2.0, 0.0))
.mass(2.0)
.friction(0.5)
.motion_quality(MotionQuality::LinearCast)
.object_layer(item),
)?;
// Two seconds at 60 Hz.
for _ in 0..120 {
let report = world.step(1.0 / 60.0)?;
assert!(report.is_complete(), "raise a WorldSettings limit: {report:?}");
}
let resting = world.body(item_body)?.position();
assert!((resting.y - 0.25).abs() < 0.03, "the crate rests on the terrain");
// Ground under the house: terrain and chunks, structures only, so the canopy is skipped.
let ground_layers = [terrain, chunk];
let ground_filter = QueryFilter::new()
.object_layers(&ground_layers)
.child_groups(1 << STRUCTURE);
let down = RayCast::new(RVec3::new(5.0, 10.0, 5.0), Vec3::new(0.0, -20.0, 0.0));
let hit = world.cast_ray(&down, &ground_filter)?.expect("the porch");
assert_eq!(hit.body, chunk_body);
assert_eq!(hit.compound_child.map(|child| child.user_data), Some(STRUCTURE));
assert!((down.point_at(hit.fraction).y - 1.0).abs() < 1e-3);
assert!(hit.normal.y > 0.99, "a floor's normal points up");
// The same ray without a group filter stops on the canopy.
let hit = world.cast_ray(&down, &QueryFilter::new())?.expect("the canopy");
assert_eq!(hit.compound_child.map(|child| child.user_data), Some(FEATURE));
// An actor capsule (feet 0.8 m below its origin) cast 5 m down onto the terrain.
let actor = Shape::new_capsule(0.5, 0.3)?;
let fall = ShapeCast::new(
&actor,
RVec3::new(-10.0, 3.0, -10.0),
Quat::IDENTITY,
Vec3::new(0.0, -5.0, 0.0),
);
let hit = world.cast_shape(&fall, &QueryFilter::new())?.expect("the terrain");
assert_eq!(hit.body, terrain_body);
assert!((hit.distance - 2.2).abs() < 1e-3);
assert!(hit.normal.y > 0.99);
// A ball sunk 0.3 m into the porch, skipping the item: one overlap, pushed out upwards.
let ball = Shape::new_sphere(0.5)?;
let probe = CollideShape::new(&ball, RVec3::new(5.0, 1.2, 5.0), Quat::IDENTITY);
let mut hits = world.collide_shape(&probe, &QueryFilter::new().exclude_body(item_body))?;
hits.sort_by_key(|hit| (hit.body.to_raw(), hit.sub_shape_id.to_raw()));
assert_eq!(hits.len(), 1);
assert!((hits[0].penetration_depth - 0.3).abs() < 1e-3);
assert!(hits[0].normal.y > 0.99);
// Floating origin: move everything so the house sits at the origin. Every body, in a
// stable order (here: creation order).
world.rebase(
&[terrain_body, chunk_body, item_body],
Quat::IDENTITY,
RVec3::new(-5.0, 0.0, -5.0),
)?;
let moved = world.body(item_body)?.position();
assert!((moved.x - (resting.x - 5.0)).abs() < 1e-4);
assert!((moved.z - (resting.z - 5.0)).abs() < 1e-4);
let down = RayCast::new(RVec3::new(0.0, 10.0, 0.0), Vec3::new(0.0, -20.0, 0.0));
let hit = world.cast_ray(&down, &ground_filter)?.expect("the porch");
assert_eq!(hit.body, chunk_body);
// The world keeps stepping in the new frame.
for _ in 0..60 {
assert!(world.step(1.0 / 60.0)?.is_complete());
}
Ok(())
}A character walks on a planet of radius 50 m, a static sphere at the origin, so up is radial and changes every tick. Gravity is applied by the caller. The helpers that turn radial up into a rotation and a walking direction work at every point of the sphere, which the example checks by starting a short walk at both poles and on the equator. A chunk compound stands in its path: a feature (a bush) the character walks through and a structure (a wall) that stops it, told apart by the filter's group mask. Halfway through the walk the character's state is saved, and a rebuilt world continues from it bit for bit.
use oxijolt::*;
/// The game's collision groups, stored as compound child user data.
const STRUCTURE: u32 = 1;
const FEATURE: u32 = 2;
const PLANET_RADIUS: f32 = 50.0;
const GRAVITY: f32 = 9.8;
const DT: f32 = 1.0 / 60.0;
fn scale(v: Vec3, s: f32) -> Vec3 {
Vec3::new(v.x * s, v.y * s, v.z * s)
}
fn add(a: Vec3, b: Vec3) -> Vec3 {
Vec3::new(a.x + b.x, a.y + b.y, a.z + b.z)
}
fn dot(a: Vec3, b: Vec3) -> f32 {
a.x * b.x + a.y * b.y + a.z * b.z
}
fn normalize(v: Vec3) -> Vec3 {
scale(v, 1.0 / dot(v, v).sqrt())
}
/// Radial up at `p`, away from the planet's centre.
fn up_at(p: RVec3) -> Vec3 {
normalize(Vec3::new(p.x as f32, p.y as f32, p.z as f32))
}
/// A rotation that turns +Y into the unit vector `up`. On the upper half it is the shortest one;
/// on the lower half it first turns +Y half a turn about X to -Y and then takes the shortest way,
/// so no division comes near zero.
fn rotation_to(up: Vec3) -> Quat {
let q = if up.y >= 0.0 {
Quat::from_xyzw(up.z, 0.0, -up.x, 1.0 + up.y)
} else {
Quat::from_xyzw(1.0 - up.y, up.x, 0.0, up.z)
};
let length = (q.x * q.x + q.y * q.y + q.z * q.z + q.w * q.w).sqrt();
Quat::from_xyzw(q.x / length, q.y / length, q.z / length, q.w / length)
}
/// The walking direction at unit `up`: east (+X along the ground), or +Z along the ground near
/// the two points where up is ±X and east does not exist.
fn forward_at(up: Vec3) -> Vec3 {
let reference = if up.x.abs() < 0.9 {
Vec3::new(1.0, 0.0, 0.0)
} else {
Vec3::new(0.0, 0.0, 1.0)
};
normalize(add(reference, scale(up, -dot(reference, up))))
}
/// Distance of `p` above the planet's surface.
fn height(p: RVec3) -> f32 {
let p = Vec3::new(p.x as f32, p.y as f32, p.z as f32);
dot(p, p).sqrt() - PLANET_RADIUS
}
/// The planet, a chunk 3 m east of the north pole and a character standing on the ground where
/// up is `start`.
fn build(start: Vec3) -> Result<(PhysicsWorld, CharacterId), Box<dyn std::error::Error>> {
// Radial gravity is the caller's, so the world has none.
let mut world = PhysicsWorld::new(WorldSettings::default().gravity(Vec3::ZERO))?;
let planet = Shape::new_sphere(PLANET_RADIUS)?;
world.create_body(&planet, &BodySettings::new_static())?;
// The chunk's origin is on the ground with its local Y along up: a bush 1.5 m before a
// 3 m wall, both sunk 0.5 m into the ground.
let angle = 3.0 / PLANET_RADIUS;
let ground = Vec3::new(PLANET_RADIUS * angle.sin(), PLANET_RADIUS * angle.cos(), 0.0);
let wall = Shape::new_box(Vec3::new(0.25, 1.5, 2.0))?;
let bush = Shape::new_cylinder(1.0, 0.4)?;
let chunk = Shape::new_compound(&[
CompoundChild {
shape: &wall,
position: Vec3::new(0.0, 1.0, 0.0),
rotation: Quat::IDENTITY,
user_data: STRUCTURE,
},
CompoundChild {
shape: &bush,
position: Vec3::new(-1.5, 0.5, 0.0),
rotation: Quat::IDENTITY,
user_data: FEATURE,
},
])?;
world.create_body(
&chunk,
&BodySettings::new_static()
.position(RVec3::new(ground.x as Real, ground.y as Real, 0.0))
.rotation(rotation_to(normalize(ground))),
)?;
// A capsule 1.6 m tall whose bottom is at the character position.
let capsule = Shape::new_capsule(0.5, 0.3)?;
let settings = CharacterSettings::new(&capsule)
.shape_offset(Vec3::new(0.0, 0.8, 0.0))
.max_slope_angle(45.0_f32.to_radians());
let ground = scale(start, PLANET_RADIUS);
let position = RVec3::new(ground.x as Real, ground.y as Real, ground.z as Real);
let id = world.create_character(&settings, position, rotation_to(start))?;
// A new character knows no ground until its contacts are refreshed.
world.refresh_character_contacts(id, &walk_filter())?;
Ok((world, id))
}
/// What the character collides with: every body, but of compounds only the structures.
fn walk_filter() -> QueryFilter<'static> {
QueryFilter::new().child_groups(1 << STRUCTURE)
}
/// One tick: walk forward at 2 m/s along the ground. `vel_up` is the vertical speed the caller
/// carries from tick to tick.
fn tick(
world: &mut PhysicsWorld,
id: CharacterId,
vel_up: &mut f32,
) -> Result<(), Box<dyn std::error::Error>> {
let character = world.character(id)?;
let up = up_at(character.position());
*vel_up = if character.ground_state() == GroundState::OnGround {
// One tick of gravity, so that stick to floor has something to follow.
-GRAVITY * DT
} else {
*vel_up - GRAVITY * DT
};
let forward = forward_at(up);
let mut character = world.character_mut(id)?;
character.set_up(up)?;
character.set_rotation(rotation_to(up))?;
character.set_linear_velocity(add(scale(forward, 2.0), scale(up, *vel_up)))?;
// Stick to floor along -up; walk stairs off, as for a game with sharp steps.
let extended = ExtendedUpdateSettings::default()
.stick_to_floor_step_down(scale(up, -0.3))
.walk_stairs_step_up(Vec3::ZERO);
world.update_character(id, DT, scale(up, -GRAVITY), &extended, &walk_filter())?;
Ok(())
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let north = Vec3::new(0.0, 1.0, 0.0);
let (mut world, id) = build(north)?;
assert_eq!(world.character(id)?.ground_state(), GroundState::OnGround);
// One second of walking, then save the state and the caller's vertical speed.
let mut vel_up = 0.0;
for _ in 0..60 {
tick(&mut world, id, &mut vel_up)?;
}
let saved = (world.character(id)?.save_state(), vel_up);
// Another second: the walk ends at the wall, after passing through the bush.
for _ in 0..60 {
tick(&mut world, id, &mut vel_up)?;
}
let character = world.character(id)?;
assert_eq!(character.ground_state(), GroundState::OnGround);
let p = character.position();
let above = height(p);
assert!(above.abs() < 0.01, "on the ground: {above}");
let walked = PLANET_RADIUS * (p.x as f32).atan2(p.y as f32);
assert!((2.3..2.5).contains(&walked), "stopped at the wall: {walked}");
let wall = character
.active_contacts()
.iter()
.filter(|contact| contact.had_collision)
.find_map(|contact| character.contact_compound_child(contact))
.expect("a contact with the wall");
assert_eq!(wall.user_data, STRUCTURE);
let finished = character.save_state();
// A world built the same way, restored from the saved state, ends the same, bit for bit.
let (mut replay, replay_id) = build(north)?;
replay.character_mut(replay_id)?.restore_state(&saved.0)?;
let mut vel_up = saved.1;
for _ in 0..60 {
tick(&mut replay, replay_id, &mut vel_up)?;
}
assert_eq!(replay.character(replay_id)?.save_state(), finished);
// Half a second of walking from the south pole and from the four equator points stays on
// the ground and covers about a metre.
for start in [
Vec3::new(0.0, -1.0, 0.0),
Vec3::new(1.0, 0.0, 0.0),
Vec3::new(-1.0, 0.0, 0.0),
Vec3::new(0.0, 0.0, 1.0),
Vec3::new(0.0, 0.0, -1.0),
] {
let (mut world, id) = build(start)?;
let mut vel_up = 0.0;
for _ in 0..30 {
tick(&mut world, id, &mut vel_up)?;
}
let character = world.character(id)?;
assert_eq!(character.ground_state(), GroundState::OnGround);
let p = character.position();
assert!(height(p).abs() < 0.01, "on the ground from {start:?}: {p:?}");
let ground = scale(start, PLANET_RADIUS);
let moved = Vec3::new(p.x as f32 - ground.x, p.y as f32 - ground.y, p.z as f32 - ground.z);
let moved = dot(moved, moved).sqrt();
assert!((0.9..1.1).contains(&moved), "walked from {start:?}: {moved}");
}
Ok(())
}A vehicle is Jolt's VehicleConstraint with the wheeled controller, attached to a dynamic body the
caller created, its chassis. PhysicsWorld::create_wheeled_vehicle(chassis, &settings) adds it; from then
on it runs inside every step: at the start of the step each wheel casts against the scene and
gravity is applied, then engine, brakes and tire friction act through the constraint. Tracked
vehicles and motorcycles work the same way and have a guide of their own; what
follows holds for every kind unless it names the wheeled settings.
- Chassis. The chassis shape gives the vehicle its centre of mass, and its mass and inertia
unless
BodySettings::massormass_and_inertiaoverrides them (bodies).Shape::new_offset_center_of_massmoves only the centre of mass, not the collision surface, so a box hull can carry a low centre of mass that keeps the car from rolling over.remove_bodyrefuses a chassis while its vehicle exists; callremove_vehiclefirst. - Settings.
WheeledVehicleSettings::new(wheels, differentials, collision_tester), with builders for the rest; the defaults are Jolt's. Positions and directions are in the chassis body's space, with up +Y and forward +Z by default. Wheels are numbered in the order given, and differentials and anti-roll bars name them by index; Jolt's samples put the left wheels at +X. A vehicle needs at least one differential, and the engine torque ratios of all differentials add up to 1. Values Jolt asserts on or divides by are checked first:create_wheeled_vehiclereturnsVehicleError::InvalidValuefor them and creates nothing.WheeledVehicleSettings::caris a ready front-wheel-drive car of four wheels mirrored from the front left one, with Jolt's default engine and transmission. - Wheels and the ground.
VehicleCollisionTester::ray,cast_sphereorcast_cylinder. The wheels see the bodies whose object layer collides with the tester's object layer, never their own chassis. A tester cannot skip compound children by group, so give the wheels their own object layer that collides with exactly what they drive on. - Gravity. For gravity the game applies itself (radial on a planet), create the chassis with
gravity_factor(0.0)andallow_sleeping(false), and callVehicleMut::set_gravitywith the gravity at the car before every step. Jolt then adds that gravity times the chassis mass as a force on every step while the chassis is awake, once per collision step withstep_with_collision_steps, as world gravity acts; a sleeping chassis gets nothing. The opposite of that gravity is also the up of the pitch and roll limit (WheeledVehicleSettings::max_pitch_roll_angle). - Driving.
set_driver_input(DriverInput { forward, right, brake, hand_brake }): forward and right in[-1, 1], the brakes in[0, 1]. Right 1 steers fully right, which Jolt reports as a negative steer angle. The input stays until it is set again. - Readout.
VehicleRef::wheelsgives each wheel'sWheelState: the ground contact (body, sub-shape, point, normal), the suspension length, the wheel's rotation speed and the impulses of the last step;engine_rpmandcurrent_gearread the drivetrain. Contacts are found at the start of the step, at the chassis pose before the step moved it. Normals are the ground's outward normal, as in queries. A wheel whose cast starts inside a solid body reports suspension length 0 andhit_hard_point, not how deep it is;collide_shapegives the depth.wheel_world_transformgives a wheel's centre and rotation in world space for drawing it, for a wheel model whose axle is its local Y. - Rebase.
PhysicsWorld::rebasemoves the chassis like any body and rotates each vehicle's gravity override, the up of a ray or sphere tester and a motorcycle's target lean; the reported contacts stay in the old frame until the next step. - Tester accuracy.
wheel_contacts_match_the_ground_geometrychecks every tester on flat ground within 1e-3 m. A one-off measurement on a chassis rolled 6° or pitched -4° over box and heightfield ground and a 6° slope found the ray and sphere testers' suspension length within 1.1e-7 m of the analytic value and their contact point within 2.5e-6 m. The cylinder tester's suspension length was within 5.6e-5 m, but its contact point slid up to 1.7 mm along the rim (Jolt resolves it with GJK/EPA, which converges in height, not along the rim). Judge cylinder contacts by suspension length, normal and distance to the ground. - Soft bodies. Wheels look through soft bodies and report only the rigid ground below them: Jolt's vehicle constraint solves the body under a wheel as a rigid body.
A ragdoll is Jolt's Ragdoll: one body per joint of a Skeleton, each joined to its parent part
by a constraint. RagdollSettings::new(&skeleton, &parts) builds the settings once. Each
RagdollPart is a shape, BodySettings (motion type, layer, mass, friction, damping, gravity
factor, initial velocity) at the part's bind pose in world space, and the joint to its parent.
- Joints.
RagdollJoint::SwingTwist(spine, neck, shoulders),Hingewith limits (elbows, knees) andSixDof(hips). By default their frames are given in world space at the bind pose. In a joint's frame X is the twist or hinge axis and Y and Z are the swing axes. A cone swing has symmetric limits;SwingType::Pyramidallows a different range on each side, which a hip needs. - Collisions. Parts of one ragdoll never collide with each other: every pair of parts is
disabled in a group filter, and each ragdoll gets its own group, so two ragdolls still collide.
To keep ragdolls out of the main simulation, put them in a second
PhysicsWorldwhose static bodies are made from the sameShapehandles as the main world's: Jolt shares a shape, it does not copy it. - Gravity. For the game's own gravity, give every part
gravity_factor(0.0)and add gravity times mass to each part before every step (BodyRef::mass,BodyMut::add_force). - Pose and drive.
RagdollRef::poseandRagdollMut::set_poseread and write every part at once.drive_to_pose_using_motorsaims each joint's motors at its part's rotation relative to the parent part in the pose. Motors keep the parts awake only while something wakes them, so drive every tick.drive_to_pose_using_kinematicsmoves kinematic parts to the pose in one step. - Rest.
SettleDetector::default()reports a ragdoll as settled once every part has moved slower than 0.05 m/s and 0.1 rad/s for 30 updates in a row; the timeout is the caller's. In the repository's tests a humanoid came to rest reliably only with joint friction (about 1 N·m on every joint,max_friction_torqueandmax_friction); without it the near-spherical head kept rocking on the ground and several drops did not settle. - Joint limits on impact. In each solver iteration Jolt solves contacts after constraints, so
when a falling ragdoll hits the ground the contacts win and joints pass their limits for a few
dozen ticks before the constraints pull them mostly back. These are measurements, not bounds:
in the repository's drop test (a 12-part humanoid dropped with its pelvis 1.5 m above the
terrain) the worst overshoot is 0.29 rad and 0.0037 rad remain at rest; the test's bounds,
0.40 rad during the fall and 0.01 rad at rest, hold for that drop only. A sweep of 126 drops
of that humanoid, run once outside the test suite and not kept in the repository (7 sideways
and 3 forward offsets, drops of 1.0, 1.5 and 2.5 m with different yaw, raw and stabilized
masses, caller-applied gravity), found an overshoot above 0.40 rad in 4 drops (up to 0.48 rad),
joints more than 0.01 rad outside their limits at rest in 40 (up to 0.15 rad), 3 drops that
needed more than 600 ticks to settle, and hinges bent about their fixed axes by up to 0.57 rad
on impact. Check joint limits at rest, with a tolerance, not on every tick. In that sweep the
centre of a thin limb dropped from 2.5 m ended up to 0.15 m below the terrain surface with the
default
MotionQuality::Discrete;MotionQuality::LinearCaston the limbs is the setting to try for high falls. - Lifecycle.
remove_ragdollremoves the parts and wakes what rested on them;remove_bodyrefuses a part. - Animation skeletons. A
SkeletonMapper(Jolt'sSkeletonMapper) maps poses between the ragdoll's skeleton and a detailed animation skeleton. Joints are matched by name, and the animation skeleton keeps the ragdoll's hierarchy; it may add joints between the ragdoll's, above its root and below its leaves.map_reverseturns an animation pose into a ragdoll pose from the matched joints only: a target forset_poseand both drives.mapturns the simulatedRagdollRef::poseand the animation's local pose into an animation pose to render. Unmatched joints between a ragdoll joint and one of its children form a chain: the chain's start turns so that the chain points at the child's ragdoll joint. Jolt builds one chain per start joint, along the longest path (on a tie, toward the lowest ragdoll joint index); the joints between the start and its other children keep their local transforms, as do extra roots (relative to the root offset) and leaves.TranslationLockskeep animation joints at their neutral offset from their parent, which hides the joints' stretch under load; Jolt applies them after the unmatched joints, so an unmatched child of a locked joint follows the joint's unlocked position. Both neutral poses describe the character standing in one place. A chain shorter thanlimits::MIN_MAPPED_CHAIN_LENGTHis refused withRagdollError::DegenerateChain; far from the pose's root offset, rounding makes the allowance larger (limits.md).
use oxijolt::*;
# fn main() -> Result<(), RagdollError> {
let joint = |name, parent| SkeletonJoint { name, parent };
let ragdoll = Skeleton::new(&[joint("pelvis", None), joint("chest", Some(0))])?;
let animation = Skeleton::new(&[
joint("pelvis", None),
joint("spine", Some(0)),
joint("chest", Some(1)),
])?;
// Joints along +Y at these heights above a root offset 1 m up.
let pose = |heights: &[f32]| SkeletonPose {
root_offset: RVec3::new(0.0, 1.0, 0.0),
joints: heights
.iter()
.map(|&y| JointTransform {
translation: Vec3::new(0.0, y, 0.0),
rotation: Quat::IDENTITY,
})
.collect(),
};
let mapper = SkeletonMapper::new(
MappedSkeleton { skeleton: &ragdoll, neutral_pose: &pose(&[0.0, 0.5]) },
MappedSkeleton { skeleton: &animation, neutral_pose: &pose(&[0.0, 0.25, 0.5]) },
TranslationLocks::None,
)?;
// Animation to ragdoll: a target for `set_pose` or the drives.
let target = mapper.map_reverse(&pose(&[0.0, 0.3, 0.6]))?;
assert_eq!(target.joints.len(), 2);
// Ragdoll to animation: the local pose (each joint relative to its parent) lays out the spine.
let local = pose(&[0.0, 0.25, 0.25]).joints;
let shown = mapper.map(&target, &local)?;
assert_eq!(shown.joints.len(), 3);
# Ok(())
# }A car drives over rolling terrain in the main world while a ragdoll falls in a second world that shares the terrain shape. Neither world has gravity of its own: the caller applies it, as on a planet, where it would point at the centre. The ragdoll faces +Z in its bind pose: a pelvis, chest and head joined by swing-twist joints, thighs on six-DOF hips with asymmetric limits and shins on hinged knees.
use oxijolt::*;
type Error = Box<dyn std::error::Error>;
const DT: f32 = 1.0 / 60.0;
/// The gravity the caller applies. On a planet it would point at the centre and change with the
/// position; here it is constant to keep the example short.
const GRAVITY: Vec3 = Vec3::new(0.0, -9.81, 0.0);
/// The object layers of both worlds.
struct Layers {
ground: ObjectLayer,
moving: ObjectLayer,
wheels: ObjectLayer,
}
/// A world without gravity of its own, in which wheels see the ground only.
fn world() -> Result<(PhysicsWorld, Layers), Error> {
let mut table = CollisionLayers::new(2);
let fixed = BroadPhaseLayer::new(0);
let moving = BroadPhaseLayer::new(1);
let layers = Layers {
ground: table.add_object_layer(fixed),
moving: table.add_object_layer(moving),
wheels: table.add_object_layer(moving),
};
table
.enable_collision(layers.moving, layers.ground)
.enable_collision(layers.moving, layers.moving)
.enable_collision(layers.wheels, layers.ground);
let world = PhysicsWorld::new(WorldSettings::default().gravity(Vec3::ZERO).layers(table))?;
Ok((world, layers))
}
/// A gentle swell, metres.
fn swell(x: f32, z: f32) -> f32 {
0.3 * (0.35 * x).sin() * (0.25 * z).cos()
}
/// The terrain: 33 x 33 samples of the swell, one per metre, x and z in [-16, 16].
fn terrain() -> Result<Shape, Error> {
let mut samples = Vec::with_capacity(33 * 33);
for z in 0..33 {
for x in 0..33 {
samples.push(swell(x as f32 - 16.0, z as f32 - 16.0));
}
}
let settings = HeightFieldSettings::default().offset(Vec3::new(-16.0, 0.0, -16.0));
Ok(Shape::new_height_field(33, &samples, &settings)?)
}
/// The height of the ground below `(x, z)`.
fn ground_at(world: &PhysicsWorld, layers: &Layers, x: Real, z: Real) -> Result<Real, Error> {
let ray = RayCast::new(RVec3::new(x, 10.0, z), Vec3::new(0.0, -20.0, 0.0));
let ground = [layers.ground];
let hit = world.cast_ray(&ray, &QueryFilter::new().object_layers(&ground))?;
Ok(ray.point_at(hit.ok_or("no ground")?.fraction).y)
}
/// Adds the force of `GRAVITY` to a dynamic body.
fn apply_gravity(world: &mut PhysicsWorld, body: BodyId) -> Result<(), Error> {
let mass = world.body(body)?.mass().ok_or("a dynamic body")?;
let force = Vec3::new(GRAVITY.x * mass, GRAVITY.y * mass, GRAVITY.z * mass);
world.body_mut(body)?.add_force(force)?;
Ok(())
}
/// A front-wheel-drive car with a low centre of mass, 8 m south of the centre, facing +Z.
fn add_car(world: &mut PhysicsWorld, layers: &Layers) -> Result<(BodyId, VehicleId), Error> {
let hull = Shape::new_box(Vec3::new(0.9, 0.3, 2.0))?;
let chassis_shape = Shape::new_offset_center_of_mass(&hull, Vec3::new(0.0, -0.3, 0.0))?;
let ground = ground_at(world, layers, 0.0, -8.0)?;
let chassis = world.create_body(
&chassis_shape,
&BodySettings::new_dynamic()
.position(RVec3::new(0.0, ground + 1.0, -8.0))
.object_layer(layers.moving)
.mass(1500.0)
.allow_sleeping(false)
.gravity_factor(0.0),
)?;
// Left wheels at +X. The front wheels steer, the hand brake holds the rear ones.
let wheel = |x: f32, z: f32| WheelSettings::new(Vec3::new(x, -0.1, z)).radius(0.35).width(0.2);
let front = |x| wheel(x, 1.4).max_steer_angle(0.5).max_hand_brake_torque(0.0);
let rear = |x| wheel(x, -1.4).max_steer_angle(0.0);
let settings = WheeledVehicleSettings::new(
vec![front(0.9), front(-0.9), rear(0.9), rear(-0.9)],
vec![VehicleDifferentialSettings::new(Some(0), Some(1))],
VehicleCollisionTester::cast_sphere(layers.wheels, 0.2),
)
.anti_roll_bars(vec![VehicleAntiRollBar::new(0, 1), VehicleAntiRollBar::new(2, 3)]);
let car = world.create_wheeled_vehicle(chassis, &settings)?;
Ok((chassis, car))
}
/// The ragdoll's skeleton: the name and parent of each joint, parents first.
const JOINTS: [(&str, Option<u32>); 7] = [
("pelvis", None),
("chest", Some(0)),
("head", Some(1)),
("thigh_l", Some(0)),
("shin_l", Some(3)),
("thigh_r", Some(0)),
("shin_r", Some(5)),
];
/// The bind pose with the pelvis at the origin: each part's centre, capsule half height and
/// radius, and mass in kg. Every capsule stands along Y.
const PARTS: [([f32; 3], f32, f32, f32); 7] = [
([0.0, 0.0, 0.0], 0.1, 0.15, 12.0),
([0.0, 0.42, 0.0], 0.12, 0.15, 18.0),
([0.0, 0.8, 0.0], 0.04, 0.11, 5.0),
([0.12, -0.42, 0.0], 0.14, 0.08, 8.0),
([0.12, -0.86, 0.0], 0.14, 0.07, 4.0),
([-0.12, -0.42, 0.0], 0.14, 0.08, 8.0),
([-0.12, -0.86, 0.0], 0.14, 0.07, 4.0),
];
/// Joint friction, N·m: it lets the ragdoll come to rest instead of rocking.
const FRICTION: f32 = 1.0;
/// How far a knee bends, radians.
const KNEE_BEND: f32 = 2.2;
/// The joint of `part` to its parent, in world space at the bind pose.
fn joint(part: usize) -> Option<RagdollJoint> {
let up = Vec3::new(0.0, 1.0, 0.0);
let down = Vec3::new(0.0, -1.0, 0.0);
let side = Vec3::new(1.0, 0.0, 0.0);
let x = PARTS[part].0[0];
let at = |y: Real| RVec3::new(x as Real, y, 0.0);
Some(match part {
0 => return None,
// Waist and neck: the twist axis runs up the spine.
1 | 2 => {
let (anchor, cone) = if part == 1 { (at(0.2), 0.3) } else { (at(0.62), 0.5) };
RagdollJoint::SwingTwist(
SwingTwistConstraintSettings::new(anchor, up, side)
.half_cone_angles(cone, cone)
.twist_limits(-0.3, 0.3)
.max_friction_torque(FRICTION),
)
}
// Hips: the twist axis runs down the thigh, Y is the side axis and Z points forward. A
// negative turn about Y swings the leg forward, a positive turn about Z swings it toward
// +X: the leg swings 1.4 rad forward but 0.3 back, 0.5 out but 0.2 in.
3 | 5 => {
let (out, inward) = (0.5, 0.2);
let about_z = if x > 0.0 { (-inward, out) } else { (-out, inward) };
let mut hip = SixDofConstraintSettings::new(at(-0.24), down, side)
.swing_type(SwingType::Pyramid);
for axis in [
SixDofConstraintAxis::TranslationX,
SixDofConstraintAxis::TranslationY,
SixDofConstraintAxis::TranslationZ,
] {
hip = hip.axis(axis, SixDofAxis::Fixed);
}
for (axis, (min, max)) in [
(SixDofConstraintAxis::RotationX, (-0.3, 0.3)),
(SixDofConstraintAxis::RotationY, (-1.4, 0.3)),
(SixDofConstraintAxis::RotationZ, about_z),
] {
hip = hip
.axis(axis, SixDofAxis::Limited { min, max })
.max_friction(axis, FRICTION);
}
RagdollJoint::SixDof(hip)
}
// Knees: a hinge about the side axis; a positive angle swings the shin back, to -Z.
_ => RagdollJoint::Hinge(
HingeConstraintSettings::new(at(-0.64), side, down)
.limits(0.0, KNEE_BEND)
.max_friction_torque(FRICTION),
),
})
}
/// The ragdoll's settings, its parts in `layer` under the caller's gravity.
fn ragdoll_settings(layer: ObjectLayer) -> Result<RagdollSettings, Error> {
let joints: Vec<SkeletonJoint<'_>> = JOINTS
.iter()
.map(|&(name, parent)| SkeletonJoint { name, parent })
.collect();
let skeleton = Skeleton::new(&joints)?;
let shapes = PARTS
.iter()
.map(|&(_, half_height, radius, _)| Shape::new_capsule(half_height, radius))
.collect::<Result<Vec<_>, _>>()?;
let parts: Vec<RagdollPart<'_>> = PARTS
.iter()
.zip(&shapes)
.enumerate()
.map(|(part, (&([x, y, z], _, _, mass), shape))| RagdollPart {
shape,
body: BodySettings::new_dynamic()
.position(RVec3::new(x as Real, y as Real, z as Real))
.object_layer(layer)
.mass(mass)
.friction(0.6)
.gravity_factor(0.0),
joint: joint(part),
})
.collect();
Ok(RagdollSettings::new(&skeleton, &parts)?)
}
/// The bind pose turned a quarter turn about Z, lying on its side, with the pelvis at `pelvis`.
fn lying_pose(pelvis: RVec3) -> SkeletonPose {
let half_angle = std::f32::consts::FRAC_PI_4;
let rotation = Quat::from_xyzw(0.0, 0.0, half_angle.sin(), half_angle.cos());
let joints = PARTS
.iter()
// The quarter turn about Z takes (x, y) to (-y, x).
.map(|&([x, y, z], ..)| JointTransform { translation: Vec3::new(-y, x, z), rotation })
.collect();
SkeletonPose { root_offset: pelvis, joints }
}
fn main() -> Result<(), Error> {
// One terrain shape in two worlds.
let terrain = terrain()?;
let (mut main, main_layers) = world()?;
let ground = BodySettings::new_static().object_layer(main_layers.ground);
let main_terrain = main.create_body(&terrain, &ground)?;
let (mut ragdolls, layers) = world()?;
let ground = BodySettings::new_static().object_layer(layers.ground);
let ragdoll_terrain = ragdolls.create_body(&terrain, &ground)?;
drop(terrain); // each body holds its own reference
assert_eq!(
ground_at(&main, &main_layers, 3.3, -2.7)?.to_bits(),
ground_at(&ragdolls, &layers, 3.3, -2.7)?.to_bits(),
);
// Three seconds at full throttle; the caller's gravity is set before every step.
let (chassis, car) = add_car(&mut main, &main_layers)?;
for _ in 0..180 {
let mut vehicle = main.vehicle_mut(car)?;
vehicle.set_gravity(GRAVITY)?;
vehicle.set_driver_input(DriverInput { forward: 1.0, ..DriverInput::default() })?;
assert!(main.step(DT)?.is_complete());
}
let body = main.body(chassis)?;
assert!(body.position().z > -4.0, "the car drove: {:?}", body.position());
// The y component of the chassis' up, the rotated +Y, is 1 - 2 (x² + z²).
let r = body.rotation();
assert!(1.0 - 2.0 * (r.x * r.x + r.z * r.z) > 0.9, "the car is upright");
let vehicle = main.vehicle(car)?;
assert!(vehicle.current_gear() >= 1);
for wheel in vehicle.wheels() {
let contact = wheel.contact.ok_or("every wheel is on the terrain")?;
assert_eq!(contact.body, main_terrain);
assert!(contact.normal.y > 0.9, "the terrain's normal points up");
}
// The ragdoll falls from 1.2 m, lying on its side, with a push; gravity is the caller's.
let settings = ragdoll_settings(layers.moving)?;
let pelvis = RVec3::new(5.0, ground_at(&ragdolls, &layers, 5.0, 5.0)? + 1.2, 5.0);
let ragdoll =
ragdolls.create_ragdoll(&settings, Some(&lying_pose(pelvis)), Activation::Activate)?;
ragdolls
.ragdoll_mut(ragdoll)?
.set_linear_and_angular_velocity(Vec3::new(1.0, 0.0, 0.5), Vec3::new(0.0, 1.0, 0.0))?;
let parts = ragdolls.ragdoll(ragdoll)?.body_ids().to_vec();
let mut detector = SettleDetector::default();
let mut landed = false;
let mut settled = false;
for _ in 0..600 {
for &part in &parts {
apply_gravity(&mut ragdolls, part)?;
}
assert!(ragdolls.step(DT)?.is_complete());
for (i, &a) in parts.iter().enumerate() {
landed |= ragdolls.were_bodies_in_contact(a, ragdoll_terrain)?;
for &b in &parts[i + 1..] {
assert!(!ragdolls.were_bodies_in_contact(a, b)?, "parts of one ragdoll touched");
}
}
if detector.update(&ragdolls.ragdoll(ragdoll)?) {
settled = true;
break;
}
}
assert!(landed && settled, "the ragdoll fell onto the terrain and came to rest");
// At rest the knees are within their limits; on impact they may have passed them.
let rest = ragdolls.ragdoll(ragdoll)?;
for knee in [4, 6] {
let Some(JointReading::Hinge { current_angle }) = rest.joint(knee) else {
return Err("a knee is a hinge".into());
};
assert!((-0.05..=KNEE_BEND + 0.05).contains(¤t_angle), "{current_angle}");
}
let pelvis = rest.pose().root_offset;
let ground = ground_at(&ragdolls, &layers, pelvis.x, pelvis.z)?;
assert!(pelvis.y - ground < 0.4, "the pelvis lies on the terrain");
// Removing the ragdoll removes its parts; the terrain stays.
ragdolls.remove_ragdoll(ragdoll)?;
assert_eq!(ragdolls.body_count(), 1);
Ok(())
}