avbd-babylon 0.3.0 → 0.4.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.
package/docs/README.md CHANGED
@@ -1,465 +1,503 @@
1
- # Integrating AVBD into a Babylon.js project
2
-
3
- Use `AvbdPhysics` for GPU collision detection and rigid-body simulation, with
4
- Babylon meshes for the interactive view. The aggregate/body methods follow
5
- familiar Babylon conventions: mass, friction, velocities, impulses and disposal.
6
- AVBD computes body motion and contacts on the GPU; Babylon renders the results.
7
- This adapter has its own world; it does not implement Babylon's full
8
- Physics V2 plugin interface or install through `scene.enablePhysics()`.
9
-
10
- **Jump to:**
11
-
12
- - [Install the library](#install-the-library)
13
- - [A complete small scene](#a-complete-small-scene)
14
- - [Move, launch and reuse bodies](#move-launch-and-reuse-bodies)
15
- - [Choose a rendering path](#choose-a-rendering-path)
16
- - [Reproduce the collision and solver benchmarks](#reproduce-the-collision-and-solver-benchmarks)
17
- - [Current shape and API support](#current-shape-and-api-support)
18
- - [Choosing collision detection](#choosing-collision-detection)
19
-
20
- ## Install the library
21
-
22
- The package is named `avbd-babylon`. For a local build, run `npm install` and
23
- `npm run build` in this repository. From your
24
- Babylon project, run `npm install /absolute/path/to/avbd` or install the archive
25
- created by `npm pack`. Import from `avbd-babylon` using your normal bundler.
26
- The ESM output already contains the solver and shader strings. No TypeScript
27
- source compilation or WGSL loader is needed in your project. The package has
28
- no Babylon runtime imports; it uses the mesh objects your project provides.
29
-
30
- `npm run build` builds only the library. `npm run build:web` builds the lab and
31
- browser tests. `npm start` builds the library and serves the lab together.
32
- `npm run build:cdn` creates `dist/avbd.global.js`, which exposes `globalThis.AVBD`.
33
- Host that file yourself and load it with a normal script tag; use
34
- `const { AvbdPhysics, AvbdPhysicsAggregate, AvbdShapeType } = AVBD`.
35
-
36
- WebGPU
37
- needs a supported browser and localhost or HTTPS. The default world requests
38
- 9 storage bindings, a 512 MiB storage binding limit and a 1 GiB buffer limit;
39
- unsupported adapters fail during creation. Babylon drawing can use either its
40
- WebGL or WebGPU engine when using the small-scene mesh bridge.
41
-
42
- For CPU physics, use the [native pipeline examples](../README.md#choose-a-solver-backend-once).
43
- Select the backend once; 2D and 3D scene builders, body commands, joints, triggers,
44
- restitution, collision masks and state reads keep the same API. The package
45
- supports WebGPU and CPU. The repository demos retain an experimental WebGL2
46
- solver for comparisons; its measured performance was too poor to include in
47
- the package. See the [backend benchmark](../README.md#backend-differences-and-benchmarks).
48
-
49
- ## A complete small scene
50
-
51
- This example expects `<canvas id="canvas"></canvas>` styled to fill the page.
52
- Use your project's existing Babylon dependency.
53
-
54
- ```js
55
- // src/main.js
56
- import {
57
- Engine,
58
- Scene,
59
- ArcRotateCamera,
60
- HemisphericLight,
61
- MeshBuilder,
62
- Vector3,
63
- } from "@babylonjs/core";
64
- import { AvbdPhysics, AvbdPhysicsAggregate, AvbdShapeType } from "avbd-babylon";
65
-
66
- const canvas = document.getElementById("canvas");
67
- const engine = new Engine(canvas, true);
68
- const scene = new Scene(engine);
69
- const camera = new ArcRotateCamera(
70
- "camera",
71
- -Math.PI / 2,
72
- Math.PI / 3,
73
- 12,
74
- new Vector3(0, 2, 0),
75
- scene,
76
- );
77
- camera.attachControl(canvas, true);
78
- new HemisphericLight("light", new Vector3(0, 1, 0), scene);
79
-
80
- const floor = MeshBuilder.CreateBox(
81
- "floor",
82
- { width: 20, height: 1, depth: 20 },
83
- scene,
84
- );
85
- floor.position.y = -0.5;
86
- const box = MeshBuilder.CreateBox("box", { size: 1 }, scene);
87
- box.position.y = 4;
88
-
89
- const physics = await AvbdPhysics.create({
90
- scene,
91
- gravity: new Vector3(0, -9.81, 0),
92
- timeStep: 1 / 60,
93
- iterations: 10,
94
- capacity: 64,
95
- });
96
- new AvbdPhysicsAggregate(floor, AvbdShapeType.BOX, { mass: 0 }, scene);
97
- const fallingBox = new AvbdPhysicsAggregate(
98
- box,
99
- AvbdShapeType.BOX,
100
- { mass: 2, friction: 0.6 },
101
- scene,
102
- );
103
-
104
- // The world attaches to the scene and allocates on its first simulation step.
105
- // Create aggregates before the render loop starts.
106
- fallingBox.body.setAngularVelocity(new Vector3(0, 1, 0.5));
107
-
108
- engine.runRenderLoop(() => scene.render());
109
- const resize = () => engine.resize();
110
- window.addEventListener("resize", resize);
111
- window.addEventListener(
112
- "pagehide",
113
- () => {
114
- window.removeEventListener("resize", resize);
115
- physics.dispose();
116
- scene.dispose();
117
- engine.dispose();
118
- },
119
- { once: true },
120
- );
121
- ```
122
-
123
- The fixed-step observer allows up to three catch-up steps per rendered frame.
124
- Mesh poses arrive asynchronously; do not await them in the render loop. `create({ scene })` attaches automatically and disposes with the scene. For a
125
- custom simulation loop, pass `autoAttach: false` and call `step()` yourself.
126
- `attachToScene()` is also available and does not attach a second observer when
127
- called again for the same scene.
128
-
129
- ## Move, launch and reuse bodies
130
-
131
- ```js
132
- fallingBox.body.setLinearVelocity(new Vector3(0, 0, 5));
133
- fallingBox.body.applyImpulse(new Vector3(3, 0, 0), box.position);
134
- fallingBox.body.applyAngularImpulse(new Vector3(0, 2, 0));
135
-
136
- // Teleport retains existing velocities; reset them explicitly for a fresh launch.
137
- fallingBox.body.teleport(new Vector3(0, 3, -5));
138
- fallingBox.body.setLinearVelocity(new Vector3(0, 0, 12));
139
- fallingBox.body.setAngularVelocity(Vector3.Zero());
140
- ```
141
-
142
- Commands are combined and applied against the current GPU state. They do not
143
- upload a stale CPU pose. Create projectile slots before initialization and reuse
144
- them for spawning. `aggregate.dispose()` disables its collision participation;
145
- the caller still owns the mesh. Dispose the world before disposing its scene.
146
-
147
- ## Choose a rendering path
148
-
149
- | Scene size and needs | Rendering path | Cost to watch |
150
- | ----------------------------------- | ---------------------------------------------------- | ----------------------------------------- |
151
- | Small interactive scenes | Default Babylon mesh bridge | Pose readback and individual mesh updates |
152
- | Repeated geometry, moderate counts | Instances or thin instances with a batched pose copy | CPU matrix updates and uploading matrices |
153
- | Tens of thousands of moving objects | Draw directly from the live GPU body buffer | GPU rendering and collision/solver work |
154
-
155
- Babylon [instances](https://doc.babylonjs.com/features/featuresDeepDive/mesh/copies/instances)
156
- share source geometry and material. [Thin instances](https://doc.babylonjs.com/features/featuresDeepDive/mesh/copies/thinInstances)
157
- store repeated transforms in buffers and avoid a separate scene object for every
158
- visible copy. Group objects by geometry and material. Instancing reduces drawing
159
- overhead; it does not reduce the number of physics contacts.
160
-
161
- For a moderate number of identical, origin-centred unit boxes, this bridge copies
162
- all poses once and updates a single thin-instance matrix buffer. Here `physics`
163
- was created with `syncMeshes: false`, and `boxes` contains box aggregates created
164
- with `sync: false`. Their original visual meshes should be hidden; `source` is
165
- an independent unit-box mesh at the origin with identity rotation and scale.
166
-
167
- ```js
168
- import { Matrix, Quaternion, Vector3 } from "@babylonjs/core";
169
-
170
- physics.initialize();
171
- const indices = boxes.map((aggregate) => aggregate.body.gpuIndex);
172
- const matrices = new Float32Array(boxes.length * 16);
173
- const matrix = Matrix.Identity();
174
- const position = Vector3.Zero();
175
- const rotation = Quaternion.Identity();
176
- const scale = Vector3.One();
177
- let pending = false;
178
-
179
- source.thinInstanceSetBuffer("matrix", matrices, 16, false);
180
- scene.onBeforeRenderObservable.add(() => {
181
- if (pending) return;
182
- pending = true;
183
- physics
184
- .readBodies()
185
- .then((poses) => {
186
- for (let i = 0; i < indices.length; i++) {
187
- const offset = indices[i] * 40;
188
- position.set(poses[offset], poses[offset + 1], poses[offset + 2]);
189
- rotation.set(...poses.subarray(offset + 4, offset + 8));
190
- Matrix.ComposeToRef(scale, rotation, position, matrix);
191
- matrix.copyToArray(matrices, i * 16);
192
- }
193
- source.thinInstanceBufferUpdated("matrix");
194
- source.thinInstanceRefreshBoundingInfo();
195
- })
196
- .catch(console.error)
197
- .finally(() => {
198
- pending = false;
199
- });
200
- });
201
- ```
202
-
203
- This is a CPU bridge for moderate counts. It still copies the full body buffer,
204
- calculates matrices and uploads them. Bounding updates also cost CPU time. For
205
- known contained motion, a deliberately conservative bounding volume can avoid
206
- rebuilding bounds each update; ensure it covers all moving instances.
207
- For hulls or meshes with offset origins, reconstruct the mesh transform using
208
- the aggregate's local centre and principal rotation as the adapter does; the
209
- unit-box example does not handle those offsets.
210
-
211
- For a large scene:
212
-
213
- ```js
214
- const physics = await AvbdPhysics.create({
215
- capacity: 100_001,
216
- iterations: 4,
217
- syncMeshes: false,
218
- });
219
- // Create all bodies before initialize(), with { sync: false }.
220
- physics.initialize();
221
- const bodyBuffer = physics.bodyBuffer;
222
- const gpuIndex = aggregate.body.gpuIndex;
223
- physics.step(); // One fixed step; submission does not await completion.
224
- ```
225
-
226
- A custom WebGPU renderer can read `bodyBuffer` on the **same GPUDevice** and draw
227
- instances from live poses. Buffers cannot be shared across devices. The adapter
228
- does not automatically connect this storage buffer to Babylon thin instances,
229
- and it does not access Babylon private engine fields. If your renderer exposes
230
- a device you own, pass it to `AvbdPhysics.create({ device, ... })`. A caller-owned
231
- device stays alive when the world is disposed.
232
-
233
- Thin instances do not automatically create colliders. The aggregate API takes
234
- one mesh per physics body, which has a startup cost for very large scenes.
235
- For 100K bodies, the laboratory's native body/scene builders avoid creating
236
- 100K Babylon mesh objects; see [canonicalScenes.js](../demo/canonicalScenes.js).
237
- Import `AvbdScene2D` or `AvbdScene3D` from `avbd-babylon/native` for the same
238
- renderer-independent integration. Both expose live body handles, bulk commands,
239
- packed velocity uploads, selected pose reads, triggers, masks, restitution,
240
- sleeping and contact events. See the package README's
241
- [native examples](../README.md#native-physics-with-any-renderer).
242
- The direct-buffer renderer and small-scene aggregate bridge are separate
243
- integration paths.
244
-
245
- See [canonicalRenderer.js](../demo/canonicalRenderer.js) and
246
- [canonicalRender.wgsl](../demo/canonicalRender.wgsl) for the direct rendering path,
247
- and [mixedColliderDemo.js](../demo/mixedColliderDemo.js) for the Babylon mesh bridge.
248
- The laboratory scene builders use z up; the Babylon adapter uses y up by
249
- default. Match your renderer's coordinates to the world you create.
250
-
251
- The 3D body stride is 40 floats / 160 bytes: position at 0, quaternion xyzw at 4,
252
- size at 16, mass at 19, principal inertia at 20, linear velocity at 32 and angular
253
- velocity at 36. Spatial sorting changes GPU order. Use `body.gpuIndex` after
254
- initialization rather than assuming creation order equals buffer order.
255
-
256
- Use shared geometry/materials and a small number of draw calls. For independent
257
- solver measurements, omit drawing entirely. For whole-app comparisons, keep
258
- resolution, antialiasing, shadows, materials and camera fixed. Enhanced colors,
259
- lighting and 4× MSAA are interactive presentation options in this app;
260
- `quality=benchmark` disables them. Keeping physics settings fixed makes a
261
- rendering optimization easier to identify.
262
-
263
- ## Reproduce the collision and solver benchmarks
264
-
265
- Start this repository with `npm start`, then open
266
- [/tests/performance.html](http://127.0.0.1:8080/tests/performance.html).
267
- Select a scene and play it before measuring. The preview closes during a run
268
- and restarts afterward. “Run all scenes” measures the complete scene catalog, including
269
- 10K, 50K and 100K dense 2D piles.
270
-
271
- For a repeatable solver-only run, use this JavaScript in the benchmark page's
272
- developer console after it is ready. `__PERFORMANCE__` is the repository's test
273
- harness, not part of the Babylon world API.
274
-
275
- ```js
276
- const measurements = [];
277
- await __BENCHMARK_VIEW__.suspendPreview();
278
- try {
279
- for (const scene of [
280
- "2d-stress-pile-10k",
281
- "2d-stress-pile-50k",
282
- "2d-stress-pile-100k",
283
- "showcase-box-columns-100k",
284
- "showcase-brick-ring-110k",
285
- ]) {
286
- for (let repeat = 0; repeat < 3; repeat++) {
287
- const result = await __PERFORMANCE__.run(scene, "production", 60, 60);
288
- measurements.push({ ...result, repeat });
289
- }
290
- }
291
- console.table(
292
- measurements.map((r) => ({
293
- scene: r.scene,
294
- objects: r.bodyCount,
295
- contacts: r.peakContacts,
296
- iterations: r.params.iterations,
297
- collisionMs: r.summary.collision.mean,
298
- movementMs: r.summary.solve.mean,
299
- physicsMs: r.summary.total.mean,
300
- slowerStepsMs: r.summary.total.p95,
301
- })),
302
- );
303
- const data = { adapter: __PERFORMANCE__.info, measurements };
304
- const url = URL.createObjectURL(
305
- new Blob([JSON.stringify(data, null, 2)], {
306
- type: "application/json",
307
- }),
308
- );
309
- const download = document.createElement("a");
310
- download.href = url;
311
- download.download = "avbd-replication.json";
312
- download.click();
313
- setTimeout(() => URL.revokeObjectURL(url), 1000);
314
- } finally {
315
- await __BENCHMARK_VIEW__.preview.load(document.getElementById("scene").value);
316
- }
317
- ```
318
-
319
- Each run starts fresh, warms up for 60 steps and records 60 distinct GPU
320
- timestamp samples. Every step checks contact capacity and scheduling conflicts.
321
- The 2D stress scenes use touching boxes, gravity, friction and ten iterations,
322
- without sleeping. The 3D showcase uses each scene's recorded settings. Compare
323
- collision time with contact count; equal body counts across dimensions do not
324
- guarantee equal collision work.
325
-
326
- For controlled engineering profiles, use `scripts/profile-solver.mjs` or
327
- `scripts/profile-paired.mjs`. Set `AVBD_PROFILE_SCENES`,
328
- `AVBD_PROFILE_VARIANTS`, `AVBD_PROFILE_WARMUP`, `AVBD_PROFILE_SAMPLES` and
329
- `AVBD_PROFILE_REPEATS` explicitly. Public benchmark controls use the production
330
- automatic choice.
331
-
332
- GPU work layout selection is automatic in `AppGpuSolver3D` and the Babylon
333
- adapter. Body count and active joints select the contact calculation; contact
334
- load selects thread sharing for very large scenes. The 100K columns preset
335
- shares face-contact points across threads. Dense rigid scenes above 250K bodies
336
- with at least 1.75 manifolds per body use eight threads per body. The same large
337
- scenes cache sorting keys for contact lists of up to sixteen entries; longer
338
- lists retain their general sorting path. Deformable shaders remain intact.
339
-
340
- ```js
341
- const world = await AvbdPhysics.create({ scene }); // automatic by default
342
- // Create aggregates before the render loop.
343
- world.initialize();
344
- console.log(world.solverDecision); // implementation and reason
345
- ```
346
-
347
- Body and joint changes are reconsidered before the next step. Contact-load
348
- adaptation reuses existing capacity-counter readbacks rather than adding a
349
- per-frame pose copy. Positions, velocities, friction, rotation and iteration
350
- counts are retained. The policy follows measurements on the test laptop;
351
- results on other GPUs may differ. Advanced `setSolverMode()` overrides remain
352
- available for controlled engineering measurements and compatibility. The
353
- browser benchmark runs the production automatic choice.
354
-
355
- Web Workers can keep scene construction and command submission from blocking
356
- the page. They do not divide the already-parallel GPU kernels among CPU threads.
357
- The current API runs on the caller's thread. A complete worker integration would
358
- own the GPU solver and an OffscreenCanvas renderer together, send inputs and
359
- small status messages, and draw directly from the GPU body buffer. Copying every
360
- body's pose between threads each frame would defeat that design. Worker-based
361
- Babylon drawing also requires a compatible Babylon engine setup and replacing
362
- DOM-dependent controls with forwarded input messages. The root [README worker example](../README.md#worker-integration) shows the
363
- OffscreenCanvas setup and message forwarding. Use the benchmark's `setup` and
364
- `summary.submission` fields to
365
- judge the CPU cost separately from GPU physics.
366
-
367
- Both GPU solvers hold poses and velocities if a step runs
368
- out of contact storage or has conflicting solve groups. The host can then grow
369
- capacity without advancing an incomplete movement solve. Counters still report
370
- the failure: a held step does not pass correctness checks. This safeguard does
371
- not change valid-step equations or remove the need to reserve enough capacity.
372
-
373
- `npm run test:web` also checks screenshots, controls, benchmark preview lifecycle,
374
- navigation, and six-second dense 2D motion. Keep other GPU workloads closed.
375
-
376
- Lower GPU milliseconds are better; higher app FPS is better. Physics timings
377
- separate collision checks, contact setup, scheduling and movement calculations.
378
- They exclude drawing, sleeping, CPU submission and copying results back.
379
- Instrumented timing passes add overhead. The “CPU + wait” column includes GPU
380
- work and readback, so it is not an isolated CPU cost. Report GPU model, driver,
381
- scene, timestep, iterations, sample counts and contact counts with comparisons.
382
-
383
- The premade reports were recorded on an **NVIDIA GeForce RTX 4070 Laptop GPU**.
384
- They are saved examples; running the browser benchmark measures your own GPU.
385
-
386
- The [solver report](http://127.0.0.1:8080/test-results/solver-performance.html)
387
- includes the [AVBD paper](https://graphics.cs.utah.edu/research/projects/avbd/Augmented_VBD-SIGGRAPH25.pdf)
388
- comparison. Its RTX 4090 / DirectX / LBVH results and this browser's
389
- RTX 4070 Laptop / WebGPU / spatial-grid results have different hardware and
390
- timing boundaries. They are workload context, not a same-hardware speed ratio.
391
-
392
- ## Current shape and API support
393
-
394
- Boxes, spheres and capsules have native collision geometry. Capsules use exact
395
- rounded segments along local Y. Cylinders and custom hulls use polygonal convex
396
- hulls, limited to 32 vertices. Meshes must be root
397
- meshes with positive scale; bake parent or negative transforms first. Colliders
398
- capture geometry, mass properties and transforms at creation. Sphere dimensions
399
- must be uniform.
400
-
401
- The adapter supports restitution, friction, velocity and impulse commands,
402
- teleports, collision masks, contact/trigger events, GPU raycasts, per-body sleep
403
- eligibility, ball joints, welds, elastic or breakable springs, 3D hinges with
404
- angle stops and torque-limited motors. Both dimensions expose GPU raycasts;
405
- 3D sphere casts and 2D circle casts include rounded corner tests. It has no
406
- complete Physics V2 plugin or concave mesh collider.
407
- Static bodies with velocity move kinematically. Portable builders include 3D
408
- ropes, ragdolls and fabric. Automatic mesh synchronization is limited to 2048
409
- bodies by default; large scenes should draw GPU poses directly.
410
- For full method signatures and ownership rules, see [BABYLON_API.md](BABYLON_API.md).
411
-
412
- ## Choosing collision detection
413
-
414
- `AvbdPhysics.create` accepts `broadphase: "auto"` (default), `"grid"` or
415
- `"hploc"`. Automatic selection uses a GPU tree for large scenes with varied
416
- collider sizes or many linked thin plates, such as the ragdoll net.
417
- Small scenes and uniform block scenes retain the grid. Measure your own workload;
418
- body count alone does not predict which method is faster.
419
-
420
- ```js
421
- const physics = await AvbdPhysics.create({
422
- scene,
423
- capacity: 50000,
424
- syncMeshes: false,
425
- broadphase: "auto",
426
- bvh: { rebuildInterval: 64 },
427
- });
428
- ```
429
-
430
- The tree refits every collider each step, including rotations and teleports.
431
- Rebuilds improve its quality; refitting preserves coverage between rebuilds.
432
- The same narrowphase, friction and rotational AVBD equations handle its pairs.
433
- See [GPU tree design and limits](HPLOC.md) for implementation details.
434
-
435
- For a controlled comparison, open `/tests/performance.html`. Its collision
436
- selector compares the grid and tree without changing solving rounds. Both
437
- methods are also available in the playable laboratory.
438
-
439
- ```js
440
- try {
441
- const pair = await __PERFORMANCE__.runPaired(
442
- "3d-mixed-sizes-50k",
443
- ["grid", "hploc"],
444
- 60,
445
- 60,
446
- );
447
- console.table(
448
- pair.results.map((r) => ({
449
- collisions: r.broadphase,
450
- collisionMs: r.summary.collision.mean,
451
- physicsMs: r.summary.total.mean,
452
- firstStepMs: r.startup.total,
453
- })),
454
- );
455
- } finally {
456
- await __BENCHMARK_VIEW__.preview.load(document.querySelector("#scene").value);
457
- }
458
- ```
459
-
460
- Use `grid-detail` / `hploc-detail` to split collision preparation, nearby-pair
461
- search, exact contact checks, body solving and contact-force updates. These
462
- extra timers add pass boundaries, so compare matching modes. Paired runs keep
463
- two independent simulations in memory; their absolute times are separate from
464
- the single-world paper comparison. Raw JSON retains startup, rebuild spikes,
465
- all samples, buffer bytes, selection decisions and noisy timing flags.
1
+ # Integrating AVBD into a Babylon.js project
2
+
3
+ Use `AvbdPhysics` for GPU collision detection and rigid-body simulation, with
4
+ Babylon meshes for the interactive view. The aggregate/body methods follow
5
+ familiar Babylon conventions: mass, friction, velocities, impulses and disposal.
6
+ AVBD computes body motion and contacts on the GPU; Babylon renders the results.
7
+ This adapter has its own world; it does not implement Babylon's full
8
+ Physics V2 plugin interface or install through `scene.enablePhysics()`.
9
+
10
+ **Jump to:**
11
+
12
+ - [Install the library](#install-the-library)
13
+ - [A complete small scene](#a-complete-small-scene)
14
+ - [Everyday body commands](#everyday-body-commands)
15
+ - [Move, launch and reuse bodies](#move-launch-and-reuse-bodies)
16
+ - [Choose a rendering path](#choose-a-rendering-path)
17
+ - [Reproduce the collision and solver benchmarks](#reproduce-the-collision-and-solver-benchmarks)
18
+ - [Current shape and API support](#current-shape-and-api-support)
19
+ - [Choosing collision detection](#choosing-collision-detection)
20
+
21
+ ## Install the library
22
+
23
+ Install AVBD and Babylon in your project:
24
+
25
+ ```sh
26
+ npm install avbd-babylon @babylonjs/core
27
+ ```
28
+
29
+ Import from `avbd-babylon` using your normal bundler. For a local development
30
+ build, run `npm install` and `npm run build` in this repository, then install
31
+ its directory or the archive created by `npm pack` into your project.
32
+ The ESM output already contains the solver and shader strings. No TypeScript
33
+ source compilation or WGSL loader is needed in your project. The package has
34
+ no Babylon runtime imports; it uses the mesh objects your project provides.
35
+
36
+ `npm run build` builds only the library. `npm run build:web` builds the lab and
37
+ browser tests. `npm start` builds the library and serves the lab together.
38
+ `npm run build:cdn` creates `dist/avbd.global.js`, which exposes `globalThis.AVBD`.
39
+ Host that file yourself and load it with a normal script tag; use
40
+ `const { AvbdPhysics, AvbdPhysicsAggregate, AvbdShapeType } = AVBD`.
41
+
42
+ WebGPU needs a supported browser and localhost or HTTPS. The world prefers up to
43
+ 9 storage bindings, a 512 MiB storage binding limit and a 1 GiB buffer limit,
44
+ and adapts these requests to the adapter's available limits. Custom convex mesh
45
+ shapes require 9 storage bindings. Creation checks 3D floor contacts once and
46
+ selects a compatible GPU path when needed, including on supported mobile devices.
47
+ Babylon drawing can use either its WebGL or WebGPU engine with the small-scene
48
+ mesh bridge.
49
+
50
+ For CPU physics, use the [native pipeline examples](../README.md#choose-a-solver-backend-once).
51
+ Select the backend once; 2D and 3D scene builders, body commands, joints, triggers,
52
+ restitution, collision masks and state reads keep the same API. The package
53
+ supports WebGPU and CPU. The repository demos retain an experimental WebGL2
54
+ solver for comparisons; its measured performance was too poor to include in
55
+ the package. See the [backend benchmark](../README.md#backend-differences-and-benchmarks).
56
+
57
+ ## A complete small scene
58
+
59
+ This example expects `<canvas id="canvas"></canvas>` styled to fill the page.
60
+ Use your project's existing Babylon dependency.
61
+
62
+ ```js
63
+ // src/main.js
64
+ import {
65
+ Engine,
66
+ Scene,
67
+ ArcRotateCamera,
68
+ HemisphericLight,
69
+ MeshBuilder,
70
+ Vector3,
71
+ } from "@babylonjs/core";
72
+ import { AvbdPhysics, AvbdPhysicsAggregate, AvbdShapeType } from "avbd-babylon";
73
+
74
+ const canvas = document.getElementById("canvas");
75
+ const engine = new Engine(canvas, true);
76
+ const scene = new Scene(engine);
77
+ const camera = new ArcRotateCamera(
78
+ "camera",
79
+ -Math.PI / 2,
80
+ Math.PI / 3,
81
+ 12,
82
+ new Vector3(0, 2, 0),
83
+ scene,
84
+ );
85
+ camera.attachControl(canvas, true);
86
+ new HemisphericLight("light", new Vector3(0, 1, 0), scene);
87
+
88
+ const floor = MeshBuilder.CreateBox(
89
+ "floor",
90
+ { width: 20, height: 1, depth: 20 },
91
+ scene,
92
+ );
93
+ floor.position.y = -0.5;
94
+ const box = MeshBuilder.CreateBox("box", { size: 1 }, scene);
95
+ box.position.y = 4;
96
+
97
+ const physics = await AvbdPhysics.create({
98
+ scene,
99
+ gravity: new Vector3(0, -9.81, 0),
100
+ timeStep: 1 / 60,
101
+ iterations: 10,
102
+ capacity: 64,
103
+ });
104
+ new AvbdPhysicsAggregate(floor, AvbdShapeType.BOX, { mass: 0 }, scene);
105
+ const fallingBox = new AvbdPhysicsAggregate(
106
+ box,
107
+ AvbdShapeType.BOX,
108
+ { mass: 2, friction: 0.6 },
109
+ scene,
110
+ );
111
+
112
+ // The world attaches to the scene and allocates on its first simulation step.
113
+ // Create aggregates before the render loop starts.
114
+ fallingBox.body.setAngularVelocity(new Vector3(0, 1, 0.5));
115
+
116
+ engine.runRenderLoop(() => scene.render());
117
+ const resize = () => engine.resize();
118
+ window.addEventListener("resize", resize);
119
+ window.addEventListener(
120
+ "pagehide",
121
+ () => {
122
+ window.removeEventListener("resize", resize);
123
+ physics.dispose();
124
+ scene.dispose();
125
+ engine.dispose();
126
+ },
127
+ { once: true },
128
+ );
129
+ ```
130
+
131
+ The fixed-step observer allows up to three catch-up steps per rendered frame.
132
+ Mesh poses arrive asynchronously; do not await them in the render loop. `create({ scene })` attaches automatically and disposes with the scene. For a
133
+ custom simulation loop, pass `autoAttach: false` and call `step()` yourself.
134
+ `attachToScene()` is also available and does not attach a second observer when
135
+ called again for the same scene.
136
+
137
+ ## Everyday body commands
138
+
139
+ Keep the aggregate and use its `body` handle for movement. Babylon `Vector3`
140
+ values work for velocity, impulses and forces; those calls also accept arrays.
141
+ Teleporting accepts a `Vector3` and an optional `Quaternion`.
142
+
143
+ | Task | Call |
144
+ | ------------------------------ | --------------------------------------------------------------------------------- |
145
+ | Set movement or spin | `body.setLinearVelocity(v)` / `body.setAngularVelocity(v)` |
146
+ | Give a sudden push | `body.applyImpulse(impulse, worldPoint)`; omit the point for a central push |
147
+ | Apply force for one fixed step | `body.applyForce(force, worldPoint)` / `body.applyTorque(torque)` |
148
+ | Move instantly | `body.teleport(position, rotation)`; omit rotation to retain the live orientation |
149
+ | Set bounce | `body.setRestitution(0.5)` |
150
+ | Set collision masks | `body.setCollisionGroups(group, collidesWith)` |
151
+ | Make an overlap sensor | `body.setTrigger(true)` |
152
+ | Allow sleeping or wake a body | `body.setSleepEnabled(true)` / `body.wakeUp()` |
153
+ | Read live physics state | `await body.readState()` |
154
+ | Remove physics participation | `aggregate.dispose()`; the mesh stays caller-owned |
155
+
156
+ Commands made before the next fixed step are batched automatically. Ordinary
157
+ per-body calls do not require a manual flush or pose download. State reads and
158
+ queries are asynchronous because they return current GPU results. Create the
159
+ world with `{ scene }` to attach stepping and disposal to Babylon's scene.
160
+
161
+ For another renderer or CPU compatibility, use the
162
+ [native 2D/3D examples](../README.md#native-physics-with-any-renderer). They use the
163
+ same movement, impulse, bounce, trigger, mask and sleep method names, with
164
+ two- or three-component coordinates. Native bulk edits and reusable packed
165
+ batches are shown in [bulk edits](../README.md#bulk-edits-and-gpu-dispatches).
166
+
167
+ ## Move, launch and reuse bodies
168
+
169
+ ```js
170
+ fallingBox.body.setLinearVelocity(new Vector3(0, 0, 5));
171
+ fallingBox.body.applyImpulse(new Vector3(3, 0, 0), box.position);
172
+ fallingBox.body.applyAngularImpulse(new Vector3(0, 2, 0));
173
+
174
+ // Teleport retains existing velocities; reset them explicitly for a fresh launch.
175
+ fallingBox.body.teleport(new Vector3(0, 3, -5));
176
+ fallingBox.body.setLinearVelocity(new Vector3(0, 0, 12));
177
+ fallingBox.body.setAngularVelocity(Vector3.Zero());
178
+ ```
179
+
180
+ Commands are combined and applied against the current GPU state. They do not
181
+ upload a stale CPU pose. Create projectile slots before initialization and reuse
182
+ them for spawning. `aggregate.dispose()` disables its collision participation;
183
+ the caller still owns the mesh. Dispose the world before disposing its scene.
184
+
185
+ ## Choose a rendering path
186
+
187
+ | Scene size and needs | Rendering path | Cost to watch |
188
+ | ----------------------------------- | ---------------------------------------------------- | ----------------------------------------- |
189
+ | Small interactive scenes | Default Babylon mesh bridge | Pose readback and individual mesh updates |
190
+ | Repeated geometry, moderate counts | Instances or thin instances with a batched pose copy | CPU matrix updates and uploading matrices |
191
+ | Tens of thousands of moving objects | Draw directly from the live GPU body buffer | GPU rendering and collision/solver work |
192
+
193
+ Babylon [instances](https://doc.babylonjs.com/features/featuresDeepDive/mesh/copies/instances)
194
+ share source geometry and material. [Thin instances](https://doc.babylonjs.com/features/featuresDeepDive/mesh/copies/thinInstances)
195
+ store repeated transforms in buffers and avoid a separate scene object for every
196
+ visible copy. Group objects by geometry and material. Instancing reduces drawing
197
+ overhead; it does not reduce the number of physics contacts.
198
+
199
+ For a moderate number of identical, origin-centred unit boxes, this bridge copies
200
+ all poses once and updates a single thin-instance matrix buffer. Here `physics`
201
+ was created with `syncMeshes: false`, and `boxes` contains box aggregates created
202
+ with `sync: false`. Their original visual meshes should be hidden; `source` is
203
+ an independent unit-box mesh at the origin with identity rotation and scale.
204
+
205
+ ```js
206
+ import { Matrix, Quaternion, Vector3 } from "@babylonjs/core";
207
+
208
+ physics.initialize();
209
+ const indices = boxes.map((aggregate) => aggregate.body.gpuIndex);
210
+ const matrices = new Float32Array(boxes.length * 16);
211
+ const matrix = Matrix.Identity();
212
+ const position = Vector3.Zero();
213
+ const rotation = Quaternion.Identity();
214
+ const scale = Vector3.One();
215
+ let pending = false;
216
+
217
+ source.thinInstanceSetBuffer("matrix", matrices, 16, false);
218
+ scene.onBeforeRenderObservable.add(() => {
219
+ if (pending) return;
220
+ pending = true;
221
+ physics
222
+ .readBodies()
223
+ .then((poses) => {
224
+ for (let i = 0; i < indices.length; i++) {
225
+ const offset = indices[i] * 40;
226
+ position.set(poses[offset], poses[offset + 1], poses[offset + 2]);
227
+ rotation.set(...poses.subarray(offset + 4, offset + 8));
228
+ Matrix.ComposeToRef(scale, rotation, position, matrix);
229
+ matrix.copyToArray(matrices, i * 16);
230
+ }
231
+ source.thinInstanceBufferUpdated("matrix");
232
+ source.thinInstanceRefreshBoundingInfo();
233
+ })
234
+ .catch(console.error)
235
+ .finally(() => {
236
+ pending = false;
237
+ });
238
+ });
239
+ ```
240
+
241
+ This is a CPU bridge for moderate counts. It still copies the full body buffer,
242
+ calculates matrices and uploads them. Bounding updates also cost CPU time. For
243
+ known contained motion, a deliberately conservative bounding volume can avoid
244
+ rebuilding bounds each update; ensure it covers all moving instances.
245
+ For hulls or meshes with offset origins, reconstruct the mesh transform using
246
+ the aggregate's local centre and principal rotation as the adapter does; the
247
+ unit-box example does not handle those offsets.
248
+
249
+ For a large scene:
250
+
251
+ ```js
252
+ const physics = await AvbdPhysics.create({
253
+ capacity: 100_001,
254
+ iterations: 4,
255
+ syncMeshes: false,
256
+ });
257
+ // Create all bodies before initialize(), with { sync: false }.
258
+ physics.initialize();
259
+ const bodyBuffer = physics.bodyBuffer;
260
+ const gpuIndex = aggregate.body.gpuIndex;
261
+ physics.step(); // One fixed step; submission does not await completion.
262
+ ```
263
+
264
+ A custom WebGPU renderer can read `bodyBuffer` on the **same GPUDevice** and draw
265
+ instances from live poses. Buffers cannot be shared across devices. The adapter
266
+ does not automatically connect this storage buffer to Babylon thin instances,
267
+ and it does not access Babylon private engine fields. If your renderer exposes
268
+ a device you own, pass it to `AvbdPhysics.create({ device, ... })`. A caller-owned
269
+ device stays alive when the world is disposed.
270
+
271
+ Thin instances do not automatically create colliders. The aggregate API takes
272
+ one mesh per physics body, which has a startup cost for very large scenes.
273
+ For 100K bodies, the laboratory's native body/scene builders avoid creating
274
+ 100K Babylon mesh objects; see [scene registry](../demo/lab/scenes.js).
275
+ Import `AvbdScene2D` or `AvbdScene3D` from `avbd-babylon/native` for the same
276
+ renderer-independent integration. Both expose live body handles, bulk commands,
277
+ packed velocity uploads, selected pose reads, triggers, masks, restitution,
278
+ sleeping and contact events. See the package README's
279
+ [native examples](../README.md#native-physics-with-any-renderer).
280
+ The direct-buffer renderer and small-scene aggregate bridge are separate
281
+ integration paths.
282
+
283
+ See [canonicalRenderer.js](../demo/shared/rendering/canonicalRenderer.js) and
284
+ [canonicalRender.wgsl](../demo/shared/rendering/canonicalRender.wgsl) for the direct rendering path,
285
+ and [mixed collider demo](../demo/mixed-colliders/index.js) for the Babylon mesh bridge.
286
+ The laboratory scene builders use z up; the Babylon adapter uses y up by
287
+ default. Match your renderer's coordinates to the world you create.
288
+
289
+ The 3D body stride is 40 floats / 160 bytes: position at 0, quaternion xyzw at 4,
290
+ size at 16, mass at 19, principal inertia at 20, linear velocity at 32 and angular
291
+ velocity at 36. Spatial sorting changes GPU order. Use `body.gpuIndex` after
292
+ initialization rather than assuming creation order equals buffer order.
293
+
294
+ Use shared geometry/materials and a small number of draw calls. For independent
295
+ solver measurements, omit drawing entirely. For whole-app comparisons, keep
296
+ resolution, antialiasing, shadows, materials and camera fixed. Enhanced colors,
297
+ lighting and 4× MSAA are interactive presentation options in this app;
298
+ `quality=benchmark` disables them. Keeping physics settings fixed makes a
299
+ rendering optimization easier to identify.
300
+
301
+ ## Reproduce the collision and solver benchmarks
302
+
303
+ Start this repository with `npm start`, then open
304
+ [/tests/performance.html](http://127.0.0.1:8080/tests/performance.html).
305
+ Select a scene and play it before measuring. The preview closes during a run
306
+ and restarts afterward. “Run all scenes” measures the complete scene catalog, including
307
+ 10K, 50K and 100K dense 2D piles.
308
+
309
+ For a repeatable solver-only run, use this JavaScript in the benchmark page's
310
+ developer console after it is ready. `__PERFORMANCE__` is the repository's test
311
+ harness, not part of the Babylon world API.
312
+
313
+ ```js
314
+ const measurements = [];
315
+ await __BENCHMARK_VIEW__.suspendPreview();
316
+ try {
317
+ for (const scene of [
318
+ "2d-stress-pile-10k",
319
+ "2d-stress-pile-50k",
320
+ "2d-stress-pile-100k",
321
+ "showcase-box-columns-100k",
322
+ "showcase-brick-ring-110k",
323
+ ]) {
324
+ for (let repeat = 0; repeat < 3; repeat++) {
325
+ const result = await __PERFORMANCE__.run(scene, "production", 60, 60);
326
+ measurements.push({ ...result, repeat });
327
+ }
328
+ }
329
+ console.table(
330
+ measurements.map((r) => ({
331
+ scene: r.scene,
332
+ objects: r.bodyCount,
333
+ contacts: r.peakContacts,
334
+ iterations: r.params.iterations,
335
+ collisionMs: r.summary.collision.mean,
336
+ movementMs: r.summary.solve.mean,
337
+ physicsMs: r.summary.total.mean,
338
+ slowerStepsMs: r.summary.total.p95,
339
+ })),
340
+ );
341
+ const data = { adapter: __PERFORMANCE__.info, measurements };
342
+ const url = URL.createObjectURL(
343
+ new Blob([JSON.stringify(data, null, 2)], {
344
+ type: "application/json",
345
+ }),
346
+ );
347
+ const download = document.createElement("a");
348
+ download.href = url;
349
+ download.download = "avbd-replication.json";
350
+ download.click();
351
+ setTimeout(() => URL.revokeObjectURL(url), 1000);
352
+ } finally {
353
+ await __BENCHMARK_VIEW__.preview.load(document.getElementById("scene").value);
354
+ }
355
+ ```
356
+
357
+ Each run starts fresh, warms up for 60 steps and records 60 distinct GPU
358
+ timestamp samples. Every step checks contact capacity and scheduling conflicts.
359
+ The 2D stress scenes use touching boxes, gravity, friction and ten iterations,
360
+ without sleeping. The 3D showcase uses each scene's recorded settings. Compare
361
+ collision time with contact count; equal body counts across dimensions do not
362
+ guarantee equal collision work.
363
+
364
+ For controlled engineering profiles, use `scripts/profile-solver.mjs` or
365
+ `scripts/profile-paired.mjs`. Set `AVBD_PROFILE_SCENES`,
366
+ `AVBD_PROFILE_VARIANTS`, `AVBD_PROFILE_WARMUP`, `AVBD_PROFILE_SAMPLES` and
367
+ `AVBD_PROFILE_REPEATS` explicitly. Public benchmark controls use the production
368
+ automatic choice.
369
+
370
+ GPU work layout selection is automatic in `AppGpuSolver3D` and the Babylon
371
+ adapter. Body count and active joints select the contact calculation; contact
372
+ load selects thread sharing for very large scenes. The 100K columns preset
373
+ shares face-contact points across threads. Dense rigid scenes above 250K bodies
374
+ with at least 1.75 manifolds per body use eight threads per body. The same large
375
+ scenes cache sorting keys for contact lists of up to sixteen entries; longer
376
+ lists retain their general sorting path. Deformable shaders remain intact.
377
+
378
+ ```js
379
+ const world = await AvbdPhysics.create({ scene }); // automatic by default
380
+ // Create aggregates before the render loop.
381
+ world.initialize();
382
+ console.log(world.solverDecision); // implementation and reason
383
+ ```
384
+
385
+ Body and joint changes are reconsidered before the next step. Contact-load
386
+ adaptation reuses existing capacity-counter readbacks rather than adding a
387
+ per-frame pose copy. Positions, velocities, friction, rotation and iteration
388
+ counts are retained. The policy follows measurements on the test laptop;
389
+ results on other GPUs may differ. Advanced `setSolverMode()` overrides remain
390
+ available for controlled engineering measurements and compatibility. The
391
+ browser benchmark runs the production automatic choice.
392
+
393
+ Web Workers can keep scene construction and command submission from blocking
394
+ the page. They do not divide the already-parallel GPU kernels among CPU threads.
395
+ The current API runs on the caller's thread. A complete worker integration would
396
+ own the GPU solver and an OffscreenCanvas renderer together, send inputs and
397
+ small status messages, and draw directly from the GPU body buffer. Copying every
398
+ body's pose between threads each frame would defeat that design. Worker-based
399
+ Babylon drawing also requires a compatible Babylon engine setup and replacing
400
+ DOM-dependent controls with forwarded input messages. The root [README worker example](../README.md#worker-integration) shows the
401
+ OffscreenCanvas setup and message forwarding. Use the benchmark's `setup` and
402
+ `summary.submission` fields to
403
+ judge the CPU cost separately from GPU physics.
404
+
405
+ Both GPU solvers hold poses and velocities if a step runs
406
+ out of contact storage or has conflicting solve groups. The host can then grow
407
+ capacity without advancing an incomplete movement solve. Counters still report
408
+ the failure: a held step does not pass correctness checks. This safeguard does
409
+ not change valid-step equations or remove the need to reserve enough capacity.
410
+
411
+ `npm run test:web` also checks screenshots, controls, benchmark preview lifecycle,
412
+ navigation, and six-second dense 2D motion. Keep other GPU workloads closed.
413
+
414
+ Lower GPU milliseconds are better; higher app FPS is better. Physics timings
415
+ separate collision checks, contact setup, scheduling and movement calculations.
416
+ They exclude drawing, sleeping, CPU submission and copying results back.
417
+ Instrumented timing passes add overhead. The “CPU + wait” column includes GPU
418
+ work and readback, so it is not an isolated CPU cost. Report GPU model, driver,
419
+ scene, timestep, iterations, sample counts and contact counts with comparisons.
420
+
421
+ The premade reports were recorded on an **NVIDIA GeForce RTX 4070 Laptop GPU**.
422
+ They are saved examples; running the browser benchmark measures your own GPU.
423
+
424
+ The [solver report](http://127.0.0.1:8080/test-results/solver-performance.html)
425
+ includes the [AVBD paper](https://graphics.cs.utah.edu/research/projects/avbd/Augmented_VBD-SIGGRAPH25.pdf)
426
+ comparison. Its RTX 4090 / DirectX / LBVH results and this browser's
427
+ RTX 4070 Laptop / WebGPU / spatial-grid results have different hardware and
428
+ timing boundaries. They are workload context, not a same-hardware speed ratio.
429
+
430
+ ## Current shape and API support
431
+
432
+ Boxes, spheres and capsules have native collision geometry. Capsules use exact
433
+ rounded segments along local Y. Cylinders and custom hulls use polygonal convex
434
+ hulls, limited to 32 vertices. Meshes must be root
435
+ meshes with positive scale; bake parent or negative transforms first. Colliders
436
+ capture geometry, mass properties and transforms at creation. Sphere dimensions
437
+ must be uniform.
438
+
439
+ The adapter supports restitution, friction, velocity and impulse commands,
440
+ teleports, collision masks, contact/trigger events, GPU raycasts, per-body sleep
441
+ eligibility, ball joints, welds, elastic or breakable springs, 3D hinges with
442
+ angle stops and torque-limited motors. Both dimensions expose GPU raycasts;
443
+ 3D sphere casts and 2D circle casts include rounded corner tests. It has no
444
+ complete Physics V2 plugin or concave mesh collider.
445
+ Static bodies with velocity move kinematically. Portable builders include 3D
446
+ ropes, ragdolls and fabric. Automatic mesh synchronization is limited to 2048
447
+ bodies by default; large scenes should draw GPU poses directly.
448
+ For full method signatures and ownership rules, see [BABYLON_API.md](BABYLON_API.md).
449
+
450
+ ## Choosing collision detection
451
+
452
+ `AvbdPhysics.create` accepts `broadphase: "auto"` (default), `"grid"` or
453
+ `"hploc"`. Automatic selection uses a GPU tree for large scenes with varied
454
+ collider sizes or many linked thin plates, such as the ragdoll net.
455
+ Small scenes and uniform block scenes retain the grid. Measure your own workload;
456
+ body count alone does not predict which method is faster.
457
+
458
+ ```js
459
+ const physics = await AvbdPhysics.create({
460
+ scene,
461
+ capacity: 50000,
462
+ syncMeshes: false,
463
+ broadphase: "auto",
464
+ bvh: { rebuildInterval: 64 },
465
+ });
466
+ ```
467
+
468
+ The tree refits every collider each step, including rotations and teleports.
469
+ Rebuilds improve its quality; refitting preserves coverage between rebuilds.
470
+ The same narrowphase, friction and rotational AVBD equations handle its pairs.
471
+ See [GPU tree design and limits](HPLOC.md) for implementation details.
472
+
473
+ For a controlled comparison, open `/tests/performance.html`. Its collision
474
+ selector compares the grid and tree without changing solving rounds. Both
475
+ methods are also available in the playable laboratory.
476
+
477
+ ```js
478
+ try {
479
+ const pair = await __PERFORMANCE__.runPaired(
480
+ "3d-mixed-sizes-50k",
481
+ ["grid", "hploc"],
482
+ 60,
483
+ 60,
484
+ );
485
+ console.table(
486
+ pair.results.map((r) => ({
487
+ collisions: r.broadphase,
488
+ collisionMs: r.summary.collision.mean,
489
+ physicsMs: r.summary.total.mean,
490
+ firstStepMs: r.startup.total,
491
+ })),
492
+ );
493
+ } finally {
494
+ await __BENCHMARK_VIEW__.preview.load(document.querySelector("#scene").value);
495
+ }
496
+ ```
497
+
498
+ Use `grid-detail` / `hploc-detail` to split collision preparation, nearby-pair
499
+ search, exact contact checks, body solving and contact-force updates. These
500
+ extra timers add pass boundaries, so compare matching modes. Paired runs keep
501
+ two independent simulations in memory; their absolute times are separate from
502
+ the single-world paper comparison. Raw JSON retains startup, rebuild spikes,
503
+ all samples, buffer bytes, selection decisions and noisy timing flags.