Bodies and kernels
A kernel is the rigid-body engine under the host: it owns the bodies, integrates them, resolves contacts and joints. Every capability is written against the one interface, PhysicsKernel, so a game picks the engine and the capabilities do not change.
What it is for#
Use a kernel when something has to move under forces and collide: a crate, a cannonball, a hull, a door on a hinge. Static scenery does not need one (characters walk against Babylon meshes with checkCollisions; see layers, triggers and rays).
Choosing a kernel#
LightKernel | HavokKernel | |
|---|---|---|
| Needs | nothing | the Havok WASM, loaded by physics-havok |
| Dynamic shapes | spheres | sphere, box, capsule, cylinder, convex hull, compound |
| Static shapes | box, plane, sphere | those plus triangle mesh |
| Stacking, resting boxes | no | yes |
| Joints | rod, rope, spring | those plus ball, hinge, fixed |
| Contact hooks (change friction before the solve) | yes | no: friction and restitution come from materials |
| Rolling resistance | native | added by the friction capability as a torque |
| Fast bodies | sub-steps scaled by speed | native swept collision |
| Deterministic | yes: the same inputs give the same run | yes on the same build |
| Use it for | projectiles, balls, debris, sliding and rolling tests, small games | everything with boxes, vessels, vehicles, mechanisms |
A game that needs stacking, joints beyond ropes or any non-sphere dynamic shape needs Havok. A capability that needs those (needsBodies) refuses to switch on over the light kernel with a message that says so.
import { PhysicsHost, LightKernel } from '@ferret/physics';
const host = world.addService(new PhysicsHost({ kernel: new LightKernel({ gravity: [0, -9.81, 0] }) }));
const ground = host.kernel.addBody({ shape: { type: 'plane', normal: [0, 1, 0], offset: 0 } });
const ball = host.kernel.addBody({ shape: { type: 'sphere', radius: 0.5 }, mass: 2, position: [0, 3, 0] });
for (let i = 0; i < 60 * 3; i++) host.fixedUpdate(1 / 60);
The same with Havok. The WASM is loaded once and handed to the kernel; the game decides when to pay for it:
import { PhysicsHost, HavokKernel } from '@ferret/physics';
const host = world.addService(new PhysicsHost({ kernel: new HavokKernel(await loadHavok(), { gravity: [0, -9.81, 0] }) }));
host.kernel.addBody({ shape: { type: 'plane', normal: [0, 1, 0], offset: 0 } });
const crate = host.kernel.addBody({ shape: { type: 'box', half: [0.5, 0.5, 0.5] }, mass: 20, position: [0, 2, 0] });
for (let i = 0; i < 60 * 4; i++) host.fixedUpdate(1 / 60);
Bodies#
A body is described by a BodyDef:
| Field | Meaning | Notes |
|---|---|---|
shape | what it is | see the table above |
kind | dynamic (moves under forces), static (never moves), kinematic (moved by you, pushes dynamic ones) | planes are static; the host turns a kinematic body's node movement into velocity so contacts see a moving wall |
mass | kg | derived from the material's density and the shape's volume when missing |
material | name in the material library | see materials |
position, orientation, velocity, angularVelocity | start state | SI units; orientation is a quaternion [x, y, z, w] |
linearDamping, angularDamping | 1/s | for a world without a medium capability; with physics-media on, use the medium instead |
gravityScale | multiplies gravity | 0 = floats |
lockRotation | no spin | a crate that slides instead of a sphere that rolls |
noCollide | touches nothing, joints still hold it | pivots and anchors; Havok only |
On an entity, a RigidBody component creates the body from parameters (shape: 'sphere' | 'box' | 'capsule' | 'cylinder' | 'plane', radius, height, size, mass, material, kind, ...) and moves the entity's node after every step. rotate: false keeps a model upright while it translates. Anything the component cannot say (a hull, a compound) goes in geometry, which wins over shape.
Units are SI everywhere: metres, kilograms, seconds, newtons. A level made in centimetres must be scaled before it meets the kernel.
Kinematic bodies that turn#
A kinematic body moves by its velocity and turns by its angularVelocity on both kernels, and contacts use the speed of the point they touch (v + ω × r): a turning drum carries what rests on it by friction, a paddle wheel pushes what it meets. Forces on a kinematic body are ignored.
Shapes you will actually use#
- Sphere, box, capsule, cylinder: the cheap ones. Prefer them.
- Convex hull (
convex, a list of points in body space): crates with bevels, rocks. The cost grows with the point count; 12–24 points is plenty. - Compound (
childrenwith shapes and offsets): a vehicle from boxes, a table from a top and four legs. Cheaper and more stable than a mesh. - Mesh (
positions,indices): static or kinematic only: level geometry that bodies hit. Never dynamic.
Joints#
addConstraint links two bodies:
| Type | What it does | Kernel |
|---|---|---|
rod | rigid distance (pushes and pulls) | both |
rope | longest distance (pulls only, slack when closer) | both |
spring | Hooke's law with damping, implicit so stable at any stiffness | both |
ball | shared point, turns freely (a shoulder, a hook) | Havok |
hinge | shared point, one axis, optional limits in radians (a door, a wheel) | Havok |
fixed | welded in the present pose (a bracket) | Havok |
Every constraint reports force (N, positive in tension) and stretch (m) for the last step, and maxForce makes it break: it is removed and onConstraintBreak fires. The strength capability and bridges read the same data.
import { PhysicsHost, HavokKernel } from '@ferret/physics';
const host = world.addService(new PhysicsHost({ kernel: new HavokKernel(await loadHavok(), { gravity: [0, -9.81, 0] }) }));
const ceiling = host.kernel.addBody({ shape: { type: 'box', half: [0.2, 0.2, 0.2] }, kind: 'static', position: [0, 5, 0] });
const weight = host.kernel.addBody({ shape: { type: 'sphere', radius: 0.3 }, mass: 10, position: [0, 3, 0] });
const rope = host.kernel.addConstraint({ type: 'rope', a: ceiling, b: weight, length: 2, maxForce: 1000 });
for (let i = 0; i < 60 * 3; i++) host.fixedUpdate(1 / 60);
Contacts and events#
After each step the kernel reports, per contact, the total normal impulse (N·s), the largest closing speed (m/s), the slide speed and whether something asked the bodies to stick. The host puts them on the world's bus as physics:contact, in one batch after the step:
import { PhysicsHost, LightKernel } from '@ferret/physics';
const host = world.addService(new PhysicsHost({ kernel: new LightKernel({ gravity: [0, -9.81, 0] }) }));
host.kernel.addBody({ shape: { type: 'plane', normal: [0, 1, 0], offset: 0 } });
host.kernel.addBody({ shape: { type: 'sphere', radius: 0.5 }, mass: 1, position: [0, 5, 0] });
const hits: number[] = [];
world.events.on('physics:contact', (e: { speed: number }) => hits.push(e.speed));
for (let i = 0; i < 60 * 2; i++) host.fixedUpdate(1 / 60);
Capabilities read the same stream: impact for sounds and damage, strength for breakage. Your game can listen for effects (a thud sized by speed, a spark above a given impulse).
Layers, triggers and rays#
The non-realistic part of the physics package, always available: PhysicsService. Up to 31 named collision layers (World, Pawn, NPC, Trigger, Interactable, Projectile, Camera by default), Collider components (box, sphere, capsule) that act as triggers firing trigger:enter and trigger:exit for what overlaps them, raycast against static meshes, and overlapSphere. Level meshes become blocking for characters with markStatic(root, layer). These are cheap and are the right tool for pickups, doors that open when someone stands there, damage volumes: no rigid body needed.
Parameters#
| Option | Where | Default | Effect |
|---|---|---|---|
gravity | kernel | [0, -9.81, 0] | used when the gravity capability is off; with it on, the kernel's own is zeroed and the field supplies every force |
substeps | both kernels | 1 (light: scaled by speed up to maxSubsteps) | more = steadier fast motion, costlier |
iterations | light | solver passes | more = firmer contacts |
restitutionThreshold | light | closing speed below which a bounce is a rest | raise it to stop jitter on resting bodies |
Budget#
Cost is per body and per contact. The light kernel is linear in contacts with sub-steps; Havok handles hundreds of bodies per step on the desktop tier (see budgets and tiers). Bodies that stop moving go to sleep (sleeping) and cost almost nothing until touched; wake(b) and sleep(b) are there for you.
Debugging#
| Symptom | Cause |
|---|---|
| A box falls through a plane | on the light kernel dynamic boxes are not supported: use a sphere, or Havok |
| A hinge or ball joint throws | the light kernel has no joints beyond rod, rope and spring |
| Bodies jitter at rest | restitution threshold too low, or two overlapping start positions: separate them |
| A thin fast body tunnels | raise substeps; Havok sweeps natively |
body.state says sleeping and it will not move | apply an impulse (it wakes the body) or wake(b) |
| Mass is huge or tiny | density of the material times shape volume: check the material and the units (metres) |
| Friction seems ignored on Havok | per-pair tables are not used there: set the materials themselves, see materials |
Reference#
PhysicsKernel, BodyDef, Body, Shape, ConstraintDef, Constraint, Contact, ContactEvent, KernelCaps, PhysicsService, Collider, CollisionLayers.