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