Rigid bodies
Open the live demo · Read the source · View on GitHub
Stage one of two: mass, gravity, impulses, being pushed, and coming to rest. A rigid body here has no rotation and no inertia tensor, which is what makes contact between two boxes exact and cheap rather than a search over a manifold. Which is why the crates on this page slide and stack but never tumble.
Step 1: A world with something to fall onto #
Dynamics is the solver; CollisionWorld is still what everything, moving
or not, lives in.
_world = CollisionWorld();
_world.addBox(Vector3(0.0, -0.5, 0.0), Vector3(6.0, 1.0, 6.0));
_dynamics = Dynamics(world: _world);
Step 2: Bodies above the floor #
A rigid body's collider is added for you, the same as a character controller's. From the collision world's point of view the only questions are whether it moves and whether it blocks, and the answer to both is yes. The crates are dropped from different heights so they land one after another.
// Eight crates, each higher up and a little off to one side of the last,
// so they arrive one at a time and land on one another as often as on
// the floor.
for (var i = 0; i < _count; i++) {
final double half = 0.3 + 0.08 * (i % 3);
final RigidBody crate = _dynamics.add(
RigidBody(
world: _world,
shape: CollisionBox(Vector3(half, half, half)),
position: Vector3(
0.5 * math.sin(i * 2.4),
1.2 + i * 1.1,
0.5 * math.cos(i * 1.9),
),
mass: 2.0,
friction: 0.7,
),
);
_crates.add(crate);
_halves.add(half);
final MeshNode mesh = MeshNode(
DeviceMesh.upload(
context.device,
CuboidShape(size: Vector3.all(half * 2)).build(),
),
Material(
name: 'crate $i',
baseColor: Vector4(
0.55 + 0.4 * math.sin(i * 0.9),
0.5 + 0.3 * math.sin(i * 1.7 + 1.0),
0.35 + 0.3 * math.sin(i * 2.3 + 2.0),
1.0,
),
roughness: 0.7,
),
name: 'crate $i',
)..setPositionFrom(crate.position);
_meshes.add(mesh);
scene.add(mesh);
}
Step 3: Step it every frame #
One step a frame, at a fixed sixtieth of a second: the same drop lands the
same way on every machine, which is what makes the snapshot below worth
having.
_age += dt;
// A step of a sixtieth of a second, however long the frame took: the
// same drop lands the same way on every machine.
_dynamics.step(_step);
Step 4: A snapshot before anything moves #
save is everything a body needs to be put back exactly where it was:
position, velocity, and whether it had fallen asleep. It carries no
reference to the world or the collider, so it can sit in a save file.
// Taken before a single step: everything a body needs to be put back
// exactly here — position, velocity, whether it is asleep.
for (final RigidBody crate in _crates) {
_start.add(crate.save());
}
Step 5: Disturb the pile, then undo it #
Shove them gives every crate an impulse, which wakes the ones that had
gone to sleep. When the pile has been at rest for a moment, or Rewind to
the start is pressed, restore puts every crate back where the snapshot
says and the drop begins again.
void _shove() {
for (var i = 0; i < _count; i++) {
_crates[i].applyImpulse(
Vector3(math.sin(i * 2.1) * 3.0, 4.0, math.cos(i * 1.7) * 3.0),
);
}
_atRest = 0.0;
}
void _rewind() {
for (var i = 0; i < _count; i++) {
_crates[i].restore(_start[i]);
}
_atRest = 0.0;
_age = 0.0;
}
Step 6: What the pile should prove #
Left alone, every crate has to end up on the floor or on another crate, and asleep. Restoring the snapshot has to put each one back exactly where it was saved.
// Let the pile come to rest. Every crate has to end up on the floor or
// on another crate: none fallen through, none left hanging in the air.
for (var i = 0; i < 600; i++) {
_dynamics.step(_step);
}
for (var i = 0; i < _count; i++) {
final double y = _crates[i].position.y;
if (y < _halves[i] - 0.05) {
throw StateError('crate $i fell through the floor: y = $y');
}
if (y > 8.0) {
throw StateError('crate $i is still in the air: y = $y');
}
}
if (!_crates.every((RigidBody c) => c.isAsleep)) {
throw StateError('the pile should have come to rest and gone to sleep');
}
// Winding back has to undo all of it, exactly.
_rewind();
for (var i = 0; i < _count; i++) {
final Vector3 back = _crates[i].position;
final Vector3 saved = Vector3(
0.5 * math.sin(i * 2.4),
1.2 + i * 1.1,
0.5 * math.cos(i * 1.9),
);
if ((back - saved).length > 1e-6) {
throw StateError('restore should have put crate $i back where it was');
}
}
Note. A sleeping body costs nothing to leave alone: the solver skips it entirely until something touching it moves fast enough to be worth waking it for. Ten crates settled in a stack are ten bodies the physics stops paying for.