Skip to main content

Simulation

Struct Simulation 

Source
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_core::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

Source

pub fn with_capacity(capacity: usize) -> Self

Reserve room for capacity bodies, with the default tuning.

Source

pub fn new(config: SimConfig, capacity: usize) -> Self

Reserve room for capacity bodies, tuned by config.

Source

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.

Source

pub fn workers(&self) -> usize

Workers the step splits into at most: what Simulation::reserve_workers settled on.

Source

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.

Source

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.

Source

pub fn config(&self) -> &SimConfig

The tuning this simulation steps with.

Source

pub fn capacity(&self) -> usize

Bodies the pool can hold.

Source

pub fn body_count(&self) -> usize

Bodies currently in the simulation.

Source

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.

Source

pub fn joint_count(&self) -> usize

Joints currently constraining bodies.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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_core::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");
Source

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.

Source

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.

Source

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.

Source

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_core::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]
);
Source

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.

Source

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_core::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");
Source

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.

Source

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.

Source

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.

Source

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.

Source

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_core::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");
Source

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.

Source

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.

Source

pub fn mass(&self, handle: BodyHandle) -> Option<f32>

A body’s mass in kilograms. Immovable bodies report 0.

Source

pub fn step(&mut self, dt: f32)

Advance the simulation by dt seconds on the calling thread.

Source

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_core::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");

Trait Implementations§

Source§

impl Debug for Simulation

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.