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/LICENSE +21 -0
- package/README.md +2606 -2
- package/dist/NOTICE +145 -0
- package/dist/avbd.d.ts +3 -0
- package/dist/avbd.global.d.ts +2 -0
- package/dist/avbd.global.js +4884 -0
- package/dist/avbd.js +4884 -0
- package/dist/babylonAvbd.d.ts +285 -0
- package/dist/gpuDevice.d.ts +10 -0
- package/dist/native.d.ts +2 -0
- package/dist/native.js +1 -0
- package/dist/nativeScenes.d.ts +735 -0
- package/docs/BABYLON_API.md +356 -0
- package/docs/HPLOC.md +116 -0
- package/docs/README.md +459 -0
- package/docs/RELEASE.md +167 -0
- package/package.json +112 -4
- package/tests/README.md +170 -0
package/README.md
CHANGED
|
@@ -1,3 +1,2607 @@
|
|
|
1
|
-
#
|
|
1
|
+
# AVBD for Babylon.js
|
|
2
2
|
|
|
3
|
-
|
|
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
|
+
| [](test-results/screenshots/voronoi-120.png)<br>216 convex stones fill a bounded Voronoi partition; shared faces carry breakable welds. | [](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
|
+
| [](test-results/screenshots/mixed-gallery-settled.png)<br>Babylon meshes follow the GPU poses, with visible contacts and shadows. | [](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
|
+
| [](test-results/screenshots/route-cannon.png)<br>Breakable mortar holds the castle together until a cannonball hits. | [](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
|
+
| [](test-results/screenshots/showcase-brick-ring-110k-gpu.png)<br>A circular masonry wall with a localized impact. | [](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
|
+
| [](test-results/screenshots/columns-enhanced.png)<br>Five solver iterations, batch drawing and optional enhanced colors and lighting. | [](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
|
+
| [](test-results/screenshots/slingshot-impact.png)<br>Breakable forts, ropes, springs, powered gates, sensors and ragdolls. | [](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
|
+
| [](test-results/screenshots/showcase-ragdolls-on-cloth-24k-six-seconds.png)<br>Linked rigid plates catch the ragdolls; this scene uses five solver iterations. | [](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
|
+
| [](test-results/screenshots/paper-cloth-35k-gpu.png)<br>A spring-based fabric workload with point-sampled collisions. | [](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
|
+
| [](test-results/screenshots/chainmail-settled.png)<br>A network of rigid bodies connected by joints. | [](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
|
+
| [](test-results/screenshots/3d-rope-gpu.png)<br>A rope built from connected rigid bodies. | [](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
|
+
| [](test-results/screenshots/3d-convex-hulls-gpu.png)<br>Custom hull geometry participates in the GPU contact solver. | [](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
|
+
| [](test-results/screenshots/3d-limited-hinges.png)<br>Angle stops, torque-limited motors and live reverse/brake controls. | [](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
|
+
| [](test-results/screenshots/3d-triggers-and-masks.png)<br>Non-solving overlap detection, collision filtering and ray/sphere casts. | [](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
|
+
| [](test-results/screenshots/collision-sprites.png)<br>GPU particles are drawn as sprites around the rotating collision target. | [](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
|
+
[](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
|
+

|
|
2517
|
+
|
|
2518
|
+
| Babylon: 2D shapes | Babylon: torn fabric |
|
|
2519
|
+
| --- | --- |
|
|
2520
|
+
|  |  |
|
|
2521
|
+
| Babylon: ragdolls on a net | Babylon: 100K columns |
|
|
2522
|
+
|  |  |
|
|
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.
|