@perplexdotgg/bounce 1.0.6 → 1.1.1
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.
- package/README.md +6 -5
- package/build/bounce.d.ts +15851 -37152
- package/build/bounce.js +534 -383
- package/docs/documentation.md +1051 -0
- package/package.json +2 -2
|
@@ -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)`
|