crashcat
v0.0.1
Published
a 3d physics library for games and simulations
Downloads
518
Maintainers
Readme

> npm install crashcatcrashcat
crashcat is physics engine for javascript, built for games, simulations, and creative websites.
Features
- 🎯 rigid body simulation
- 📦 support for convex shapes, triangle mesh shapes, custom shapes
- 🔗 constraints with motors and springs (hinge, slider, distance, point, fixed, cone, swing-twist, six-dof)
- ⚡ continuous collision detection (ccd) for fast-moving objects
- 🎭 flexible collision filtering
- 🔧 hooks for listening to and modifying physics events
- 🌳 broadphase spatial acceleration with dynamic bvh
- 😴 rigid body sleeping
- 👻 sensor rigid bodies
- 🌲 pure javascript, written to be highly tree-shakeable, only pay for what you use
- 🔌 works with any javascript engine/library - babylon.js, playcanvas, three.js, or your own engine
API Documentation
This readme provides curated explanations, guides, and examples to help you get started with crashcat.
Auto-generated API documentation can be found at crashcat.dev/docs.
Changelog
See the CHANGELOG.md for a detailed list of changes in each version.
Examples
Table of Contents
- Quick Start
- Physics World
- Rigid Bodies
- Shapes
- Listener
- Queries
- Constraints
- Character Controllers
- Multiple Physics Worlds
- World State Serialization
- Tree Shaking
- Common Mistakes
- Optimization Tips
- Determinism
- Custom Shapes
- Library Integrations
- FAQ
- Community
- Acknowledgements
Quick Start
Below is a minimal example of creating a physics world with a static ground and some dynamic boxes:
import {
addBroadphaseLayer,
addObjectLayer,
box,
type CollideShapeHit,
type ContactManifold,
type ContactSettings,
ContactValidateResult,
createWorld,
createWorldSettings,
enableCollision,
type Listener,
MotionType,
type RigidBody,
registerAll,
rigidBody,
updateWorld,
} from 'crashcat';
import type { Vec3 } from 'mathcat';
// register all shapes
registerAll();
// create a simple world
const worldSettings = createWorldSettings();
export const BROADPHASE_LAYER_MOVING = addBroadphaseLayer(worldSettings);
export const BROADPHASE_LAYER_NOT_MOVING = addBroadphaseLayer(worldSettings);
export const OBJECT_LAYER_MOVING = addObjectLayer(worldSettings, BROADPHASE_LAYER_MOVING);
export const OBJECT_LAYER_NOT_MOVING = addObjectLayer(worldSettings, BROADPHASE_LAYER_NOT_MOVING);
enableCollision(worldSettings, OBJECT_LAYER_MOVING, OBJECT_LAYER_NOT_MOVING);
enableCollision(worldSettings, OBJECT_LAYER_MOVING, OBJECT_LAYER_MOVING);
worldSettings.gravity = [0, -9.81, 0];
const world = createWorld(worldSettings);
// create a static ground
rigidBody.create(world, {
motionType: MotionType.STATIC,
shape: box.create({ halfExtents: [10, 1, 10] }),
objectLayer: OBJECT_LAYER_NOT_MOVING,
});
// create a stack of dynamic boxes
for (let i = 0; i < 5; i++) {
rigidBody.create(world, {
motionType: MotionType.DYNAMIC,
shape: box.create({ halfExtents: [1, 1, 1] }),
objectLayer: OBJECT_LAYER_MOVING,
position: [0, 2 + i * 2, 0],
});
}
// simulate 10 seconds
for (let i = 0; i < 60 * 10; i++) {
// typically you will do this in a loop, e.g. requestAnimationFrame or setInterval
// pass 'undefined' for no physics listener
updateWorld(world, undefined, 1 / 60);
}Physics World
A physics world contains all bodies, constraints, and simulation state. It manages collision detection, constraint solving, and integration.
crashcat uses a two-tier layer system for finding potential collisions efficiently:
Broadphase Layers partition space for performance. Each broadphase layer has its own spatial acceleration structure (dynamic bvh tree). Bodies in different broadphase layers use separate trees, improving query performance.
- simple approach: two layers - "moving" and "not moving"
- advanced: separate by update frequency - static terrain, kinematic platforms, dynamic debris
- each body belongs to exactly one broadphase layer
Object Layers control collision filtering. They define which bodies can collide with each other. Each object layer belongs to a single broadphase layer.
- examples: "player", "enemy", "terrain", "projectile", "debris"
- you define collision rules between object layers (e.g., "projectiles hit enemies but not other projectiles")
- this is the primary mechanism for controlling what collides with what
import { addBroadphaseLayer, addObjectLayer, createWorld, createWorldSettings, enableCollision, registerAll } from 'crashcat';
// we can use registerShapes and registerConstraints to granularly declare
// which shapes and constraints we want to use for the best tree shaking.
// but early in development, it's easier to just register everything.
registerAll();
// this is a container for all settings related to world simulation.
// in a real project, you'd put this in a e.g. "physics-world-settings.ts" file seperate from the physics world
// creation, and import it and below constants where needed.
const worldSettings = createWorldSettings();
// earth gravity
worldSettings.gravity = [0, -9.81, 0];
// we're first up going to define "broadphase layers".
// for simple projects, a "moving" and "not moving" broadphase layer is a good start.
export const BROADPHASE_LAYER_MOVING = addBroadphaseLayer(worldSettings);
export const BROADPHASE_LAYER_NOT_MOVING = addBroadphaseLayer(worldSettings);
// next, we'll define some "object layers".
export const OBJECT_LAYER_MOVING = addObjectLayer(worldSettings, BROADPHASE_LAYER_MOVING);
export const OBJECT_LAYER_NOT_MOVING = addObjectLayer(worldSettings, BROADPHASE_LAYER_NOT_MOVING);
// here we declare that "moving" objects should collide with "not moving" objects, and with other "moving" objects.
enableCollision(worldSettings, OBJECT_LAYER_MOVING, OBJECT_LAYER_NOT_MOVING);
enableCollision(worldSettings, OBJECT_LAYER_MOVING, OBJECT_LAYER_MOVING);
// time to create the physics world from the settings we've defined
const world = createWorld(worldSettings);See the WorldSettings type for all available settings and their documentation: https://crashcat.dev/docs/types/crashcat.WorldSettings.html
Stepping the Simulation
After creating a world, you advance the simulation by calling updateWorld(world, listener, deltaTime) in your game loop.
The deltaTime parameter is the time in seconds to advance the simulation. For a 60 FPS game loop, this is typically 1/60 (≈0.0167 seconds).
You can pass a listener to updateWorld to listen to and modify physics events, see the Listener section.
For simple use cases, you can use the frame delta time directly to do variable time stepping:
let lastTime = performance.now();
const maxDelta = 1 / 30;
function gameLoopVariableTimestep() {
const currentTime = performance.now();
const delta = Math.min((currentTime - lastTime) / 1000, maxDelta);
lastTime = currentTime;
updateWorld(world, undefined, delta);
// ... render ...
requestAnimationFrame(gameLoopVariableTimestep);
}For maximum stability and determinism, use a fixed physics timestep with an accumulator:
const PHYSICS_DT = 1 / 60;
let accumulator = 0;
let lastTimeFixed = performance.now();
function gameLoopFixedTimestep() {
const currentTime = performance.now();
const frameTime = Math.min((currentTime - lastTimeFixed) / 1000, 0.25);
lastTimeFixed = currentTime;
accumulator += frameTime;
// step physics at fixed rate
while (accumulator >= PHYSICS_DT) {
updateWorld(world, undefined, PHYSICS_DT);
accumulator -= PHYSICS_DT;
}
// ... render with interpolation ...
// const alpha = accumulator / PHYSICS_DT;
// interpolate body positions using alpha for smooth rendering
requestAnimationFrame(gameLoopFixedTimestep);
}This decouples physics simulation rate from render frame rate. Physics always steps at exactly 60 Hz regardless of how fast rendering runs. For smooth visuals, interpolate body positions between physics steps using the alpha value.
Read more on this here: https://gafferongames.com/post/fix_your_timestep/
Units and Scale
crashcat uses SI units and OpenGL conventions:
- Length: meters (m)
- Mass: kilograms (kg)
- Time: seconds (s)
- Force: newtons (N)
- Gravity: -9.81 m/s² (earth gravity)
- Coordinate System: positive y is "up" by default, OpenGL right-handed system
- Triangle Winding: counter-clockwise (CCW) is front face
Scale matters: a box with halfExtents: [100, 100, 100] is a 100-meter cube (skyscraper-sized), which will appear to fall slowly relative to its size.
If your renderer uses a different coordinate system (e.g., z-up, left-handed), transform coordinates when transferring data between crashcat and your renderer.
Rigid Bodies
Rigid bodies are the fundamental simulation objects in crashcat. They have attached shapes that define their collision geometry, and properties that control their simulation behavior.
Creation and Removal
// create a dynamic box
const dynamicBox = rigidBody.create(world, {
shape: box.create({ halfExtents: [1, 1, 1] }),
motionType: MotionType.DYNAMIC,
objectLayer: OBJECT_LAYER_MOVING,
position: [0, 5, 0],
});
// create a static ground
const ground = rigidBody.create(world, {
shape: box.create({ halfExtents: [10, 0.5, 10] }),
motionType: MotionType.STATIC,
objectLayer: OBJECT_LAYER_NOT_MOVING,
position: [0, -0.5, 0],
});
// remove a body from the world
rigidBody.remove(world, dynamicBox);⚠️ Storing references to bodies
Bodies are pooled internally for performance. As such, be careful with storing long-lived references to body objects! Store body.id instead and use rigidBody.get(world, id) to look up bodies when needed.
Body ids contain an index and sequence number. When bodies are removed, their ids are invalidated and indices can be reused.
// ❌ BAD: storing direct reference
type MyEntity = {
body: RigidBody;
};
// ✅ GOOD: store body.id instead
type MyEntityGood = {
bodyId: number;
};
// look up body by id when needed
const storedId = dynamicBox.id;
const bodyById = rigidBody.get(world, storedId);
if (bodyById) {
rigidBody.addForce(world, bodyById, [0, 10, 0], true);
}Motion Types
Bodies can be static, dynamic, or kinematic. Choose the type based on how the object should behave:
Static: immovable objects
- Use for: terrain, walls, buildings, fixed obstacles
- Properties: infinite mass, never moves, collides only with dynamic bodies
- Examples: ground, level geometry, walls, buildings
Dynamic: physical objects
- Use for: objects that should respond naturally to physics forces and collisions
- Properties: has mass, affected by forces/gravity, collides with all body types
- Examples: falling boxes, bouncing balls, physics props, debris, projectiles
Kinematic: nonphysical moving objects
- Use for: objects that move via script/animation but should push dynamic bodies
- Properties: user-controlled velocity, pushes dynamic bodies but isn't pushed back
- Examples: moving platforms, elevators, automated doors, scripted animations
// static: cannot move, infinite mass, not affected by forces
const staticBody = rigidBody.create(world, {
shape: box.create({ halfExtents: [5, 0.5, 5] }),
motionType: MotionType.STATIC,
objectLayer: OBJECT_LAYER_NOT_MOVING,
});
// dynamic: fully simulated, affected by forces, gravity, and contacts
const dynamicBody = rigidBody.create(world, {
shape: sphere.create({ radius: 1 }),
motionType: MotionType.DYNAMIC,
objectLayer: OBJECT_LAYER_MOVING,
});
// kinematic: user-controlled velocity, pushes dynamic bodies
const kinematicBody = rigidBody.create(world, {
shape: box.create({ halfExtents: [2, 0.2, 2] }),
motionType: MotionType.KINEMATIC,
objectLayer: OBJECT_LAYER_MOVING,
});Position and Rotation
The position of a rigid body represents its location (translation) in 3d world-space. The quaternion represents its orientation (rotation).
Position and rotation can be set when creating a body, or modified after creation using the APIs below.
// read position and quaternion
const position = dynamicBody.position; // [x, y, z]
const quaternion = dynamicBody.quaternion; // [x, y, z, w]
// set position and quaternion together
const newPosition = vec3.fromValues(0, 10, 0);
const newQuaternion = quat.create();
rigidBody.setTransform(world, kinematicBody, newPosition, newQuaternion, false);
// set position and quaternion separately
rigidBody.setPosition(world, kinematicBody, newPosition, false);
rigidBody.setQuaternion(world, kinematicBody, newQuaternion, false);position vs centerOfMassPosition:
position: Location of the shape's origin (where the collision shape is)centerOfMassPosition: Location of the body's center of mass in world-space
For simple shapes (sphere, box, capsule), these are the same. For compound shapes or shapes with offset center of mass, they differ. The physics engine uses centerOfMassPosition internally for simulation.
Velocity
Linear and angular velocity can be read directly from rigid body objects. The rigidBody namespace provides APIs for modifying velocities.
// read velocities
const linearVelocity = dynamicBody.motionProperties.linearVelocity; // [x, y, z] in m/s
const angularVelocity = dynamicBody.motionProperties.angularVelocity; // [x, y, z] in rad/s
// set linear velocity
rigidBody.setLinearVelocity(world, dynamicBody, [5, 0, 0]); // shoot to the right
// set angular velocity
rigidBody.setAngularVelocity(world, dynamicBody, [0, 1, 0]); // spin around y-axisForces and Impulses
Forces accumulate until the next physics step, then get cleared. Impulses apply instant velocity changes. Use addForceAtPosition or addImpulseAtPosition to generate rotation.
// add force at center of mass (accumulates until next physics step)
const force = vec3.fromValues(0, 100, 0); // upward force
rigidBody.addForce(world, dynamicBody, force, true); // last arg: wake if sleeping
// add force at specific position (generates torque)
const worldPosition = vec3.fromValues(1, 0, 0); // apply force at right edge
rigidBody.addForceAtPosition(world, dynamicBody, force, worldPosition, true);
// add impulse (instant velocity change at center of mass)
const impulse = vec3.fromValues(0, 10, 0);
rigidBody.addImpulse(world, dynamicBody, impulse);
// add impulse at position (instant velocity + angular velocity change)
rigidBody.addImpulseAtPosition(world, dynamicBody, impulse, worldPosition);Mass Properties
For most shapes, mass properties are computed automatically. For triangle meshes you need to provide them explicitly to use them with kinematic or static bodies.
// mass properties are computed automatically from shape and density
const body = rigidBody.create(world, {
shape: sphere.create({ radius: 1, density: 1000 }), // density in kg/m³
motionType: MotionType.DYNAMIC,
objectLayer: OBJECT_LAYER_MOVING,
});
// override just the mass (inertia is scaled automatically)
const customMassBody = rigidBody.create(world, {
shape: box.create({ halfExtents: [1, 1, 1] }),
motionType: MotionType.DYNAMIC,
objectLayer: OBJECT_LAYER_MOVING,
mass: 50, // kg
});
// completely override mass properties (useful for triangle meshes)
const triangleMeshMassProperties = massProperties.create();
massProperties.setMassAndInertiaOfSolidBox(triangleMeshMassProperties, [2, 2, 2], 1000); // boxSize, density
const dynamicTriangleMeshBody = rigidBody.create(world, {
shape: box.create({ halfExtents: [1, 1, 1] }),
motionType: MotionType.DYNAMIC,
objectLayer: OBJECT_LAYER_MOVING,
massPropertiesOverride: triangleMeshMassProperties,
});
// read mass properties
const mass = 1 / body.motionProperties.invMass; // mass in kg
const invMass = body.motionProperties.invMass; // 1/mass, used internallyDamping
Damping simulates air resistance or drag. Higher values make objects slow down faster.
// linear damping reduces linear velocity over time (0-1, higher = more drag)
const dampedBody = rigidBody.create(world, {
shape: sphere.create({ radius: 1 }),
motionType: MotionType.DYNAMIC,
objectLayer: OBJECT_LAYER_MOVING,
linearDamping: 0.5, // default is 0.05
angularDamping: 0.8, // default is 0.05
});
// damping can also be modified after creation
dampedBody.motionProperties.linearDamping = 0.2;
dampedBody.motionProperties.angularDamping = 0.2;Maximum Velocities
Clamping velocities prevents instability and tunneling from extreme speeds.
// clamp maximum velocities to prevent instability
const fastBody = rigidBody.create(world, {
shape: sphere.create({ radius: 1 }),
motionType: MotionType.DYNAMIC,
objectLayer: OBJECT_LAYER_MOVING,
maxLinearVelocity: 100, // m/s, default is 500
maxAngularVelocity: 10, // rad/s, default is ~47 (0.25 * PI * 60)
});Degrees of Freedom
Restricting degrees of freedom is useful for 2D games or objects that should only move on specific axes.
// degrees of freedom control which axes a body can move/rotate on
// useful for 2d games or constrained movement
// only allow movement in x and z (2d platformer on xz plane)
const platformerBody = rigidBody.create(world, {
shape: box.create({ halfExtents: [0.5, 1, 0.5] }),
motionType: MotionType.DYNAMIC,
objectLayer: OBJECT_LAYER_MOVING,
allowedDegreesOfFreedom: dof(true, false, true, false, true, false), // tx, tz, ry
});
// dof args: translateX, translateY, translateZ, rotateX, rotateY, rotateZSleeping
Sleeping improves performance by skipping simulation for bodies at rest.
const sleepyBody = rigidBody.create(world, {
shape: box.create({ halfExtents: [1, 1, 1] }),
motionType: MotionType.DYNAMIC,
objectLayer: OBJECT_LAYER_MOVING,
allowSleeping: true, // default
});
// check if sleeping
const isSleeping = sleepyBody.sleeping;
// manually control sleep state
rigidBody.sleep(world, sleepyBody);
rigidBody.wake(world, sleepyBody);You can also wake all sleeping bodies within a specific region:
// wake all sleeping bodies within a region
// useful after explosions, level loading, or regional activation
rigidBody.wakeInAABB(world, [[-10, 0, -10], [10, 20, 10]]);Gravity Factor
Gravity factor multiplies the world gravity for a specific body. Set to 0 for floating objects, less than 1 for lighter-than-normal gravity, or greater than 1 for heavier gravity.
// no gravity
const floatingBody = rigidBody.create(world, {
shape: sphere.create({ radius: 1 }),
motionType: MotionType.DYNAMIC,
objectLayer: OBJECT_LAYER_MOVING,
gravityFactor: 0,
});
// double gravity
const heavyBody = rigidBody.create(world, {
shape: sphere.create({ radius: 1 }),
motionType: MotionType.DYNAMIC,
objectLayer: OBJECT_LAYER_MOVING,
gravityFactor: 2,
});
// modify after creation
heavyBody.motionProperties.gravityFactor = 0.5;Moving Kinematic Bodies
moveKinematic takes a target position and quaternion, and computes the velocities needed to reach them, ensuring physical interactions with dynamic bodies rather than a direct teleportation.
Prefer using moveKinematic over setTransform for kinematic bodies such as moving platforms.
const platform = rigidBody.create(world, {
shape: box.create({ halfExtents: [2, 0.2, 2] }),
motionType: MotionType.KINEMATIC,
objectLayer: OBJECT_LAYER_MOVING,
position: [0, 2, 0],
});
// move each frame by computing velocities
const deltaTime = 1 / 60;
const targetPosition = vec3.fromValues(5, 2, 0);
const targetQuaternion = quat.create();
quat.setAxisAngle(targetQuaternion, vec3.fromValues(0, 1, 0), Math.PI / 4);
rigidBody.moveKinematic(platform, targetPosition, targetQuaternion, deltaTime);Continuous Collision Detection
Use CCD for fast-moving objects like bullets or vehicles to prevent tunneling through thin walls.
const bullet = rigidBody.create(world, {
shape: sphere.create({ radius: 0.1 }),
motionType: MotionType.DYNAMIC,
objectLayer: OBJECT_LAYER_MOVING,
motionQuality: MotionQuality.LINEAR_CAST, // enables ccd
});
// configure threshold in world settings (default 0.05 = 5%)
// worldSettings.ccd.linearCastThreshold = 0.05;User Data
User data lets you attach game-specific data to bodies for easy lookup during collision callbacks.
const player = rigidBody.create(world, {
shape: box.create({ halfExtents: [0.5, 1, 0.5] }),
motionType: MotionType.DYNAMIC,
objectLayer: OBJECT_LAYER_MOVING,
userData: 123, // store entity id
});
const entityId = player.userData as number;Collision Groups and Masks
Collision groups and masks provide fine-grained collision filtering using 32-bit bitmasks. This works alongside object layer filtering - both must pass for bodies to collide.
A collision occurs when (groupA & maskB) != 0 AND (groupB & maskA) != 0. Use the bitmask helper to define named groups.
const GROUPS = bitmask.createFlags(['player', 'enemy', 'debris', 'projectile'] as const);
// player collides with enemies and projectiles, but not debris
const playerBody = rigidBody.create(world, {
shape: sphere.create({ radius: 1 }),
motionType: MotionType.DYNAMIC,
objectLayer: OBJECT_LAYER_MOVING,
collisionGroups: GROUPS.player,
collisionMask: GROUPS.enemy | GROUPS.projectile,
});
// enemy collides with player and debris, but not other enemies
const enemyBody = rigidBody.create(world, {
shape: sphere.create({ radius: 1 }),
motionType: MotionType.DYNAMIC,
objectLayer: OBJECT_LAYER_MOVING,
collisionGroups: GROUPS.enemy,
collisionMask: GROUPS.player | GROUPS.debris,
});
// debris collides with everything
const debrisBody = rigidBody.create(world, {
shape: box.create({ halfExtents: [0.5, 0.5, 0.5] }),
motionType: MotionType.DYNAMIC,
objectLayer: OBJECT_LAYER_MOVING,
collisionGroups: GROUPS.debris,
collisionMask: GROUPS.player | GROUPS.enemy | GROUPS.debris | GROUPS.projectile,
});Material Properties
Friction and restitution control surface interaction. Combine modes determine how material properties mix when two bodies collide.
// low friction, high restitution
const bouncyBall = rigidBody.create(world, {
shape: sphere.create({ radius: 1 }),
motionType: MotionType.DYNAMIC,
objectLayer: OBJECT_LAYER_MOVING,
friction: 0.1,
restitution: 0.9,
});
// high friction, no bounce
const stickyBox = rigidBody.create(world, {
shape: box.create({ halfExtents: [1, 1, 1] }),
motionType: MotionType.DYNAMIC,
objectLayer: OBJECT_LAYER_MOVING,
friction: 1.0,
restitution: 0.0,
});
// custom combine modes
const customCombine = rigidBody.create(world, {
shape: sphere.create({ radius: 1 }),
motionType: MotionType.DYNAMIC,
objectLayer: OBJECT_LAYER_MOVING,
friction: 0.5,
restitution: 0.5,
frictionCombineMode: MaterialCombineMode.MIN,
restitutionCombineMode: MaterialCombineMode.MAX,
});Sensors
Sensor bodies detect collisions without applying physical forces. Use them for trigger zones, pickups, or detection areas.
const triggerZone = rigidBody.create(world, {
shape: box.create({ halfExtents: [5, 5, 5] }),
motionType: MotionType.STATIC,
objectLayer: OBJECT_LAYER_NOT_MOVING,
sensor: true,
});
// detect when bodies enter/exit sensor
const listener: Listener = {
onContactAdded: (bodyA, bodyB, manifold, settings) => {
if (bodyA.id === triggerZone.id || bodyB.id === triggerZone.id) {
// otherBody entered trigger zone
const otherBody = bodyA.id === triggerZone.id ? bodyB : bodyA;
}
},
};Updating Shape
You can change a body's shape after creation. This recalculates mass properties, inertia, and the axis-aligned bounding box.
// change a body's shape after creation
const changingBody = rigidBody.create(world, {
shape: box.create({ halfExtents: [1, 1, 1] }),
motionType: MotionType.DYNAMIC,
objectLayer: OBJECT_LAYER_MOVING,
});
// later: change to a sphere
changingBody.shape = sphere.create({ radius: 1.5 });
rigidBody.updateShape(world, changingBody); // recalculates mass, inertia, aabbChanging Object Layer
Object layers control which bodies can collide. You can change a body's layer at runtime to modify collision behavior.
// move a body to a different object layer
const movableBody = rigidBody.create(world, {
shape: box.create({ halfExtents: [1, 1, 1] }),
motionType: MotionType.DYNAMIC,
objectLayer: OBJECT_LAYER_MOVING,
});
// change to a different layer (e.g. when picked up by player)
rigidBody.setObjectLayer(world, movableBody, OBJECT_LAYER_NOT_MOVING);Changing Motion Type
You can change a body's motion type at runtime to switch between static, kinematic, and dynamic behavior.
// change a body's motion type at runtime
const switchableBody = rigidBody.create(world, {
shape: box.create({ halfExtents: [1, 1, 1] }),
motionType: MotionType.DYNAMIC,
objectLayer: OBJECT_LAYER_MOVING,
});
// make it kinematic (e.g. player grabs it)
rigidBody.setMotionType(world, switchableBody, MotionType.KINEMATIC, true);
// make it dynamic again (e.g. player drops it)
rigidBody.setMotionType(world, switchableBody, MotionType.DYNAMIC, true);
// make it static (e.g. permanently attach to world)
rigidBody.setMotionType(world, switchableBody, MotionType.STATIC, false);Shapes
Shapes determine how rigid bodies collide with each other. crashcat provides primitive shapes, complex shapes like triangle meshes, and decorator shapes for advanced use cases.
Convex Shapes
A convex shape is one where, if you pick any two points inside the shape, the line segment between them is also inside the shape. This property enables fast collision detection with the GJK/EPA algorithms.
To speed up collision detection, all convex shapes use a convex radius. The shape is first shrunk by the convex radius, then inflated again by the same amount, resulting in a rounded shape.
This rounding improves performance and contact manifold quality, but makes geometry slightly less accurate. Adjust the radius to balance speed vs precision.
Sphere
The simplest and fastest convex shape.
// simplest and fastest convex shape
sphere.create({ radius: 1 });
// with density for mass calculation
sphere.create({ radius: 1, density: 1000 }); // kg/m³Box
Defined by half extents from the center.
// defined by half extents from center
box.create({ halfExtents: [1, 2, 0.5] });
// with density
box.create({ halfExtents: [1, 1, 1], density: 500 });Capsule
A cylinder with hemispherical caps on each end.
// cylinder with hemispherical caps
capsule.create({
halfHeightOfCylinder: 1, // half height of cylindrical section
radius: 0.5,
});
// with density
capsule.create({
halfHeightOfCylinder: 1,
radius: 0.5,
density: 800,
});Cylinder
Defined by half height and radius.
// defined by half height and radius
cylinder.create({
halfHeight: 1,
radius: 0.5,
});
// with density
cylinder.create({
halfHeight: 1,
radius: 0.5,
density: 1200,
});Convex Hull
The convex hull of a set of points.
// convex envelope of a set of points
convexHull.create({
positions: [0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1], // flat array [x,y,z, x,y,z, ...]
});
// with density
convexHull.create({
positions: [0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1],
density: 900,
});Triangle Mesh Shape
Triangle meshes represent complex geometry using triangles. Typically used for static terrain and level geometry.
// triangle mesh: for complex static geometry like terrain
const meshShape = triangleMesh.create({
positions: [-10, 0, -10, 10, 0, -10, 10, 0, 10, -10, 0, 10],
indices: [0, 1, 2, 0, 2, 3],
});
// triangle meshes typically used with static bodies
rigidBody.create(world, {
shape: meshShape,
motionType: MotionType.STATIC,
objectLayer: OBJECT_LAYER_NOT_MOVING,
});
// for dynamic triangle meshes, provide mass properties explicitly
const dynamicMeshProps = massProperties.create();
massProperties.setMassAndInertiaOfSolidBox(dynamicMeshProps, [2, 2, 2], 1000);
rigidBody.create(world, {
shape: meshShape,
motionType: MotionType.DYNAMIC,
objectLayer: OBJECT_LAYER_MOVING,
massPropertiesOverride: dynamicMeshProps,
});⚠️ Dynamic triangle meshes
Avoid using triangle meshes for dynamic bodies. Performance is poor (collision detection against triangle meshes is usually more expensive), and fast-moving meshes can tunnel through other objects easily. Use convex hulls or compound shapes instead for dynamic objects.
Compound Shape
Compound shapes combine multiple child shapes into a single shape. Useful for complex objects like vehicles or characters.
// compound: combine multiple shapes into one
compound.create({
children: [
{
position: [0, 0, 0],
quaternion: quat.create(),
shape: box.create({ halfExtents: [2, 0.5, 1] }), // main body
},
{
position: [0, 1, 0],
quaternion: quat.create(),
shape: box.create({ halfExtents: [0.5, 0.5, 0.5] }), // turret
},
],
});
// useful for complex objects like vehicles, characters, furniture
rigidBody.create(world, {
shape: compound.create({
children: [
{
position: [0, 0, 0],
quaternion: quat.create(),
shape: box.create({ halfExtents: [1, 1, 1] }),
},
],
}),
motionType: MotionType.DYNAMIC,
objectLayer: OBJECT_LAYER_MOVING,
});Decorator Shapes
Decorator shapes modify other shapes without changing their collision shape.
Scaled
Apply non-uniform scaling to any shape.
// non-uniform scaling of any shape
scaled.create({
shape: box.create({ halfExtents: [1, 1, 1] }),
scale: [2, 0.5, 1], // stretch in x, squash in y
});
// works with any shape
scaled.create({
shape: sphere.create({ radius: 1 }),
scale: [1, 2, 1], // creates an ellipsoid
});Offset Center of Mass
Shift the center of mass without changing collision shape. Useful for improving stability of tall objects.
// shift center of mass without changing collision geometry
// useful for stability (e.g., lowering COM on tall objects)
const stableShape = offsetCenterOfMass.create({
shape: box.create({ halfExtents: [0.5, 2, 0.5] }), // tall box
offset: [0, -1, 0], // lower center of mass
});
rigidBody.create(world, {
shape: stableShape,
motionType: MotionType.DYNAMIC,
objectLayer: OBJECT_LAYER_MOVING,
});Reusing Shapes
Shapes can be created once and reused across multiple bodies. This saves memory and improves performance.
// create a shape once
const sharedBoxShape = box.create({ halfExtents: [1, 1, 1] });
// reuse it for multiple bodies
rigidBody.create(world, {
shape: sharedBoxShape,
motionType: MotionType.DYNAMIC,
objectLayer: OBJECT_LAYER_MOVING,
position: [0, 5, 0],
});
rigidBody.create(world, {
shape: sharedBoxShape, // same shape instance
motionType: MotionType.DYNAMIC,
objectLayer: OBJECT_LAYER_MOVING,
position: [5, 5, 0],
});Offline Shape Generation
Shapes are JSON-serializable objects. You can generate complex shapes offline (especially triangle meshes, which perform sanitization, active edge computation, and BVH construction) and load the JSON at runtime.
// offline (node.js, build step, bundler macro, etc)
const pregeneratedTriangleMeshShape = triangleMesh.create({
positions: [/* large terrain data */],
indices: [/* large index data */],
});
// serialize to JSON
const triangleMeshShapeJson = JSON.stringify(pregeneratedTriangleMeshShape);
// save to file or bundle with app
// runtime (browser):
const triangleMeshShape = JSON.parse(triangleMeshShapeJson);
// use the pre-processed shape directly
rigidBody.create(world, {
shape: triangleMeshShape,
motionType: MotionType.STATIC,
objectLayer: OBJECT_LAYER_NOT_MOVING,
});Listener
The listener lets you react to and modify physics events during world updates. Pass a listener to updateWorld() to receive callbacks for collision events.
Basic Usage
// create a listener to react to physics events
const listener: Listener = {
onContactAdded: (bodyA, bodyB, manifold, settings) => {
// called when a new contact is detected
console.log('contact added between', bodyA.id, 'and', bodyB.id);
},
onContactPersisted: (bodyA, bodyB, manifold, settings) => {
// called when a contact from last frame is still active
},
onContactRemoved: (bodyIdA, bodyIdB, subShapeIdA, subShapeIdB) => {
// called when a contact is no longer active
// WARNING: bodies may be destroyed, only body IDs are safe to use
},
};
// pass listener to updateWorld
updateWorld(world, listener, 1 / 60);
// WARNING: do NOT remove bodies inside listener callbacks!
// the physics system is in the middle of processing contacts and removing bodies
// will corrupt internal state. instead, store the body IDs and remove them after
// updateWorld completes:
//
// const bodiesToRemove: number[] = [];
// const listener: Listener = {
// onContactAdded: (bodyA, bodyB) => {
// if (shouldDestroy(bodyA)) {
// bodiesToRemove.push(bodyA.id);
// }
// }
// };
// updateWorld(world, listener, 1 / 60);
// for (const id of bodiesToRemove) {
// removeBody(world, id);
// }
// bodiesToRemove.length = 0;Body Pair Validation
Runs before expensive narrowphase collision detection. Use this when filtering logic is too complex for object layers or collision groups/masks (which are faster). Prefer those simpler mechanisms when possible.
// most efficient place to filter collisions - before narrowphase runs
const validateListener: Listener = {
onBodyPairValidate: (bodyA, bodyB) => {
// custom filtering logic
// e.g., prevent ragdoll self-collision, faction systems, etc.
// example: prevent bodies with same userData from colliding
if (bodyA.userData === bodyB.userData) {
return false; // skip collision detection
}
return true; // allow collision
},
};Contact Validation
Called after collision detection but before adding the contact constraint. Rejecting contacts here is expensive since narrowphase has already run - prefer onBodyPairValidate or object layer filtering where possible. Use this for special cases where you use the contact information (contact point, normal, etc) to make a decision, such as one-way platforms or material-based effects.
// called after collision detection, before adding contact constraint
const contactValidateListener: Listener = {
onContactValidate: (bodyA, bodyB, baseOffset, hit) => {
// expensive to reject here - narrowphase already ran
// prefer onBodyPairValidate or object layer filtering
// example: one-way platform - only collide if falling down
const relativeVelocity = bodyA.motionProperties.linearVelocity[1] - bodyB.motionProperties.linearVelocity[1];
if (relativeVelocity > 0) {
// moving up through platform
return ContactValidateResult.REJECT_CONTACT;
}
return ContactValidateResult.ACCEPT_CONTACT;
},
};Modifying Contact Behavior
Adjust friction, restitution, and other properties for specific contacts.
// modify contact behavior by changing settings
const modifyContactListener: Listener = {
onContactAdded: (bodyA, bodyB, manifold, settings) => {
// increase friction for this specific contact
settings.combinedFriction = 2.0;
// disable restitution (no bounce)
settings.combinedRestitution = 0.0;
// collision normal (points from bodyA to bodyB)
const normal = manifold.worldSpaceNormal;
// access contact points in world space
const worldPointA = vec3.create();
const worldPointB = vec3.create();
for (let i = 0; i < manifold.numContactPoints; i++) {
// get world-space positions of contact points
getWorldSpaceContactPointOnA(worldPointA, manifold, i);
getWorldSpaceContactPointOnB(worldPointB, manifold, i);
// example: spawn particle effect at contact point
// spawnContactEffect(worldPointA);
// example: apply damage based on penetration depth
// const damage = manifold.penetrationDepth * 10;
}
},
};Queries
Queries let you ask questions about the physics world without running a full simulation step. Use them for raycasts, shape sweeps, overlap tests, and more.
Cast Ray
Cast a ray through the world to find bodies along a line. Useful for line-of-sight checks, projectile trajectories, and mouse picking.
// cast a ray through the world to find bodies
const rayOrigin = vec3.fromValues(0, 5, 0);
const rayDirection = vec3.fromValues(0, -1, 0);
const rayLength = 100;
// create a filter to control what the ray can hit
const queryFilter = filter.create(world.settings.layers);
// closest: finds the nearest hit along the ray
const closestCollector = createClosestCastRayCollector();
const raySettings = createDefaultCastRaySettings();
castRay(world, closestCollector, raySettings, rayOrigin, rayDirection, rayLength, queryFilter);
if (closestCollector.hit.status === CastRayStatus.COLLIDING) {
const hitDistance = closestCollector.hit.fraction * rayLength;
const hitPoint = vec3.scaleAndAdd(vec3.create(), rayOrigin, rayDirection, hitDistance);
const hitBody = rigidBody.get(world, closestCollector.hit.bodyIdB)!;
const surfaceNormal = rigidBody.getSurfaceNormal(vec3.create(), hitBody, hitPoint, closestCollector.hit.subShapeId);
console.log('closest hit at', hitPoint);
console.log('surface normal:', surfaceNormal);
}
// any: finds the first hit (fast early-out, useful for line-of-sight checks)
const anyCollector = createAnyCastRayCollector();
anyCollector.reset();
castRay(world, anyCollector, raySettings, rayOrigin, rayDirection, rayLength, queryFilter);
if (anyCollector.hit.status === CastRayStatus.COLLIDING) {
console.log('ray hit something');
}
// all: collects every hit along the ray
const allCollector = createAllCastRayCollector();
allCollector.reset();
castRay(world, allCollector, raySettings, rayOrigin, rayDirection, rayLength, queryFilter);
for (const hit of allCollector.hits) {
if (hit.status === CastRayStatus.COLLIDING) {
console.log('hit at fraction', hit.fraction);
}
}Cast Shape
Sweep a shape through the world to find what it would hit. Essential for character movement, projectile prediction, and object placement.
// sweep a shape through the world (useful for character movement, projectiles)
const castPosition = vec3.fromValues(0, 5, 0);
const castQuaternion = quat.create();
const castScale = vec3.fromValues(1, 1, 1);
const castDisplacement = vec3.fromValues(0, -10, 0);
const sweepShape = sphere.create({ radius: 0.5 });
// closest: finds the nearest hit along the sweep
const closestShapeCollector = createClosestCastShapeCollector();
const shapeSettings = createDefaultCastShapeSettings();
castShape(
world,
closestShapeCollector,
shapeSettings,
sweepShape,
castPosition,
castQuaternion,
castScale,
castDisplacement,
queryFilter,
);
if (closestShapeCollector.hit.status === CastShapeStatus.COLLIDING) {
const hitFraction = closestShapeCollector.hit.fraction;
const hitPosition = vec3.create();
vec3.scaleAndAdd(hitPosition, castPosition, castDisplacement, hitFraction);
console.log('shape hit at', hitPosition);
}
// any: finds the first hit (fast early-out)
const anyShapeCollector = createAnyCastShapeCollector();
anyShapeCollector.reset();
castShape(
world,
anyShapeCollector,
shapeSettings,
sweepShape,
castPosition,
castQuaternion,
castScale,
castDisplacement,
queryFilter,
);
if (anyShapeCollector.hit.status === CastShapeStatus.COLLIDING) {
console.log('shape hit something');
}
// all: collects every hit along the sweep
const allShapeCollector = createAllCastShapeCollector();
allShapeCollector.reset();
castShape(
world,
allShapeCollector,
shapeSettings,
sweepShape,
castPosition,
castQuaternion,
castScale,
castDisplacement,
queryFilter,
);
for (const hit of allShapeCollector.hits) {
if (hit.status === CastShapeStatus.COLLIDING) {
console.log('shape hit at fraction', hit.fraction);
}
}Collide Point
Test if a point is inside any bodies. Useful for trigger zones, item pickups, and spatial checks.
// test if a point is inside any bodies (useful for triggers, pickups)
const point = vec3.fromValues(0, 2, 0);
// any: checks if the point is inside any body (fast early-out)
const anyPointCollector = createAnyCollidePointCollector();
const pointSettings = createDefaultCollidePointSettings();
collidePoint(world, anyPointCollector, pointSettings, point, queryFilter);
if (anyPointCollector.hit !== null) {
console.log('point is inside body', anyPointCollector.hit.bodyIdB);
}
// all: finds every body containing the point
const allPointCollector = createAllCollidePointCollector();
allPointCollector.reset();
collidePoint(world, allPointCollector, pointSettings, point, queryFilter);
console.log('point is inside', allPointCollector.hits.length, 'bodies');Collide Shape
Test if a shape overlaps any bodies. Perfect for area triggers, placement validation, and explosion radius checks.
// test if a shape overlaps any bodies (useful for area triggers, placement validation)
const queryShape = box.create({ halfExtents: vec3.fromValues(1, 1, 1) });
const queryPosition = vec3.fromValues(0, 2, 0);
const queryQuaternion = quat.create();
const queryScale = vec3.fromValues(1, 1, 1);
// any: checks if the shape overlaps any body (fast early-out)
const anyShapeOverlapCollector = createAnyCollideShapeCollector();
const shapeOverlapSettings = createDefaultCollideShapeSettings();
collideShape(
world,
anyShapeOverlapCollector,
shapeOverlapSettings,
queryShape,
queryPosition,
queryQuaternion,
queryScale,
queryFilter,
);
if (anyShapeOverlapCollector.hit !== null) {
console.log('shape overlaps body', anyShapeOverlapCollector.hit.bodyIdB);
}
// all: finds every body overlapping the shape
const allShapeOverlapCollector = createAllCollideShapeCollector();
allShapeOverlapCollector.reset();
collideShape(
world,
allShapeOverlapCollector,
shapeOverlapSettings,
queryShape,
queryPosition,
queryQuaternion,
queryScale,
queryFilter,
);
console.log('shape overlaps', allShapeOverlapCollector.hits.length, 'bodies');Broadphase Queries
For advanced scenarios, you can query the broadphase spatial acceleration structure directly. This is faster than narrowphase queries but less precise - it only tests axis-aligned bounding boxes (AABBs), not exact shapes.
// for advanced scenarios: query the broadphase spatial acceleration structure directly
// useful when you need custom traversal logic or want to avoid narrowphase overhead
// intersectAABB: find all bodies whose AABBs overlap a box
const queryAABB: Box3 = [
[-5, -5, -5],
[5, 5, 5],
];
const aabbVisitor: BodyVisitor = {
shouldExit: false,
visit(body: RigidBody) {
console.log('body AABB overlaps query AABB:', body.id);
// set shouldExit = true to stop traversal early
},
};
broadphase.intersectAABB(world, queryAABB, queryFilter, aabbVisitor);
// intersectPoint: find all bodies whose AABBs contain a point
const queryPoint = vec3.fromValues(0, 5, 0);
const pointVisitor: BodyVisitor = {
shouldExit: false,
visit(body: RigidBody) {
console.log('body AABB contains point:', body.id);
},
};
broadphase.intersectPoint(world, queryPoint, queryFilter, pointVisitor);Shape vs Shape
For advanced scenarios, you can query shape-vs-shape directly.
Query Filters
Filters control what queries can hit using object layers, broadphase layers, collision groups/masks, and custom callbacks.
Filters apply three levels of filtering in order:
- Object/Broadphase Layers: Fast spatial partitioning (configured in world settings)
- Collision Groups/Masks: Bitwise filtering using 32-bit masks (checks against
rigidBody.collisionGroupsandrigidBody.collisionMasks) - Body Filter Callback: Custom logic for complex filtering (slowest, use sparingly)
All three must pass for a query to pass for a body.
// filters control what queries can hit, using object layers and collision groups/masks
// basic: create a filter with all layers enabled
const worldQueryFilter = filter.create(world.settings.layers);
// filter specific object layers
filter.disableObjectLayer(worldQueryFilter, world.settings.layers, OBJECT_LAYER_DEBRIS);
filter.enableObjectLayer(worldQueryFilter, world.settings.layers, OBJECT_LAYER_MOVING);
// filter specific broadphase layers
filter.disableBroadphaseLayer(worldQueryFilter, world.settings.layers, BROADPHASE_LAYER_MOVING);
filter.enableBroadphaseLayer(worldQueryFilter, world.settings.layers, BROADPHASE_LAYER_NOT_MOVING);
// filter everything, then selectively enable
filter.disableAllLayers(worldQueryFilter, world.settings.layers);
filter.enableObjectLayer(worldQueryFilter, world.settings.layers, OBJECT_LAYER_MOVING);
// collision groups and masks (works alongside layer filtering)
worldQueryFilter.collisionGroups = 0b0001; // query belongs to group 1
worldQueryFilter.collisionMask = 0b0010 | 0b0100; // query hits groups 2 and 4
// custom body filter callback
worldQueryFilter.bodyFilter = (body: RigidBody) => {
// custom logic - exclude specific bodies
if (body.userData === 'player') return false;
// only hit dynamic bodies
if (body.motionType !== MotionType.DYNAMIC) return false;
return true;
};
// setFromBody: configure filter to match what a body can collide with
const playerBody = rigidBody.get(world, playerId)!;
filter.setFromBody(worldQueryFilter, world.settings.layers, playerBody);
// copy filter settings
const rayFilter = filter.create(world.settings.layers);
filter.copy(rayFilter, worldQueryFilter);
// use filter in queries
castRay(world, closestCollector, raySettings, rayOrigin, rayDirection, rayLength, worldQueryFilter);export type Filter = {
/** enabled object layers (1 = enabled, 0 = disabled) */
enabledObjectLayers: number[];
/** enabled broadphase layers (1 = enabled, 0 = disabled) */
enabledBroadphaseLayers: number[];
/** collision mask */
collisionMask: number;
/** collision group */
collisionGroups: number;
/** body filter callback */
bodyFilter: ((body: RigidBody) => boolean) | undefined;
};