FERRETGame engine over Babylon.js
Ferret

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#

LightKernelHavokKernel
Needsnothingthe Havok WASM, loaded by physics-havok
Dynamic shapesspheressphere, box, capsule, cylinder, convex hull, compound
Static shapesbox, plane, spherethose plus triangle mesh
Stacking, resting boxesnoyes
Jointsrod, rope, springthose plus ball, hinge, fixed
Contact hooks (change friction before the solve)yesno: friction and restitution come from materials
Rolling resistancenativeadded by the friction capability as a torque
Fast bodiessub-steps scaled by speednative swept collision
Deterministicyes: the same inputs give the same runyes on the same build
Use it forprojectiles, balls, debris, sliding and rolling tests, small gameseverything 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:

FieldMeaningNotes
shapewhat it issee the table above
kinddynamic (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
masskgderived from the material's density and the shape's volume when missing
materialname in the material librarysee materials
position, orientation, velocity, angularVelocitystart stateSI units; orientation is a quaternion [x, y, z, w]
linearDamping, angularDamping1/sfor a world without a medium capability; with physics-media on, use the medium instead
gravityScalemultiplies gravity0 = floats
lockRotationno spina crate that slides instead of a sphere that rolls
noCollidetouches nothing, joints still hold itpivots 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 (children with 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:

TypeWhat it doesKernel
rodrigid distance (pushes and pulls)both
ropelongest distance (pulls only, slack when closer)both
springHooke's law with damping, implicit so stable at any stiffnessboth
ballshared point, turns freely (a shoulder, a hook)Havok
hingeshared point, one axis, optional limits in radians (a door, a wheel)Havok
fixedwelded 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#

OptionWhereDefaultEffect
gravitykernel[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
substepsboth kernels1 (light: scaled by speed up to maxSubsteps)more = steadier fast motion, costlier
iterationslightsolver passesmore = firmer contacts
restitutionThresholdlightclosing speed below which a bounce is a restraise 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#

SymptomCause
A box falls through a planeon the light kernel dynamic boxes are not supported: use a sphere, or Havok
A hinge or ball joint throwsthe light kernel has no joints beyond rod, rope and spring
Bodies jitter at restrestitution threshold too low, or two overlapping start positions: separate them
A thin fast body tunnelsraise substeps; Havok sweeps natively
body.state says sleeping and it will not moveapply an impulse (it wakes the body) or wake(b)
Mass is huge or tinydensity of the material times shape volume: check the material and the units (metres)
Friction seems ignored on Havokper-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.