@perplexdotgg/bounce 1.0.5 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1051 @@
1
+ # Bounce API Reference
2
+
3
+ Bounce is a fast, deterministic 3D physics library for TypeScript and JavaScript. It's written in pure TypeScript (no WebAssembly).
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm i @perplexdotgg/bounce
9
+ ```
10
+
11
+ ---
12
+
13
+ ## Getting Started
14
+
15
+ 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.
16
+
17
+ ```js
18
+ import { World } from '@perplexdotgg/bounce';
19
+
20
+ const world = new World({
21
+ gravity: { x: 0, y: -9.81, z: 0 },
22
+ });
23
+
24
+ // ground
25
+ const groundShape = world.createBox({
26
+ width: 10,
27
+ height: 2,
28
+ depth: 10,
29
+ });
30
+
31
+ const ground = world.createStaticBody({
32
+ shape: groundShape,
33
+ position: { x: 0, y: 10, z: 0 },
34
+ orientation: { x: 0, y: 0, z: 0, w: 1 }, // if w is omitted, this is treated as an euler (radians) input
35
+ friction: 0.4,
36
+ restitution: 0.3,
37
+ });
38
+
39
+ // ball
40
+ const ballShape = world.createSphere({ radius: 0.5 });
41
+
42
+ const ball = world.createDynamicBody({
43
+ shape: ballShape,
44
+ position: [0, -1, 0], // vec3 can be specified as an array, see note below
45
+ orientation: [0, Math.PI, 0], // euler (radians) or quaternion as input, based on the presence of 3 or 4 numbers
46
+ friction: 0.4,
47
+ restitution: 0.3,
48
+ mass: 0.5,
49
+ });
50
+
51
+
52
+ // simulate 10 seconds
53
+ const timeStepSizeSeconds = 1 / 60;
54
+ for (let i = 0; i < 600; i++) {
55
+ world.takeOneStep(timeStepSizeSeconds);
56
+ }
57
+ ```
58
+
59
+ > [!NOTE]
60
+ >
61
+ > For convenience, the **Vec3** and **Quat** (quaternion angle) classes allow you to input values values as arrays.
62
+ > Note that the array is **only used for input**. Once `ball` is created, `ball.orientation` is a quaternion with quaternion
63
+ > methods, even though the input was an euler array. If the orientation input array had 4 numbers, it would be treated as a
64
+ > 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.
65
+ >
66
+ > Separately, many examples below use array notation in the comments to indicate what console.logs would show. This
67
+ > is again just shorthand for the sake of readability of the documentation, the actual logs would show instances of vec3
68
+
69
+ ---
70
+
71
+ ## World
72
+
73
+ The `World` is the main simulation container. It manages bodies, shapes, constraints, collision detection, and the solver.
74
+
75
+ ### World Options
76
+
77
+ When constructing a world, you can configure gravity, solver iterations, damping, timestep, and other physics parameters.
78
+ Default values are shown below for the most commonly used options:
79
+
80
+ ```ts
81
+ const world = new World({
82
+ gravity: [0, -9.80665, 0],
83
+ timeStepSizeSeconds: 1 / 60,
84
+
85
+ // more is higher fidelity, but uses more CPU
86
+ solveVelocityIterations: 6,
87
+ solvePositionIterations: 2,
88
+
89
+ // most of these options are for stability
90
+ linearDamping: 0.05,
91
+ angularDamping: 0.05,
92
+ baumgarte: 0.2,
93
+ penetrationSlop: 0.02,
94
+ maxPenetrationDistance: 0.2,
95
+ speculativeContactDistance: 0.02,
96
+ collisionTolerance: 1e-4,
97
+ maxLinearSpeed: 30.0,
98
+ maxAngularSpeed: 30.0,
99
+ isWarmStartingEnabled: true,
100
+ });
101
+ ```
102
+
103
+ ### Stepping the Simulation
104
+
105
+ - `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)
106
+ - `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
107
+
108
+ ---
109
+
110
+ ## Shapes
111
+
112
+ Shapes define the collision geometry of bodies. Bounce provides several built-in shape types.
113
+
114
+ ### Primitive Shapes
115
+
116
+ Create shapes using the world's factory methods:
117
+
118
+ ```js
119
+ const sphere = world.createSphere({ radius: 1.5 });
120
+ const box = world.createBox({ width: 1, height: 1, depth: 1 });
121
+ const capsule = world.createCapsule({ radius: 1.0, height: 2.0 });
122
+ const cylinder = world.createCylinder({ halfHeight: 1, radius: 0.5 });
123
+ ```
124
+
125
+ ### Convex Hull
126
+
127
+ Convex hulls can be created from a point cloud (flat array of vertex positions):
128
+
129
+ ```js
130
+ // from point cloud (flat vertex array)
131
+ const vertexPositions = new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1]);
132
+
133
+ const shape = world.createConvexHull(
134
+ vertexPositions, // Float32Array of vertex positions
135
+ 0.02, // convexRadius (optional, default 0.02)
136
+ 1e-3 // hullTolerance (optional, default 1e-3)
137
+ );
138
+
139
+ const body = world.createDynamicBody({ shape: shape, position: [0, 0, 5] });
140
+ ```
141
+
142
+ ### Triangle Mesh
143
+
144
+ Triangle meshes are useful for complex static geometry like terrain or building interiors:
145
+
146
+ ```js
147
+ // vertex positions (x, y, z triplets)
148
+ const vertices = new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0]);
149
+
150
+ // face indices (triangle vertex indices)
151
+ const indices = new Uint32Array([
152
+ 0,
153
+ 1,
154
+ 2, // first triangle
155
+ 1,
156
+ 3,
157
+ 2, // second triangle
158
+ ]);
159
+
160
+ const shape = world.createTriangleMesh({
161
+ vertexPositions: vertices,
162
+ faceIndices: indices,
163
+ skipHullCreation: true, // skip hull for static-only meshes (allows >1k verts)
164
+ });
165
+
166
+ const body = world.createStaticBody({ shape: shape });
167
+ ```
168
+
169
+ > [!NOTE]
170
+ > 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.
171
+
172
+ ### Height Map
173
+
174
+ Height maps are optimized for terrain. They must be square and have power-of-two subdivisions:
175
+
176
+ ```js
177
+ // 9x9 vertices = 8x8 quads (power of two)
178
+ const vertexCount = { width: 9, depth: 9 };
179
+
180
+ // terrain size in world units
181
+ const extents = { x: 100, y: 10, z: 100 };
182
+
183
+ // heights normalized to [0, 1] range
184
+ const heights = new Float32Array(9 * 9);
185
+ for (let z = 0; z < 9; z++) {
186
+ for (let x = 0; x < 9; x++) {
187
+ const idx = z * 9 + x;
188
+ heights[idx] = Math.random(); // 0.0 to 1.0
189
+ }
190
+ }
191
+
192
+ const shape = world.createHeightMap(vertexCount, extents, heights);
193
+ const terrain = world.createStaticBody({ shape: shape });
194
+ ```
195
+
196
+ **Requirements:**
197
+
198
+ - Must be square: `width === depth`
199
+ - Vertex count must be power-of-two + 1 (e.g., 3x3, 5x5, 9x9, 17x17, 33x33, 65x65, etc.)
200
+ - Heights normalized to [0, 1] range (multiplied by `extents.y`)
201
+
202
+ ### Compound Shapes
203
+
204
+ Compound shapes combine multiple sub-shapes into a single shape. This is useful for non-convex objects or complex geometry:
205
+
206
+ ```js
207
+ const subShape1 = world.createSphere({ radius: 1.0 });
208
+ const subShape2 = world.createBox({ width: 2.0, height: 3.0, depth: 4.0 });
209
+
210
+ // combine shapes (useful for non-convex objects)
211
+ const shape = world.createCompoundShape([
212
+ { shape: subShape1, transform: { position: [0, -2, 0] } },
213
+ {
214
+ shape: subShape2,
215
+ transform: { position: [0, +2, 0], rotation: [0, Math.PI / 4, 0] },
216
+ },
217
+ ]);
218
+
219
+ const body = world.createDynamicBody({ shape: shape, position: [0, 0, 5] });
220
+ ```
221
+
222
+ ### Reusing Shapes
223
+
224
+ Shapes can be shared across multiple bodies. This is more efficient than creating a new shape for each body:
225
+
226
+ ```js
227
+ // inefficient - new shape per body:
228
+ for (let i = 0; i < 100; i++) {
229
+ const shape = world.createBox({ width: 1, height: 1, depth: 1 });
230
+ const body = world.createDynamicBody({ shape: shape });
231
+ }
232
+
233
+ // efficient - share one shape:
234
+ const shape = world.createBox({ width: 1, height: 1, depth: 1 });
235
+ for (let i = 0; i < 100; i++) {
236
+ const body = world.createDynamicBody({ shape: shape });
237
+ }
238
+ ```
239
+
240
+ ### Translating Shapes in Local Space
241
+
242
+ By default, shapes are centered at the origin. You can offset a shape's center of mass relative to the body's position:
243
+
244
+ ```js
245
+ // default: centered at origin
246
+ const sphere1 = world.createSphere({ radius: 1.0 });
247
+ const body1 = world.createDynamicBody({ shape: sphere1 });
248
+
249
+ // translated from origin
250
+ const sphere2 = world.createSphere({ radius: 1.0, position: [3, 0, 0] });
251
+ const body2 = world.createDynamicBody({ shape: sphere2 });
252
+
253
+ // character controller capsule: bottom at origin
254
+ const radius = 1.0;
255
+ const height = 2.0;
256
+ const halfCapsuleHeight = radius + height / 2;
257
+ const shape = world.createCapsule({
258
+ radius,
259
+ height,
260
+ position: [0, -halfCapsuleHeight, 0],
261
+ });
262
+ const body = world.createDynamicBody({ shape: shape });
263
+ ```
264
+
265
+ ### Updating Shape Properties
266
+
267
+ If you modify a shape's properties after creation (like dimensions), you must call `commitChanges()` to update derived values:
268
+
269
+ ```js
270
+ const shape = world.createBox({ width: 1, height: 1, depth: 1 });
271
+ console.log(shape.computedVolume); // 1.0 m^3
272
+
273
+ const body = world.createDynamicBody({ shape: shape, density: 1000.0 });
274
+ console.log(body.mass); // 1000.0 kg
275
+
276
+ shape.width = 2;
277
+ shape.height = 2;
278
+ shape.depth = 2;
279
+
280
+ console.log(shape.computedVolume); // 1.0 m^3 (not updated yet!)
281
+ console.log(body.mass); // 1000.0 kg (stale)
282
+
283
+ shape.commitChanges(); // update world
284
+
285
+ console.log(shape.computedVolume); // 8.0 m^3 (updated!)
286
+ console.log(body.mass); // 8000.0 kg (updated!)
287
+ ```
288
+
289
+ ---
290
+
291
+ ## Bodies
292
+
293
+ Bodies are the physical objects in your simulation. Each body has a shape, position, orientation, velocity, and physical properties like mass and friction.
294
+
295
+ ### Body Types
296
+
297
+ Bounce supports three body types:
298
+
299
+ - **Dynamic** — affected by forces, impulses, and collisions
300
+ - **Kinematic** — user-controlled motion, participates in collisions but isn't affected by forces
301
+ - **Static** — immovable, used for ground/walls
302
+
303
+ ```js
304
+ const sphere = world.createSphere({ radius: 1.5 });
305
+
306
+ const dynamicBody = world.createDynamicBody({ shape: sphere });
307
+ const staticBody = world.createStaticBody({ shape: sphere });
308
+ const kinematicBody = world.createKinematicBody({ shape: sphere });
309
+
310
+ // or specify type explicitly
311
+ const dynamicBody2 = world.createBody({
312
+ shape: sphere,
313
+ type: BodyType.dynamic,
314
+ });
315
+ ```
316
+
317
+ ### Iterating Over Bodies
318
+
319
+ You can iterate over all bodies of a specific type or all bodies:
320
+
321
+ ```js
322
+ const boxShape = world.createBox({ width: 1, height: 1, depth: 1 });
323
+ const body1 = world.createDynamicBody({ shape: boxShape, position: [0, 0, 0] });
324
+ const body2 = world.createDynamicBody({ shape: boxShape, position: [0, 5, 0] });
325
+ const body3 = world.createDynamicBody({
326
+ shape: boxShape,
327
+ position: [0, 20, 0],
328
+ });
329
+
330
+ // iterate by type
331
+ for (const body of world.dynamicBodies) {
332
+ console.log(body.position);
333
+ }
334
+
335
+ // also available: world.kinematicBodies, world.staticBodies
336
+
337
+ // or iterate all bodies at once
338
+ for (const body of world.bodies()) {
339
+ console.log(body.position, body.type);
340
+ }
341
+ ```
342
+
343
+ ### Updating Body Properties
344
+
345
+ Like shapes, if you directly mutate body properties (like `position`), you must call `commitChanges()` for the world to recognize the change:
346
+
347
+ ```js
348
+ import { Sphere } from "@perplexdotgg/bounce";
349
+
350
+ function onHit(result) {
351
+ console.log("hit");
352
+ }
353
+ const queryShape = Sphere.create({ radius: 5 });
354
+
355
+ const world = new World();
356
+
357
+ const shape = world.createSphere({ radius: 2 });
358
+ const body = world.createKinematicBody({ shape: shape, position: [0, 0, 0] });
359
+
360
+ console.log(body.position); // [0, 0, 0]
361
+ world.intersectShape(onHit, queryShape, { position: { x: 0, y: 10, z: 0 } }); // no hit
362
+
363
+ body.position.set([0, 5, 0]);
364
+ console.log(body.position); // [0, 5, 0]
365
+
366
+ world.intersectShape(onHit, queryShape, { position: { x: 0, y: 10, z: 0 } }); // still no hit! (stale)
367
+
368
+ body.commitChanges(); // update world
369
+
370
+ world.intersectShape(onHit, queryShape, { position: { x: 0, y: 10, z: 0 } }); // hit! (updated)
371
+ ```
372
+
373
+ ### Destroying Bodies and Shapes
374
+
375
+ 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:
376
+
377
+ ```js
378
+ const shape = world.createSphere({ radius: 1.5 });
379
+ const body1 = world.createStaticBody({ shape: shape });
380
+ const body2 = world.createStaticBody({ shape: shape });
381
+ const body3 = world.createStaticBody({ shape: shape });
382
+
383
+ // destroy a single body
384
+ world.destroyBody(body1);
385
+ // body1 is now destroyed, but shape still exists (since it may be used by other bodies now or in the future)
386
+
387
+ // destroy shape (destroys all remaining bodies using it)
388
+ world.destroyShape(shape);
389
+ // now body2 and body3 are also destroyed
390
+ ```
391
+
392
+ ---
393
+
394
+ ## Forces and Impulses
395
+
396
+ You can apply forces (gradual acceleration over time) or impulses (instant velocity changes) to dynamic bodies.
397
+
398
+ ```js
399
+ const world = new World();
400
+
401
+ const shape = world.createCapsule({ radius: 5.0, height: 2.0 });
402
+ const body = world.createDynamicBody({ shape, position: [0, 5, 0] });
403
+
404
+ // Impulses (instant velocity change)
405
+ body.applyLinearImpulse({ x: 0, y: 1000, z: 0 }); // at center of mass
406
+ body.applyAngularImpulse({ x: 0, y: 0, z: 1000 }); // around local axis
407
+ body.applyImpulse({ x: 0, y: 1000, z: 0 }, { x: 0, y: 7, z: 0 }); // at world point
408
+ body.applyImpulse({ x: 0, y: 1000, z: 0 }, { x: 0, y: 3, z: 0 }, false); // useLocalFrame
409
+
410
+ // Forces (gradual acceleration over time)
411
+ body.applyLinearForce({ x: 0, y: 1000, z: 0 }); // at center of mass
412
+ body.applyAngularForce({ x: 0, y: 0, z: 1000 }); // around local axis
413
+ body.applyForce({ x: 0, y: 1000, z: 0 }, { x: 0, y: 7, z: 0 }); // at world point
414
+ body.applyForce({ x: 0, y: 1000, z: 0 }, { x: 0, y: 7, z: 0 }, false); // useLocalFrame
415
+
416
+ body.clearForces(); // forces persist, clear if needed
417
+ ```
418
+
419
+ ---
420
+
421
+ ## Collision Filtering
422
+
423
+ By default, all bodies collide with each other. You can use collision groups and masks to control which bodies interact.
424
+
425
+ Use `CollisionFilter.createBitFlags()` to define named collision groups, then set `belongsToGroups` and `collidesWithGroups` on each body:
426
+
427
+ ```js
428
+ import { CollisionFilter } from '@perplexdotgg/bounce';
429
+
430
+ const flags = CollisionFilter.createBitFlags(["Player", "Monster", "Ghost"] as const);
431
+
432
+ const shape = world.createSphere({ radius: 2.0 });
433
+ const player = world.createKinematicBody({
434
+ shape: shape,
435
+ position: [0, 0, 0],
436
+ belongsToGroups: flags.Player,
437
+ collidesWithGroups: flags.Monster,
438
+ });
439
+ const ghost = world.createKinematicBody({
440
+ shape: shape,
441
+ position: [-1.5, 0, 0],
442
+ belongsToGroups: flags.Ghost,
443
+ collidesWithGroups: flags.None, // collides with nothing
444
+ });
445
+ const monster1 = world.createKinematicBody({
446
+ shape: shape,
447
+ position: [+1.5, 0, 0],
448
+ belongsToGroups: flags.Monster,
449
+ collidesWithGroups: flags.Player | flags.Monster,
450
+ });
451
+ const monster2 = world.createKinematicBody({
452
+ shape: shape,
453
+ position: [+2.5, 0, 0],
454
+ belongsToGroups: flags.Monster,
455
+ collidesWithGroups: flags.Player | flags.Monster,
456
+ });
457
+
458
+ // Result: player↔monster1, monster1↔monster2
459
+ ```
460
+
461
+ ---
462
+
463
+ ## Scene Queries
464
+
465
+ Scene queries let you test for collisions without stepping the simulation. Useful for raycasting, overlap tests, and sweep tests.
466
+
467
+ ### Overlap / Intersection Test
468
+
469
+ Find all bodies that overlap with a shape at a given position:
470
+
471
+ ```js
472
+ import { World, Sphere } from "@perplexdotgg/bounce";
473
+
474
+ const world = new World();
475
+
476
+ const ground = world.createStaticBody({
477
+ position: { x: 0, y: 0, z: 0 },
478
+ orientation: { x: 0, y: 0, z: 0, w: 1 },
479
+ shape: world.createBox({ width: 100, height: 5, depth: 100 }),
480
+ });
481
+
482
+ const capsuleShape = world.createCapsule({ radius: 1.0, height: 2.0 });
483
+
484
+ // 100 random capsules
485
+ for (let i = 0; i < 100; i++) {
486
+ const x = (Math.random() - 0.5) * 100;
487
+ const y = 15 + (Math.random() - 0.5) * 10;
488
+ const z = (Math.random() - 0.5) * 100;
489
+
490
+ const body = world.createDynamicBody({
491
+ position: [x, y, z], // array syntax
492
+ orientation: [0, 0, 0, 1],
493
+ friction: 0.4,
494
+ restitution: 0.3,
495
+ shape: capsuleShape,
496
+ mass: 1.0,
497
+ });
498
+ }
499
+
500
+ // query sphere at (20, 7, -30)
501
+ const intersectionShape = Sphere.create({ radius: 5 });
502
+ const intersectingBodies = [];
503
+
504
+ function onHit(result) {
505
+ intersectingBodies.push(result.body);
506
+ return false; // return true to stop early
507
+ }
508
+
509
+ world.intersectShape(onHit, intersectionShape, {
510
+ position: { x: 20, y: 7, z: -30 },
511
+ });
512
+
513
+ for (const body of intersectingBodies) {
514
+ console.log(body.position);
515
+ }
516
+ ```
517
+
518
+ ### Query Precision
519
+
520
+ You can control the precision (and performance) of scene queries:
521
+
522
+ ```js
523
+ // precise with contact points (default)
524
+ world.intersectShape(
525
+ (result) => console.log(result.pointA),
526
+ queryShape,
527
+ { position: { x: 0, y: 0, z: 0 } },
528
+ { precision: QueryPrecision.preciseWithContacts }
529
+ );
530
+
531
+ // precise without contacts
532
+ world.intersectShape(
533
+ (result) => console.log("precise hit"),
534
+ queryShape,
535
+ { position: { x: 0, y: 0, z: 0 } },
536
+ { precision: QueryPrecision.precise }
537
+ );
538
+
539
+ // approximate (AABB only, fastest)
540
+ world.intersectShape(
541
+ (result) => console.log("approximate hit"),
542
+ queryShape,
543
+ { position: { x: 0, y: 0, z: 0 } },
544
+ { precision: QueryPrecision.approximate }
545
+ );
546
+ ```
547
+
548
+ ### Raycasting
549
+
550
+ Cast a ray through the scene to find what it hits:
551
+
552
+ ```js
553
+ import { Ray } from "@perplexdotgg/bounce";
554
+
555
+ const boxShape = world.createBox({ width: 1, height: 1, depth: 1 });
556
+ const body1 = world.createDynamicBody({ shape: boxShape, position: [0, 0, 0] });
557
+ const body2 = world.createDynamicBody({ shape: boxShape, position: [0, 5, 0] });
558
+ const body3 = world.createDynamicBody({
559
+ shape: boxShape,
560
+ position: [0, 20, 0],
561
+ });
562
+
563
+ const ray = Ray.create({
564
+ origin: [0, -10, 0],
565
+ direction: [0, 1, 0],
566
+ length: 100,
567
+ });
568
+
569
+ world.castRay(
570
+ (result) => console.log(result.body.position, result.pointA),
571
+ ray,
572
+ {
573
+ returnClosestOnly: false,
574
+ precision: QueryPrecision.preciseWithContacts,
575
+ }
576
+ ); // logs 3 hits (unsorted)
577
+
578
+ world.castRay(
579
+ (result) => console.log(result.body.position, result.pointA),
580
+ ray,
581
+ {
582
+ returnClosestOnly: true,
583
+ precision: QueryPrecision.preciseWithContacts,
584
+ }
585
+ ); // logs 1 hit (closest)
586
+
587
+ world.castRay((result) => console.log(result.body.position), ray, {
588
+ returnClosestOnly: false,
589
+ precision: QueryPrecision.approximate, // AABB only, faster
590
+ }); // logs 3 hits (unsorted)
591
+ ```
592
+
593
+ ### Shape Casting (Sweep Tests)
594
+
595
+ Sweep a shape through the scene to find collisions along a path. Useful for character controllers:
596
+
597
+ ```js
598
+ import { Capsule } from "@perplexdotgg/bounce";
599
+
600
+ const boxShape = world.createBox({ width: 1, height: 1, depth: 1 });
601
+ const body1 = world.createDynamicBody({ shape: boxShape, position: [0, 0, 0] });
602
+ const body2 = world.createDynamicBody({ shape: boxShape, position: [0, 5, 0] });
603
+ const body3 = world.createDynamicBody({
604
+ shape: boxShape,
605
+ position: [0, 20, 0],
606
+ });
607
+
608
+ const capsule = Capsule.create({ radius: 1.0, length: 2.0 });
609
+
610
+ // sweep using displacement vector
611
+ world.castShape(
612
+ (result) => result.body.position,
613
+ capsule,
614
+ { position: { x: 0, y: -10, z: 0 } },
615
+ { x: 0, y: 15, z: 0 }, // displacement
616
+ { treatAsDisplacement: true }
617
+ ); // hits: [0, 0, 0], [0, 5, 0]
618
+
619
+ // sweep using end position
620
+ world.castShape(
621
+ (result) => result.body.position,
622
+ capsule,
623
+ { position: { x: 0, y: -10, z: 0 } },
624
+ { x: 0, y: 5, z: 0 }, // end position
625
+ { treatAsDisplacement: false }
626
+ ); // hits: [0, 0, 0], [0, 5, 0]
627
+ ```
628
+
629
+ ---
630
+
631
+ ## Contact Manifolds
632
+
633
+ After stepping the simulation, you can query which bodies are in contact.
634
+
635
+ ### Check if Two Bodies Collided
636
+
637
+ ```js
638
+ const boxShape = world.createBox({ width: 1, height: 2, depth: 1 });
639
+ const body1 = world.createDynamicBody({ shape: boxShape, position: [0, 0, 0] });
640
+ const body2 = world.createDynamicBody({
641
+ shape: boxShape,
642
+ position: [0, 0.5, 0],
643
+ });
644
+ const body3 = world.createDynamicBody({
645
+ shape: boxShape,
646
+ position: [0, 20, 0],
647
+ });
648
+
649
+ world.takeOneStep(1 / 60);
650
+
651
+ console.log(world.didBodiesCollide(body1, body2)); // true
652
+ console.log(world.didBodiesCollide(body1, body3)); // false
653
+ ```
654
+
655
+ ### Iterate Over Contact Manifolds
656
+
657
+ You can iterate over all contacts involving a body, a pair of bodies, or all contacts in the world:
658
+
659
+ ```js
660
+ const boxShape = world.createBox({ width: 1, height: 2, depth: 1 });
661
+ const body1 = world.createDynamicBody({ shape: boxShape, position: [0, 0, 0] });
662
+ const body2 = world.createDynamicBody({
663
+ shape: boxShape,
664
+ position: [0, 0.5, 0],
665
+ });
666
+ const body3 = world.createDynamicBody({
667
+ shape: boxShape,
668
+ position: [0, -1.5, 0],
669
+ });
670
+ const body4 = world.createDynamicBody({
671
+ shape: boxShape,
672
+ position: [0, 20, 0],
673
+ });
674
+ const body5 = world.createDynamicBody({
675
+ shape: boxShape,
676
+ position: [0, 20.5, 0],
677
+ });
678
+
679
+ world.takeOneStep(1 / 60);
680
+
681
+ // all manifolds involving body1
682
+ for (const manifold of world.iterateContactManifolds(body1)) {
683
+ console.log([manifold.bodyA, manifold.bodyB]);
684
+ }
685
+
686
+ // manifolds between body1 and body2
687
+ for (const manifold of world.iterateContactManifolds(body1, body2)) {
688
+ console.log([manifold.bodyA, manifold.bodyB]);
689
+ }
690
+
691
+ // all manifolds in world
692
+ for (const manifold of world.iterateContactManifolds()) {
693
+ console.log([manifold.bodyA, manifold.bodyB]);
694
+ }
695
+ ```
696
+
697
+ ### Estimating Collision Response
698
+
699
+ You can estimate the velocity changes that would result from a collision without actually applying them:
700
+
701
+ ```js
702
+ const boxShape = world.createBox({ width: 1, height: 1, depth: 1 });
703
+ const boxBody = world.createDynamicBody({
704
+ shape: boxShape,
705
+ position: [0, 0, 0],
706
+ mass: 5.0,
707
+ });
708
+
709
+ // body created outside world
710
+ import { Body, BodyType, Sphere } from "@perplexdotgg/bounce";
711
+
712
+ const sphereShape = Sphere.create({ radius: 1.5 });
713
+ const sphereBody = Body.create({
714
+ shape: sphereShape,
715
+ position: [2, 0, 0],
716
+ mass: 10.0,
717
+ linearVelocity: [5, 0, 0],
718
+ type: BodyType.dynamic,
719
+ });
720
+
721
+ // estimate velocity change (query only, doesn't apply)
722
+ // Note: EstimateCollisionResponseResult may need internal import
723
+ const result = {
724
+ deltaLinearVelocityA: null,
725
+ deltaAngularVelocityA: null,
726
+ deltaLinearVelocityB: null,
727
+ deltaAngularVelocityB: null,
728
+ };
729
+ world.estimateCollisionResponse(result, sphereBody, boxBody);
730
+ console.log(result.deltaLinearVelocityA, result.deltaAngularVelocityA);
731
+ console.log(result.deltaLinearVelocityB, result.deltaAngularVelocityB);
732
+ ```
733
+
734
+ ---
735
+
736
+ ## Serialization
737
+
738
+ Bounce supports serializing and deserializing world state. This is useful for save/load, networking, or rollback.
739
+
740
+ ### Serialize and Restore a World
741
+
742
+ ```js
743
+ const world1 = new World();
744
+ const shape = world.createSphere({ radius: 1.5 });
745
+ const body = world.createDynamicBody({
746
+ shape,
747
+ position: [0, 5, 0],
748
+ });
749
+
750
+ const world2 = new World();
751
+ for (const body of world2.dynamicBodies) {
752
+ console.log(body.position.y); // no bodies yet
753
+ }
754
+
755
+ const array = new Float32Array(10000);
756
+ world1.toArray(array);
757
+ world2.fromArray(array);
758
+
759
+ for (const body of world2.dynamicBodies) {
760
+ console.log(body.position.y); // 5
761
+ }
762
+ ```
763
+
764
+ ### Serialize a Single Body
765
+
766
+ ```js
767
+ const world = new World({ gravity: [0, 0, 0] });
768
+
769
+ const body = world.createDynamicBody({
770
+ linearVelocity: [5, 0, 0],
771
+ position: [0, 0, 0],
772
+ shape: world.createSphere({ radius: 1.5 }),
773
+ });
774
+
775
+ const bodyArray = [];
776
+
777
+ body.toArray(bodyArray);
778
+
779
+ world.takeOneStep(1);
780
+ console.log(body.position); // [5, 0, 0]
781
+
782
+ world.takeOneStep(1);
783
+ console.log(body.position); // [10, 0, 0]
784
+
785
+ body.fromArray(bodyArray); // restore
786
+
787
+ console.log(body.position); // [0, 0, 0]
788
+ ```
789
+
790
+ ### Rollback All Dynamic Bodies
791
+
792
+ You can serialize just the dynamic bodies for efficient rollback:
793
+
794
+ ```js
795
+ const world = new World({ gravity: [0, 0, 0] });
796
+
797
+ const shape = world.createSphere({ radius: 1.5 });
798
+ const body1 = world.createDynamicBody({
799
+ shape,
800
+ position: [0, 0, 10],
801
+ linearVelocity: [1, 0, 0],
802
+ });
803
+ const body2 = world.createDynamicBody({
804
+ shape,
805
+ position: [0, 0, 20],
806
+ linearVelocity: [2, 0, 0],
807
+ });
808
+ const body3 = world.createDynamicBody({
809
+ shape,
810
+ position: [0, 0, 30],
811
+ linearVelocity: [4, 0, 0],
812
+ });
813
+
814
+ let bufferIndex = 0;
815
+ const rollbackBuffers = [
816
+ new Float32Array(1000),
817
+ new Float32Array(1000),
818
+ new Float32Array(1000),
819
+ new Float32Array(1000),
820
+ new Float32Array(1000),
821
+ ];
822
+
823
+ for (const body of world.dynamicBodies) {
824
+ console.log(body.position); // [0, 0, 10], [0, 0, 20], [0, 0, 30]
825
+ }
826
+
827
+ for (let i = 0; i < rollbackBuffers.length; i++) {
828
+ world.dynamicBodies.toArray(rollbackBuffers[bufferIndex]);
829
+ bufferIndex++;
830
+ world.takeOneStep(1); // 1 second per step
831
+ }
832
+
833
+ for (const body of world.dynamicBodies) {
834
+ console.log(body.position); // [10, 0, 10], [20, 0, 20], [40, 0, 30]
835
+ }
836
+
837
+ world.dynamicBodies.fromArray(rollbackBuffers[2]); // restore step 3
838
+
839
+ for (const body of world.dynamicBodies) {
840
+ console.log(body.position); // [2, 0, 10], [4, 0, 20], [8, 0, 30]
841
+ }
842
+ ```
843
+
844
+ ---
845
+
846
+ ## Constraints
847
+
848
+ Constraints connect two bodies and restrict their relative motion. Bounce provides several constraint types:
849
+
850
+ - **PointConstraint** — keeps two points on two bodies together (ball-and-socket joint)
851
+ - **DistanceConstraint** — keeps two points at a fixed distance (with optional min/max range and spring)
852
+ - **FixedConstraint** — locks both position and rotation between two bodies
853
+ - **HingeConstraint** — allows rotation around a single axis (like a door hinge), with optional limits, motor, and spring
854
+
855
+ ### Point Constraint (Ball-and-Socket)
856
+
857
+ Connects two points together, allowing rotation but no translation:
858
+
859
+ ```js
860
+ const constraint = world.createPointConstraint({
861
+ bodyA: bodyA,
862
+ bodyB: bodyB,
863
+
864
+ // everything below is optional, default values are shown here
865
+ referenceFrame: ReferenceFrame.local,
866
+ positionA: { x: 0, y: 0, z: 0 }, // local to bodyA
867
+ positionB: { x: 0, y: 0, z: 0 }, // local to bodyB
868
+ translationComponent: {
869
+ options: {
870
+ positionBaumgarte: 0.8,
871
+ velocityBaumgarte: 1.0,
872
+ strength: 1.0,
873
+ },
874
+ },
875
+ });
876
+ ```
877
+
878
+ ### Distance Constraint
879
+
880
+ Maintains a distance between two points, with optional min/max range and spring:
881
+
882
+ ```js
883
+ const constraint = world.createDistanceConstraint({
884
+ // required
885
+ bodyA: bodyA,
886
+ bodyB: bodyB,
887
+
888
+ // everything below is optional, default values are shown here
889
+ referenceFrame: ReferenceFrame.local,
890
+ positionA: { x: 0, y: 0, z: 0 },
891
+ positionB: { x: 0, y: 0, z: 0 },
892
+ minDistance: -1,
893
+ maxDistance: -1,
894
+ spring: {
895
+ mode: SpringMode.UseFrequency,
896
+ damping: 1.0,
897
+ frequency: 2.0,
898
+ stiffness: 2.0,
899
+ },
900
+ axisComponent: {
901
+ options: {
902
+ positionBaumgarte: 0.8,
903
+ velocityBaumgarte: 1.0,
904
+ strength: 1.0,
905
+ },
906
+ mR1PlusUxAxis: { x: 0, y: 0, z: 0 },
907
+ mR2xAxis: { x: 0, y: 0, z: 0 },
908
+ mInvI1_R1PlusUxAxis: { x: 0, y: 0, z: 0 },
909
+ mInvI2_R2xAxis: { x: 0, y: 0, z: 0 },
910
+ effectiveMass: 0,
911
+ totalLambda: 0,
912
+ springComponent: {
913
+ mode: SpringMode.UseFrequency,
914
+ damping: 1.0,
915
+ frequency: 2.0,
916
+ stiffness: 2.0,
917
+ },
918
+ },
919
+ });
920
+ ```
921
+
922
+ ### Fixed Constraint
923
+
924
+ Locks both position and rotation between two bodies:
925
+
926
+ ```js
927
+ const constraint = world.createFixedConstraint({
928
+ // required
929
+ bodyA: bodyA,
930
+ bodyB: bodyB,
931
+
932
+ // everything below is optional, default values are shown here
933
+ referenceFrame: ReferenceFrame.local,
934
+ positionA: { x: 0, y: 0, z: 0 },
935
+ positionB: { x: 0, y: 0, z: 0 },
936
+ axisXA: { x: 1, y: 0, z: 0 },
937
+ axisXB: { x: 1, y: 0, z: 0 },
938
+ axisYA: { x: 0, y: 1, z: 0 },
939
+ axisYB: { x: 0, y: 1, z: 0 },
940
+ translationComponent: {
941
+ options: {
942
+ positionBaumgarte: 0.8,
943
+ velocityBaumgarte: 1.0,
944
+ strength: 1.0,
945
+ },
946
+ },
947
+ rotationComponent: {
948
+ options: {
949
+ positionBaumgarte: 0.8,
950
+ velocityBaumgarte: 1.0,
951
+ strength: 1.0,
952
+ },
953
+ },
954
+ });
955
+ ```
956
+
957
+ ### Hinge Constraint
958
+
959
+ Allows rotation around a single axis with optional limits and motor:
960
+
961
+ ```js
962
+ const hinge = world.createHingeConstraint({
963
+ // required
964
+ bodyA: bodyA,
965
+ bodyB: bodyB,
966
+
967
+ // everything below is optional, default values are shown here
968
+ referenceFrame: ReferenceFrame.local,
969
+ pointA: { x: 0, y: 0, z: 0 }, // pivot point in bodyA
970
+ pointB: { x: 0, y: 0, z: 0 }, // pivot point in bodyB
971
+ hingeA: { x: 1, y: 0, z: 0 }, // hinge axis in bodyA
972
+ hingeB: { x: 1, y: 0, z: 0 }, // hinge axis in bodyB
973
+ normalA: { x: 0, y: 1, z: 0 }, // normal direction
974
+ normalB: { x: 0, y: 1, z: 0 }, // normal direction
975
+ minHingeAngle: -Math.PI / 2, // optional
976
+ maxHingeAngle: Math.PI / 2, // optional
977
+ maxFrictionTorque: 0,
978
+ targetAngularSpeed: 0,
979
+ targetAngle: 0,
980
+ motor: {
981
+ mode: MotorMode.Off,
982
+ minForce: -Infinity,
983
+ maxForce: +Infinity,
984
+ minTorque: -Infinity,
985
+ maxTorque: +Infinity,
986
+ spring: {
987
+ mode: SpringMode.UseFrequency,
988
+ damping: 1.0,
989
+ frequency: 2.0,
990
+ stiffness: 2.0,
991
+ },
992
+ },
993
+ pointConstraintPart: {
994
+ options: {
995
+ positionBaumgarte: 0.8,
996
+ velocityBaumgarte: 1.0,
997
+ strength: 1.0,
998
+ },
999
+ },
1000
+ rotationConstraintPart: {
1001
+ options: {
1002
+ positionBaumgarte: 0.8,
1003
+ velocityBaumgarte: 1.0,
1004
+ strength: 1.0,
1005
+ },
1006
+ },
1007
+ rotationLimitsConstraintPart: {
1008
+ options: {
1009
+ positionBaumgarte: 0.8,
1010
+ velocityBaumgarte: 1.0,
1011
+ strength: 1.0,
1012
+ },
1013
+ },
1014
+ motorConstraintPart: {
1015
+ options: {
1016
+ positionBaumgarte: 0.8,
1017
+ velocityBaumgarte: 1.0,
1018
+ strength: 1.0,
1019
+ },
1020
+ },
1021
+ });
1022
+ ```
1023
+
1024
+ ### Enabling and disabling constraints
1025
+
1026
+ All constraints can be enabled and disabled at runtime, simply by setting `constraint.isEnabled` to `true` or `false`.
1027
+
1028
+ ### Destroying Constraints
1029
+
1030
+ ```js
1031
+ world.destroyConstraint(constraint);
1032
+ ```
1033
+
1034
+ ---
1035
+
1036
+ ## Math Utilities
1037
+
1038
+ Bounce exports common math types for working with 3D physics:
1039
+
1040
+ - **Vec3** — 3D vector with x, y, z components
1041
+ - **Quat** — quaternion for rotations (x, y, z, w)
1042
+ - **Mat3 / Mat4** — 3x3 and 4x4 matrices
1043
+ - **BasicTransform** — position + rotation + scale
1044
+ - **Isometry** — combined rotation and translation
1045
+ - **Ray** — origin, direction, length
1046
+
1047
+ Scalar helpers:
1048
+
1049
+ - `clamp(value, min, max)`
1050
+ - `squared(value)`
1051
+ - `degreesToRadians(degrees)`