Bounce is a fast, deterministic 3D physics library for TypeScript and JavaScript projects. It's written in pure TypeScript (no WebAssembly).
npm i @perplexdotgg/bounceThe 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
The World is the main simulation container. It manages bodies, shapes, constraints, collision detection, and the solver.
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,
},
});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 define the collision geometry of bodies. Bounce provides several built-in shape types.
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 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 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 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 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] });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 });
}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 });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 are the physical objects in your simulation. Each body has a shape, position, orientation, velocity, and physical properties like mass and friction.
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,
});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);
}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)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 destroyedYou 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 neededBy 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↔monster2Scene queries let you test for collisions without stepping the simulation. Useful for raycasting, overlap tests, and sweep tests.
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);
}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)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]After stepping the simulation, you can query which bodies are in contact.
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)); // falseYou 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]);
}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);Bounce supports serializing and deserializing world state. This is useful for save/load, networking, or rollback.
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
}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]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 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
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,
},
},
});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,
},
},
});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,
},
},
});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,
},
},
});All constraints can be enabled and disabled at runtime, simply by setting constraint.isEnabled to true or false.
world.destroyConstraint(constraint);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)