Skip to content

Latest commit

 

History

History
1042 lines (809 loc) · 28.4 KB

File metadata and controls

1042 lines (809 loc) · 28.4 KB

Bounce API Reference

Bounce is a fast, deterministic 3D physics library for TypeScript and JavaScript projects. It's written in pure TypeScript (no WebAssembly).

Installation

npm i @perplexdotgg/bounce

Getting Started

The core of Bounce is the World class. You create a world, add shapes and bodies to it, then step the simulation forward in time.

import { World } from '@perplexdotgg/bounce';

const world = new World({
  gravity: { x: 0, y: -9.81, z: 0 },
});

// ground
const groundShape = world.createBox({
  width: 10,
  height: 2,
  depth: 10,
});

const ground = world.createStaticBody({
  shape: groundShape,
  position: { x: 0, y: 10, z: 0 },
  orientation: { x: 0, y: 0, z: 0, w: 1 }, // if w is omitted, this is treated as an euler (radians) input
  friction: 0.4,
  restitution: 0.3,
});

// ball
const ballShape = world.createSphere({ radius: 0.5 });

const ball = world.createDynamicBody({
  shape: ballShape,
  position: [0, -1, 0], // vec3 can be specified as an array, see note below
  orientation: [0, Math.PI, 0], // euler (radians) or quaternion as input, based on the presence of 3 or 4 numbers
  friction: 0.4,
  restitution: 0.3,
  mass: 0.5,
});


// simulate 10 seconds
const timeStepSizeSeconds = 1 / 60;
for (let i = 0; i < 600; i++) {
  world.takeOneStep(timeStepSizeSeconds);
}

Note

For convenience, the Vec3 and Quat (quaternion angle) classes allow you to input values values as arrays. Note that the array is only used for input. Once ball is created, ball.orientation is a quaternion with quaternion methods, even though the input was an euler array. If the orientation input array had 4 numbers, it would be treated as a quaternion, e.g. [0, 1, 0, 0]. Likewise, ball.position is a vec3 instance with x, y and z fields, even though the input was an array.

Separately, many examples below use array notation in the comments to indicate what console.logs would show. This is again just shorthand for the sake of readability of the documentation, the actual logs would show instances of vec3


World

The World is the main simulation container. It manages bodies, shapes, constraints, collision detection, and the solver.

World Options

When constructing a world, you can configure gravity, solver iterations, damping, timestep, and other physics parameters. Default values are shown below for the most commonly used options:

const world = new World({
  gravity: [0, -9.80665, 0],
  timeStepSizeSeconds: 1 / 60,

  // more is higher fidelity, but uses more CPU
  solveVelocityIterations: 6,
  solvePositionIterations: 2,

  // most of these options are for stability
  linearDamping: 0.05,
  angularDamping: 0.05,
  baumgarte: 0.2,
  penetrationSlop: 0.02,
  maxPenetrationDistance: 0.2,
  speculativeContactDistance: 0.02,
  collisionTolerance: 1e-4,
  maxLinearSpeed: 30.0,
  maxAngularSpeed: 30.0,
  isWarmStartingEnabled: true,

  // body defaults, can be overridden per body
  restitution: 0.2,
  friction: 0.5,

  // contact manifold limits
  // by default since v1.3.0, these are unlimited
  // if set to a finite amount, an error is thrown when an allocation is attempted past the limit
  // this may be desired during development to stay within a strict memory budget, for example
  // 1024 is a reasonable limit for many use cases (it was the default prior to v1.3.0)
  contactManifoldOptions: {
    maxContactManifolds: Infinity,
    maxContactPairs: Infinity,
  },
});

Stepping the Simulation

  • world.takeOneStep(deltaTimeInSeconds?) — advances by one time step. if deltaTimeInSeconds is not specified, time is advanced by the world's timeStepSizeSeconds (optionally specified on world creation, defaults to 1/60)
  • world.advanceTime(deltaTimeInSeconds?, timeToSimulate?) — accumulates time and steps as needed. useful for game loops if you want a fixed step size for the physics, independent of frame rate

Shapes

Shapes define the collision geometry of bodies. Bounce provides several built-in shape types.

Primitive Shapes

Create shapes using the world's factory methods:

const sphere = world.createSphere({ radius: 1.5 });
const box = world.createBox({ width: 1, height: 1, depth: 1 });
const capsule = world.createCapsule({ radius: 1.0, height: 2.0 });
const cylinder = world.createCylinder({ halfHeight: 1, radius: 0.5 });

Convex Hull

Convex hulls can be created from a point cloud (flat array of vertex positions):

// from point cloud (flat vertex array)
const vertexPositions = new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1]);

const shape = world.createConvexHull(
  vertexPositions, // Float32Array of vertex positions
  0.02, // convexRadius (optional, default 0.02)
  1e-3, // hullTolerance (optional, default 1e-3 = 0.001)
  1000, // maxPoints (optional, default 1000)
  1000  // maxVertexIndices (optional, default 1000)
);

const body = world.createDynamicBody({ shape: shape, position: [0, 0, 5] });

Triangle Mesh

Triangle meshes are useful for complex static geometry like terrain or building interiors:

// vertex positions (x, y, z triplets)
const vertices = new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0]);

// face indices (triangle vertex indices)
const indices = new Uint32Array([
  0,
  1,
  2, // first triangle
  1,
  3,
  2, // second triangle
]);

const shape = world.createTriangleMesh({
  vertexPositions: vertices,
  faceIndices: indices,

  // optional, if you want to estimate the volume or inertia tensor based on the convex hull
  // note that a convex hull is automatically created if a triangle mesh is used as a dynamic body
  // that said, triangle meshes are generally not recommended as dynamic bodies, for performance reasons
  forceCreateConvexHull: false,
});

const body = world.createStaticBody({ shape: shape });

Note

If skipHullCreation is false (default), the mesh is limited to 1000 vertices due to convex hull generation limits. Set to true if the mesh will only be used for static bodies. A convex hull is generated as a way to very roughly estimate the center of mass and the inertia tensor of the shape.

Height Map

Height maps are optimized for terrain. They must be square and have power-of-two subdivisions:

// 9x9 vertices = 8x8 quads (power of two)
const vertexCount = { width: 9, depth: 9 };

// terrain size in world units
const extents = { x: 100, y: 10, z: 100 };

// heights normalized to [0, 1] range
const heights = new Float32Array(9 * 9);
for (let z = 0; z < 9; z++) {
  for (let x = 0; x < 9; x++) {
    const idx = z * 9 + x;
    heights[idx] = Math.random(); // 0.0 to 1.0
  }
}

const shape = world.createHeightMap(vertexCount, extents, heights);
const terrain = world.createStaticBody({ shape: shape });

Requirements:

  • Must be square: width === depth
  • Vertex count must be power-of-two + 1 (e.g., 3x3, 5x5, 9x9, 17x17, 33x33, 65x65, etc.)
  • Heights normalized to [0, 1] range (multiplied by extents.y)

Compound Shapes

Compound shapes combine multiple sub-shapes into a single shape. This is useful for non-convex objects or complex geometry:

const subShape1 = world.createSphere({ radius: 1.0 });
const subShape2 = world.createBox({ width: 2.0, height: 3.0, depth: 4.0 });

// combine shapes (useful for non-convex objects)
const shape = world.createCompoundShape([
  { shape: subShape1, transform: { position: [0, -2, 0] } },
  {
    shape: subShape2,
    transform: { position: [0, +2, 0], rotation: [0, Math.PI / 4, 0] },
  },
]);

const body = world.createDynamicBody({ shape: shape, position: [0, 0, 5] });

Reusing Shapes

Shapes can be shared across multiple bodies. This is more efficient than creating a new shape for each body:

// inefficient - new shape per body:
for (let i = 0; i < 100; i++) {
  const shape = world.createBox({ width: 1, height: 1, depth: 1 });
  const body = world.createDynamicBody({ shape: shape });
}

// efficient - share one shape:
const shape = world.createBox({ width: 1, height: 1, depth: 1 });
for (let i = 0; i < 100; i++) {
  const body = world.createDynamicBody({ shape: shape });
}

Translating Shapes in Local Space

By default, shapes are centered at the origin. You can offset a shape's center of mass relative to the body's position:

// default: centered at origin
const sphere1 = world.createSphere({ radius: 1.0 });
const body1 = world.createDynamicBody({ shape: sphere1 });

// translated from origin
const sphere2 = world.createSphere({ radius: 1.0, position: [3, 0, 0] });
const body2 = world.createDynamicBody({ shape: sphere2 });

// character controller capsule: bottom at origin
const radius = 1.0;
const height = 2.0;
const halfCapsuleHeight = radius + height / 2;
const shape = world.createCapsule({
  radius,
  height,
  position: [0, -halfCapsuleHeight, 0],
});
const body = world.createDynamicBody({ shape: shape });

Updating Shape Properties

If you modify a shape's properties after creation (like dimensions), you must call commitChanges() to update derived values:

const shape = world.createBox({ width: 1, height: 1, depth: 1 });
console.log(shape.computedVolume); // 1.0 m^3

const body = world.createDynamicBody({ shape: shape, density: 1000.0 });
console.log(body.mass); // 1000.0 kg

shape.width = 2;
shape.height = 2;
shape.depth = 2;

console.log(shape.computedVolume); // 1.0 m^3 (not updated yet!)
console.log(body.mass); // 1000.0 kg (stale)

shape.commitChanges(); // update world

console.log(shape.computedVolume); // 8.0 m^3 (updated!)
console.log(body.mass); // 8000.0 kg (updated!)

Bodies

Bodies are the physical objects in your simulation. Each body has a shape, position, orientation, velocity, and physical properties like mass and friction.

Body Types

Bounce supports three body types:

  • Dynamic — affected by forces, impulses, and collisions
  • Kinematic — user-controlled motion, participates in collisions but isn't affected by forces
  • Static — immovable, used for ground/walls
const sphere = world.createSphere({ radius: 1.5 });

const dynamicBody = world.createDynamicBody({ shape: sphere });
const staticBody = world.createStaticBody({ shape: sphere });
const kinematicBody = world.createKinematicBody({ shape: sphere });

// or specify type explicitly
const dynamicBody2 = world.createBody({
  shape: sphere,
  type: BodyType.dynamic,
});

Iterating Over Bodies

You can iterate over all bodies of a specific type or all bodies:

const boxShape = world.createBox({ width: 1, height: 1, depth: 1 });
const body1 = world.createDynamicBody({ shape: boxShape, position: [0, 0, 0] });
const body2 = world.createDynamicBody({ shape: boxShape, position: [0, 5, 0] });
const body3 = world.createDynamicBody({
  shape: boxShape,
  position: [0, 20, 0],
});

// iterate by type
for (const body of world.dynamicBodies) {
  console.log(body.position);
}

// also available: world.kinematicBodies, world.staticBodies

// or iterate all bodies at once
for (const body of world.bodies()) {
  console.log(body.position, body.type);
}

Updating Body Properties

Like shapes, if you directly mutate body properties (like position), you must call commitChanges() for the world to recognize the change:

import { Sphere } from "@perplexdotgg/bounce";

function onHit(result) {
  console.log("hit");
}
const queryShape = Sphere.create({ radius: 5 });

const world = new World();

const shape = world.createSphere({ radius: 2 });
const body = world.createKinematicBody({ shape: shape, position: [0, 0, 0] });

console.log(body.position); // [0, 0, 0]
world.intersectShape(onHit, queryShape, { position: { x: 0, y: 10, z: 0 } }); // no hit

body.position.set([0, 5, 0]);
console.log(body.position); // [0, 5, 0]

world.intersectShape(onHit, queryShape, { position: { x: 0, y: 10, z: 0 } }); // still no hit! (stale)

body.commitChanges(); // update world

world.intersectShape(onHit, queryShape, { position: { x: 0, y: 10, z: 0 } }); // hit! (updated)

Destroying Bodies and Shapes

Bodies and shapes can be removed from the world. Note that destroying a body does not destroy its shape (it may be shared), but destroying a shape destroys all bodies using it:

const shape = world.createSphere({ radius: 1.5 });
const body1 = world.createStaticBody({ shape: shape });
const body2 = world.createStaticBody({ shape: shape });
const body3 = world.createStaticBody({ shape: shape });

// destroy a single body
world.destroyBody(body1);
// body1 is now destroyed, but shape still exists (since it may be used by other bodies now or in the future)

// destroy shape (destroys all remaining bodies using it)
world.destroyShape(shape);
// now body2 and body3 are also destroyed

Forces and Impulses

You can apply forces (gradual acceleration over time) or impulses (instant velocity changes) to dynamic bodies.

const world = new World();

const shape = world.createCapsule({ radius: 5.0, height: 2.0 });
const body = world.createDynamicBody({ shape, position: [0, 5, 0] });

// Impulses (instant velocity change)
body.applyLinearImpulse({ x: 0, y: 1000, z: 0 }); // at center of mass
body.applyAngularImpulse({ x: 0, y: 0, z: 1000 }); // around local axis
body.applyImpulse({ x: 0, y: 1000, z: 0 }, { x: 0, y: 7, z: 0 }); // at world point
body.applyImpulse({ x: 0, y: 1000, z: 0 }, { x: 0, y: 3, z: 0 }, false); // useLocalFrame

// Forces (gradual acceleration over time)
body.applyLinearForce({ x: 0, y: 1000, z: 0 }); // at center of mass
body.applyAngularForce({ x: 0, y: 0, z: 1000 }); // around local axis
body.applyForce({ x: 0, y: 1000, z: 0 }, { x: 0, y: 7, z: 0 }); // at world point
body.applyForce({ x: 0, y: 1000, z: 0 }, { x: 0, y: 7, z: 0 }, false); // useLocalFrame

body.clearForces(); // forces persist, clear if needed

Collision Filtering

By default, all bodies collide with each other. You can use collision groups and masks to control which bodies interact.

Use CollisionFilter.createBitFlags() to define named collision groups, then set belongsToGroups and collidesWithGroups on each body:

import { CollisionFilter } from '@perplexdotgg/bounce';

const flags = CollisionFilter.createBitFlags(["Player", "Monster", "Ghost"] as const);

const shape = world.createSphere({ radius: 2.0 });
const player = world.createKinematicBody({
  shape: shape,
  position: [0, 0, 0],
  belongsToGroups: flags.Player,
  collidesWithGroups: flags.Monster,
});
const ghost = world.createKinematicBody({
  shape: shape,
  position: [-1.5, 0, 0],
  belongsToGroups: flags.Ghost,
  collidesWithGroups: flags.None, // collides with nothing
});
const monster1 = world.createKinematicBody({
  shape: shape,
  position: [+1.5, 0, 0],
  belongsToGroups: flags.Monster,
  collidesWithGroups: flags.Player | flags.Monster,
});
const monster2 = world.createKinematicBody({
  shape: shape,
  position: [+2.5, 0, 0],
  belongsToGroups: flags.Monster,
  collidesWithGroups: flags.Player | flags.Monster,
});

// Result: player↔monster1, monster1↔monster2

Scene Queries

Scene queries let you test for collisions without stepping the simulation. Useful for raycasting, overlap tests, and sweep tests.

Overlap / Intersection Test

Find all bodies that overlap with a shape at a given position:

import { World, Sphere } from "@perplexdotgg/bounce";

const world = new World();

const ground = world.createStaticBody({
  position: { x: 0, y: 0, z: 0 },
  orientation: { x: 0, y: 0, z: 0, w: 1 },
  shape: world.createBox({ width: 100, height: 5, depth: 100 }),
});

const capsuleShape = world.createCapsule({ radius: 1.0, height: 2.0 });

// 100 random capsules
for (let i = 0; i < 100; i++) {
  const x = (Math.random() - 0.5) * 100;
  const y = 15 + (Math.random() - 0.5) * 10;
  const z = (Math.random() - 0.5) * 100;

  const body = world.createDynamicBody({
    position: [x, y, z], // array syntax
    orientation: [0, 0, 0, 1],
    friction: 0.4,
    restitution: 0.3,
    shape: capsuleShape,
    mass: 1.0,
  });
}

// query sphere at (20, 7, -30)
const intersectionShape = Sphere.create({ radius: 5 });
const intersectingBodies = [];

function onHit(result) {
  intersectingBodies.push(result.body);
  return false; // return true to stop early
}

world.intersectShape(onHit, intersectionShape, {
  position: { x: 20, y: 7, z: -30 },
});

for (const body of intersectingBodies) {
  console.log(body.position);
}

Raycasting

Cast a ray through the scene to find what it hits:

import { Ray } from "@perplexdotgg/bounce";

const boxShape = world.createBox({ width: 1, height: 1, depth: 1 });
const body1 = world.createStaticBody({ shape: boxShape, position: [0, 0, 0] });
const body2 = world.createStaticBody({ shape: boxShape, position: [0, 5, 0] });
const body3 = world.createStaticBody({
  shape: boxShape,
  position: [0, 20, 0],
});

// to make sure the broadphase is updated
world.takeOneStep();


const ray = Ray.create({
  origin: [0, -10, 0],
  direction: [0, 1, 0],
  length: 100,
});

world.castRay(
  (result) => console.log(result.bodyA.position, result.contactPointA),
  ray,
  false
); // logs 3 hits (unsorted)

world.castRay(
  (result) => console.log(result.bodyA.position, result.contactPointA),
  ray,
  true
); // logs 1 hit (closest)

world.castRayApproximate(
  (body) => console.log(body.position),
  ray
); // logs 3 hits (unsorted)

Shape Casting (Sweep Tests)

Sweep a shape through the scene to find collisions along a path. Useful for character controllers:

import { Capsule } from "@perplexdotgg/bounce";

const boxShape = world.createBox({ width: 1, height: 1, depth: 1 });
const body1 = world.createStaticBody({ shape: boxShape, position: [0, 0, 0] });
const body2 = world.createStaticBody({ shape: boxShape, position: [0, 5, 0] });
const body3 = world.createStaticBody({
  shape: boxShape,
  position: [0, 20, 0],
});

// to make sure the broadphase is updated
world.takeOneStep();

const capsule = Capsule.create({ radius: 1.0, length: 2.0 });

// sweep using displacement vector
world.castShape(
  (result) => result.bodyB.position,
  capsule,
  { position: { x: 0, y: -10, z: 0 } },
  { x: 0, y: 15, z: 0 }, // displacement
  { treatAsDisplacement: true }
); // hits: [0, 0, 0], [0, 5, 0]

// sweep using end position
world.castShape(
  (result) => result.bodyB.position,
  capsule,
  { position: { x: 0, y: -10, z: 0 } },
  { x: 0, y: 5, z: 0 }, // end position
  { treatAsDisplacement: false }
); // hits: [0, 0, 0], [0, 5, 0]

Contact Manifolds

After stepping the simulation, you can query which bodies are in contact.

Check if Two Bodies Collided

const boxShape = world.createBox({ width: 1, height: 2, depth: 1 });
const body1 = world.createDynamicBody({ shape: boxShape, position: [0, 0, 0] });
const body2 = world.createDynamicBody({
  shape: boxShape,
  position: [0, 0.5, 0],
});
const body3 = world.createDynamicBody({
  shape: boxShape,
  position: [0, 20, 0],
});

world.takeOneStep(1 / 60);

console.log(world.didBodiesCollide(body1, body2)); // true
console.log(world.didBodiesCollide(body1, body3)); // false

Iterate Over Contact Manifolds

You can iterate over all contacts involving a body, a pair of bodies, or all contacts in the world:

const boxShape = world.createBox({ width: 1, height: 2, depth: 1 });
const body1 = world.createDynamicBody({ shape: boxShape, position: [0, 0, 0] });
const body2 = world.createDynamicBody({
  shape: boxShape,
  position: [0, 0.5, 0],
});
const body3 = world.createDynamicBody({
  shape: boxShape,
  position: [0, -1.5, 0],
});
const body4 = world.createDynamicBody({
  shape: boxShape,
  position: [0, 20, 0],
});
const body5 = world.createDynamicBody({
  shape: boxShape,
  position: [0, 20.5, 0],
});

world.takeOneStep(1 / 60);

// all manifolds involving body1
for (const manifold of world.iterateContactManifolds(body1)) {
  console.log([manifold.bodyA, manifold.bodyB]);
}

// manifolds between body1 and body2
for (const manifold of world.iterateContactManifolds(body1, body2)) {
  console.log([manifold.bodyA, manifold.bodyB]);
}

// all manifolds in world
for (const manifold of world.iterateContactManifolds()) {
  console.log([manifold.bodyA, manifold.bodyB]);
}

Estimating Collision Response

You can estimate the velocity changes that would result from a collision without actually applying them:

const boxShape = world.createBox({ width: 1, height: 1, depth: 1 });
const boxBody = world.createDynamicBody({
  shape: boxShape,
  position: [0, 0, 0],
  mass: 5.0,
});

// body created outside world
import { Body, BodyType, Sphere } from "@perplexdotgg/bounce";

const sphereShape = Sphere.create({ radius: 1.5 });
const sphereBody = Body.create({
  shape: sphereShape,
  position: [2, 0, 0],
  mass: 10.0,
  linearVelocity: [5, 0, 0],
  type: BodyType.dynamic,
});

// estimate velocity change (query only, doesn't apply)
// Note: EstimateCollisionResponseResult may need internal import
const result = {
  deltaLinearVelocityA: null,
  deltaAngularVelocityA: null,
  deltaLinearVelocityB: null,
  deltaAngularVelocityB: null,
};
world.estimateCollisionResponse(result, sphereBody, boxBody);
console.log(result.deltaLinearVelocityA, result.deltaAngularVelocityA);
console.log(result.deltaLinearVelocityB, result.deltaAngularVelocityB);

Serialization

Bounce supports serializing and deserializing world state. This is useful for save/load, networking, or rollback.

Serialize and Restore a World

const world1 = new World();
const shape = world.createSphere({ radius: 1.5 });
const body = world.createDynamicBody({
  shape,
  position: [0, 5, 0],
});

const world2 = new World();
for (const body of world2.dynamicBodies) {
  console.log(body.position.y); // no bodies yet
}

const array = new Float32Array(10000);
world1.toArray(array);
world2.fromArray(array);

for (const body of world2.dynamicBodies) {
  console.log(body.position.y); // 5
}

Serialize a Single Body

const world = new World({ gravity: [0, 0, 0] });

const body = world.createDynamicBody({
  linearVelocity: [5, 0, 0],
  position: [0, 0, 0],
  shape: world.createSphere({ radius: 1.5 }),
});

const bodyArray = [];

body.toArray(bodyArray);

world.takeOneStep(1);
console.log(body.position); // [5, 0, 0]

world.takeOneStep(1);
console.log(body.position); // [10, 0, 0]

body.fromArray(bodyArray); // restore

console.log(body.position); // [0, 0, 0]

Rollback All Dynamic Bodies

You can serialize just the dynamic bodies for efficient rollback:

const world = new World({ gravity: [0, 0, 0] });

const shape = world.createSphere({ radius: 1.5 });
const body1 = world.createDynamicBody({
  shape,
  position: [0, 0, 10],
  linearVelocity: [1, 0, 0],
});
const body2 = world.createDynamicBody({
  shape,
  position: [0, 0, 20],
  linearVelocity: [2, 0, 0],
});
const body3 = world.createDynamicBody({
  shape,
  position: [0, 0, 30],
  linearVelocity: [4, 0, 0],
});

let bufferIndex = 0;
const rollbackBuffers = [
  new Float32Array(1000),
  new Float32Array(1000),
  new Float32Array(1000),
  new Float32Array(1000),
  new Float32Array(1000),
];

for (const body of world.dynamicBodies) {
  console.log(body.position); // [0, 0, 10], [0, 0, 20], [0, 0, 30]
}

for (let i = 0; i < rollbackBuffers.length; i++) {
  world.dynamicBodies.toArray(rollbackBuffers[bufferIndex]);
  bufferIndex++;
  world.takeOneStep(1); // 1 second per step
}

for (const body of world.dynamicBodies) {
  console.log(body.position); // [10, 0, 10], [20, 0, 20], [40, 0, 30]
}

world.dynamicBodies.fromArray(rollbackBuffers[2]); // restore step 3

for (const body of world.dynamicBodies) {
  console.log(body.position); // [2, 0, 10], [4, 0, 20], [8, 0, 30]
}

Constraints

Constraints connect two bodies and restrict their relative motion. Bounce provides several constraint types:

  • PointConstraint — keeps two points on two bodies together (ball-and-socket joint)
  • DistanceConstraint — keeps two points at a fixed distance (with optional min/max range and spring)
  • FixedConstraint — locks both position and rotation between two bodies
  • HingeConstraint — allows rotation around a single axis (like a door hinge), with optional limits, motor, and spring

Point Constraint (Ball-and-Socket)

Connects two points together, allowing rotation but no translation:

const constraint = world.createPointConstraint({
  bodyA: bodyA,
  bodyB: bodyB,

// everything below is optional, default values are shown here
  referenceFrame: ReferenceFrame.local,
  positionA: { x: 0, y: 0, z: 0 }, // local to bodyA
  positionB: { x: 0, y: 0, z: 0 }, // local to bodyB
  translationComponent: {
    options: {
      positionBaumgarte: 0.8,
      velocityBaumgarte: 1.0,
      strength: 1.0,
    },
  },
});

Distance Constraint

Maintains a distance between two points, with optional min/max range and spring:

const constraint = world.createDistanceConstraint({
  // required
  bodyA: bodyA,
  bodyB: bodyB,

  // everything below is optional, default values are shown here
  referenceFrame: ReferenceFrame.local,
  positionA: { x: 0, y: 0, z: 0 },
  positionB: { x: 0, y: 0, z: 0 },
  minDistance: -1,
  maxDistance: -1,
  spring: {
    mode: SpringMode.UseFrequency,
    damping: 1.0,
    frequency: 2.0,
    stiffness: 2.0,
  },
  axisComponent: {
    options: {
      positionBaumgarte: 0.8,
      velocityBaumgarte: 1.0,
      strength: 1.0,
    },
    mR1PlusUxAxis: { x: 0, y: 0, z: 0 },
    mR2xAxis: { x: 0, y: 0, z: 0 },
    mInvI1_R1PlusUxAxis: { x: 0, y: 0, z: 0 },
    mInvI2_R2xAxis: { x: 0, y: 0, z: 0 },
    effectiveMass: 0,
    totalLambda: 0,
    springComponent: {
      mode: SpringMode.UseFrequency,
      damping: 1.0,
      frequency: 2.0,
      stiffness: 2.0,
    },
  },
});

Fixed Constraint

Locks both position and rotation between two bodies:

const constraint = world.createFixedConstraint({
  // required
  bodyA: bodyA,
  bodyB: bodyB,

  // everything below is optional, default values are shown here
  referenceFrame: ReferenceFrame.local,
  positionA: { x: 0, y: 0, z: 0 },
  positionB: { x: 0, y: 0, z: 0 },
  axisXA: { x: 1, y: 0, z: 0 },
  axisXB: { x: 1, y: 0, z: 0 },
  axisYA: { x: 0, y: 1, z: 0 },
  axisYB: { x: 0, y: 1, z: 0 },
  translationComponent: {
    options: {
      positionBaumgarte: 0.8,
      velocityBaumgarte: 1.0,
      strength: 1.0,
    },
  },
  rotationComponent: {
    options: {
      positionBaumgarte: 0.8,
      velocityBaumgarte: 1.0,
      strength: 1.0,
    },
  },
});

Hinge Constraint

Allows rotation around a single axis with optional limits and motor:

const hinge = world.createHingeConstraint({
  // required
  bodyA: bodyA,
  bodyB: bodyB,

  // everything below is optional, default values are shown here
  referenceFrame: ReferenceFrame.local,
  pointA: { x: 0, y: 0, z: 0 }, // pivot point in bodyA
  pointB: { x: 0, y: 0, z: 0 }, // pivot point in bodyB
  hingeA: { x: 1, y: 0, z: 0 }, // hinge axis in bodyA
  hingeB: { x: 1, y: 0, z: 0 }, // hinge axis in bodyB
  normalA: { x: 0, y: 1, z: 0 }, // normal direction
  normalB: { x: 0, y: 1, z: 0 }, // normal direction
  minHingeAngle: -Math.PI / 2, // optional
  maxHingeAngle: Math.PI / 2, // optional
  maxFrictionTorque: 0,
  targetAngularSpeed: 0,
  targetAngle: 0,
  motor: {
    mode: MotorMode.Off,
    minForce: -Infinity,
    maxForce: +Infinity,
    minTorque: -Infinity,
    maxTorque: +Infinity,
    spring: {
      mode: SpringMode.UseFrequency,
      damping: 1.0,
      frequency: 2.0,
      stiffness: 2.0,
    },
  },
  pointConstraintPart: {
    options: {
      positionBaumgarte: 0.8,
      velocityBaumgarte: 1.0,
      strength: 1.0,
    },
  },
  rotationConstraintPart: {
    options: {
      positionBaumgarte: 0.8,
      velocityBaumgarte: 1.0,
      strength: 1.0,
    },
  },
  rotationLimitsConstraintPart: {
    options: {
      positionBaumgarte: 0.8,
      velocityBaumgarte: 1.0,
      strength: 1.0,
    },
  },
  motorConstraintPart: {
    options: {
      positionBaumgarte: 0.8,
      velocityBaumgarte: 1.0,
      strength: 1.0,
    },
  },
});

Enabling and disabling constraints

All constraints can be enabled and disabled at runtime, simply by setting constraint.isEnabled to true or false.

Destroying Constraints

world.destroyConstraint(constraint);

Math Utilities

Bounce exports common math types for working with 3D physics:

  • Vec3 — 3D vector with x, y, z components
  • Quat — quaternion for rotations (x, y, z, w)
  • Mat3 / Mat4 — 3x3 and 4x4 matrices
  • BasicTransform — position + rotation + scale
  • Isometry — combined rotation and translation
  • Ray — origin, direction, length

Scalar helpers:

  • clamp(value, min, max)
  • squared(value)
  • degreesToRadians(degrees)