npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

crashcat

v0.0.1

Published

a 3d physics library for games and simulations

Downloads

518

Readme

cover

Version GitHub Workflow Status (with event) Downloads

> npm install crashcat

crashcat

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

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-axis

Forces 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 internally

Damping

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, rotateZ

Sleeping

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, aabb

Changing 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:

  1. Object/Broadphase Layers: Fast spatial partitioning (configured in world settings)
  2. Collision Groups/Masks: Bitwise filtering using 32-bit masks (checks against rigidBody.collisionGroups and rigidBody.collisionMasks)
  3. 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;
};

Constraints