avbd-babylon 0.0.0-stage → 0.2.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/README.md CHANGED
@@ -1,3 +1,2607 @@
1
- # Temporary Holding Version
1
+ # AVBD for Babylon.js
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ **TRY ME!!!** [**babylon-avbd.netlify.app**](https://babylon-avbd.netlify.app/)
4
+
5
+ `avbd-babylon` runs AVBD rigid-body physics and collision detection on WebGPU,
6
+ with an aggregate/body API that follows familiar Babylon.js conventions.
7
+ The package bundles its solver and shaders, with **no Babylon runtime dependency**.
8
+ Automatic GPU work sharing and collision selection are enabled by default.
9
+ AVBD calculates collisions, movement, rotation and friction. Babylon renders
10
+ the resulting poses; `AvbdPhysics` owns the physics world and its GPU solver.
11
+
12
+ **Jump to:**
13
+
14
+ - [Install](#install)
15
+ - [What this package adds](#what-this-package-adds)
16
+ - [Feature coverage](#feature-coverage)
17
+ - [Playable feature demos](#playable-feature-demos)
18
+ - [Screenshot gallery](#screenshot-gallery)
19
+ - [Gallery: destruction and large scenes](#destruction-and-large-scenes)
20
+ - [Gallery: cloth and connected bodies](#cloth-and-connected-bodies)
21
+ - [Gallery: colliders, controls and rendering](#colliders-controls-and-rendering)
22
+ - [Complete falling-box example](#complete-falling-box-example)
23
+ - [World and body settings](#world-and-body-settings)
24
+ - [Mixed collider gallery](#mixed-collider-gallery)
25
+ - [Brick wall and reusable cannonball](#brick-wall-and-reusable-cannonball)
26
+ - [Large pile with thin instances](#large-pile-with-thin-instances)
27
+ - [Direct GPU rendering](#direct-gpu-rendering)
28
+ - [Native physics with any renderer](#native-physics-with-any-renderer)
29
+ - [Native 3D world](#native-3d-world)
30
+ - [Native 2D world](#native-2d-world)
31
+ - [Bulk edits and GPU dispatches](#bulk-edits-and-gpu-dispatches)
32
+ - [Reading selected bodies and contacts](#reading-selected-bodies-and-contacts)
33
+ - [Joints, welds, springs, ropes and ragdolls](#joints-welds-springs-ropes-and-ragdolls)
34
+ - [Ball joints, welds and springs](#ball-joints-welds-and-springs)
35
+ - [Ropes and ragdolls](#ropes-and-ragdolls)
36
+ - [Tearable cloth](#tearable-cloth)
37
+ - [Controlling bodies](#controlling-bodies)
38
+ - [Bounce, triggers, masks and sleeping](#bounce-triggers-masks-and-sleeping)
39
+ - [3D hinges, angle stops and motors](#3d-hinges-angle-stops-and-motors)
40
+ - [Queries, callbacks and sleeping](#queries-callbacks-and-sleeping)
41
+ - [2D GPU physics](#2d-gpu-physics)
42
+ - [2D sleeping and hinge stops](#2d-sleeping-and-hinge-stops)
43
+ - [Slingshot siege demo](#slingshot-siege-demo)
44
+ - [Manual stepping and cleanup](#manual-stepping-and-cleanup)
45
+ - [Worker integration](#worker-integration)
46
+ - [Benchmark replication](#benchmark-replication)
47
+ - [Babylon renderer comparison](#babylon-renderer-comparison)
48
+ - [Script-tag usage](#script-tag-usage)
49
+ - [Build and run the repository](#build-and-run-the-repository)
50
+ - [Validation and attribution](#validation-and-attribution)
51
+ - [First release checks](docs/RELEASE.md)
52
+
53
+ ## Install
54
+
55
+ With a published release and your project's ESM bundler:
56
+
57
+ ```sh
58
+ npm install avbd-babylon @babylonjs/core
59
+ ```
60
+
61
+ For local development, run `npm install` and `npm run build` in this repository,
62
+ then run `npm install /absolute/path/to/avbd` in your Babylon project. `npm pack`
63
+ creates an installable `avbd-babylon-0.2.0.tgz` archive too.
64
+
65
+ The package includes ESM, TypeScript declarations and a browser global build.
66
+ Babylon 9 is an optional peer dependency; integration examples are checked with
67
+ 9.13.0. Installing AVBD alone does not install Babylon. For TypeScript Babylon
68
+ projects, install `@babylonjs/core` alongside AVBD as shown above.
69
+ For portable 2D/3D physics without Babylon, import from `avbd-babylon/native`.
70
+ That entry includes `AvbdScene2D`, `AvbdScene3D` and `createWebGPUDevice`, with
71
+ independent declarations. Current TypeScript DOM libraries include the WebGPU
72
+ types; older compilers may need `@webgpu/types` in the project's `types` setting.
73
+ Your application does not need a WGSL loader or the repository's TypeScript sources.
74
+ WebGPU requires a supported browser on localhost or HTTPS. Small-scene drawing
75
+ can use Babylon's WebGL or WebGPU engine; physics uses WebGPU in either case.
76
+
77
+ The scene browser runs a small 3D floor-contact check once per GPU device before
78
+ opening a 3D GPU scene. A box, sphere and capsule must settle on a thin floor.
79
+ GPUs that pass keep the normal batched execution and scene-based collision
80
+ selection. If it fails, the check tests GPU compatibility alternatives for contact
81
+ data, contact calculations, work sharing and collision search. It uses a path only after it
82
+ passes the same physics checks. These compatibility paths keep the equations,
83
+ timestep and iterations unchanged. The check runs at startup; it adds no
84
+ readbacks during normal simulation.
85
+ If every path fails, the demo stops and offers a diagnostic report under
86
+ **GPU contact check** in the sidebar. The report includes contact results, GPU
87
+ limits, collision counts, contact forces, solver dispatches and browser details.
88
+ It also checks whether the contact inputs and calculated forces agree on the GPU.
89
+ `npm run test:mobile-contacts` checks the same physics
90
+ with eight and nine storage bindings and checks the mobile layout with both
91
+ renderers; testing the layout does not substitute for testing a phone's GPU.
92
+ The [physical-phone floor check](test-results/connected-phone-floor.json) verifies
93
+ boxes, spheres and capsules in Android Brave. `npm run test:connected-phone`
94
+ checks 2D/3D collisions, joints, bounce, triggers, masks, sleeping and queries
95
+ on an authorized, unlocked Android phone running the local demo.
96
+
97
+ `AvbdPhysics.create()` prepares its device automatically. Native 3D applications
98
+ can qualify a new device when creating it, or prepare a renderer-owned device
99
+ once before creating any solvers. A failed check throws instead of running
100
+ physics with broken contacts. 2D does not need the 3D check.
101
+
102
+ ```js
103
+ import { createWebGPUDevice, prepareWebGPUDevice3D } from "avbd-babylon/native";
104
+
105
+ const { device } = await createWebGPUDevice({
106
+ validate3D: true,
107
+ preferredLimits: { maxStorageBuffersPerShaderStage: 9 },
108
+ });
109
+ // Or, for an existing renderer-owned device:
110
+ await prepareWebGPUDevice3D(rendererDevice);
111
+ // scene3D.createSolver(device) now uses the verified GPU path.
112
+ ```
113
+
114
+ [Direct renderer on a mobile layout](test-results/screenshots/mobile-contact-webgpu.png) ·
115
+ [Babylon renderer on a mobile layout](test-results/screenshots/mobile-contact-babylon.png).
116
+
117
+ ## What this package adds
118
+
119
+ The package builds on the upstream AVBD solver with GPU collision detection,
120
+ portable 2D/3D APIs and a Babylon integration:
121
+
122
+ - **GPU collision trees and automatic selection.** The HPLOC++ path builds a
123
+ hierarchy of nearby objects, updates its bounds every step and periodically
124
+ rebuilds it. Automatic selection uses a grid for uniform piles and walls,
125
+ and the tree for large scenes with varied object sizes or linked thin plates.
126
+ Both feed the same contact solver. The tree uses a portable hierarchical PLOC
127
+ implementation with GPU treelet clustering and stackless traversal.
128
+ See [the algorithm, selection rules and benchmarks](docs/HPLOC.md).
129
+ - **A broad 2D and 3D feature set.** Both GPU APIs provide rounded primitives,
130
+ custom convex hulls, bounce, collision masks, non-solving triggers, contact
131
+ events, filtered ray/shape casts and live body commands. Both support anchors,
132
+ breakable joints, springs, hinge limits and torque-limited motors. 3D helpers
133
+ also build ropes, ragdolls and elastic or tearable fabric. The table below
134
+ records differences and limits; the playable demos show the features together.
135
+ - **GPU execution for large scenes.** Scene-dependent work layouts and contact
136
+ scheduling share work across the GPU while retaining the selected AVBD
137
+ equations, timestep and iteration count. Direct GPU pose buffers let a
138
+ compatible renderer draw many bodies without downloading every pose. When
139
+ CPU poses are needed, callers can request only the bodies they use.
140
+ - **Sleeping and live editing.** Per-body sleep permission, support checks and
141
+ impact checks allow resting objects to sleep and wake again. Velocities,
142
+ impulses, teleports, filters and motor targets can change during simulation.
143
+ Welds capture their rest rotation from live GPU poses, and released
144
+ constraint slots can be reused.
145
+ - **Babylon and portable integration.** Aggregate/body handles, automatic scene
146
+ stepping and disposal, fixed-step control, module-worker compatibility,
147
+ TypeScript declarations and ESM/CDN builds are included. Babylon remains an
148
+ optional peer dependency. The portable APIs create physics without creating
149
+ rendering meshes. Native 2D and 3D body handles share velocity, impulse,
150
+ teleport, trigger, restitution, mask and sleep controls. Bulk edits reuse GPU
151
+ upload buffers and group commands by body.
152
+ - **Validation and reproducible measurements.** GPU tests check physical
153
+ behavior, collider pair coverage, queries, constraints, sleeping and failure
154
+ recovery. Collision overflow or conflicting parallel groups hold a step and
155
+ report the problem instead of silently dropping work. Browser tests cover all
156
+ 68 scenes, controls, screenshots and stage timings; replication examples
157
+ distinguish physics, drawing and the paper's measurements.
158
+
159
+ The underlying solver and retained upstream code are credited in
160
+ [Validation and attribution](#validation-and-attribution).
161
+
162
+ ## Feature coverage
163
+
164
+ The package includes both the Babylon aggregate API and portable GPU scene
165
+ builders (`AvbdScene3D` and `AvbdScene2D`). Integration examples use package exports;
166
+ benchmark replication also uses the repository's browser test harness.
167
+ It is an alternative physics integration, **not a drop-in Havok/Physics V2
168
+ plugin**: use `AvbdPhysicsAggregate`, rather than Babylon's `PhysicsAggregate`
169
+ and `scene.enablePhysics()`.
170
+
171
+ | Feature | 3D GPU | 2D GPU | Babylon / package usage |
172
+ | ------------------------------------------------------ | -------------------- | --------------------- | -------------------------------------------------------------------------------------------------------- |
173
+ | Static and dynamic bodies, rotation, inertia, friction | Yes | Yes | Aggregates in 3D; native 2D boxes, circles and capsules. |
174
+ | Boxes | Yes | Yes | Native collision shapes. |
175
+ | Spheres / circles | Analytic | Analytic | 3D `addSphere()`; 2D `addCircle()`. Exact rounded surfaces. |
176
+ | Capsules | Analytic | Analytic | 3D local Y, 2D local X. Full height / length includes both caps. |
177
+ | Finite lines / infinite planes | Use static colliders | Yes | 2D `addSegment()` is two-sided; `addPlane()` defines a solid half-space. |
178
+ | Cylinders | Convex hulls | No | Polygonal 3D approximations from mesh vertices. |
179
+ | Custom convex hulls | Yes | Yes | `addHull()` in both dimensions. Up to 32 boundary vertices. |
180
+ | Concave triangle surfaces | No | No | Convex proxies fill concavities. |
181
+ | Body velocity, forces, impulses and teleporting | Yes | Yes | Native `gpu.body()` handles or indexed solver methods in both dimensions; live GPU poses. |
182
+ | Batched body commands and packed velocity edits | Yes | Yes | `editBodies()` groups motion and property edits; packed linear/angular updates use one dispatch. |
183
+ | Automatic Babylon stepping and disposal | Yes | Yes | `AvbdPhysics` or native `gpu.attachToScene(scene)` in either dimension. |
184
+ | Manual fixed steps / worker execution | Yes | Yes | No renderer or DOM dependency in either solver. |
185
+ | Mobile browsers with WebGPU | Yes | Yes | Verified on Android Brave. Babylon's async factory and the scene browser prepare 3D automatically; native 3D apps use `validate3D:true` or `prepareWebGPUDevice3D()`. Requires WebGPU and HTTPS or localhost. |
186
+ | Automatic grid / HPLOC++ choice | Yes | Grid | 3D auto selection uses body sizes and workload. |
187
+ | GPU work sharing | Yes | Yes | Retains the configured physics and iteration counts. |
188
+ | Mesh / instance rendering | Yes | Yes | Use batch drawing for large scenes; portable builders do not create meshes. |
189
+ | Direct GPU pose drawing | Yes | Yes | Renderer and solver must share a GPUDevice. Body layouts differ by dimension. |
190
+ | Ball joints / planar hinges | Yes | Yes | 3D `addJoint()`; 2D joint with free angular row. |
191
+ | World anchors | Yes | Yes | Null body A in 3D; body index `-1` in 2D. |
192
+ | Welds and breakable joints | Yes | Yes | 3D `addWeld()`; 2D joint with all three rows locked and optional `breakForce`. |
193
+ | Springs, linked ropes and ragdolls | Yes | Yes | 3D helpers; 2D bodies linked with joints/springs. |
194
+ | Hinges | Axis + angle limits | Planar + angle limits | `addHinge()` in both dimensions; optional angle bounds relative to creation. |
195
+ | Torque-limited motors | Yes | Yes | Both dimensions expose motor handles and live `gpu.setMotor()` controls. |
196
+ | Elastic / tearable fabric | Yes | Build spring networks | 3D `addFabric()` has point collisions and tensile fracture. The demo preserves material after tearing. |
197
+ | Per-body sleeping / waking | Yes | Yes | World `sleeping`, body `allowSleep`, live `setSleepEnabled()`; supported bodies rest, impacts wake them. |
198
+ | Restitution / bounce | Yes | Yes | Coefficient 0..1. Zero adds no bounce passes. |
199
+ | Collision masks | Yes | Yes | Both bodies must allow the pair. Includes sensors. |
200
+ | Contact / trigger events | Yes | Yes | Babylon `onContact()`; native `gpu.watchContacts()` + `readContactEvents()` in both dimensions. |
201
+ | Non-solving triggers | Yes | Yes | `isTrigger:true`; no contact forces. Static–static pairs are not scanned. |
202
+ | Raycasts | Yes | Yes | `physics.raycast()` or portable `gpu.raycast()`; masks and ignored bodies. |
203
+ | Sphere / circle casts | Yes | Yes | 3D `sphereCast()`; 2D `circleCast()`; exact rounded corner tests. |
204
+ | Continuous collision detection | No | No | Discrete contacts; timestep matters for fast objects. |
205
+ | Full Babylon Physics V2 / Havok plugin | No | No | Use AVBD aggregates, rather than `scene.enablePhysics()`. |
206
+
207
+ See the [API reference](docs/BABYLON_API.md) and [GPU validation guide](tests/README.md)
208
+ for supported methods, numerical checks and limitations.
209
+
210
+ ## Playable feature demos
211
+
212
+ Run `npm start` and open the links below. Every scene uses GPU physics. The
213
+ [scene browser and benchmark](tests/performance.html)
214
+ includes all 68 presets and lets you play them before measuring performance.
215
+ The [scene check report](test-results/report.html) contains
216
+ screenshots and results after `npm run test:web`.
217
+
218
+ | Features to try | 2D example | 3D example |
219
+ | ------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
220
+ | Rounded colliders, custom hulls, friction and bounce | [Mixed colliders](index.html?demo=canonical&scene=2d-mixed-shapes&backend=gpu) | [Mixed gallery](index.html?demo=showcase), [hulls](index.html?demo=canonical&scene=3d-convex-hulls&backend=gpu) |
221
+ | Hinges, angle limits and motors | [Reverse or brake the motors](index.html?demo=canonical&scene=2d-limited-hinges&backend=gpu) | [Reverse or brake the motors](index.html?demo=canonical&scene=3d-limited-hinges&backend=gpu) |
222
+ | Triggers, collision masks and live filter changes | [Toggle shelf collisions and detection](index.html?demo=canonical&scene=2d-triggers-and-masks&backend=gpu) | [Toggle shelf collisions and detection](index.html?demo=canonical&scene=3d-triggers-and-masks&backend=gpu) |
223
+ | Filtered ray and circle/sphere casts | [Cast buttons](index.html?demo=canonical&scene=2d-triggers-and-masks&backend=gpu) | [Cast buttons](index.html?demo=canonical&scene=3d-triggers-and-masks&backend=gpu) |
224
+ | Anchors, ropes, springs, ragdolls, fracture and live impulses | [Slingshot siege](index.html?demo=slingshot) | [Ropes](index.html?demo=canonical&scene=3d-rope&backend=gpu), [rigid net and ragdolls](index.html?demo=canonical&scene=showcase-ragdolls-on-cloth-24k&backend=gpu) |
225
+ | Welded construction and breakable joints | [Slingshot forts](index.html?demo=slingshot) | [Castle siege](index.html?demo=cannon) |
226
+ | Sleeping and waking | [Resting pile](index.html?demo=canonical&scene=2d-sleeping-pile&backend=gpu), [slingshot impacts](index.html?demo=slingshot) | [100K rook impact](index.html?demo=gpu-stress) |
227
+ | Deformable fabric and persistent tearing | Spring networks in [slingshot siege](index.html?demo=slingshot) | [Tearable cloth](index.html?demo=canonical&scene=3d-tearable-cloth&backend=gpu), [fabric and blocks](index.html?demo=canonical&scene=paper-cloth-35k&backend=gpu) |
228
+ | Large collision workloads and batch rendering | [100K pile](index.html?demo=canonical&scene=2d-stress-pile-100k&backend=gpu) | [100K columns](index.html?demo=canonical&scene=showcase-box-columns-100k&backend=gpu), [half-million walls](index.html?demo=canonical&scene=paper-walls-510k-4&backend=gpu) |
229
+
230
+ The ragdoll net and 100K columns use five solver iterations. Individual scenes
231
+ keep their own timestep and iteration count; the benchmark records those
232
+ settings. Native builders create physics bodies without rendering objects.
233
+ Babylon integration, workers, disposal and body commands are covered by the
234
+ usage examples below and package tests rather than separate visual scenes.
235
+
236
+ The [Voronoi demolition](index.html?demo=canonical&scene=3d-voronoi-demolition&backend=gpu) adds a suspended wrecking ball, irregular convex stone pieces and breakable face welds. The pieces extend through the wall thickness and remain in the scene after breaking loose.
237
+
238
+ ## Screenshot gallery
239
+
240
+ These captures come directly from the [browser test results](test-results/report.html).
241
+ Click an image to open the full-size screenshot. Use the
242
+ [playable demo links](#playable-feature-demos) to explore the scenes yourself.
243
+ The saved runs use an **RTX 4070 Laptop GPU**. Timing overlays show a captured
244
+ step; the [solver performance report](test-results/solver-performance.html)
245
+ contains the repeated measurements and hardware details.
246
+
247
+ | Intact Voronoi stone wall | Wrecking-ball fracture |
248
+ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
249
+ | [![A suspended wrecking ball beside an intact irregular stone wall](test-results/screenshots/voronoi-120.png)](test-results/screenshots/voronoi-120.png)<br>216 convex stones fill a bounded Voronoi partition; shared faces carry breakable welds. | [![A wrecking ball opening a hole in the wall with persistent stone rubble](test-results/screenshots/voronoi-1440.png)](test-results/screenshots/voronoi-1440.png)<br>[Play the demolition](index.html?demo=canonical&scene=3d-voronoi-demolition&backend=gpu). Released stones remain physical debris. |
250
+
251
+ | Mixed collider gallery | Chain-mail ring spacing |
252
+ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
253
+ | [![Spheres, boxes, capsules, cylinders and convex wedges on a shadowed floor](test-results/screenshots/mixed-gallery-settled.png)](test-results/screenshots/mixed-gallery-settled.png)<br>Babylon meshes follow the GPU poses, with visible contacts and shadows. | [![Interlocking rings with matching wire contact at their rest spacing](test-results/screenshots/chainmail-rest.png)](test-results/screenshots/chainmail-rest.png)<br>Ring sizes follow the link spacing. The net uses jointed rigid proxies; the donuts are decorative geometry. |
254
+
255
+ ### Destruction and large scenes
256
+
257
+ | Castle siege | 100,000-brick impact |
258
+ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
259
+ | [![Castle with mortared walls, towers, battlements and cone roofs](test-results/screenshots/route-cannon.png)](test-results/screenshots/route-cannon.png)<br>Breakable mortar holds the castle together until a cannonball hits. | [![A cannonball has opened a hole through a wall of 100,000 bricks](test-results/screenshots/rook-360.png)](test-results/screenshots/rook-360.png)<br>After six simulated seconds, fallen bricks fill the opening while the outer wall remains standing. |
260
+
261
+ | 110K brick ring | Half-million brick walls |
262
+ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
263
+ | [![A ball striking a circular wall containing roughly 110,000 bricks](test-results/screenshots/showcase-brick-ring-110k-gpu.png)](test-results/screenshots/showcase-brick-ring-110k-gpu.png)<br>A circular masonry wall with a localized impact. | [![Hundreds of triangular brick walls collapsing around rolling balls](test-results/screenshots/paper-walls-510k-4-hploc-360.png)](test-results/screenshots/paper-walls-510k-4-hploc-360.png)<br>505,923 bodies, four solver iterations and the HPLOC++ collision tree. |
264
+
265
+ | 100K box columns | 32K box pile |
266
+ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
267
+ | [![100,000 individually colored boxes drawn together on the GPU](test-results/screenshots/columns-enhanced.png)](test-results/screenshots/columns-enhanced.png)<br>Five solver iterations, batch drawing and optional enhanced colors and lighting. | [![A dense three-dimensional pile of roughly 32,000 boxes](test-results/screenshots/showcase-box-pile-32k-gpu.png)](test-results/screenshots/showcase-box-pile-32k-gpu.png)<br>A dense contact workload for GPU collision detection and solving. |
268
+
269
+ | 2D slingshot siege | 100K 2D collision stress test |
270
+ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
271
+ | [![A slingshot shot has knocked down brick forts and their hanging targets](test-results/screenshots/slingshot-impact.png)](test-results/screenshots/slingshot-impact.png)<br>Breakable forts, ropes, springs, powered gates, sensors and ragdolls. | [![A tightly packed two-dimensional pile of 100,000 boxes](test-results/screenshots/2d-stress-pile-100k-gpu.png)](test-results/screenshots/2d-stress-pile-100k-gpu.png)<br>100,000 touching boxes with gravity, friction and GPU drawing. |
272
+
273
+ ### Cloth and connected bodies
274
+
275
+ | Ragdolls on a rigid net | Persistent torn fabric |
276
+ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
277
+ | [![Thousands of colorful ragdoll limbs resting on a flexible net](test-results/screenshots/showcase-ragdolls-on-cloth-24k-six-seconds.png)](test-results/screenshots/showcase-ragdolls-on-cloth-24k-six-seconds.png)<br>Linked rigid plates catch the ragdolls; this scene uses five solver iterations. | [![A torn fabric patch rests on the floor below an elastic sheet supported at its corners](test-results/screenshots/cloth-fragments-960.png)](test-results/screenshots/cloth-fragments-960.png)<br>The detached patch falls and remains visible and physical, with no automatic lifetime. |
278
+
279
+ | Deformable cloth and 35K blocks | Flag in the wind |
280
+ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
281
+ | [![Thousands of blocks interacting with a deformable cloth sheet](test-results/screenshots/paper-cloth-35k-gpu.png)](test-results/screenshots/paper-cloth-35k-gpu.png)<br>A spring-based fabric workload with point-sampled collisions. | [![A patterned flag made from connected bodies moving in the wind](test-results/screenshots/showcase-flag-in-the-wind-1.5k-gpu.png)](test-results/screenshots/showcase-flag-in-the-wind-1.5k-gpu.png)<br>Connected bodies form a flexible flag. |
282
+
283
+ | Chain mail | Jointed bridge |
284
+ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
285
+ | [![A flexible sheet of linked chain-mail bodies](test-results/screenshots/chainmail-settled.png)](test-results/screenshots/chainmail-settled.png)<br>A network of rigid bodies connected by joints. | [![A three-dimensional bridge built from constrained rigid bodies](test-results/screenshots/3d-bridge-gpu.png)](test-results/screenshots/3d-bridge-gpu.png)<br>Anchored bodies and joints carry the bridge load. |
286
+
287
+ | 3D rope | 2D soft body |
288
+ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
289
+ | [![Jointed rigid segments forming a hanging three-dimensional rope](test-results/screenshots/3d-rope-gpu.png)](test-results/screenshots/3d-rope-gpu.png)<br>A rope built from connected rigid bodies. | [![A two-dimensional connected-body structure deforming under gravity](test-results/screenshots/2d-soft-body-gpu.png)](test-results/screenshots/2d-soft-body-gpu.png)<br>Compliant constraints connect the bodies into a deformable structure. |
290
+
291
+ ### Colliders, controls and rendering
292
+
293
+ | 3D convex hulls | Mixed 2D colliders |
294
+ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
295
+ | [![Custom three-dimensional convex collision shapes](test-results/screenshots/3d-convex-hulls-gpu.png)](test-results/screenshots/3d-convex-hulls-gpu.png)<br>Custom hull geometry participates in the GPU contact solver. | [![Circles, capsules, boxes and custom hulls in a two-dimensional scene](test-results/screenshots/2d-mixed-shapes-gpu.png)](test-results/screenshots/2d-mixed-shapes-gpu.png)<br>Rounded and polygonal shapes share the same 2D world. |
296
+
297
+ | 3D hinges and motors | 2D hinges and motors |
298
+ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
299
+ | [![Powered arms, a limited pendulum and a motorized wheel in three dimensions](test-results/screenshots/3d-limited-hinges.png)](test-results/screenshots/3d-limited-hinges.png)<br>Angle stops, torque-limited motors and live reverse/brake controls. | [![Powered arms and rotating bodies with planar hinge limits](test-results/screenshots/2d-limited-hinges.png)](test-results/screenshots/2d-limited-hinges.png)<br>The same motor and limit controls in a planar world. |
300
+
301
+ | 3D triggers and masks | 2D triggers and masks |
302
+ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
303
+ | [![Colored balls passing through a golden sensor and selectively colliding with a shelf](test-results/screenshots/3d-triggers-and-masks.png)](test-results/screenshots/3d-triggers-and-masks.png)<br>Non-solving overlap detection, collision filtering and ray/sphere casts. | [![Filtered colored balls, a sensor and a shelf in two dimensions](test-results/screenshots/2d-triggers-and-masks.png)](test-results/screenshots/2d-triggers-and-masks.png)<br>Live mask changes, trigger events and ray/circle casts. |
304
+
305
+ | D20 collision benchmark | Direct GPU pose rendering |
306
+ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
307
+ | [![Blue particle sprites surrounding a rotating twenty-sided collider](test-results/screenshots/collision-sprites.png)](test-results/screenshots/collision-sprites.png)<br>GPU particles are drawn as sprites around the rotating collision target. | [![4,096 colored boxes rendered directly from the physics GPU buffer](test-results/screenshots/readme-direct-gpu.png)](test-results/screenshots/readme-direct-gpu.png)<br>The [rendering example below](#direct-gpu-rendering) draws 4,096 boxes without downloading their poses. |
308
+
309
+ ## Complete falling-box example
310
+
311
+ Add this HTML to a bundled Babylon application and use `src/main.js` as its entry:
312
+
313
+ ```html
314
+ <style>
315
+ html,
316
+ body {
317
+ margin: 0;
318
+ width: 100%;
319
+ height: 100%;
320
+ overflow: hidden;
321
+ }
322
+ #canvas {
323
+ width: 100%;
324
+ height: 100%;
325
+ display: block;
326
+ touch-action: none;
327
+ }
328
+ </style>
329
+ <canvas id="canvas"></canvas>
330
+ <script type="module" src="/src/main.js"></script>
331
+ ```
332
+
333
+ ```js
334
+ // src/main.js
335
+ import {
336
+ Engine,
337
+ Scene,
338
+ ArcRotateCamera,
339
+ HemisphericLight,
340
+ MeshBuilder,
341
+ Vector3,
342
+ } from "@babylonjs/core";
343
+ import { AvbdPhysics, AvbdPhysicsAggregate, AvbdShapeType } from "avbd-babylon";
344
+
345
+ const canvas = document.getElementById("canvas");
346
+ const engine = new Engine(canvas, true);
347
+ const scene = new Scene(engine);
348
+ const camera = new ArcRotateCamera(
349
+ "camera",
350
+ -Math.PI / 2,
351
+ Math.PI / 3,
352
+ 12,
353
+ new Vector3(0, 2, 0),
354
+ scene,
355
+ );
356
+ camera.attachControl(canvas, true);
357
+ new HemisphericLight("light", new Vector3(0, 1, 0), scene);
358
+
359
+ const physics = await AvbdPhysics.create({
360
+ scene,
361
+ gravity: new Vector3(0, -9.81, 0),
362
+ timeStep: 1 / 60,
363
+ iterations: 10,
364
+ capacity: 64,
365
+ });
366
+ const floor = MeshBuilder.CreateBox(
367
+ "floor",
368
+ { width: 20, height: 1, depth: 20 },
369
+ scene,
370
+ );
371
+ floor.position.y = -0.5;
372
+ new AvbdPhysicsAggregate(floor, AvbdShapeType.BOX, { mass: 0 }, scene);
373
+ const box = MeshBuilder.CreateBox("box", { size: 1 }, scene);
374
+ box.position.y = 4;
375
+ const fallingBox = new AvbdPhysicsAggregate(
376
+ box,
377
+ AvbdShapeType.BOX,
378
+ { mass: 2, friction: 0.6 },
379
+ scene,
380
+ );
381
+ fallingBox.body.setAngularVelocity(new Vector3(0, 1, 0.5));
382
+
383
+ engine.runRenderLoop(() => scene.render());
384
+ const resize = () => engine.resize();
385
+ window.addEventListener("resize", resize);
386
+ window.addEventListener(
387
+ "pagehide",
388
+ () => {
389
+ window.removeEventListener("resize", resize);
390
+ scene.dispose(); // also disposes its AVBD world
391
+ engine.dispose();
392
+ },
393
+ { once: true },
394
+ );
395
+ ```
396
+
397
+ The world attaches fixed-step simulation and disposal to the scene. The render
398
+ loop only calls `scene.render()`; do not also call `physics.step()` there.
399
+ Pose synchronization is asynchronous, with one outstanding copy and at most
400
+ three catch-up physics steps per rendered frame.
401
+
402
+ Create **all aggregates before the first step or `initialize()`**. Reading
403
+ `bodyBuffer`, `body.gpuIndex` or `solverDecision` also initializes the world.
404
+ Reserve reusable bodies for objects you plan to launch later.
405
+
406
+ ## World and body settings
407
+
408
+ | World option | Default | Use |
409
+ | ----------------- | --------------- | -------------------------------------------------------- |
410
+ | `scene` | None | Attach updates and disposal to a Babylon scene. |
411
+ | `gravity` | `[0, -10, 0]` | `Vector3` or a three-component array; y up by default. |
412
+ | `timeStep` | `1 / 60` | Simulated seconds advanced by each `step()`. |
413
+ | `iterations` | `10` | Solving rounds per fixed step. |
414
+ | `capacity` | `4096` | Aggregate count, including static and reserved bodies. |
415
+ | `autoAttach` | `true` | Set false for a manually managed loop. |
416
+ | `syncMeshes` | `true` | Asynchronously mirror GPU poses to Babylon meshes. |
417
+ | `maxSyncedBodies` | `2048` | Guard against mirroring a huge scene one mesh at a time. |
418
+ | `broadphase` | `"auto"` | Automatic, or explicit `"grid"` / `"hploc"`. |
419
+ | `solverMode` | `"auto"` | Automatic GPU implementation selection. |
420
+ | `bvh` | `{}` | Tree options such as `{ rebuildInterval: 64 }`. |
421
+ | `device` | Created by AVBD | Optional caller-owned GPUDevice. |
422
+
423
+ Aggregate options include `mass` (default 1), `friction` (0.6), `restitution`
424
+ (0, valid range 0–1), `isTrigger` (false), `allowSleep`, `group`, `collidesWith`
425
+ and `sync` (true). Set mesh position, rotation and positive scale before creating
426
+ its aggregate. Meshes must be unparented; bake parent transforms first. Later
427
+ visual scaling does not resize the collider.
428
+
429
+ The final aggregate argument can be the scene or the world, or be omitted when
430
+ the mesh's scene has an AVBD world. Supported Babylon `PhysicsShapeType` enum
431
+ values work too. Add bodies before initialization:
432
+
433
+ ```js
434
+ import { PhysicsShapeType } from "@babylonjs/core";
435
+
436
+ const spherePhysics = physics.addAggregate(
437
+ sphereMesh,
438
+ PhysicsShapeType.SPHERE,
439
+ { mass: 3, friction: 0.5 },
440
+ );
441
+ ```
442
+
443
+ ## Mixed collider gallery
444
+
445
+ Save this helper as `src/mixed-gallery.js`. It recreates a small version of the
446
+ mixed gallery: 30 dynamic objects, a static floor and all five collider types.
447
+ Replace the complete example's world/floor/box block with
448
+ `const { physics } = await createMixedColliderScene(scene)` and import the helper.
449
+ Keep its camera, light and render loop; omit the `fallingBox` command.
450
+
451
+ ```js
452
+ // src/mixed-gallery.js
453
+ import {
454
+ MeshBuilder,
455
+ Quaternion,
456
+ StandardMaterial,
457
+ Color3,
458
+ } from "@babylonjs/core";
459
+ import { AvbdPhysics, AvbdShapeType } from "avbd-babylon";
460
+
461
+ export async function createMixedColliderScene(scene) {
462
+ const physics = await AvbdPhysics.create({
463
+ scene,
464
+ capacity: 31,
465
+ iterations: 10,
466
+ });
467
+ const floor = MeshBuilder.CreateBox(
468
+ "floor",
469
+ { width: 18, height: 1, depth: 12 },
470
+ scene,
471
+ );
472
+ floor.position.y = -0.5;
473
+ physics.addAggregate(floor, AvbdShapeType.BOX, { mass: 0, friction: 0.7 });
474
+ const kinds = [
475
+ [AvbdShapeType.BOX, (n) => MeshBuilder.CreateBox(n, { size: 0.8 }, scene)],
476
+ [
477
+ AvbdShapeType.SPHERE,
478
+ (n) =>
479
+ MeshBuilder.CreateSphere(n, { diameter: 0.8, segments: 12 }, scene),
480
+ ],
481
+ [
482
+ AvbdShapeType.CAPSULE,
483
+ (n) =>
484
+ MeshBuilder.CreateCapsule(
485
+ n,
486
+ {
487
+ height: 1.2,
488
+ radius: 0.2,
489
+ tessellation: 8,
490
+ subdivisions: 2,
491
+ capSubdivisions: 2,
492
+ },
493
+ scene,
494
+ ),
495
+ ],
496
+ [
497
+ AvbdShapeType.CYLINDER,
498
+ (n) =>
499
+ MeshBuilder.CreateCylinder(
500
+ n,
501
+ {
502
+ height: 1,
503
+ diameter: 0.6,
504
+ tessellation: 12,
505
+ },
506
+ scene,
507
+ ),
508
+ ],
509
+ [
510
+ AvbdShapeType.CONVEX_HULL,
511
+ (n) => MeshBuilder.CreatePolyhedron(n, { type: 0, size: 0.5 }, scene),
512
+ ],
513
+ ];
514
+ const materials = ["#e3a044", "#de5656", "#4bbedd", "#59c9a6", "#b59be6"].map(
515
+ (color, i) => {
516
+ const material = new StandardMaterial(`shape-${i}`, scene);
517
+ material.diffuseColor = Color3.FromHexString(color);
518
+ return material;
519
+ },
520
+ );
521
+ const bodies = [];
522
+ for (let row = 0; row < 6; row++) {
523
+ for (let kind = 0; kind < kinds.length; kind++) {
524
+ const [type, createMesh] = kinds[kind];
525
+ const mesh = createMesh(`shape-${row}-${kind}`);
526
+ mesh.position.set(
527
+ (kind - 2) * 2.1,
528
+ 1.2 + Math.floor(row / 3) * 1.7,
529
+ ((row % 3) - 1) * 2,
530
+ );
531
+ mesh.rotationQuaternion = Quaternion.FromEulerAngles(
532
+ 0.15 * row,
533
+ 0.3 * kind,
534
+ 0.1 * row,
535
+ );
536
+ mesh.material = materials[kind];
537
+ bodies.push(physics.addAggregate(mesh, type, { mass: 1, friction: 0.6 }));
538
+ }
539
+ }
540
+ return { physics, bodies };
541
+ }
542
+ ```
543
+
544
+ Sphere, box and capsule collisions use native geometry. Capsules are exact rounded
545
+ segments, along the mesh's local Y axis, with equal X/Z diameters. Their height
546
+ includes both end caps. Rotate the mesh to orient the capsule in the world.
547
+ Cylinders and custom meshes use convex hulls simplified to 32 vertices. A convex
548
+ hull fills input concavities.
549
+
550
+ ## Brick wall and reusable cannonball
551
+
552
+ Save this as `src/brick-wall.js` and use it instead of the small example's body
553
+ creation. It creates 96 bricks, a floor and one projectile before initialization.
554
+ Call `shoot()` from a button or keyboard handler.
555
+
556
+ This is a loose brick wall. The castle's breakable mortar uses native joints;
557
+ the [native constraint examples](#joints-welds-springs-ropes-and-ragdolls) show that API.
558
+
559
+ ```js
560
+ // src/brick-wall.js
561
+ import { MeshBuilder, Vector3, Quaternion } from "@babylonjs/core";
562
+ import { AvbdPhysics, AvbdShapeType } from "avbd-babylon";
563
+
564
+ export async function createBrickWallScene(scene) {
565
+ const physics = await AvbdPhysics.create({
566
+ scene,
567
+ capacity: 98,
568
+ iterations: 10,
569
+ });
570
+ const floor = MeshBuilder.CreateBox(
571
+ "floor",
572
+ { width: 24, height: 1, depth: 24 },
573
+ scene,
574
+ );
575
+ floor.position.y = -0.5;
576
+ physics.addAggregate(floor, AvbdShapeType.BOX, { mass: 0, friction: 0.7 });
577
+ const bricks = [];
578
+ for (let row = 0; row < 8; row++) {
579
+ for (let column = 0; column < 12; column++) {
580
+ const mesh = MeshBuilder.CreateBox(
581
+ `brick-${row}-${column}`,
582
+ {
583
+ width: 0.9,
584
+ height: 0.45,
585
+ depth: 0.6,
586
+ },
587
+ scene,
588
+ );
589
+ mesh.position.set(
590
+ (column - 5.5) * 0.9 + (row % 2) * 0.45,
591
+ 0.225 + row * 0.45,
592
+ 0,
593
+ );
594
+ bricks.push(
595
+ physics.addAggregate(mesh, AvbdShapeType.BOX, {
596
+ mass: 1,
597
+ friction: 0.65,
598
+ }),
599
+ );
600
+ }
601
+ }
602
+ const ball = MeshBuilder.CreateSphere(
603
+ "cannonball",
604
+ { diameter: 1.2, segments: 16 },
605
+ scene,
606
+ );
607
+ ball.position.set(0, 1.3, -8);
608
+ const projectile = physics.addAggregate(ball, AvbdShapeType.SPHERE, {
609
+ mass: 20,
610
+ friction: 0.4,
611
+ });
612
+ function shoot() {
613
+ projectile.body.teleport(new Vector3(0, 1.3, -8), Quaternion.Identity());
614
+ projectile.body.setAngularVelocity(Vector3.Zero());
615
+ projectile.body.setLinearVelocity(new Vector3(0, 0, 22));
616
+ }
617
+ return { physics, bricks, projectile, shoot };
618
+ }
619
+ ```
620
+
621
+ Reusing a projectile preserves its GPU slot. `teleport()` retains velocity,
622
+ so set linear and angular velocity explicitly for each shot. Changing the
623
+ visual mesh position after initialization does not move the physical body.
624
+
625
+ ## Large pile with thin instances
626
+
627
+ This helper draws 2,000 unit boxes through one thin-instance mesh. Each collider
628
+ still has a hidden Babylon mesh during setup. Save as `src/box-pile.js`, use it
629
+ in place of the small example's body creation, and increase the camera radius.
630
+
631
+ ```js
632
+ // src/box-pile.js
633
+ import { MeshBuilder, Matrix, Quaternion, Vector3 } from "@babylonjs/core";
634
+ import { AvbdPhysics, AvbdShapeType } from "avbd-babylon";
635
+
636
+ export async function createBoxPileScene(scene, count = 2000) {
637
+ const physics = await AvbdPhysics.create({
638
+ scene,
639
+ capacity: count + 1,
640
+ iterations: 10,
641
+ syncMeshes: false,
642
+ });
643
+ const floor = MeshBuilder.CreateBox(
644
+ "floor",
645
+ { width: 30, height: 1, depth: 30 },
646
+ scene,
647
+ );
648
+ floor.position.y = -0.5;
649
+ physics.addAggregate(floor, AvbdShapeType.BOX, { mass: 0, sync: false });
650
+ const source = MeshBuilder.CreateBox("drawn-boxes", { size: 1 }, scene);
651
+ const matrices = new Float32Array(count * 16),
652
+ bodies = [];
653
+ for (let i = 0; i < count; i++) {
654
+ const mesh = MeshBuilder.CreateBox(`collider-${i}`, { size: 1 }, scene);
655
+ mesh.position.set(
656
+ ((i % 10) - 4.5) * 1.05,
657
+ 0.55 + Math.floor(i / 100) * 1.05,
658
+ ((Math.floor(i / 10) % 10) - 4.5) * 1.05,
659
+ );
660
+ mesh.isVisible = false;
661
+ bodies.push(
662
+ physics.addAggregate(mesh, AvbdShapeType.BOX, {
663
+ mass: 1,
664
+ friction: 0.6,
665
+ sync: false,
666
+ }),
667
+ );
668
+ mesh.computeWorldMatrix(true).copyToArray(matrices, i * 16);
669
+ }
670
+ physics.initialize();
671
+ const mirroredBodies = bodies.map((a) => a.body);
672
+ source.thinInstanceSetBuffer("matrix", matrices, 16, false);
673
+ source.thinInstanceRefreshBoundingInfo();
674
+ const position = Vector3.Zero(),
675
+ rotation = Quaternion.Identity();
676
+ const scale = Vector3.One(),
677
+ matrix = Matrix.Identity();
678
+ let pending = false;
679
+ scene.onBeforeRenderObservable.add(() => {
680
+ if (pending) return;
681
+ pending = true;
682
+ physics
683
+ .readBodies(mirroredBodies)
684
+ .then((poses) => {
685
+ if (scene.isDisposed) return;
686
+ for (let i = 0; i < mirroredBodies.length; i++) {
687
+ const o = i * 40;
688
+ position.set(poses[o], poses[o + 1], poses[o + 2]);
689
+ rotation.set(poses[o + 4], poses[o + 5], poses[o + 6], poses[o + 7]);
690
+ Matrix.ComposeToRef(scale, rotation, position, matrix);
691
+ matrix.copyToArray(matrices, i * 16);
692
+ }
693
+ source.thinInstanceBufferUpdated("matrix");
694
+ source.thinInstanceRefreshBoundingInfo();
695
+ })
696
+ .catch((error) => {
697
+ if (!scene.isDisposed) console.error(error);
698
+ })
699
+ .finally(() => {
700
+ pending = false;
701
+ });
702
+ });
703
+ return { physics, bodies, source };
704
+ }
705
+ ```
706
+
707
+ Instancing reduces drawing work. This bridge reads poses, calculates
708
+ matrices on the CPU, uploads them and updates bounds. It assumes centered unit
709
+ boxes; custom hulls require the aggregate's center and principal-frame transform.
710
+
711
+ For 100K–500K bodies, use a custom renderer reading `physics.bodyBuffer` on the
712
+ **same GPUDevice** and omit pose readbacks. Spatial sorting changes GPU order;
713
+ use `aggregate.body.gpuIndex` after initialization. The adapter does not
714
+ bind that storage buffer to Babylon thin instances automatically. Native
715
+ laboratory builders also avoid making one Babylon mesh per collider. See
716
+ [rendering at scale](docs/README.md#choose-a-rendering-path) for the body layout,
717
+ renderer source and coordinate conventions.
718
+
719
+ ## Direct GPU rendering
720
+
721
+ This example draws a box world in one instanced WebGPU draw call. The vertex
722
+ shader reads AVBD's live positions, quaternions and sizes. No pose readback,
723
+ CPU transform loop or per-frame instance-matrix upload is needed.
724
+
725
+ Save it as `src/direct-gpu.js` in a bundled application. The scene builder and
726
+ renderer share one device. Babylon's matrix helpers supply the camera math;
727
+ the drawing uses a WebGPU canvas. Call `createDirectGpuExample(canvas)` to
728
+ start, and call its `dispose()` when removing the view.
729
+
730
+ ```js
731
+ // src/direct-gpu.js
732
+ import { Matrix, Vector3 } from "@babylonjs/core/Maths/math.vector.js";
733
+ import { AvbdScene3D, createWebGPUDevice } from "avbd-babylon/native";
734
+
735
+ const boxShader = `
736
+ struct Camera { vp: mat4x4f }
737
+ @group(0) @binding(0) var<storage, read> bodies: array<vec4f>;
738
+ @group(0) @binding(1) var<uniform> camera: Camera;
739
+ struct VertexOut {
740
+ @builtin(position) position: vec4f,
741
+ @location(0) normal: vec3f,
742
+ @location(1) color: vec3f,
743
+ }
744
+ fn rotate(q: vec4f, v: vec3f) -> vec3f {
745
+ return v + 2. * cross(q.xyz, cross(q.xyz, v) + q.w * v);
746
+ }
747
+ @vertex fn vertex(@builtin(vertex_index) v: u32,
748
+ @builtin(instance_index) instance: u32) -> VertexOut {
749
+ let corners = array<vec3f, 8>(
750
+ vec3f(-.5,-.5,-.5), vec3f(.5,-.5,-.5),
751
+ vec3f(.5,.5,-.5), vec3f(-.5,.5,-.5),
752
+ vec3f(-.5,-.5,.5), vec3f(.5,-.5,.5),
753
+ vec3f(.5,.5,.5), vec3f(-.5,.5,.5));
754
+ let indices = array<u32, 36>(
755
+ 0,2,1, 0,3,2, 4,5,6, 4,6,7,
756
+ 0,4,7, 0,7,3, 1,2,6, 1,6,5,
757
+ 0,1,5, 0,5,4, 3,7,6, 3,6,2);
758
+ let normals = array<vec3f, 6>(
759
+ vec3f(0,0,-1), vec3f(0,0,1), vec3f(-1,0,0),
760
+ vec3f(1,0,0), vec3f(0,-1,0), vec3f(0,1,0));
761
+ // A 3D body occupies ten vec4s (40 floats / 160 bytes).
762
+ let offset = instance * 10u;
763
+ let position = bodies[offset].xyz;
764
+ let rotation = bodies[offset + 1u];
765
+ let size = bodies[offset + 4u].xyz;
766
+ var out: VertexOut;
767
+ out.position = camera.vp * vec4f(position + rotate(rotation, corners[indices[v]] * size), 1);
768
+ out.normal = rotate(rotation, normals[v / 6u]);
769
+ let hue = f32(instance % 17u) / 17.;
770
+ out.color = vec3f(.35 + .45 * hue, .55, .8 - .45 * hue);
771
+ return out;
772
+ }
773
+ @fragment fn fragment(input: VertexOut) -> @location(0) vec4f {
774
+ let light = .35 + .65 * max(dot(normalize(input.normal), normalize(vec3f(-.4,1,-.6))), 0.);
775
+ return vec4f(input.color * light, 1);
776
+ }`;
777
+
778
+ export function createGpuBoxRenderer(canvas, getBinding) {
779
+ const device = getBinding().device;
780
+ const context = canvas.getContext("webgpu");
781
+ const format = navigator.gpu.getPreferredCanvasFormat();
782
+ context.configure({ device, format, alphaMode: "opaque" });
783
+ const camera = device.createBuffer({
784
+ size: 64,
785
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
786
+ });
787
+ const module = device.createShaderModule({ code: boxShader });
788
+ const pipeline = device.createRenderPipeline({
789
+ layout: "auto",
790
+ vertex: { module, entryPoint: "vertex" },
791
+ fragment: { module, entryPoint: "fragment", targets: [{ format }] },
792
+ primitive: { topology: "triangle-list", frontFace: "cw", cullMode: "back" },
793
+ depthStencil: {
794
+ format: "depth24plus",
795
+ depthWriteEnabled: true,
796
+ depthCompare: "less",
797
+ },
798
+ });
799
+ let depth, width, height, bodyBuffer, bindGroup;
800
+ return {
801
+ draw(viewProjection) {
802
+ const binding = getBinding();
803
+ if (binding.device !== device || binding.stride !== 160)
804
+ throw Error(
805
+ "The renderer needs the same GPUDevice and the 3D body layout",
806
+ );
807
+ // Rebind if adding bodies caused the solver to grow its buffer.
808
+ if (bodyBuffer !== binding.buffer) {
809
+ bodyBuffer = binding.buffer;
810
+ bindGroup = device.createBindGroup({
811
+ layout: pipeline.getBindGroupLayout(0),
812
+ entries: [
813
+ { binding: 0, resource: { buffer: bodyBuffer } },
814
+ { binding: 1, resource: { buffer: camera } },
815
+ ],
816
+ });
817
+ }
818
+ if (width !== canvas.width || height !== canvas.height) {
819
+ depth?.destroy();
820
+ width = canvas.width;
821
+ height = canvas.height;
822
+ depth = device.createTexture({
823
+ size: [width, height],
824
+ format: "depth24plus",
825
+ usage: GPUTextureUsage.RENDER_ATTACHMENT,
826
+ });
827
+ }
828
+ device.queue.writeBuffer(
829
+ camera,
830
+ 0,
831
+ new Float32Array(viewProjection.asArray()),
832
+ );
833
+ const encoder = device.createCommandEncoder();
834
+ const pass = encoder.beginRenderPass({
835
+ colorAttachments: [
836
+ {
837
+ view: context.getCurrentTexture().createView(),
838
+ clearValue: { r: 0.92, g: 0.94, b: 0.97, a: 1 },
839
+ loadOp: "clear",
840
+ storeOp: "store",
841
+ },
842
+ ],
843
+ depthStencilAttachment: {
844
+ view: depth.createView(),
845
+ depthClearValue: 1,
846
+ depthLoadOp: "clear",
847
+ depthStoreOp: "store",
848
+ },
849
+ });
850
+ pass.setPipeline(pipeline);
851
+ pass.setBindGroup(0, bindGroup);
852
+ pass.draw(36, binding.count);
853
+ pass.end();
854
+ // Queue order makes this draw see the preceding physics step.
855
+ device.queue.submit([encoder.finish()]);
856
+ },
857
+ dispose() {
858
+ depth?.destroy();
859
+ camera.destroy();
860
+ context.unconfigure();
861
+ },
862
+ };
863
+ }
864
+
865
+ export async function createDirectGpuExample(canvas, count = 4096) {
866
+ const { device } = await createWebGPUDevice({
867
+ requiredLimits: { maxStorageBuffersPerShaderStage: 9 },
868
+ });
869
+ const world = new AvbdScene3D({ timeStep: 1 / 120, iterations: 5 });
870
+ world.addBox([40, 1, 40], { density: 0, position: [0, -0.5, 0] });
871
+ for (let i = 0; i < count; i++)
872
+ world.addBox([1, 1, 1], {
873
+ position: [
874
+ ((i % 16) - 7.5) * 1.05,
875
+ 0.5 + Math.floor(i / 256) * 1.05,
876
+ ((Math.floor(i / 16) % 16) - 7.5) * 1.05,
877
+ ],
878
+ });
879
+ const gpu = world.createSolver(device);
880
+ const drawing = createGpuBoxRenderer(canvas, () => ({
881
+ device,
882
+ buffer: gpu.bodyBuffer,
883
+ stride: 160,
884
+ count: gpu.bodyCount,
885
+ }));
886
+ let previous,
887
+ accumulator = 0,
888
+ request;
889
+ function frame(now) {
890
+ // Limit catch-up after a hidden tab; each executed step still uses 1/120 s.
891
+ accumulator +=
892
+ previous === undefined ? 0 : Math.min((now - previous) / 1000, 0.05);
893
+ previous = now;
894
+ while (accumulator >= 1 / 120) {
895
+ gpu.step();
896
+ accumulator -= 1 / 120;
897
+ }
898
+ const ratio = Math.min(devicePixelRatio || 1, 2);
899
+ const width = Math.max(1, Math.round(canvas.clientWidth * ratio));
900
+ const height = Math.max(1, Math.round(canvas.clientHeight * ratio));
901
+ if (canvas.width !== width) canvas.width = width;
902
+ if (canvas.height !== height) canvas.height = height;
903
+ const view = Matrix.LookAtLH(
904
+ new Vector3(24, 22, -32),
905
+ new Vector3(0, 6, 0),
906
+ Vector3.Up(),
907
+ );
908
+ const projection = Matrix.PerspectiveFovLH(
909
+ 0.75,
910
+ canvas.width / canvas.height,
911
+ 0.1,
912
+ 200,
913
+ true,
914
+ );
915
+ drawing.draw(view.multiply(projection));
916
+ request = requestAnimationFrame(frame);
917
+ }
918
+ request = requestAnimationFrame(frame);
919
+ return {
920
+ device,
921
+ gpu,
922
+ drawing,
923
+ stop() {
924
+ cancelAnimationFrame(request);
925
+ },
926
+ dispose() {
927
+ cancelAnimationFrame(request);
928
+ drawing.dispose();
929
+ gpu.destroy();
930
+ device.destroy();
931
+ },
932
+ };
933
+ }
934
+ ```
935
+
936
+ The helper renders centered **boxes** in GPU order, including the floor. Hulls,
937
+ capsules and other graphics need their own geometry and any center/principal-frame
938
+ conversion. The [demo renderer](demo/canonicalRenderer.js) shows those paths.
939
+ 2D uses a different packed layout; see [the 2D renderer](demo/gpuRenderer2D.js).
940
+
941
+ For an existing Babylon world, pass `() => physics.getRenderBinding(device)`
942
+ to `createGpuBoxRenderer()`. Configure that world with `syncMeshes: false` and
943
+ `autoStep: false`, call `physics.step()` from your fixed-step loop, then call
944
+ `drawing.draw(viewProjection)`. Create each aggregate with `{ sync: false }`
945
+ when its Babylon mesh is only collider input. Keep the renderer and solver on
946
+ the same caller-owned device and use a canvas dedicated to this renderer.
947
+ Babylon thin instances use the separate matrix-upload example above.
948
+
949
+ Selective CPU inspection remains available independently of drawing:
950
+
951
+ ```js
952
+ // Copy just these two bodies, in this order, rather than the whole world.
953
+ const poses = await physics.readBodies([projectile.body, gate.body]);
954
+ const projectilePosition = poses.subarray(0, 3);
955
+ const gatePosition = poses.subarray(40, 43);
956
+ ```
957
+
958
+ ## Native physics with any renderer
959
+
960
+ Import `avbd-babylon/native` for WebGPU physics without Babylon. These worlds
961
+ also work in module workers. Create bodies and constraints in a scene builder,
962
+ then create its GPU solver once. Rendering and simulation can share a GPUDevice.
963
+ The canonical web demos and solver benchmarks use these native solvers by default.
964
+
965
+ Both dimensions provide `gpu.body()`, `editBodies()`, `advance()`,
966
+ `readSelectedBodies()`, triggers, bounce, masks, sleeping, contact events and
967
+ shape queries. Native joints, welds, springs, hinges, motors, ropes, ragdolls and
968
+ fabric use the builders shown in the following sections. They do not require
969
+ Babylon meshes.
970
+
971
+ ### Native 3D world
972
+
973
+ This builds a box pile, reusable capsule projectile, trigger volume and motorized
974
+ hinge. The default gravity axis is Y. Keep builder body references for bulk
975
+ edits; `gpu.body(reference)` creates a convenient handle only when requested.
976
+
977
+ ```js
978
+ // src/portable-3d.js
979
+ import { AvbdScene3D, createWebGPUDevice } from "avbd-babylon/native";
980
+
981
+ export async function createPortable3D({ device, count = 4096 } = {}) {
982
+ const ownsDevice = !device;
983
+ if (!device) ({ device } = await createWebGPUDevice());
984
+ const world = new AvbdScene3D({ gravity: -9.81, iterations: 5 });
985
+ world.addBox([100, 1, 100], { mass: 0, position: [0, -0.5, 0] });
986
+ const bodies = [];
987
+ for (let i = 0; i < count; i++) {
988
+ bodies.push(
989
+ world.addBox([0.8, 0.8, 0.8], {
990
+ position: [
991
+ (i % 32) - 16,
992
+ 2 + Math.floor(i / 1024),
993
+ (Math.floor(i / 32) % 32) - 16,
994
+ ],
995
+ friction: 0.6,
996
+ }),
997
+ );
998
+ }
999
+ const projectile = world.addCapsule(0.3, 1.4, {
1000
+ position: [-22, 4, 0],
1001
+ restitution: 0.3,
1002
+ allowSleep: false,
1003
+ });
1004
+ const sensor = world.addSphere(2, {
1005
+ mass: 0,
1006
+ position: [20, 2, 0],
1007
+ isTrigger: true,
1008
+ });
1009
+ const gate = world.addBox([4, 0.3, 0.3], { position: [0, 12, 0] });
1010
+ const hinge = world.addHinge(null, gate, {
1011
+ axisA: [0, 0, 1],
1012
+ axisB: [0, 0, 1],
1013
+ minAngle: -0.7,
1014
+ maxAngle: 0.7,
1015
+ motor: { speed: 1, maxTorque: 10 },
1016
+ });
1017
+ const gpu = world.createSolver(device, {
1018
+ broadphase: "auto",
1019
+ sleeping: true,
1020
+ });
1021
+ return {
1022
+ device,
1023
+ gpu,
1024
+ bodies,
1025
+ hinge,
1026
+ projectile: gpu.body(projectile),
1027
+ sensor: gpu.body(sensor),
1028
+ dispose() {
1029
+ gpu.destroy();
1030
+ if (ownsDevice) device.destroy();
1031
+ },
1032
+ };
1033
+ }
1034
+ ```
1035
+
1036
+ `projectile.applyImpulse([20, 0, 0])` uses its current GPU centre. A world-point
1037
+ argument also adds the appropriate rotation. `teleport(position, quaternion?)`
1038
+ preserves velocity and clears old contact warm starts for that body. Position
1039
+ means centre of mass; custom hulls use their principal frame. Numeric arguments
1040
+ to `gpu.body()` are **GPU slots**; use builder references or `bodyIndex()` to
1041
+ account for the initial spatial sorting.
1042
+
1043
+ ### Native 2D world
1044
+
1045
+ 2D uses xy vectors and scalar angles/angular velocities. Density sets mass from
1046
+ shape area; zero density makes a static body. This example includes rounded
1047
+ colliders, a floor, a finite ledge, a convex hull, a limited motorized hinge,
1048
+ a spring and a breakable weld.
1049
+
1050
+ ```js
1051
+ // src/portable-2d.js
1052
+ import { AvbdScene2D, createWebGPUDevice } from "avbd-babylon/native";
1053
+
1054
+ export async function createPortable2D({ device, count = 4096 } = {}) {
1055
+ const ownsDevice = !device;
1056
+ if (!device) ({ device } = await createWebGPUDevice());
1057
+ const world = new AvbdScene2D({ gravity: -9.81, iterations: 5 });
1058
+ world.addPlane([0, 1]);
1059
+ world.addSegment([-20, 4], [-16, 4]);
1060
+ world.addHull(
1061
+ [
1062
+ [-1, -1],
1063
+ [1, -1],
1064
+ [0, 1],
1065
+ ],
1066
+ { position: [22, 2] },
1067
+ );
1068
+ const bodies = [];
1069
+ for (let i = 0; i < count; i++) {
1070
+ bodies.push(
1071
+ world.addBox([0.8, 0.8], {
1072
+ position: [(i % 64) - 32, 2 + Math.floor(i / 64)],
1073
+ }),
1074
+ );
1075
+ }
1076
+ const projectile = world.addCapsule(0.3, 1.4, {
1077
+ position: [-40, 4],
1078
+ restitution: 0.3,
1079
+ });
1080
+ const sensor = world.addCircle(2, {
1081
+ density: 0,
1082
+ position: [40, 2],
1083
+ isTrigger: true,
1084
+ });
1085
+ const gate = world.addBox([3, 0.3], { position: [0, 80] });
1086
+ const hinge = world.addHinge(-1, gate, [0, 80], [0, 0], {
1087
+ minAngle: -0.7,
1088
+ maxAngle: 0.7,
1089
+ motor: { speed: 1, maxTorque: 10 },
1090
+ });
1091
+ const a = world.addCircle(0.4, { position: [-10, 80] });
1092
+ const b = world.addCircle(0.4, { position: [-8, 80] });
1093
+ world.addSpring(a, b);
1094
+ world.addJoint(a, b, [1, 0], [-1, 0], {
1095
+ stiffness: [Infinity, Infinity, Infinity],
1096
+ breakForce: 100,
1097
+ });
1098
+ const gpu = world.createSolver(device, { sleeping: true });
1099
+ return {
1100
+ device,
1101
+ gpu,
1102
+ bodies,
1103
+ hinge,
1104
+ projectile: gpu.body(projectile),
1105
+ sensor: gpu.body(sensor),
1106
+ dispose() {
1107
+ gpu.destroy();
1108
+ if (ownsDevice) device.destroy();
1109
+ },
1110
+ };
1111
+ }
1112
+ ```
1113
+
1114
+ Both examples return the same style of body controls:
1115
+
1116
+ ```js
1117
+ projectile.setTrigger(false).setRestitution(0.5).setSleepEnabled(false);
1118
+ projectile.setCollisionGroups(2, 1 | 2);
1119
+ // 3D uses xyz; use [20, 0] for the 2D projectile.
1120
+ projectile.applyImpulse([20, 0, 0]);
1121
+ gpu.setMotor(hinge.motor.slot, { speed: -1, maxTorque: 10 });
1122
+ ```
1123
+
1124
+ ### Bulk edits and GPU dispatches
1125
+
1126
+ Queue an array or generator of edits, then step or flush once. `editBodies()`
1127
+ validates the whole batch before enqueueing it. A record applies a teleport
1128
+ first, velocity setters next, then impulses and forces. Records for the same
1129
+ body execute in order. Individual handle calls also join the pending batch.
1130
+
1131
+ ```js
1132
+ // src/portable-batches.js
1133
+ export function launchAll(gpu, bodies, dimension = 3) {
1134
+ function* edits() {
1135
+ for (let i = 0; i < bodies.length; i++) {
1136
+ const x = (i % 7) - 3;
1137
+ yield {
1138
+ body: bodies[i],
1139
+ linearVelocity: dimension === 3 ? [x, 4, 0] : [x, 4],
1140
+ impulse: dimension === 3 ? [0, 0.2, 0] : [0, 0.2],
1141
+ };
1142
+ }
1143
+ }
1144
+ gpu.editBodies(edits());
1145
+ gpu.flushEdits(); // One grouped edit dispatch; no pose downloads.
1146
+ }
1147
+ ```
1148
+
1149
+ For 100,000 bodies this example uploads 200,000 commands together, rather than
1150
+ submitting a GPU job per command. Each GPU invocation owns one edited body and
1151
+ executes its commands in order. Command/range buffers grow when needed and are
1152
+ reused. Velocity/force batches dispatch only edited bodies; teleport batches
1153
+ also scan previous manifolds to clear affected contact warm starts. Native 3D
1154
+ teleport cleanup shares the edit submission.
1155
+
1156
+ ```js
1157
+ // Change masks for many bodies in one call. Both sides must allow a collision.
1158
+ const indices = Uint32Array.from(bodies, (body) => gpu.bodyIndex(body));
1159
+ const groups = new Uint32Array(indices.length).fill(2);
1160
+ const collidesWith = new Uint32Array(indices.length).fill(1 | 2);
1161
+ gpu.setFilters(indices, groups, collidesWith);
1162
+ ```
1163
+
1164
+ Filter edits coalesce automatically, including individual body calls. Contiguous
1165
+ settings share uploads; scattered settings share a GPU dispatch. Keep each
1166
+ group/mask aligned with its index. Body handle edits are convenient for a few
1167
+ objects; `editBodies()` accepts builder references or numeric GPU slots directly,
1168
+ so a bulk edit does not need a handle for every object. Apply forces each fixed
1169
+ step for a continuous force; velocity setters persist until changed by physics
1170
+ or another command.
1171
+
1172
+ Collision settings can also go in `editBodies()` in either dimension. The whole
1173
+ batch is validated first. Repeated property changes keep the last value for each
1174
+ field; motion commands retain their order.
1175
+
1176
+ ```js
1177
+ // src/portable-properties.js
1178
+ export function configureBodies(gpu, bodies) {
1179
+ gpu.editBodies((function* () {
1180
+ for (const body of bodies) yield {
1181
+ body,
1182
+ isTrigger: false,
1183
+ restitution: 0.4,
1184
+ group: 2,
1185
+ collidesWith: 1 | 2,
1186
+ allowSleep: true,
1187
+ };
1188
+ })());
1189
+ gpu.step(); // Applies the edits, waking, bounce and solving in one submission.
1190
+ }
1191
+ ```
1192
+
1193
+ Trigger, bounce, mask, sleep-eligibility and motor-target setters collect updates
1194
+ until the next step, read or query. `flushEdits()` applies them without advancing
1195
+ time. Batch motor changes by calling `setMotor()` for each motor and stepping
1196
+ once; the uploads touch the target fields and preserve live constraint forces.
1197
+ Removing or reusing a constraint applies its earlier edits first.
1198
+
1199
+ For frequent velocity updates, use packed arrays. This submits one dispatch,
1200
+ after flushing earlier queued edits. Each index must be a unique GPU slot in
1201
+ ascending order. The arrays can be reused each frame; values are copied into GPU
1202
+ storage when the method is called. Other body properties are preserved.
1203
+
1204
+ ```js
1205
+ // src/portable-velocities.js
1206
+ export function createVelocityController(gpu, bodies, dimension = 3) {
1207
+ const indices = Uint32Array.from(bodies, (body) =>
1208
+ gpu.bodyIndex(body),
1209
+ ).sort();
1210
+ const velocities = new Float32Array(indices.length * dimension);
1211
+ return {
1212
+ indices,
1213
+ velocities,
1214
+ update() {
1215
+ gpu.setLinearVelocities(indices, velocities);
1216
+ },
1217
+ };
1218
+ }
1219
+ ```
1220
+
1221
+ Fill `velocities` with xy pairs for 2D or xyz triples for 3D, then call `update()`.
1222
+ Updating 100,000 bodies uploads 1.2 MB in 2D or 1.6 MB in 3D, including indices.
1223
+ Use `editBodies()` for mixed commands, world-point impulses and teleports. Use
1224
+ `setLinearVelocities()` when packed velocity updates match the workload.
1225
+
1226
+ Use `setAngularVelocities(indices, angular)` for spin alone, or `setVelocities()`
1227
+ for both fields together. Angular arrays have one radians-per-second value per
1228
+ 2D body, or xyz triples per 3D body. Object setters and `editBodies()` automatically
1229
+ use compact uploads when the batch contains only linear and angular replacements.
1230
+
1231
+ ```js
1232
+ // src/portable-motion.js
1233
+ export function createMotionController(gpu, bodies, dimension = 3) {
1234
+ const indices = Uint32Array.from(bodies, body => gpu.bodyIndex(body)).sort();
1235
+ const linear = new Float32Array(indices.length * dimension);
1236
+ const angular = new Float32Array(indices.length * (dimension === 3 ? 3 : 1));
1237
+ return {
1238
+ indices, linear, angular,
1239
+ update() { gpu.setVelocities(indices, { linear, angular }); },
1240
+ updateSpin() { gpu.setAngularVelocities(indices, angular); },
1241
+ };
1242
+ }
1243
+ ```
1244
+
1245
+ A combined 100,000-body motion upload uses 2.0 MB in 2D or 3.2 MB in 3D.
1246
+ Spin alone uses 0.8 MB in 2D or 1.6 MB in 3D.
1247
+ The packed methods submit immediately; ordinary queued edits share the next
1248
+ physics-step submission. Stable solver settings reuse pass constants. Changing
1249
+ iterations, coloring or stabilization settings refreshes those constants.
1250
+
1251
+ Saved 100,000-body edit measurements from the machine with an **RTX 4070 Laptop
1252
+ GPU**, using five samples after warmup:
1253
+
1254
+ | World | Individual body calls, CPU time | `editBodies()`, CPU time | Packed arrays, CPU time | Upload for each method |
1255
+ | ----- | ------------------------------: | ----------------------: | ----------------------: | ---------------------: |
1256
+ | 2D | 11.8 ms | 21.0 ms | 3.2 ms | 1.2 MB |
1257
+ | 3D | 39.8 ms | 70.9 ms | 5.2 ms | 1.6 MB |
1258
+
1259
+ These times measure CPU work to prepare and submit velocity edits. They exclude
1260
+ physics solving and drawing; the GPU completion measurements are recorded
1261
+ separately. This compares three ways to submit the same edits within this package.
1262
+ Selected full body records matched exactly. Run `npm run benchmark:native-edits`
1263
+ to measure your own computer. Caller arrays and body handles are prepared before
1264
+ timing and reused. [Raw results](test-results/native-api-performance.json).
1265
+
1266
+ The [native API report](test-results/package-performance.html) also measures
1267
+ combined movement and spin, 10,000 collision-setting changes, and 64 ray queries.
1268
+ It compares batching choices within this package and separates API/query costs
1269
+ from physics solving and drawing. Use `raycastAll()` or the batched shape-cast
1270
+ methods for many independent queries. Repeated casts reuse a bounded buffer
1271
+ pool; concurrent requests keep separate storage and return independent results.
1272
+
1273
+ [![Packed velocity edit timings for 100,000 bodies on an RTX 4070 Laptop GPU](test-results/screenshots/package-bulk-edits-1440.png)](test-results/package-performance.html)
1274
+
1275
+ ### Reading selected bodies and contacts
1276
+
1277
+ Rendering directly from `gpu.bodyBuffer` avoids pose downloads. Use the same
1278
+ GPUDevice and the documented [direct rendering layout](#direct-gpu-rendering).
1279
+ The full stride is 160 bytes in 3D and 96 bytes in 2D. `step()` automatically
1280
+ flushes pending edits. Call `flushEdits()` before drawing if you want to show an
1281
+ edit without taking a physics step.
1282
+ Small repeated selections reuse readback storage. Concurrent requests use
1283
+ separate staging buffers; returned arrays own their data. Idle storage is bounded
1284
+ and released when the world is destroyed.
1285
+
1286
+ ```js
1287
+ // With any renderer: pass elapsed seconds, then draw its GPU buffers.
1288
+ gpu.advance(elapsedSeconds, { maxSubSteps: 4 });
1289
+ renderer.drawBodies(gpu.bodyBuffer, gpu.bodyCount);
1290
+
1291
+ // Only download the objects needed by CPU gameplay or a CPU-driven renderer.
1292
+ const indices = [projectile.gpuIndex, sensor.gpuIndex];
1293
+ const poses = await gpu.readSelectedBodies(indices, { posesOnly: true });
1294
+ const projectileState = await projectile.readState();
1295
+ ```
1296
+
1297
+ `posesOnly` returns eight floats per 3D body (xyz/friction, quaternion) or four
1298
+ per 2D body (xy, angle, friction). Full selected records contain 40 or 24 floats
1299
+ respectively. These reads are explicit and asynchronous. Normal `step()` and
1300
+ `advance()` do not download poses or wait for the GPU. A renderer with a Babylon
1301
+ scene can instead use `gpu.attachToScene(scene)` for automatic stepping/disposal
1302
+ in either dimension; native mesh synchronization remains renderer-owned.
1303
+
1304
+ ```js
1305
+ // Watch just gameplay-relevant pairs instead of every contact in a large pile.
1306
+ gpu.watchContacts({ indices: [projectile.gpuIndex, sensor.gpuIndex] });
1307
+ // After stepping, drain events when gameplay needs them:
1308
+ const { events, dropped, full } = await gpu.readContactEvents();
1309
+ for (const event of events) {
1310
+ if (event.type === "begin") {
1311
+ console.log(
1312
+ event.isTrigger ? "Entered trigger" : "Impact",
1313
+ event.a,
1314
+ event.b,
1315
+ );
1316
+ }
1317
+ }
1318
+ if (dropped || full) console.warn("Increase the contact event capacities");
1319
+
1320
+ // 3D query; 2D also offers raycast(), with xy vectors, and circleCast().
1321
+ const hit = await gpu.sphereCast([0, 5, -10], 0.3, [0, 0, 1], {
1322
+ maxDistance: 30,
1323
+ includeTriggers: false,
1324
+ });
1325
+ if (hit) gpu.body(hit.index).applyImpulse([0, 0, 2], hit.point);
1326
+ ```
1327
+
1328
+ Event watching allocates GPU event storage and adds a contact pass when enabled.
1329
+ Event reads transfer only the queued events. Queries see pending edits and return
1330
+ numeric GPU indices. Reserve sufficient event capacity and drain events regularly.
1331
+ Dispose the solver when finished; dispose the device only if your application
1332
+ owns it.
1333
+
1334
+ ## Joints, welds, springs, ropes and ragdolls
1335
+
1336
+ Babylon bodies can be connected directly. Anchors are expressed in the visible
1337
+ mesh's local coordinates, including the centre/principal-frame conversion for
1338
+ convex colliders. With a null first body, anchor A is a world position:
1339
+
1340
+ ```js
1341
+ // After creating aggregates a and b in the same world:
1342
+ const ballJoint = physics.addJoint(null, a.body, {
1343
+ anchorA: [0, 6, 0],
1344
+ anchorB: [0, 0.5, 0],
1345
+ });
1346
+ const mortar = physics.addWeld(a.body, b.body, {
1347
+ anchorA: [0.5, 0, 0],
1348
+ anchorB: [-0.5, 0, 0],
1349
+ breakForce: 120,
1350
+ breakOnPull: true,
1351
+ });
1352
+ const spring = physics.addSpring(a.body, b.body, {
1353
+ stiffness: 100,
1354
+ rest: 1,
1355
+ });
1356
+ ballJoint.setWorldAnchor([1, 6, 0]);
1357
+ const state = await mortar.readState(); // explicit, small GPU readback
1358
+ console.log("Broken", state.broken);
1359
+ spring.dispose(); // releases a reusable constraint slot
1360
+ ```
1361
+
1362
+ A weld captures the bodies' relative rotation on the GPU, including when added
1363
+ to moving bodies. It does not force differently rotated meshes into matching
1364
+ orientations. A spring without `rest` captures the current anchor separation.
1365
+ `breakOnPull` adds linear force to the angular-force break test; it is not a
1366
+ separate tensile-only material model. Joint changes and commands wake sleeping
1367
+ bodies. Constraints can be created before initialization or during simulation.
1368
+ Springs do not fracture; a finite `breakForce` on a Babylon spring is rejected.
1369
+
1370
+ For large scenes, the portable builders below avoid making one Babylon mesh per
1371
+ collider. They are also exported by the package, require no custom shader loader,
1372
+ and never step a CPU solver. The first portable example uses **z up** explicitly;
1373
+ the rope, ragdoll and fabric helpers below use **y up**.
1374
+
1375
+ ### Ball joints, welds and springs
1376
+
1377
+ Save as `demo/readme-constraints.js` in this repository. A ball joint holds two
1378
+ anchor points together while allowing rotation. A weld also locks angular
1379
+ movement; a finite fracture threshold makes it breakable. A spring pulls two
1380
+ anchors toward a rest distance.
1381
+
1382
+ ```js
1383
+ // demo/readme-constraints.js
1384
+ import { AvbdScene3D, createWebGPUDevice } from "avbd-babylon/native";
1385
+
1386
+ export async function createJointExamples() {
1387
+ const { device } = await createWebGPUDevice({
1388
+ requiredLimits: { maxStorageBuffersPerShaderStage: 9 },
1389
+ });
1390
+ const ref = new AvbdScene3D({
1391
+ timeStep: 1 / 60,
1392
+ iterations: 10,
1393
+ up: [0, 0, 1],
1394
+ });
1395
+ ref.addBox([30, 30, 1], {
1396
+ density: 0,
1397
+ friction: 0.7,
1398
+ position: [0, 0, -0.5],
1399
+ });
1400
+ const pendulum = ref.addBox([1, 1, 1], { position: [-6, 0, 5.5] });
1401
+ ref.addJoint(null, pendulum, [-6, 0, 6], [0, 0, 0.5]);
1402
+ pendulum.velocityLin.set([0, 2, 0]);
1403
+
1404
+ const a = ref.addBox([1, 1, 1], { position: [-1, 0, 5] });
1405
+ const b = ref.addBox([1, 1, 1], { position: [0, 0, 5] });
1406
+ ref.addJoint(a, b, [0.5, 0, 0], [-0.5, 0, 0], {
1407
+ angularStiffness: Infinity,
1408
+ breakForce: 120,
1409
+ });
1410
+
1411
+ const anchor = ref.addBox([0.5, 0.5, 0.5], {
1412
+ density: 0,
1413
+ position: [5, 0, 8],
1414
+ });
1415
+ const weight = ref.addBox([1, 1, 1], { position: [5, 0, 4] });
1416
+ ref.addSpring(anchor, weight, [0, 0, 0], [0, 0, 0], {
1417
+ stiffness: 100,
1418
+ rest: 4,
1419
+ });
1420
+ const gpu = ref.createSolver(device, {
1421
+ bodyCapacity: ref.bodies.length,
1422
+ minimumColors: 16,
1423
+ colorRounds: 32,
1424
+ });
1425
+ return { device, gpu, ref };
1426
+ }
1427
+ ```
1428
+
1429
+ Joint anchors are local to their bodies; with `bodyA: null`, anchor A is a
1430
+ world position. Linear/angular stiffness `Infinity` makes hard rows, `0`
1431
+ disables them and finite values make compliant rows. The example's weld starts
1432
+ with matching orientations, but differently rotated welds are supported too.
1433
+ Fracture in this portable example is driven by angular constraint force. It is not
1434
+ a universal linear-force break threshold.
1435
+
1436
+ Portable bodies accept **density** or an explicit **mass**; Babylon aggregates take **mass**.
1437
+ Create native bodies and constraints before the GPU solver. Advance with
1438
+ `gpu.step()`, inspect with `await gpu.readBodies()`, and release with
1439
+ `gpu.destroy(); device.destroy()`. Use the native renderer for GPU drawing;
1440
+ mesh synchronization is not supplied by this native example.
1441
+
1442
+ ### Ropes and ragdolls
1443
+
1444
+ The portable builders provide actual linked-body ropes and ragdolls, not a
1445
+ single soft mesh. Save this next helper as `demo/readme-rigs.js`:
1446
+
1447
+ ```js
1448
+ // demo/readme-rigs.js
1449
+ import { AvbdScene3D, createWebGPUDevice } from "avbd-babylon/native";
1450
+
1451
+ export async function createNativeRig(kind = "rope") {
1452
+ const ref = new AvbdScene3D({ iterations: 10 }); // y up
1453
+ if (kind === "rope") {
1454
+ ref.addRope({ length: 12, segments: 12, endMass: 20 });
1455
+ } else if (kind === "ragdolls") {
1456
+ ref.addBox([20, 1, 20], { mass: 0, position: [0, -0.5, 0] });
1457
+ ref.addFabric({ columns: 12, rows: 12, spacing: 0.5, origin: [0, 2, 0] });
1458
+ for (const x of [-1, 1])
1459
+ for (const z of [-1, 1]) ref.addRagdoll({ origin: [x, 3, z] });
1460
+ } else {
1461
+ throw Error("Expected rope or ragdolls");
1462
+ }
1463
+ const { device } = await createWebGPUDevice({
1464
+ requiredLimits: { maxStorageBuffersPerShaderStage: 9 },
1465
+ });
1466
+ const gpu = ref.createSolver(device, {
1467
+ bodyCapacity: ref.bodies.length,
1468
+ minimumColors: 16,
1469
+ colorRounds: 32,
1470
+ });
1471
+ return { device, gpu, ref };
1472
+ }
1473
+ ```
1474
+
1475
+ These helpers add to an `AvbdScene3D`. The rope is a chain with a static anchor
1476
+ and an optional heavy end block. Each ragdoll has ten rigid parts and nine ball
1477
+ joints, without anatomical limits or animation blending. `addFabric()` builds
1478
+ a deformable mass-spring sheet with point-sampled collisions, stretch, shear
1479
+ and bending links. Drawing triangles are provided in `indices`; collisions
1480
+ do not test triangle interiors. The canonical rigid-net benchmark remains a
1481
+ different workload from this fabric example.
1482
+
1483
+ For the complete rendered presets, open [Rope](index.html?demo=canonical&scene=3d-rope&backend=gpu),
1484
+ [Heavy Rope](index.html?demo=canonical&scene=3d-heavy-rope&backend=gpu),
1485
+ [Ragdolls on a rigid net](index.html?demo=canonical&scene=showcase-ragdolls-on-cloth-24k&backend=gpu),
1486
+ [Breakable](index.html?demo=canonical&scene=3d-breakable&backend=gpu)
1487
+ and [Castle siege](index.html?demo=cannon). The
1488
+ [castle builder](demo/castleScene.js) shows connected mortar and roof joints.
1489
+
1490
+ ### Tearable cloth
1491
+
1492
+ Play [Tearable cloth](index.html?demo=canonical&scene=3d-tearable-cloth&backend=gpu):
1493
+ a 50 kg ball falls onto 1,024 points joined by elastic springs. Only the four
1494
+ corners are pinned; the free edges sag and can fall after a tear. Springs stretch
1495
+ and recover under gentle loads. Tear strength rises gradually from 35% tensile
1496
+ extension at the center to 90% at the edges, with reinforced corner patches.
1497
+ The impact tears the middle while the attachments hold. Compression does not tear the sheet. Collisions sample the
1498
+ points; this does not provide continuous triangle collisions.
1499
+
1500
+ Here is the same physical construction using the published package. Save as
1501
+ `demo/readme-tearable.js`; advance it with `gpu.step()` at 120 steps per
1502
+ simulated second. At 60 display frames per second, use two steps per frame.
1503
+
1504
+ ```js
1505
+ // demo/readme-tearable.js
1506
+ import { AvbdScene3D, createWebGPUDevice } from "avbd-babylon/native";
1507
+
1508
+ export async function createTearableCloth() {
1509
+ const ref = new AvbdScene3D({ timeStep: 1 / 120, iterations: 10 }); // y up
1510
+ ref.addBox([20, 1, 20], { mass: 0, position: [0, -0.5, 0] });
1511
+ const fabric = ref.addFabric({
1512
+ columns: 32,
1513
+ rows: 32,
1514
+ spacing: 0.25,
1515
+ origin: [0, 4, 0],
1516
+ mass: 0.02,
1517
+ stiffness: 1000,
1518
+ breakStrain: 0.35,
1519
+ // Coordinates are the spring midpoint in the original sheet, from 0 to 1.
1520
+ // Spread reinforcement across a patch so the first free row also holds.
1521
+ materialAt(u, v) {
1522
+ const edge = Math.max(Math.abs(2 * u - 1), Math.abs(2 * v - 1));
1523
+ const distance = Math.hypot(Math.min(u, 1 - u), Math.min(v, 1 - v));
1524
+ const t = Math.max(0, 1 - distance / 0.28);
1525
+ const reinforcement = t * t * (3 - 2 * t);
1526
+ return {
1527
+ stiffnessScale: 1 + 3 * reinforcement,
1528
+ breakStrain: 0.35 + 0.55 * edge ** 2 + 1.1 * reinforcement,
1529
+ };
1530
+ },
1531
+ bendingStiffness: 0,
1532
+ pinCorners: true,
1533
+ });
1534
+ const { grid, connections } = fabric;
1535
+ const ball = ref.addSphere(0.4, {
1536
+ mass: 50,
1537
+ position: [0.08, 7, 0.05],
1538
+ friction: 0.3,
1539
+ });
1540
+ const { device } = await createWebGPUDevice();
1541
+ const gpu = ref.createSolver(device, {
1542
+ bodyCapacity: ref.bodies.length,
1543
+ capacity: { contacts: 32000, pairs: 20000, manifolds: 8000, colors: 16 },
1544
+ minimumColors: 16,
1545
+ minimumColorRounds: 16,
1546
+ });
1547
+ return { ref, device, gpu, grid, connections, fabric, ball };
1548
+ }
1549
+ ```
1550
+
1551
+ `materialAt(u, v)` runs once per spring while the sheet is built. It can return
1552
+ `stiffnessScale`, `breakStrain` and `breakForce`; omitted fields keep the fabric
1553
+ defaults. The GPU uses the resulting spring rows directly during simulation.
1554
+
1555
+ Each returned connection has a GPU `slot`. In `await gpu.readJoints()`, each
1556
+ slot is 32 floats; a broken spring has zero stiffness at offset 3.
1557
+ `fabric.triangleConnections` lists three indices into `fabric.connections`
1558
+ for every triangle in `fabric.indices`. A tear must separate material instead
1559
+ of deleting its faces. The web renderer partitions each face into three patches,
1560
+ one per simulated point. Intact edges stretch together; broken edges keep each
1561
+ point's share of the material, following its live pose. Detached patches remain
1562
+ visible and physical indefinitely. There is no automatic debris lifetime.
1563
+ This is a point-sampled cloth model, not an independent rigid-body shard model.
1564
+ The web demo's [cloth renderer](demo/tearableClothRenderer.js) reads those
1565
+ stiffness values on the GPU, so drawing does not download cloth poses or joint
1566
+ states. Its [scene builder](demo/tearableClothScene.js) records which
1567
+ connections support each triangle. The renderer is application code, excluded
1568
+ from the package.
1569
+
1570
+ Thin objects can cross a surface between discrete collision checks. The
1571
+ 24K ragdoll net uses overlapping collision plates with exclusions for the eight
1572
+ immediate neighbors. Other folds still collide, and the total sheet mass stays
1573
+ the same. It uses five solver iterations and runs at 240 steps per simulated
1574
+ second (four per 60 Hz frame). The smaller timestep costs more physics work; it is not continuous
1575
+ collision detection. Benchmark times are per step and include the timestep in
1576
+ the recorded settings.
1577
+
1578
+ With `broadphase: "auto"`, large scenes containing many linked thin plates use
1579
+ HPLOC++ to find potential collisions. This includes the ragdoll net. It preserves
1580
+ the same contact checks and solver settings; ordinary uniform block scenes use
1581
+ the grid. Explicit `"grid"` and `"hploc"` settings remain available for your own
1582
+ benchmarks. The choice uses body geometry and active constraint counts, so it
1583
+ also applies to package scenes rather than depending on a demo name.
1584
+
1585
+ ### Bounce, triggers, masks and sleeping
1586
+
1587
+ This complete scene shows three bounce strengths, a transparent trigger,
1588
+ a ball that the trigger's mask ignores, and a box that cannot sleep.
1589
+ Call `createMaterialExamples(scene)` after adding a Babylon camera and light.
1590
+ With `autoAttach:true`, rendering the scene advances physics automatically.
1591
+
1592
+ ```js
1593
+ // src/materials-and-sensors.js
1594
+ import { MeshBuilder, StandardMaterial, Color3 } from "@babylonjs/core";
1595
+ import { AvbdPhysics, AvbdShapeType } from "avbd-babylon";
1596
+
1597
+ export async function createMaterialExamples(scene, onTrigger = console.log) {
1598
+ const physics = await AvbdPhysics.create({
1599
+ scene,
1600
+ autoAttach: true,
1601
+ capacity: 32,
1602
+ iterations: 10,
1603
+ sleeping: { speedThreshold: 0.03, timeThreshold: 0.5 },
1604
+ });
1605
+ const PLAYER = 1,
1606
+ WORLD = 2,
1607
+ SENSOR = 4,
1608
+ IGNORED = 8;
1609
+ const floorMesh = MeshBuilder.CreateBox(
1610
+ "floor",
1611
+ {
1612
+ width: 30,
1613
+ height: 1,
1614
+ depth: 12,
1615
+ },
1616
+ scene,
1617
+ );
1618
+ floorMesh.position.y = -0.5;
1619
+ const floor = physics.addAggregate(floorMesh, AvbdShapeType.BOX, {
1620
+ mass: 0,
1621
+ group: WORLD,
1622
+ collidesWith: PLAYER | IGNORED,
1623
+ });
1624
+ const balls = [0, 0.5, 0.9].map((restitution, i) => {
1625
+ const mesh = MeshBuilder.CreateSphere(
1626
+ `bounce-${restitution}`,
1627
+ { diameter: 1 },
1628
+ scene,
1629
+ );
1630
+ mesh.position.set(-6 + i * 3, 4, 0);
1631
+ const material = new StandardMaterial(`ball-${i}`, scene);
1632
+ material.diffuseColor = [
1633
+ new Color3(0.8, 0.3, 0.2),
1634
+ new Color3(0.2, 0.7, 0.4),
1635
+ new Color3(0.2, 0.5, 0.9),
1636
+ ][i];
1637
+ mesh.material = material;
1638
+ return physics.addAggregate(mesh, AvbdShapeType.SPHERE, {
1639
+ mass: 1,
1640
+ restitution,
1641
+ group: PLAYER,
1642
+ collidesWith: WORLD | SENSOR,
1643
+ allowSleep: true,
1644
+ });
1645
+ });
1646
+ const sensorMesh = MeshBuilder.CreateBox(
1647
+ "sensor",
1648
+ {
1649
+ width: 3,
1650
+ height: 4,
1651
+ depth: 4,
1652
+ },
1653
+ scene,
1654
+ );
1655
+ sensorMesh.position.set(5, 2, 0);
1656
+ const sensorMaterial = new StandardMaterial("sensor", scene);
1657
+ sensorMaterial.diffuseColor = new Color3(0.2, 0.8, 0.7);
1658
+ sensorMaterial.alpha = 0.2;
1659
+ sensorMesh.material = sensorMaterial;
1660
+ const sensor = physics.addAggregate(sensorMesh, AvbdShapeType.BOX, {
1661
+ mass: 0,
1662
+ isTrigger: true,
1663
+ group: SENSOR,
1664
+ collidesWith: PLAYER,
1665
+ });
1666
+ // This ball travels through the trigger; sensor entry/exit are reported.
1667
+ balls[2].body.setLinearVelocity([3, 0, 0]);
1668
+ const ignoredMesh = MeshBuilder.CreateSphere(
1669
+ "ignored",
1670
+ { diameter: 1 },
1671
+ scene,
1672
+ );
1673
+ ignoredMesh.position.set(2, 1, 1.5);
1674
+ const ignored = physics.addAggregate(ignoredMesh, AvbdShapeType.SPHERE, {
1675
+ mass: 1,
1676
+ group: IGNORED,
1677
+ collidesWith: WORLD | SENSOR,
1678
+ });
1679
+ ignored.body.setLinearVelocity([3, 0, 0]);
1680
+ // The sensor only accepts PLAYER, so IGNORED produces no overlap event.
1681
+ const awakeMesh = MeshBuilder.CreateBox("always-awake", { size: 1 }, scene);
1682
+ awakeMesh.position.set(-9, 0.5, 0);
1683
+ const alwaysAwake = physics.addAggregate(awakeMesh, AvbdShapeType.BOX, {
1684
+ mass: 1,
1685
+ allowSleep: false,
1686
+ group: PLAYER,
1687
+ collidesWith: WORLD,
1688
+ });
1689
+ const unsubscribe = physics.onContact((event) => {
1690
+ if (event.isTrigger) onTrigger(event);
1691
+ });
1692
+ return { physics, floor, balls, sensor, ignored, alwaysAwake, unsubscribe };
1693
+ }
1694
+ ```
1695
+
1696
+ Live controls use these same returned handles:
1697
+
1698
+ ```js
1699
+ balls[1].body.setRestitution(0.8);
1700
+ sensor.body.setTrigger(false); // becomes a solid wall
1701
+ sensor.body.setTrigger(true); // becomes an overlap sensor again
1702
+ ignored.body.setCollisionGroups(PLAYER, WORLD | SENSOR); // now detected
1703
+ alwaysAwake.body.setSleepEnabled(true); // may sleep once supported and quiet
1704
+ balls[0].body.setSleepEnabled(false); // wakes it and keeps it awake
1705
+ ```
1706
+
1707
+ Springs and welds have complete examples under [Joints, ropes and ragdolls](#joints-welds-springs-ropes-and-ragdolls)
1708
+ and [Tearable cloth](#tearable-cloth). The [2D example](#2d-gpu-physics) covers
1709
+ planar joints and motors; the following example covers 3D hinges and motors.
1710
+
1711
+ ### 3D hinges, angle stops and motors
1712
+
1713
+ Save this as `src/hinge-motor.js`. The bar rotates around its pivot and stops at
1714
+ the configured angles. Axes and anchors are in each body's local mesh frame;
1715
+ angles are radians, speed is radians/second and maximum torque is N·m. Limits
1716
+ refer to the relative orientation when the hinge is created.
1717
+
1718
+ ```js
1719
+ // src/hinge-motor.js
1720
+ import { MeshBuilder } from "@babylonjs/core";
1721
+ import { AvbdPhysics, AvbdShapeType } from "avbd-babylon";
1722
+
1723
+ export async function createHingeExample(scene) {
1724
+ const physics = await AvbdPhysics.create({
1725
+ scene,
1726
+ autoAttach: false,
1727
+ capacity: 8,
1728
+ iterations: 20,
1729
+ });
1730
+ const ground = MeshBuilder.CreateBox(
1731
+ "ground",
1732
+ { width: 12, height: 0.5, depth: 12 },
1733
+ scene,
1734
+ );
1735
+ ground.position.y = -0.25;
1736
+ physics.addAggregate(ground, AvbdShapeType.BOX, { mass: 0 });
1737
+ const pivot = MeshBuilder.CreateBox("pivot", { size: 0.3 }, scene);
1738
+ pivot.position.y = 3;
1739
+ const base = physics.addAggregate(pivot, AvbdShapeType.BOX, { mass: 0 });
1740
+ const bar = MeshBuilder.CreateBox(
1741
+ "bar",
1742
+ { width: 2, height: 0.3, depth: 0.3 },
1743
+ scene,
1744
+ );
1745
+ bar.position.y = 3;
1746
+ const rotor = physics.addAggregate(bar, AvbdShapeType.BOX, { mass: 1 });
1747
+ const hinge = physics.addHinge(base.body, rotor.body, {
1748
+ axisA: [0, 0, 1],
1749
+ axisB: [0, 0, 1],
1750
+ anchorA: [0, 0, 0],
1751
+ anchorB: [0, 0, 0],
1752
+ minAngle: -0.7,
1753
+ maxAngle: 0.7,
1754
+ motor: { speed: 1.5, maxTorque: 5 },
1755
+ });
1756
+ return { physics, hinge, rotor };
1757
+ }
1758
+ ```
1759
+
1760
+ Call `physics.step()` in a fixed loop, or use `autoAttach:true` for automatic
1761
+ Babylon stepping. Change direction with `hinge.motor.setMotor({speed:-1.5})`;
1762
+ set `maxTorque:0` to turn its drive off. `hinge.dispose()` releases all of its
1763
+ constraint rows. `physics.addMotor(a,b,options)` drives an angular axis without
1764
+ locking the other axes; attach a hinge when only one rotation axis should move.
1765
+
1766
+ ## Controlling bodies
1767
+
1768
+ Use `fallingBox`, `box` and `physics` from the complete example:
1769
+
1770
+ ```js
1771
+ import { Vector3, Quaternion } from "@babylonjs/core";
1772
+
1773
+ fallingBox.body.setLinearVelocity(new Vector3(0, 0, 5));
1774
+ fallingBox.body.setAngularVelocity(new Vector3(0, 2, 0));
1775
+ fallingBox.body.applyImpulse(new Vector3(3, 0, 0)); // through center of mass
1776
+ fallingBox.body.applyImpulse(new Vector3(0, 2, 0), new Vector3(0.4, 1, 0)); // world-space point
1777
+ fallingBox.body.applyAngularImpulse(new Vector3(0, 1, 0));
1778
+ ```
1779
+
1780
+ To restart a body after its previous commands have been stepped, reset its pose
1781
+ and both velocities:
1782
+
1783
+ ```js
1784
+ fallingBox.body.teleport(new Vector3(0, 4, 0), Quaternion.Identity());
1785
+ fallingBox.body.setLinearVelocity(Vector3.Zero());
1786
+ fallingBox.body.setAngularVelocity(Vector3.Zero());
1787
+ physics.setGravity(new Vector3(0, -9.81, 0));
1788
+ ```
1789
+
1790
+ Commands apply at the next step against live GPU mass, inertia and position.
1791
+ Impulses queued for the same body are combined. Omitting the application point
1792
+ applies an impulse through the center of mass. Teleport retains velocity unless
1793
+ you also set it. To remove an object, dispose its aggregate and its mesh:
1794
+
1795
+ ```js
1796
+ fallingBox.dispose(); // removes collisions; does not dispose the visual mesh
1797
+ box.dispose();
1798
+ ```
1799
+
1800
+ New aggregates can be added after initialization, within the reserved body
1801
+ capacity. Body disposal does not reclaim that slot; reusable projectile bodies
1802
+ avoid exhausting capacity. Constraint disposal does reclaim its slot.
1803
+
1804
+ ## Queries, callbacks and sleeping
1805
+
1806
+ Raycasts query current GPU colliders, including pending body commands. They
1807
+ return asynchronously and test boxes, spheres, exact capsules and convex hulls.
1808
+ A query scans the bodies; use a few interactive queries rather than thousands
1809
+ per frame in a half-million-body world. Batch methods named `raycastAll()` and
1810
+ `sphereCastAll()` return one nearest hit per input query, not every hit along it.
1811
+
1812
+ ```js
1813
+ const hit = await physics.raycast([0, 5, -10], [0, 0, 1], {
1814
+ maxDistance: 30,
1815
+ ignore: [fallingBox.body],
1816
+ });
1817
+ if (hit) hit.body?.applyImpulse([0, 0, 5], hit.point);
1818
+ const sweptBall = await physics.sphereCast([0, 5, -10], 0.4, [0, 0, 1], {
1819
+ maxDistance: 30,
1820
+ collidesWith: 0xffffffff,
1821
+ includeTriggers: false,
1822
+ });
1823
+ if (sweptBall) {
1824
+ console.log(
1825
+ sweptBall.distance,
1826
+ sweptBall.center,
1827
+ sweptBall.point,
1828
+ sweptBall.normal,
1829
+ );
1830
+ }
1831
+ const stopListening = physics.onContact((event) => {
1832
+ if (event.type === "begin")
1833
+ console.log("Impact", event.impulse, event.a, event.b);
1834
+ });
1835
+ // Later: stopListening();
1836
+ ```
1837
+
1838
+ Contact events contain body handles and a step number. Begin events also contain
1839
+ a position, normal and normal impulse. Their GPU passes and asynchronous
1840
+ readbacks are optional. `maxContactPairs` and `maxContactEvents` reserve event
1841
+ storage. `readContactEvents()` reports `dropped` and `full`; automatic callbacks
1842
+ also report exhausted storage in `physics.errors`. An event's `isTrigger` flag
1843
+ distinguishes a non-solving overlap from a physical contact.
1844
+
1845
+ Use bounce, masks and trigger volumes through aggregate options:
1846
+
1847
+ ```js
1848
+ const PLAYER = 1,
1849
+ WORLD = 2,
1850
+ SENSOR = 4;
1851
+ const ballPhysics = physics.addAggregate(ballMesh, AvbdShapeType.SPHERE, {
1852
+ mass: 1,
1853
+ restitution: 0.8,
1854
+ group: PLAYER,
1855
+ collidesWith: WORLD | SENSOR,
1856
+ });
1857
+ const sensorPhysics = physics.addAggregate(sensorMesh, AvbdShapeType.BOX, {
1858
+ mass: 0,
1859
+ isTrigger: true,
1860
+ group: SENSOR,
1861
+ collidesWith: PLAYER,
1862
+ });
1863
+ physics.onContact((event) => {
1864
+ if (event.isTrigger) console.log(event.type, event.a, event.b);
1865
+ });
1866
+ // Change settings without rebuilding the world:
1867
+ ballPhysics.body.setRestitution(0); // disable bounce
1868
+ sensorPhysics.body.setTrigger(false); // make it a solid collider
1869
+ ```
1870
+
1871
+ Both collision masks must permit a pair: `(a.group & b.collidesWith) !== 0`
1872
+ and `(b.group & a.collidesWith) !== 0`. Masks also apply to trigger events.
1873
+ Triggers use the same discrete collision shapes and contact margin as solid
1874
+ contacts. Static–static pairs are not scanned; a static trigger detects dynamic
1875
+ bodies. Bounce uses the larger coefficient of the pair. Impacts below 1 m/s
1876
+ do not bounce, which prevents resting stacks from continually hopping.
1877
+ The optional bounce solver uses normal impulses at the manifold centroid;
1878
+ it is separate from AVBD's position solve and does not add collision friction.
1879
+
1880
+ Enable sleeping at world creation to let supported resting bodies stop solving:
1881
+
1882
+ ```js
1883
+ // Use this option when creating the world in the complete example:
1884
+ const physics = await AvbdPhysics.create({
1885
+ scene,
1886
+ sleeping: { speedThreshold: 0.03, timeThreshold: 0.5 },
1887
+ });
1888
+ // After adding bodies:
1889
+ fallingBox.body.setSleepEnabled(true); // eligible; also enables the world policy
1890
+ alwaysMovingBox.body.setSleepEnabled(false); // this body always stays awake
1891
+ fallingBox.body.wakeUp();
1892
+ console.log(await physics.readSleepStats());
1893
+ ```
1894
+
1895
+ Impulses, teleports, constraint edits, new bodies and removed supports wake the
1896
+ world. Impacts and moving joint endpoints also wake it conservatively. Under
1897
+ gravity, unsupported slow-moving bodies stay awake; supports extend upward
1898
+ through resting contacts. This does not guarantee that every suspended or
1899
+ jointed object will sleep. Sleeping is off by default unless a body explicitly
1900
+ opts in with `allowSleep:true` or `setSleepEnabled(true)`. Enabling it on the
1901
+ world makes bodies eligible by default; `allowSleep:false` excludes a body.
1902
+ Paper comparisons keep sleeping disabled. Portable 3D solvers accept these
1903
+ same world and initial-body options.
1904
+
1905
+ The [browser performance page](tests/performance.html) has a separate sleeping
1906
+ benchmark, including the cost of sleep checks while bodies are awake. It reports
1907
+ the number of boxes asleep and the difference in their final positions. See
1908
+ [saved results from an RTX 4070 Laptop GPU](test-results/package-performance.html).
1909
+ `body.readState()` reports physical `mass`, solver `effectiveMass` (zero while
1910
+ asleep), and a `sleeping` flag.
1911
+
1912
+ ## 2D GPU physics
1913
+
1914
+ The 2D API includes boxes, circles, capsules, finite lines and infinite planes,
1915
+ plus restitution, masks, non-solving triggers, raycasts and circle casts. It
1916
+ uses numeric body indices. This complete factory creates a bouncing circle,
1917
+ a trigger, a powered planar hinge and a welded pair:
1918
+
1919
+ ```js
1920
+ // demo/readme-2d.js
1921
+ import { AvbdScene2D, createWebGPUDevice } from "avbd-babylon/native";
1922
+
1923
+ export async function create2DExamples() {
1924
+ const { device } = await createWebGPUDevice();
1925
+ const scene2D = new AvbdScene2D({ iterations: 20 });
1926
+ scene2D.addPlane([0, 1], 0, { group: 2 });
1927
+ const ball = scene2D.addCircle(0.25, {
1928
+ position: [-2, 5],
1929
+ velocity: [1.5, 0, 0],
1930
+ restitution: 0.75,
1931
+ group: 1,
1932
+ collidesWith: 2 | 4,
1933
+ });
1934
+ const trigger = scene2D.addBox([1, 3], {
1935
+ density: 0,
1936
+ position: [0, 2],
1937
+ isTrigger: true,
1938
+ group: 4,
1939
+ collidesWith: 1,
1940
+ });
1941
+ const wheel = scene2D.addCircle(0.5, { position: [3, 3] });
1942
+ scene2D.addJoint(-1, wheel, [3, 3], [0, 0], {
1943
+ stiffness: [Infinity, Infinity, 0], // free angular row: planar hinge
1944
+ });
1945
+ const motor = scene2D.addMotor(-1, wheel, { speed: 2, maxTorque: 10 });
1946
+ const attached = scene2D.addBox([1, 1], { position: [4, 3] });
1947
+ scene2D.addJoint(wheel, attached, [0.5, 0], [-0.5, 0], {
1948
+ stiffness: [Infinity, Infinity, Infinity], // lock all three rows: weld
1949
+ breakForce: 1000,
1950
+ });
1951
+ const gpu = scene2D.createSolver(device, { bodyCapacity: 8 });
1952
+ gpu.watchContacts({ indices: [ball, trigger] }); // only pairs touching these bodies report events
1953
+ return { scene2D, device, gpu, ball, trigger, wheel, attached, motor };
1954
+ }
1955
+ ```
1956
+
1957
+ Use the returned `gpu` in a fixed loop. Query current poses asynchronously:
1958
+
1959
+ 2D joints, springs and motors return handles with `slot`, `dispose()` and
1960
+ `destroy()`. Disposal is safe before initialization, during simulation or after
1961
+ solver destruction. For a motor, call `gpu.setMotor(motor.slot, { speed: -2,
1962
+ maxTorque: 10 })` to change its drive without rebuilding the world. Zero torque
1963
+ leaves free rotation. In 2D, speed is angular velocity of body A minus body B;
1964
+ with world body A (`-1`), positive speed drives B clockwise. To inspect fracture,
1965
+ use the live GPU joint records; handle `disposed` describes manual removal.
1966
+
1967
+ ```js
1968
+ const ray = await gpu.raycast([-5, 2], [1, 0], { maxDistance: 10 });
1969
+ const circle = await gpu.circleCast([-5, 2], 0.4, [1, 0], {
1970
+ maxDistance: 10,
1971
+ includeTriggers: false,
1972
+ collidesWith: 2,
1973
+ });
1974
+ gpu.step();
1975
+ const { events, dropped } = await gpu.readContactEvents();
1976
+ for (const event of events)
1977
+ console.log(event.type, event.a, event.b, event.isTrigger);
1978
+ if (dropped) console.warn("Increase maxEvents in watchContacts()");
1979
+ gpu.setRestitution(ball, 0.5);
1980
+ gpu.setFilters([ball], [1], [2]); // exclude the trigger on subsequent steps
1981
+ gpu.setSensor(trigger, false); // becomes a solid collider
1982
+ ```
1983
+
1984
+ `raycastAll()` and `circleCastAll()` batch multiple queries. Cast results include
1985
+ the body `index`, distance travelled by the query center, surface normal,
1986
+ query `center` and contact `point` on the target. A cast starting overlapped
1987
+ returns distance zero and a normal opposite the query direction; it does not
1988
+ calculate a penetration depth or a unique target surface point. Queries do not turn on continuous collision
1989
+ detection in the simulation. Release `gpu` and `device` when done.
1990
+
1991
+ Circles and capsules use analytic curved contacts and their exact area mass
1992
+ and inertia. They collide with each other, boxes, convex polygons, segments and planes, and work
1993
+ with joints, friction, bounce, triggers and casts. Capsules lie along local X;
1994
+ `length` includes both caps. Rotate them with `angle` in radians.
1995
+
1996
+ ```js
1997
+ // demo/readme-2d-shapes.js
1998
+ import { AvbdScene2D, createWebGPUDevice } from "avbd-babylon/native";
1999
+
2000
+ export async function create2DShapeExamples() {
2001
+ const { device } = await createWebGPUDevice();
2002
+ const scene2D = new AvbdScene2D({ gravity: -10, timeStep: 1 / 120 });
2003
+ scene2D.addPlane([0, 1]); // an infinite floor at y = 0
2004
+ scene2D.addPlane([1, 0], -8); // left wall; allowed side is x >= -8
2005
+ scene2D.addPlane([-1, 0], -8); // right wall; allowed side is x <= 8
2006
+ scene2D.addSegment([-7, 4], [-1, 2]); // finite, two-sided, zero thickness
2007
+ scene2D.addSegment([1, 2], [7, 4], { radius: 0.15 }); // rounded static rail
2008
+
2009
+ const ball = scene2D.addCircle(0.5, { position: [-3, 6], restitution: 0.5 });
2010
+ const capsule = scene2D.addCapsule(0.3, 2, {
2011
+ position: [3, 6],
2012
+ angle: Math.PI / 4,
2013
+ velocity: [0, 0, 1],
2014
+ });
2015
+ const polygon = scene2D.addHull(
2016
+ [
2017
+ [-0.8, -0.4],
2018
+ [0.5, -0.5],
2019
+ [0.8, 0.2],
2020
+ [0, 0.8],
2021
+ [-0.6, 0.4],
2022
+ ],
2023
+ { position: [0, 6], density: 2, restitution: 0.2 },
2024
+ );
2025
+ const gpu = scene2D.createSolver(device, { bodyCapacity: 256 });
2026
+
2027
+ gpu.setLinearVelocity(ball, [2, 0]).setAngularVelocity(ball, -4);
2028
+ gpu.applyImpulse(capsule, [0, 2]); // at the center of mass
2029
+ const current = await gpu.readBodyState(capsule);
2030
+ gpu.applyImpulse(
2031
+ capsule,
2032
+ [0, 2],
2033
+ [current.position[0] + 0.5, current.position[1]],
2034
+ );
2035
+ gpu.applyForce(ball, [10, 0]); // one fixed step; repeat each step for a continuous force
2036
+ gpu.applyAngularImpulse(capsule, 0.5); // scalar angular impulse in 2D
2037
+ gpu.applyTorque(capsule, 2); // one fixed step
2038
+ gpu
2039
+ .teleport(ball, [-3, 6], 0)
2040
+ .setLinearVelocity(ball, [0, 0])
2041
+ .setAngularVelocity(ball, 0);
2042
+
2043
+ // Spawn after initialization, within the reserved body capacity.
2044
+ const spawned = gpu.addCapsule(0.25, 1.5, { position: [0, 8] });
2045
+ const spawnedHull = gpu.addHull([-0.6, -0.4, 0.6, -0.4, 0, 0.8], {
2046
+ position: [4, 8],
2047
+ angle: 0.3,
2048
+ });
2049
+ gpu.step();
2050
+ return { scene2D, device, gpu, ball, capsule, polygon, spawned, spawnedHull };
2051
+ }
2052
+ ```
2053
+
2054
+ Commands execute in order for each body and use its current GPU mass, inertia
2055
+ and position. `teleport()` preserves velocity and, if omitted, the current
2056
+ angle. Queries and state reads include pending commands. `readBodyState()`
2057
+ downloads one body, so it is suitable for inspection; drawing many bodies should
2058
+ read `gpu.bodyBuffer` directly. Vector commands also accept Babylon `Vector2`.
2059
+
2060
+ `addSegment(start,end)` takes world-space endpoints and is static by default.
2061
+ A dynamic segment requires a positive `radius` and `density`; it is then a
2062
+ capsule. Zero-thickness lines remain static. Lines are two-sided and have finite
2063
+ endpoints. A plane is static and infinite: its normalized normal points toward
2064
+ the allowed side, `dot(normal, point) >= offset`. It is a solid half-space,
2065
+ not a thin infinite line. A zero-density circle or capsule is also static.
2066
+
2067
+ `addHull(points, options)` accepts flat xy pairs (including typed arrays) or an
2068
+ array of `[x,y]` pairs. It removes duplicate and interior points and constructs
2069
+ a convex boundary of 3–32 vertices. Concavities become filled. Collinear clouds
2070
+ and hulls with more than 32 boundary vertices are rejected. The polygon is
2071
+ centered on its area centroid: `position` is its center of mass, and `angle`
2072
+ rotates it around that center. Mass and angular inertia come from its exact area,
2073
+ not its bounding box. GPU contacts clip polygon edges; raycasts and circle casts
2074
+ test the actual edges and rounded corners. Static hulls use `density:0`.
2075
+
2076
+ Try [Mixed 2D colliders](index.html?demo=canonical&scene=2d-mixed-shapes&backend=gpu).
2077
+ The same scene appears in the all-scene browser benchmark. Box-only worlds
2078
+ use the box collision kernels and allocate no extra shape buffer.
2079
+
2080
+ For a Babylon project, attach the native 2D solver to your existing scene:
2081
+
2082
+ ```js
2083
+ const gpu = scene2D.createSolver(device, { bodyCapacity: 256 });
2084
+ gpu.attachToScene(scene); // fixed steps before rendering; destroys gpu when scene is disposed
2085
+ // Optional: gpu.detachFromScene() to take over stepping yourself.
2086
+ ```
2087
+
2088
+ The default loop uses the configured timestep, allows at most six catch-up steps
2089
+ per render frame and caps elapsed frame time at 50 ms. Pass
2090
+ `{ maxSubSteps: 4, maxFrameTime: 0.04, afterStep: (gpu) => {} }` to customize it.
2091
+ Attaching twice to the same scene keeps one loop. Detach before attaching to
2092
+ another scene, and do not call `gpu.step()` separately while attached. This
2093
+ handles simulation timing and solver cleanup; you still choose how to draw 2D
2094
+ bodies. The shared GPUDevice remains yours to dispose.
2095
+
2096
+ Coordinates are x/y, with gravity along y. A joint with free angular stiffness
2097
+ acts as a hinge; motors drive relative angular speed.
2098
+
2099
+ ```js
2100
+ import { AvbdScene2D, createWebGPUDevice } from "avbd-babylon/native";
2101
+
2102
+ const { device } = await createWebGPUDevice();
2103
+ const scene2D = new AvbdScene2D({ gravity: -10, iterations: 10 });
2104
+ scene2D.addBox([120, 1], { density: 0, position: [0, -0.5] });
2105
+ for (let i = 0; i < 10_000; i++) {
2106
+ scene2D.addBox([1, 1], {
2107
+ position: [(i % 100) - 49.5, Math.floor(i / 100) + 0.5],
2108
+ });
2109
+ }
2110
+ const gpu = scene2D.createSolver(device, { bodyCapacity: 10_001 });
2111
+ gpu.step();
2112
+ const counters = await gpu.readCounters(); // explicit inspection
2113
+ gpu.adapt(counters);
2114
+ // 24 floats per GPU body: x, y and angle at offsets 0, 1 and 2.
2115
+ // Draw gpu.bodyBuffer directly, or mirror a small scene with readBodies().
2116
+ gpu.destroy();
2117
+ device.destroy();
2118
+ ```
2119
+
2120
+ On a fresh builder, a hinged wheel and a motor use:
2121
+
2122
+ ```js
2123
+ const wheel = scene2D.addBox([1, 1], { position: [0, 3] });
2124
+ scene2D.addJoint(-1, wheel, [0, 3], [0, 0], {
2125
+ stiffness: [Infinity, Infinity, 0],
2126
+ });
2127
+ scene2D.addMotor(-1, wheel, { speed: 2, maxTorque: 10 });
2128
+ ```
2129
+
2130
+ Create 2D constraints before `createSolver()`. For stress tests, inspect overflow/clash counters and
2131
+ adapt: incomplete contact data must not be treated as a valid result.
2132
+
2133
+ The default 2D settings use the upstream GPU configuration: `alpha: 0.95`,
2134
+ nearest-contact matching and no extra position-correction pass. The original
2135
+ [AVBD 2D reference](https://github.com/savant117/avbd-demo2d) runs on the CPU
2136
+ and defaults to `alpha: 0.99` plus a final correction pass. For an explicit
2137
+ comparison, construct `AvbdScene2D({ alpha: 0.99, postStabilize: true,
2138
+ matchNearest: false })`. This changes solver settings, not the execution
2139
+ backend; the package still steps on the GPU. Neither option changes the core
2140
+ AVBD method.
2141
+
2142
+ ## 2D sleeping and hinge stops
2143
+
2144
+ 2D uses the same portable builder, with body indices rather than mesh handles.
2145
+ Enable sleeping on the solver, or opt in with a body's `allowSleep: true`.
2146
+ `allowSleep: false` keeps that body awake. Sleeping is off by default.
2147
+
2148
+ ```js
2149
+ // demo/readme-2d-policies.js
2150
+ import { AvbdScene2D, createWebGPUDevice } from "avbd-babylon/native";
2151
+
2152
+ export async function create2DPolicyExamples() {
2153
+ const { device } = await createWebGPUDevice();
2154
+ const scene = new AvbdScene2D({ timeStep: 1 / 120, iterations: 10 });
2155
+ scene.addPlane();
2156
+ const stack = [];
2157
+ for (let y = 0; y < 4; y++) {
2158
+ for (let x = 0; x < 16; x++) {
2159
+ stack.push(scene.addBox([1, 1], { position: [x * 1.02, y + 0.5] }));
2160
+ }
2161
+ }
2162
+ const arm = scene.addCapsule(0.2, 2, {
2163
+ position: [20, 3],
2164
+ allowSleep: false,
2165
+ });
2166
+ const hinge = scene.addHinge(-1, arm, [20, 3], [0, 0], {
2167
+ minAngle: -Math.PI / 4,
2168
+ maxAngle: Math.PI / 4,
2169
+ motor: { speed: 2, maxTorque: 10 },
2170
+ });
2171
+ const gpu = scene.createSolver(device, {
2172
+ sleeping: { speedThreshold: 0.03, timeThreshold: 0.3 },
2173
+ });
2174
+ return { scene, device, gpu, stack, arm, hinge };
2175
+ }
2176
+ ```
2177
+
2178
+ After creating this example, use live controls:
2179
+
2180
+ ```js
2181
+ const example = await create2DPolicyExamples();
2182
+ const { gpu, stack, hinge } = example;
2183
+ gpu.setMotor(hinge.motor.slot, { speed: -2, maxTorque: 5 });
2184
+ hinge.limits.setLimits({ minAngle: -0.2, maxAngle: 0.6 });
2185
+ gpu.setSleepEnabled(stack[0], false);
2186
+ gpu.applyImpulse(stack[1], [1, 0]); // also wakes sleeping bodies
2187
+ gpu.step();
2188
+ console.log(await gpu.readSleepStats());
2189
+ console.log((await gpu.readBodyState(stack[1])).sleeping);
2190
+ // gpu.attachToScene(babylonScene) can own timing and disposal.
2191
+ // A manual loop uses gpu.step() at the configured fixed timestep.
2192
+ hinge.dispose();
2193
+ gpu.destroy();
2194
+ example.device.destroy();
2195
+ ```
2196
+
2197
+ In 2D, motor speed and limit angles use A-minus-B rotation. Positive speed
2198
+ rotates body B clockwise when A is the world. Bounds are in radians relative
2199
+ to the starting relative angle. They can span more than one turn; there is no
2200
+ implicit wrap at ±π. A hinge with no bounds rotates freely. Equal bounds lock
2201
+ its relative angle. `addAngularLimit(a,b,{minAngle,maxAngle})` adds angle stops
2202
+ without connecting positions. Disposing a hinge releases its joint, stops and
2203
+ motor together; its individual handles can also be disposed separately.
2204
+
2205
+ Sleeping retains each body's mass and inertia, and keeps collision detection
2206
+ active. Under gravity, quiet supports establish a path from static ground or a
2207
+ world anchor through the pile. Unsupported slow falls stay awake. A powered
2208
+ motor stays awake even when pressing against a stop. Waking is conservative:
2209
+ an impact or edit can wake the whole world, so savings are largest after settling.
2210
+ `gpu.enableSleeping(options)` also updates thresholds; `disableSleeping()`
2211
+ wakes every body and removes the policy's buffers. These policies need no pose
2212
+ readbacks while stepping. A custom 2D renderer can inspect the sleeping flag in
2213
+ `velocity.w` (float 15 of each 24-float body); mass and inertia remain floats 22/23.
2214
+
2215
+ Try the `2d-sleeping-pile` and `2d-limited-hinges` scenes in the browser.
2216
+ The native GPU API benchmark measures sleeping in both dimensions, includes all sleep
2217
+ checks, and shows the cost while every body is awake as well as after settling.
2218
+
2219
+ ## Slingshot siege demo
2220
+
2221
+ After `npm start`, open [Slingshot siege](index.html?demo=slingshot).
2222
+ Pull the red ball back and release; Space fires, Next ball reloads and Rebuild
2223
+ restores the forts. Choose 1,000, 5,000 or 10,000 bricks. Target counts and bricks
2224
+ moved show the impact; the collapsible display shows frame rate, contacts,
2225
+ sleeping bodies and drawing time. Toggle Sleeping to compare the same scene.
2226
+ Scroll to zoom; Shift-drag pans the view. Boost gives each launched ball one
2227
+ extra impulse and a short thrust. The fixed timestep is 1/120 second with ten
2228
+ solver rounds; the meter shows the actual physics rate alongside frame rate.
2229
+
2230
+ The level uses every supported category of 2D physics:
2231
+
2232
+ | Feature | Where to see it |
2233
+ | ----------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
2234
+ | Boxes, circles, capsules, convex hulls, segments and planes | Bricks, balls, chain links, roof tiles, launcher and ground |
2235
+ | Joints, world anchors and ropes | Hanging targets and linked ragdoll limbs |
2236
+ | Welds and fracture | Small masonry panels and roof tiles use locked joints with a break force; impacts break the mortar |
2237
+ | Springs | Elastic platforms under the small ragdolls |
2238
+ | Limited hinges and motors | Powered blue gates reverse at their angle stops; toggle Powered gates |
2239
+ | Friction and restitution | Bricks grip their supports; Bouncy ball changes the projectile's bounce |
2240
+ | Masks, overlap sensors and contact events | The pale scoring arch reports ball crossings without blocking the ball; the held ball cannot collide |
2241
+ | Raycasts and circle casts | Drag and hold to aim; the red ring predicts the ball's first obstacle, allowing for its radius; the small cross marks the straight center ray |
2242
+ | Sleeping, waking and live edits | Toggle Sleeping; fire, reload or boost to wake and move bodies |
2243
+ | Velocity, force, torque and impulses | Launch sets velocity; Boost adds an impulse and a short thrust with spin |
2244
+
2245
+ The renderer draws the GPU body buffer directly. Periodic diagnostic copies count
2246
+ moved bricks, update gate controls and show broken supports; drawing does not
2247
+ download poses. Aiming queries run only while aiming. Predictions sample the
2248
+ current scene and do not provide continuous collision detection.
2249
+ The source is in `demo/slingshotScene2D.js` (level construction) and
2250
+ `demo/slingshotDemo.js` (GPU controls, events and queries).
2251
+ Its `2d-slingshot-siege` scene is also in the all-scene benchmark,
2252
+ which keeps sleeping off for consistent solver measurements.
2253
+
2254
+ ## Manual stepping and cleanup
2255
+
2256
+ For a custom fixed-step schedule, use this instead of automatic world creation.
2257
+ Here `scene` has no AVBD world yet and `floor` is a Babylon mesh; add all other
2258
+ aggregates before initialization:
2259
+
2260
+ ```js
2261
+ import { AvbdPhysics, AvbdShapeType } from "avbd-babylon";
2262
+
2263
+ const physics = await AvbdPhysics.create({
2264
+ scene,
2265
+ autoAttach: false,
2266
+ syncMeshes: false,
2267
+ timeStep: 1 / 60,
2268
+ });
2269
+ physics.addAggregate(floor, AvbdShapeType.BOX, { mass: 0 });
2270
+ physics.initialize();
2271
+ for (let step = 0; step < 120; step++) physics.step(); // two simulated seconds
2272
+ await physics.syncMeshes(); // explicit inspection rather than a per-frame wait
2273
+ const poses = await physics.readBodies();
2274
+ console.log(
2275
+ "Steps",
2276
+ physics.steps,
2277
+ "Finite data",
2278
+ poses.every(Number.isFinite),
2279
+ );
2280
+ physics.dispose();
2281
+ ```
2282
+
2283
+ `step()` submits one fixed step without waiting for GPU completion. Readbacks
2284
+ wait for the submitted data they inspect. Manually managed worlds must be
2285
+ explicitly disposed. A world-created device is destroyed with the world;
2286
+ an explicitly supplied device stays owned by the caller.
2287
+
2288
+ ## Worker integration
2289
+
2290
+ Keep Babylon and AVBD together in a module worker. Transfer an OffscreenCanvas
2291
+ once and send small input/resize messages from the page. Save the brick-wall
2292
+ helper alongside the worker. This uses Babylon WebGL drawing and AVBD WebGPU
2293
+ physics; there is no separate AVBD solver implementation to select.
2294
+
2295
+ ```js
2296
+ // src/worker-main.js — use instead of src/main.js
2297
+ const canvas = document.getElementById("canvas");
2298
+ const worker = new Worker(new URL("./render-worker.js", import.meta.url), {
2299
+ type: "module",
2300
+ });
2301
+ const dimensions = () => ({
2302
+ width: Math.max(1, canvas.clientWidth),
2303
+ height: Math.max(1, canvas.clientHeight),
2304
+ });
2305
+ const offscreen = canvas.transferControlToOffscreen();
2306
+ worker.postMessage({ type: "init", canvas: offscreen, ...dimensions() }, [
2307
+ offscreen,
2308
+ ]);
2309
+ window.addEventListener("resize", () =>
2310
+ worker.postMessage({ type: "resize", ...dimensions() }),
2311
+ );
2312
+ window.addEventListener("keydown", (event) => {
2313
+ if (event.code === "Space") {
2314
+ event.preventDefault();
2315
+ worker.postMessage({ type: "shoot" });
2316
+ }
2317
+ });
2318
+ worker.addEventListener("message", ({ data }) => {
2319
+ if (data.type === "error") console.error(data.message);
2320
+ });
2321
+ window.addEventListener("pagehide", () => worker.terminate(), { once: true });
2322
+ ```
2323
+
2324
+ ```js
2325
+ // src/render-worker.js
2326
+ import {
2327
+ Engine,
2328
+ Scene,
2329
+ FreeCamera,
2330
+ HemisphericLight,
2331
+ Vector3,
2332
+ } from "@babylonjs/core";
2333
+ import { createBrickWallScene } from "./brick-wall.js";
2334
+
2335
+ let engine, scene, physics, shoot;
2336
+ self.onmessage = async ({ data }) => {
2337
+ try {
2338
+ if (data.type === "init") {
2339
+ data.canvas.width = data.width;
2340
+ data.canvas.height = data.height;
2341
+ engine = new Engine(data.canvas, true);
2342
+ scene = new Scene(engine);
2343
+ const camera = new FreeCamera("camera", new Vector3(0, 5, -16), scene);
2344
+ camera.setTarget(new Vector3(0, 2, 0));
2345
+ new HemisphericLight("light", new Vector3(0, 1, 0), scene);
2346
+ ({ physics, shoot } = await createBrickWallScene(scene));
2347
+ engine.runRenderLoop(() => scene.render());
2348
+ self.postMessage({ type: "ready" });
2349
+ } else if (data.type === "resize" && engine) {
2350
+ engine.setSize(data.width, data.height);
2351
+ } else if (data.type === "shoot" && shoot) {
2352
+ shoot();
2353
+ }
2354
+ } catch (error) {
2355
+ self.postMessage({ type: "error", message: error.message });
2356
+ }
2357
+ };
2358
+ ```
2359
+
2360
+ Camera controls that use DOM events need similar forwarding. Workers keep
2361
+ setup and submission off the UI thread; they do not accelerate GPU kernels.
2362
+ For large scenes, keep direct GPU rendering in that worker instead of copying
2363
+ every pose back to the page.
2364
+
2365
+ ## Benchmark replication
2366
+
2367
+ Start this repository with `npm start`. Play scenes while the benchmark is
2368
+ idle; drawing and sleeping stop during solver-only measurements.
2369
+ Check the GPU shown in the results before comparing timings. On computers with
2370
+ two GPUs, a browser can choose the integrated GPU even when high performance is
2371
+ requested. The repository's isolated Chromium checks request its high-performance
2372
+ GPU and saved results record the adapter that actually ran each benchmark.
2373
+
2374
+ | Demo | What it exercises |
2375
+ | ------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------- |
2376
+ | [Mixed gallery](index.html?demo=showcase) | All five collider types, rotation and floor contacts. |
2377
+ | [Castle siege](index.html?demo=cannon) | Mortar, walls, towers, roofs and local fracture. |
2378
+ | [100K rook impact](index.html?demo=gpu-stress) | Local projectile damage and direct GPU drawing. |
2379
+ | [28K brick ring](index.html?demo=canonical&scene=showcase-brick-ring-28k&backend=gpu) | A projectile striking a circular wall. |
2380
+ | [100K columns](index.html?demo=canonical&scene=showcase-box-columns-100k&backend=gpu) | Sparse contacts and shared contact-point work. |
2381
+ | [2D hinges and motors](index.html?demo=canonical&scene=2d-limited-hinges&backend=gpu) | Angle stops, reverse drive and braking. |
2382
+ | [3D hinges and motors](index.html?demo=canonical&scene=3d-limited-hinges&backend=gpu) | Axis alignment, angle stops, reverse drive and braking. |
2383
+ | [2D triggers and masks](index.html?demo=canonical&scene=2d-triggers-and-masks&backend=gpu) | Non-solving overlaps, mutual masks, live filter edits, ray and circle casts. |
2384
+ | [3D triggers and masks](index.html?demo=canonical&scene=3d-triggers-and-masks&backend=gpu) | Non-solving overlaps, mutual masks, live filter edits, ray and sphere casts. |
2385
+ | [Tearable fabric](index.html?demo=canonical&scene=3d-tearable-cloth&backend=gpu) | Elastic stretching, reinforced corner supports and persistent fragments. |
2386
+ | [Rigid net and ragdolls](index.html?demo=canonical&scene=showcase-ragdolls-on-cloth-24k&backend=gpu) | Connected rigid parts and a plate net. |
2387
+ | [Fabric and blocks](index.html?demo=canonical&scene=paper-cloth-35k&backend=gpu) | Deformable fabric, joints and rigid/fabric contacts. |
2388
+ | [Half-million walls](index.html?demo=canonical&scene=paper-walls-510k-4&backend=gpu&quality=benchmark) | Dense contacts and four AVBD iterations. |
2389
+ | [100K 2D pile](index.html?demo=canonical&scene=2d-stress-pile-100k&backend=gpu) | Dense 2D contacts and ten iterations. |
2390
+ | [D20 particles](index.html?demo=benchmark) | Collision timing and particle sprites; not an AVBD rigid-body benchmark. |
2391
+
2392
+ Open [the browser benchmark](tests/performance.html), choose
2393
+ 60 warmup steps, 60 samples and three repeats, with Automatic collision selection.
2394
+ For scripted replication, run this in that page's developer console:
2395
+
2396
+ ```js
2397
+ // On /tests/performance.html after the page is ready.
2398
+ // These globals are repository test helpers, not the npm API.
2399
+ await __BENCHMARK_VIEW__.suspendPreview();
2400
+ try {
2401
+ const results = [];
2402
+ for (const scene of [
2403
+ "3d-sphere-contacts",
2404
+ "3d-convex-hulls",
2405
+ "3d-rope",
2406
+ "3d-breakable",
2407
+ "showcase-ragdolls-on-cloth-24k",
2408
+ "showcase-box-columns-100k",
2409
+ "showcase-brick-ring-110k",
2410
+ "paper-walls-510k-4",
2411
+ "2d-stress-pile-100k",
2412
+ ]) {
2413
+ for (let repeat = 0; repeat < 3; repeat++) {
2414
+ results.push({
2415
+ ...(await __PERFORMANCE__.run(scene, "production", 60, 60)),
2416
+ repeat,
2417
+ });
2418
+ }
2419
+ }
2420
+ console.table(
2421
+ results.map((r) => ({
2422
+ scene: r.scene,
2423
+ objects: r.bodyCount,
2424
+ contacts: r.peakContacts,
2425
+ iterations: r.params.iterations,
2426
+ collisionMs: r.summary.collision.mean,
2427
+ movementMs: r.summary.solve.mean,
2428
+ physicsMs: r.summary.total.mean,
2429
+ slowerStepsMs: r.summary.total.p95,
2430
+ cpuSubmissionMs: r.summary.submission.mean,
2431
+ })),
2432
+ );
2433
+ console.log({ adapter: __PERFORMANCE__.info, results });
2434
+ } finally {
2435
+ await __BENCHMARK_VIEW__.preview.load(document.getElementById("scene").value);
2436
+ }
2437
+ ```
2438
+
2439
+ Each run starts fresh and checks contact storage, parallel-work conflicts and
2440
+ finite poses. GPU stages measure finding contacts, preparing them, planning
2441
+ parallel work and moving objects. CPU submission and startup have separate
2442
+ fields; wall time also includes waiting for measurements. These are physics
2443
+ timings, not rendered FPS. The [integration guide](docs/README.md#reproduce-the-collision-and-solver-benchmarks)
2444
+ includes downloads and profiling commands.
2445
+
2446
+ Saved reports were recorded on an **NVIDIA GeForce RTX 4070 Laptop GPU**. Live
2447
+ runs measure your GPU. The paper's desktop RTX 4090 numbers use different
2448
+ hardware and reconstructed workloads; they are a separate comparison.
2449
+ Keep other GPU workloads closed when measuring. Enhanced lighting and AA are
2450
+ for playing; use `quality=benchmark` for rendering comparisons.
2451
+
2452
+ ## Babylon renderer comparison
2453
+
2454
+ Every canonical 2D and 3D scene, plus the slingshot game, has a **Renderer**
2455
+ selector. Choose **Babylon WebGPU** to play with Babylon drawing the scene;
2456
+ switching renderers restarts it. Direct WebGPU is the default. The benchmark
2457
+ page's playground has the same choice.
2458
+
2459
+ Try [100K boxes in Babylon](index.html?demo=canonical&scene=showcase-box-columns-100k&backend=gpu&renderer=babylon),
2460
+ [tearable cloth](index.html?demo=canonical&scene=3d-tearable-cloth&backend=gpu&renderer=babylon)
2461
+ or [the slingshot game](index.html?demo=slingshot&renderer=babylon).
2462
+
2463
+ Babylon uses thin instances for shared shapes and merges unique hull geometry
2464
+ for the castle and fractured pieces. Materials and world matrices are frozen;
2465
+ CPU bounds updates and mesh picking are disabled. Body poses stay on the GPU:
2466
+ AVBD and Babylon use the same device, and GPU copies supply Babylon-owned
2467
+ storage buffers to the vertex shaders. Instance matrices are uploaded once.
2468
+ Colors are copied again only when edited. Camera controls, contact overlays,
2469
+ shape sizes and cloth tearing use the viewer's existing geometry and shaders.
2470
+
2471
+ The [Babylon renderer source](demo/babylonGpuRenderer.js) and
2472
+ [WGSL adapter](demo/babylonShaders.js) show the integration. This is a custom
2473
+ Babylon shader renderer; ordinary Babylon materials use the matrix-update path
2474
+ shown in [Large pile with thin instances](#large-pile-with-thin-instances).
2475
+ The demo isolates its access to Babylon 9's internal `_device` field in
2476
+ `createBabylonEngine()`. The npm package remains independent of this demo code
2477
+ and has no Babylon runtime dependency.
2478
+
2479
+ Open [the renderer benchmark](tests/performance.html#renderer-comparison) to
2480
+ compare either the selected scene or every scene. It advances physics once,
2481
+ then draws that same state with both renderers, alternating which draws first.
2482
+ Both use the same device, camera, geometry and shaders at 960 × 540 pixels,
2483
+ with antialiasing and debug overlays off. The preview stops during measurement.
2484
+ An all-scene run keeps one engine/device and creates a fresh physics world for
2485
+ each scene and repeat. Each scene combines the measured draws across repeats;
2486
+ individual runs remain available in its details.
2487
+ Blue bars show GPU drawing; orange shows Babylon's GPU buffer copies. CPU
2488
+ submission and the wait for GPU completion are separate measurements. These
2489
+ numbers describe rendering cost, rather than displayed FPS or paper results.
2490
+
2491
+ ```js
2492
+ // Repository benchmark helper, on /tests/performance.html.
2493
+ await __BENCHMARK_VIEW__.suspendPreview();
2494
+ try {
2495
+ const result = await __PERFORMANCE__.runRenderers(
2496
+ "showcase-box-columns-100k",
2497
+ { warmup: 20, samples: 30 },
2498
+ );
2499
+ console.table({
2500
+ directWebGPU: result.webgpu,
2501
+ babylonWebGPU: result.babylon,
2502
+ });
2503
+ } finally {
2504
+ await __BENCHMARK_VIEW__.preview.load(
2505
+ document.getElementById("scene").value,
2506
+ );
2507
+ }
2508
+ ```
2509
+
2510
+ Run `npm run test:renderers` for scene and benchmark checks, or
2511
+ `npm run benchmark:renderers` to refresh the three-repeat comparison of small,
2512
+ medium and large workloads. Add `-- --all` to benchmark every canonical scene.
2513
+ The [saved comparison](test-results/renderer-performance.html) identifies its
2514
+ RTX 4070 Laptop GPU and links the raw measurements and every scene screenshot.
2515
+
2516
+ ![Renderer timing comparison](test-results/screenshots/renderer-comparison-1440.png)
2517
+
2518
+ | Babylon: 2D shapes | Babylon: torn fabric |
2519
+ | --- | --- |
2520
+ | ![Babylon 2D mixed shapes](test-results/screenshots/babylon-2d-mixed-shapes.png) | ![Babylon torn cloth with detached patch](test-results/screenshots/babylon-cloth-torn.png) |
2521
+ | Babylon: ragdolls on a net | Babylon: 100K columns |
2522
+ | ![Babylon ragdolls and rigid net](test-results/screenshots/babylon-showcase-ragdolls-on-cloth-24k.png) | ![Babylon 100K box columns](test-results/screenshots/babylon-showcase-box-columns-100k.png) |
2523
+
2524
+ See the [live comparison controls](test-results/screenshots/renderer-live-comparison.png),
2525
+ [mobile comparison layout](test-results/screenshots/renderer-comparison-390.png),
2526
+ and [mobile Babylon viewer](test-results/screenshots/babylon-cloth-mobile.png).
2527
+
2528
+ ## Script-tag usage
2529
+
2530
+ Run `npm run build:cdn` and serve `dist/avbd.global.js` from your site. It assigns
2531
+ `globalThis.AVBD` and works with an existing Babylon global:
2532
+
2533
+ ```html
2534
+ <script src="./vendor/babylon.js"></script>
2535
+ <script src="./vendor/avbd.global.js"></script>
2536
+ <script type="module">
2537
+ const { AvbdPhysics, AvbdPhysicsAggregate, AvbdShapeType } = AVBD;
2538
+ // scene, floor and box belong to your existing Babylon setup.
2539
+ const physics = await AvbdPhysics.create({ scene });
2540
+ new AvbdPhysicsAggregate(floor, AvbdShapeType.BOX, { mass: 0 }, scene);
2541
+ new AvbdPhysicsAggregate(box, AvbdShapeType.BOX, { mass: 2 }, scene);
2542
+ // Start rendering after adding every aggregate.
2543
+ </script>
2544
+ ```
2545
+
2546
+ ESM and global builds expose the same API. The package's `unpkg` and `jsdelivr`
2547
+ entries select its global build for CDN use when published.
2548
+
2549
+ ## Build and run the repository
2550
+
2551
+ ```sh
2552
+ npm install
2553
+ npm run build # ESM library only: dist/avbd.js
2554
+ npm run build:cdn # Browser global: dist/avbd.global.js
2555
+ npm run build:web # Website and browser benchmarks
2556
+ npm run build:all # All three outputs
2557
+ npm start # Build library, watch and serve website
2558
+ npm pack # Build and create an installable library archive
2559
+ ```
2560
+
2561
+ Release, website and GPU-test builds omit source maps by default. They are
2562
+ debugging files, not runtime dependencies, and can be much larger than the
2563
+ JavaScript. To generate them locally:
2564
+
2565
+ ```sh
2566
+ npm run build -- --sourcemap
2567
+ npm run build:cdn -- --sourcemap
2568
+ npm run build:web -- --sourcemap
2569
+ npm test -- --sourcemap
2570
+ ```
2571
+
2572
+ `AVBD_SOURCEMAPS=1` also enables maps for these commands. Normal builds remove
2573
+ stale maps for their outputs. The npm package excludes maps even when you
2574
+ generate them locally; TypeScript declarations are still included.
2575
+
2576
+ Tinybuild configs keep the library and website separate. Package files include
2577
+ the solver, types, docs and upstream notice; demos and test output are excluded.
2578
+ `src/` contains the library, `demo/` the website, and `tests/legacy/` the fixtures
2579
+ required by active GPU regressions.
2580
+
2581
+ ## Validation and attribution
2582
+
2583
+ ```sh
2584
+ npm test # GPU physics regressions
2585
+ npm run test:complete # GPU, package/source checks and live README examples
2586
+ npm run test:castle # Mortar, impacts, rotation and responsive layout
2587
+ npm run test:package # ESM/global exports and production dependency checks
2588
+ npm run test:types # Public TypeScript usage, including Babylon and 2D handles
2589
+ npm run test:web # GPU scenes, benchmarks, controls and reports
2590
+ npm run test:features # Playable 2D/3D hinge, motor, trigger, mask and cast checks
2591
+ npm run benchmark:native # Bulk edits, sleeping and selected reads; three repeats
2592
+ npm run benchmark:native-edits # 100K motion edits, 10K property changes and batched queries
2593
+ ```
2594
+
2595
+ Results and screenshots are generated in `test-results/`. Normal physics tests
2596
+ run on the GPU; package and file-integrity checks run on the host. See
2597
+ [the test guide](tests/README.md) for coverage and limitations.
2598
+
2599
+ The solver incorporates Steven Bobyn's [three-avbd](https://github.com/sbobyn/three-avbd),
2600
+ preserved under `reference/three-avbd/` and checked against recorded upstream
2601
+ hashes. Its MIT license and the upstream Chris Giles and Erin Catto notices are
2602
+ included in [dist/NOTICE](dist/NOTICE). The package's own license is [MIT](LICENSE).
2603
+
2604
+ The [package additions](#what-this-package-adds) cover collision detection,
2605
+ GPU execution, 2D/3D features, integration and validation. Their implementations
2606
+ and regressions live alongside the preserved upstream foundation. The core
2607
+ AVBD equations retain upstream credit.