pub struct Simulation { /* private fields */ }Expand description
A rigid-body simulation: bodies fall under gravity, collide, and come to rest.
The capacity given at construction is the whole reservation. Adding past it
returns None; stepping never allocates.
§Examples
use concinnity_physics::{ColliderShape, DynamicParams, LayerMask, Simulation};
let mut sim = Simulation::with_capacity(2);
sim.add_fixed(
&ColliderShape::Cuboid { half_extents: [10.0, 0.5, 10.0] },
[0.0, -0.5, 0.0],
[0.0; 3],
0.8,
LayerMask::ALL,
);
let ball = sim
.add_dynamic(
&ColliderShape::Ball { radius: 0.5 },
[0.0, 5.0, 0.0],
[0.0; 3],
DynamicParams {
mass: 1.0,
friction: 0.5,
restitution: 0.0,
gravity_scale: 1.0,
linear_damping: 0.0,
},
LayerMask::ALL,
)
.expect("room in the pool");
for _ in 0..180 {
sim.step(1.0 / 60.0);
}
let (position, _rotation) = sim.body_pose_quat(ball).expect("a live body");
assert!(
(position[1] - 0.5).abs() < 0.02,
"the ball rests on the floor, at y = {}",
position[1]
);Implementations§
Source§impl Simulation
impl Simulation
Sourcepub fn with_capacity(capacity: usize) -> Self
pub fn with_capacity(capacity: usize) -> Self
Reserve room for capacity bodies, with the default tuning.
Sourcepub fn new(config: SimConfig, capacity: usize) -> Self
pub fn new(config: SimConfig, capacity: usize) -> Self
Reserve room for capacity bodies, tuned by config.
Sourcepub fn reserve_workers(&mut self, workers: usize) -> usize
pub fn reserve_workers(&mut self, workers: usize) -> usize
Reserve the scratch a step needs to split into workers pieces, and
return how many it will actually use.
Call this once while the world is built, with the worker count of the fan-out that will be stepping it. A simulation nobody calls this on reserves nothing and steps on the calling thread, which is what lets a host with no threads use the same simulation unchanged.
Splitting never changes what a step produces, so the reserved count is a ceiling rather than a promise: a step handed a wider fan-out uses this many pieces of it and leaves the rest of it idle.
Sourcepub fn workers(&self) -> usize
pub fn workers(&self) -> usize
Workers the step splits into at most: what
Simulation::reserve_workers settled on.
Sourcepub fn configure_character(
&mut self,
max_slope_deg: f32,
step_height: f32,
grounded: bool,
)
pub fn configure_character( &mut self, max_slope_deg: f32, step_height: f32, grounded: bool, )
Tune the character controller. grounded is true for a gravity-bound
character, which climbs steps and stays attached to the ground, and
false for a free-flying camera, which does neither. A max_slope_deg
of 0 disables the climb limit.
Sourcepub fn character_shape(half_height: f32, radius: f32) -> CharacterCapsule
pub fn character_shape(half_height: f32, radius: f32) -> CharacterCapsule
Build the capsule a character move is resolved against: a cylinder of
2 * half_height capped by hemispheres of radius.
A caller holds one per character across the fixed ticks rather than building one per move.
Sourcepub fn body_count(&self) -> usize
pub fn body_count(&self) -> usize
Bodies currently in the simulation.
Sourcepub fn collider_count(&self) -> usize
pub fn collider_count(&self) -> usize
Colliders currently in the simulation. A body carries exactly one shape, so this is the body count until compound shapes exist.
Sourcepub fn joint_count(&self) -> usize
pub fn joint_count(&self) -> usize
Joints currently constraining bodies.
Sourcepub fn reserved_bytes(&self) -> u64
pub fn reserved_bytes(&self) -> u64
Bytes reserved for bodies, bounds, contacts, the solver’s arrays, and
the per-worker buffers Simulation::reserve_workers set aside: what
the simulation costs whatever its occupancy.
Sourcepub fn add_fixed(
&mut self,
shape: &ColliderShape,
pos: [f32; 3],
euler_deg: [f32; 3],
friction: f32,
mask: LayerMask,
) -> Option<BodyHandle>
pub fn add_fixed( &mut self, shape: &ColliderShape, pos: [f32; 3], euler_deg: [f32; 3], friction: f32, mask: LayerMask, ) -> Option<BodyHandle>
Add an immovable body. None when the pool is full.
Sourcepub fn add_kinematic(
&mut self,
shape: &ColliderShape,
pos: [f32; 3],
euler_deg: [f32; 3],
friction: f32,
mask: LayerMask,
) -> Option<BodyHandle>
pub fn add_kinematic( &mut self, shape: &ColliderShape, pos: [f32; 3], euler_deg: [f32; 3], friction: f32, mask: LayerMask, ) -> Option<BodyHandle>
Add a body driven to a position rather than by forces: infinite mass,
untouched by gravity or impulses, but it pushes what it is driven
into. Move it with Simulation::set_kinematic_translation. None
when the pool is full.
Sourcepub fn add_character(
&mut self,
half_height: f32,
radius: f32,
center: [f32; 3],
mask: LayerMask,
) -> Option<BodyHandle>
pub fn add_character( &mut self, half_height: f32, radius: f32, center: [f32; 3], mask: LayerMask, ) -> Option<BodyHandle>
Add a position-driven character capsule centred on center: a
cylinder of 2 * half_height capped by hemispheres of radius.
Gravity does not move it and the solver does not push it. Resolve a
desired move with Simulation::move_character and apply the answer
with Simulation::set_kinematic_translation. None when the pool is
full.
Sourcepub fn add_sensor(
&mut self,
shape: &ColliderShape,
pos: [f32; 3],
euler_deg: [f32; 3],
tag: u64,
mask: LayerMask,
) -> Option<BodyHandle>
pub fn add_sensor( &mut self, shape: &ColliderShape, pos: [f32; 3], euler_deg: [f32; 3], tag: u64, mask: LayerMask, ) -> Option<BodyHandle>
Add a region that records what overlaps it and resists nothing.
It never collides, never blocks a query, and never moves. What crosses
its boundary is reported as a SensorCrossing carrying tag,
collected with Simulation::drain_sensor_crossings_into. Freely
simulated and position-driven bodies cross it; immovable geometry does
not, and two overlapping regions record a crossing each.
None when the pool is full.
§Examples
use concinnity_physics::{ColliderShape, DynamicParams, LayerMask, Simulation};
let mut sim = Simulation::with_capacity(2);
sim.add_sensor(
&ColliderShape::Cuboid { half_extents: [1.0, 1.0, 1.0] },
[0.0, 2.0, 0.0],
[0.0; 3],
7,
LayerMask::ALL,
)
.expect("room in the pool");
// Nothing holds the ball up, so it falls through the region.
sim.add_dynamic(
&ColliderShape::Ball { radius: 0.25 },
[0.0, 6.0, 0.0],
[0.0; 3],
DynamicParams {
mass: 1.0,
friction: 0.5,
restitution: 0.0,
gravity_scale: 1.0,
linear_damping: 0.0,
},
LayerMask::ALL,
)
.expect("room in the pool");
let mut crossings = Vec::new();
let (mut entered, mut left) = (false, false);
for _ in 0..300 {
sim.step(1.0 / 60.0);
sim.drain_sensor_crossings_into(&mut crossings);
for crossing in &crossings {
assert_eq!(crossing.tag, 7);
if crossing.entered { entered = true } else { left = true }
}
}
assert!(entered && left, "the ball went in and came out again");Sourcepub fn drain_sensor_crossings_into(&mut self, out: &mut Vec<SensorCrossing>)
pub fn drain_sensor_crossings_into(&mut self, out: &mut Vec<SensorCrossing>)
Move the boundary crossings recorded since the last drain into out,
oldest first. out is cleared first, and both it and the queue keep
their capacity, so a per-tick drain never reallocates.
Sourcepub fn set_contact_min_impulse(&mut self, min_impulse: f32, tick_dt: f32)
pub fn set_contact_min_impulse(&mut self, min_impulse: f32, tick_dt: f32)
Set the smallest contact impulse worth reporting as a
ContactHit, measured at a step of tick_dt.
The simulation gates on the force that impulse stands for, so a pair leaning on another at rest stays silent while the same pair colliding does not. It applies to every body, whenever it is called.
Sourcepub fn drain_contact_hits_into(&mut self, out: &mut Vec<ContactHit>)
pub fn drain_contact_hits_into(&mut self, out: &mut Vec<ContactHit>)
Move the contact hits recorded since the last drain into out, oldest
first. out is cleared first, and both it and the queue keep their
capacity.
Only pairs with a freely simulated body on at least one side, carrying
more than the force Simulation::set_contact_min_impulse set, appear.
Sourcepub fn add_heightfield(
&mut self,
rows: usize,
cols: usize,
heights: Vec<f32>,
scale: [f32; 3],
pos: [f32; 3],
mask: LayerMask,
) -> Option<BodyHandle>
pub fn add_heightfield( &mut self, rows: usize, cols: usize, heights: Vec<f32>, scale: [f32; 3], pos: [f32; 3], mask: LayerMask, ) -> Option<BodyHandle>
Add a static height grid: terrain, addressed like any other body.
heights is a rows * cols row-major grid of world-space y values,
with rows running along z and columns along x. scale is the whole
extent [width, height_multiplier, depth], and the grid is centred on
pos.
None when the pool is full or the grid names no surface: fewer than
two rows or columns, the wrong number of heights, or no footprint.
The body is immovable and never rotates. Contacts against it are answered along the surface’s own face normals, so a shape crossing the boundary between two cells is not caught by the edge they share.
§Examples
use concinnity_physics::{ColliderShape, DynamicParams, LayerMask, Simulation};
let mut sim = Simulation::with_capacity(2);
// A flat five-by-five grid twenty units square, its surface at y = 0.
sim.add_heightfield(
5,
5,
vec![0.0; 25],
[20.0, 1.0, 20.0],
[0.0; 3],
LayerMask::ALL,
)
.expect("room in the pool");
let ball = sim
.add_dynamic(
&ColliderShape::Ball { radius: 0.5 },
[1.0, 5.0, -2.0],
[0.0; 3],
DynamicParams {
mass: 1.0,
friction: 0.5,
restitution: 0.0,
gravity_scale: 1.0,
linear_damping: 0.0,
},
LayerMask::ALL,
)
.expect("room in the pool");
for _ in 0..240 {
sim.step(1.0 / 60.0);
}
let (position, _) = sim.body_pose_quat(ball).expect("a live body");
assert!(
(position[1] - 0.5).abs() < 0.02,
"the ball rests on the terrain, at y = {}",
position[1]
);Sourcepub fn add_dynamic(
&mut self,
shape: &ColliderShape,
pos: [f32; 3],
euler_deg: [f32; 3],
params: DynamicParams,
mask: LayerMask,
) -> Option<BodyHandle>
pub fn add_dynamic( &mut self, shape: &ColliderShape, pos: [f32; 3], euler_deg: [f32; 3], params: DynamicParams, mask: LayerMask, ) -> Option<BodyHandle>
Add a freely simulated body. None when the pool is full.
Sourcepub fn add_joint(
&mut self,
body_a: BodyHandle,
body_b: BodyHandle,
anchor_a: [f32; 3],
anchor_b: [f32; 3],
spec: JointSpec,
) -> bool
pub fn add_joint( &mut self, body_a: BodyHandle, body_b: BodyHandle, anchor_a: [f32; 3], anchor_b: [f32; 3], spec: JointSpec, ) -> bool
Constrain two bodies to each other. Anchors are in each body’s own frame, and the joint holds the relative pose the bodies are in when it is made.
Returns whether the joint was made. It needs two different live bodies;
past that, degenerate input is repaired rather than refused, so a
zero-length axis becomes +Y and limits given the wrong way round are
read low to high.
A joint is removed by removing either of the bodies it holds.
§Examples
use concinnity_physics::{
ColliderShape, DynamicParams, JointSpec, LayerMask, Simulation,
};
let mut sim = Simulation::with_capacity(2);
let post = sim
.add_fixed(
&ColliderShape::Ball { radius: 0.1 },
[0.0, 4.0, 0.0],
[0.0; 3],
0.5,
LayerMask::ALL,
)
.expect("room in the pool");
let bob = sim
.add_dynamic(
&ColliderShape::Ball { radius: 0.2 },
[1.0, 4.0, 0.0],
[0.0; 3],
DynamicParams {
mass: 1.0,
friction: 0.5,
restitution: 0.0,
gravity_scale: 1.0,
linear_damping: 0.0,
},
LayerMask::ALL,
)
.expect("room in the pool");
assert!(sim.add_joint(post, bob, [0.0; 3], [-1.0, 0.0, 0.0], JointSpec::Spherical));
for _ in 0..120 {
sim.step(1.0 / 60.0);
}
// The bob swings, but it stays one unit from the post it hangs off.
let (position, _) = sim.body_pose_quat(bob).expect("a live body");
let reach = ((position[0] - 0.0).powi(2)
+ (position[1] - 4.0).powi(2)
+ (position[2] - 0.0).powi(2))
.sqrt();
assert!((reach - 1.0).abs() < 0.01, "hanging {reach} from the post");Sourcepub fn set_kinematic_translation(
&mut self,
handle: BodyHandle,
pos: [f32; 3],
) -> bool
pub fn set_kinematic_translation( &mut self, handle: BodyHandle, pos: [f32; 3], ) -> bool
Send a position-driven body to pos over the next step. It arrives
exactly there, pushing whatever it meets on the way. Returns whether
the handle named a live position-driven body.
The target is consumed by the step, so a body that is to keep moving is given a fresh one each tick and a body that is left alone stops.
Sourcepub fn make_kinematic(&mut self, handle: BodyHandle) -> bool
pub fn make_kinematic(&mut self, handle: BodyHandle) -> bool
Switch a body to position-driven control, keeping its handle and the mass it was authored with. Returns whether the handle named a live body.
Sourcepub fn make_dynamic(
&mut self,
handle: BodyHandle,
linear_velocity: [f32; 3],
) -> bool
pub fn make_dynamic( &mut self, handle: BodyHandle, linear_velocity: [f32; 3], ) -> bool
Hand a body back to the solver with a launch velocity, restoring the mass it was authored with. Returns whether the handle named a live body.
Sourcepub fn raycast(
&self,
origin: [f32; 3],
dir: [f32; 3],
max_dist: f32,
exclude: Option<BodyHandle>,
mask: LayerMask,
) -> Option<RayHit>
pub fn raycast( &self, origin: [f32; 3], dir: [f32; 3], max_dist: f32, exclude: Option<BodyHandle>, mask: LayerMask, ) -> Option<RayHit>
Cast a ray, returning the nearest hit within max_dist.
dir need not be unit length; a zero direction misses. exclude
leaves one body out, and mask restricts the hit set to layers the
query interacts with. A ray that begins inside a body hits it at zero
distance with the normal turned back along the ray.
Sourcepub fn move_character(
&self,
shape: &CharacterCapsule,
input: &CharacterMoveInput,
) -> CharacterMove
pub fn move_character( &self, shape: &CharacterCapsule, input: &CharacterMoveInput, ) -> CharacterMove
Resolve a desired character move against the world without moving anything in it, returning the translation to apply and whether the capsule ends up on the ground.
The capsule sweeps along the desired translation and slides along
whatever it meets rather than stopping dead, up to a bounded number of
deflections. input.exclude is the mover’s own body, left out of the
query so it does not collide with itself; other characters’ capsules
stay solid to it. What counts as ground, how high an obstacle is
climbed, and whether the mover is gravity-bound at all come from
Simulation::configure_character.
Apply the result with Simulation::set_kinematic_translation.
§Examples
use concinnity_physics::{CharacterMoveInput, ColliderShape, LayerMask, Simulation};
let mut sim = Simulation::with_capacity(3);
sim.add_fixed(
&ColliderShape::Cuboid { half_extents: [10.0, 0.5, 10.0] },
[0.0, -0.5, 0.0],
[0.0; 3],
0.8,
LayerMask::ALL,
);
// A wall whose near face is at z = 1.
sim.add_fixed(
&ColliderShape::Cuboid { half_extents: [4.0, 2.0, 0.5] },
[0.0, 2.0, 1.5],
[0.0; 3],
0.8,
LayerMask::ALL,
);
// The capsule stands on the floor: half height plus radius above it.
let center = [0.0, 0.9, 0.0];
let capsule = sim
.add_kinematic(
&ColliderShape::Capsule { half_height: 0.6, radius: 0.3 },
center,
[0.0; 3],
0.8,
LayerMask::ALL,
)
.expect("room in the pool");
let shape = Simulation::character_shape(0.6, 0.3);
let moved = sim.move_character(
&shape,
&CharacterMoveInput {
center,
desired: [0.0, -0.01, 2.0],
dt: 1.0 / 60.0,
exclude: capsule,
mask: LayerMask::ALL,
},
);
// Two units of walk, and the wall stops it a radius short of its face.
assert!(
(moved.translation[2] - 0.7).abs() < 0.01,
"walked {}",
moved.translation[2]
);
assert!(moved.grounded, "the floor is still underfoot");Sourcepub fn remove_body(&mut self, handle: BodyHandle) -> bool
pub fn remove_body(&mut self, handle: BodyHandle) -> bool
Remove a body, along with every joint it was in. Returns whether the handle named a live one.
Purging the joints is part of the removal rather than something a caller does afterwards: a joint naming a slot whose body has gone would constrain whatever occupied that slot next.
Sourcepub fn body_pose_quat(&self, handle: BodyHandle) -> Option<([f32; 3], [f32; 4])>
pub fn body_pose_quat(&self, handle: BodyHandle) -> Option<([f32; 3], [f32; 4])>
A body’s world-space position and [x, y, z, w] rotation quaternion.
Sourcepub fn mass(&self, handle: BodyHandle) -> Option<f32>
pub fn mass(&self, handle: BodyHandle) -> Option<f32>
A body’s mass in kilograms. Immovable bodies report 0.
Sourcepub fn step_with(&mut self, dt: f32, fanout: &impl Fanout)
pub fn step_with(&mut self, dt: f32, fanout: &impl Fanout)
Advance the simulation by dt seconds, offering the step’s independent
work to fanout.
A step splits the same way whatever it is handed, so a world stepped
across a thread pool and the same world stepped on one thread land in
exactly the same place. Nothing is reserved for a fan-out that
Simulation::reserve_workers was not told about, so a wider one is
used only as far as the reservation goes.
§Examples
use concinnity_physics::{ColliderShape, DynamicParams, Inline, LayerMask, Simulation};
let mut sim = Simulation::with_capacity(1);
let ball = sim
.add_dynamic(
&ColliderShape::Ball { radius: 0.5 },
[0.0, 10.0, 0.0],
[0.0; 3],
DynamicParams {
mass: 1.0,
friction: 0.4,
restitution: 0.2,
gravity_scale: 1.0,
linear_damping: 0.0,
},
LayerMask::ALL,
)
.expect("room for one body");
sim.step_with(1.0 / 60.0, &Inline);
assert!(sim.body_pose_quat(ball).expect("a live body").0[1] < 10.0, "it fell");