babylon-box3d 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,57 +1,228 @@
1
1
  # babylon-box3d
2
2
 
3
- [Box3D](https://github.com/erincatto/box3d), Erin Catto's 3D rigid body engine, compiled to WebAssembly for the
4
- Babylon.js `Box3DPlugin`. This package is the Box3D counterpart of `@babylonjs/havok`: it ships the engine and a
5
- thin C shim, the Babylon.js plugin lives in Babylon.js core.
3
+ [Box3D](https://github.com/erincatto/box3d) physics for Babylon.js. Box3D is Erin Catto's 3D successor to Box2D v3:
4
+ an MIT licensed rigid body engine with a soft step solver, continuous collision, cross platform determinism and
5
+ SIMD. This extension ships the engine compiled to WebAssembly plus `Box3DPlugin`, an implementation of Babylon's
6
+ physics v2 plugin interface, so it drops in wherever the Havok plugin is used.
6
7
 
7
8
  ```ts
8
- import Box3D from "babylon-box3d";
9
- import { Box3DPlugin } from "@babylonjs/core/Physics/v2/Plugins/box3dPlugin";
9
+ import { Vector3 } from "@babylonjs/core/Maths/math.vector";
10
+ import { Box3D, Box3DPlugin } from "babylon-box3d";
10
11
 
11
12
  const box3d = await Box3D();
12
13
  scene.enablePhysics(new Vector3(0, -9.81, 0), new Box3DPlugin(true, box3d));
13
14
  ```
14
15
 
15
- From a script tag (UMD build, defines a global `Box3D` factory):
16
+ Script tags (Playground, plain HTML):
16
17
 
17
18
  ```html
18
- <script src="https://cdn.jsdelivr.net/npm/babylon-box3d/lib/umd/box3d.umd.js"></script>
19
+ <script src="https://cdn.babylonjs.com/babylon.js"></script>
20
+ <script src="https://unpkg.com/babylon-box3d/lib/umd/box3d.umd.js"></script>
21
+ <script src="https://unpkg.com/babylon-box3d/umd/babylon.box3d.min.js"></script>
19
22
  <script>
20
- const box3d = await Box3D({ locateFile: (f) => "https://cdn.jsdelivr.net/npm/babylon-box3d/lib/umd/" + f });
23
+ const box3d = await Box3D();
24
+ scene.enablePhysics(new BABYLON.Vector3(0, -9.81, 0), new BABYLONBOX3D.Box3DPlugin(true, box3d));
21
25
  </script>
22
26
  ```
23
27
 
24
- The wasm is fetched next to the loader script by default. Pass `locateFile` when it is served from somewhere else.
28
+ The wasm is fetched next to the loader script. With a bundler pass `locateFile`, for example with vite:
25
29
 
26
- ## Contents
30
+ ```ts
31
+ import { Box3D, Box3DPlugin } from "babylon-box3d";
32
+ import wasmUrl from "babylon-box3d/lib/esm/box3d.wasm?url";
33
+
34
+ const box3d = await Box3D({ locateFile: () => wasmUrl });
35
+ ```
36
+
37
+ ```ts
38
+ // vite.config.ts: emscripten loaders find their wasm through import.meta.url, keep them out of the pre-bundle
39
+ export default defineConfig({ optimizeDeps: { exclude: ["babylon-box3d"] } });
40
+ ```
41
+
42
+ `box3d.js` and `box3d.wasm` have to come from the same install. If an app copies the wasm somewhere of its own (a
43
+ `public` folder, a CDN) and points `locateFile` at the copy, that copy has to be refreshed whenever the package is,
44
+ and a bundler's dependency cache (`node_modules/.vite`) has to be cleared with it. A loader and a wasm from different
45
+ builds cannot bind to each other: the loader fails to instantiate, or `new Box3DPlugin` throws naming the entry points
46
+ it could not find. Keeping the wasm out of any copy step, as above, avoids the question entirely.
47
+
48
+ Peer dependency: `@babylonjs/core` 8 or 9 (built and tested against 8.56.2 and 9.26.1). Babylon 9 registers
49
+ `Scene.enablePhysics` in `@babylonjs/core/Physics/joinedPhysicsEngineComponent`, so import that (or the `@babylonjs/core`
50
+ index) somewhere in the app.
51
+
52
+ ### Installing a local build
53
+
54
+ ```
55
+ npm run build # dist and umd; lib (the wasm) is committed, rebuild it only after changing the shim
56
+ npm pack # babylon-box3d-<version>.tgz
57
+ npm install ../babylon-box3d/babylon-box3d-<version>.tgz # in the game
58
+ ```
59
+
60
+ A tarball is the safest route because it carries no `node_modules`. `npm install ../babylon-box3d` works too, but npm
61
+ links the checkout, so the bundler finds the extension's own copy of `@babylonjs/core` and ships Babylon twice (the
62
+ `instanceof` checks in the plugin then fail). With a link, dedupe it:
63
+
64
+ ```ts
65
+ // vite.config.ts
66
+ export default defineConfig({
67
+ resolve: { dedupe: ["@babylonjs/core"] },
68
+ optimizeDeps: { exclude: ["babylon-box3d"] },
69
+ });
70
+ ```
71
+
72
+ ## What is covered
73
+
74
+ | Babylon v2 | Box3D |
75
+ | --- | --- |
76
+ | `PhysicsShapeType.SPHERE`, `CAPSULE` | sphere, capsule |
77
+ | `BOX`, `CYLINDER`, `CONVEX_HULL` | convex hulls (hulls above box3d's 128 edge limit are simplified) |
78
+ | `MESH` | triangle mesh, static and animated bodies only |
79
+ | `HEIGHTFIELD` | height field, static bodies only, with holes (see below) |
80
+ | `CONTAINER` | multiple Box3D shapes on one body (a mesh child's transform is baked into a copy of its mesh data) |
81
+ | `BALL_AND_SOCKET` | spherical joint (cone and twist limits) |
82
+ | `HINGE` | revolute joint (rotation about `axisA`) |
83
+ | `PRISMATIC`, `SLIDER` | prismatic joint |
84
+ | `LOCK` | weld joint |
85
+ | `DISTANCE` | distance joint with a fixed length of `maxDistance` |
86
+ | `SIX_DOF` (`Physics6DoFConstraint`) | one Box3D joint chosen from the limits with Havok's axis rules, see below |
87
+ | `SpringConstraint` | distance joint spring (`stiffness` in N/m like Havok) |
88
+ | `COLLISION_STARTED` / `FINISHED` | contact begin / end touch events (started carries the manifold point, normal and normal impulse) |
89
+ | `COLLISION_CONTINUED` | contact hit events (point, normal, approach speed as `impulse`) |
90
+ | `TRIGGER_ENTERED` / `EXITED` | sensor events |
91
+ | thin instances | one Box3D body per instance |
92
+
93
+ **Height field holes.** Box3D height fields take a material index per cell, and 255 cuts the cell out: nothing
94
+ collides with it and rays pass through. Pass them as `heightFieldMaterials`, one per cell in the same row order as
95
+ `heightFieldData`:
96
+
97
+ ```ts
98
+ import { PhysicsShape } from "@babylonjs/core/Physics/v2/physicsShape";
99
+ import { PhysicsShapeType } from "@babylonjs/core/Physics/v2/IPhysicsEnginePlugin";
100
+ import { BOX3D_HEIGHT_FIELD_HOLE } from "babylon-box3d";
101
+
102
+ const materials = new Uint8Array((samplesX - 1) * (samplesZ - 1));
103
+ materials[row * (samplesX - 1) + column] = BOX3D_HEIGHT_FIELD_HOLE;
104
+ const terrain = new PhysicsShape(
105
+ {
106
+ type: PhysicsShapeType.HEIGHTFIELD,
107
+ parameters: {
108
+ heightFieldSizeX: sizeX,
109
+ heightFieldSizeZ: sizeZ,
110
+ numHeightFieldSamplesX: samplesX,
111
+ numHeightFieldSamplesZ: samplesZ,
112
+ heightFieldData: heights,
113
+ heightFieldMaterials: materials,
114
+ },
115
+ },
116
+ scene,
117
+ );
118
+ ```
119
+
120
+ A ray's `triangleIndex` is the height field triangle it hit, as it is for a mesh.
121
+
122
+ `PhysicsMassProperties` follows Havok: `inertia` is the principal moments **per unit mass** (so setting only `mass`
123
+ scales the shape's inertia with it), `inertiaOrientation` rotates those principal axes into body space, and a zero
124
+ component means infinite inertia about that axis. Box3D needs an invertible tensor, so a locked axis gets a moment
125
+ 100000 times the largest free one, and when the remaining free axis is world aligned (the usual `inertia (0, 1, 0)`
126
+ upright character) Box3D's own motion locks are added on top, which makes it exact.
127
+
128
+ Event masks use Havok's bits (1 started, 2 continued, 4 finished) and only enable what they ask for: started and
129
+ finished map to Box3D's begin/end touch events, continued to its hit events, so bodies that only want one kind (every
130
+ body of a Babylon `Ragdoll`, for instance) do not pay for the others.
131
+
132
+ `SIX_DOF` limits follow Havok: an axis that is not listed is free, `minLimit === maxLimit === 0` locks it, anything
133
+ else limits it (one sided ranges such as 0..140 degrees work). The frame is `[axis, perpAxis, axis x perpAxis]` on each
134
+ body; `ANGULAR_X` is the rotation of the child frame about `axis`, `ANGULAR_Y` about `perpAxis` and `ANGULAR_Z` about
135
+ `axis x perpAxis`, right handed, in the same directions as Havok (the tests check this against Havok). Box3D has a fixed
136
+ set of joints, so the limits pick one:
137
+
138
+ | linear axes | angular axes | Box3D joint |
139
+ | --- | --- | --- |
140
+ | all locked | all locked | weld |
141
+ | all locked | one limited or free | revolute about that axis with its limits |
142
+ | all locked | two or three open | spherical: twist limits from `ANGULAR_X`, one symmetric cone from `ANGULAR_Y`/`ANGULAR_Z` |
143
+ | two locked | all locked | prismatic along the open axis |
144
+ | only `LINEAR_DISTANCE` | free | distance joint (a spring when min == max and a stiffness is set, a rope otherwise) |
145
+ | free | free | filter joint (only disables collision between the pair) |
146
+
147
+ Box3D's cone is symmetric: when the `ANGULAR_Y` and `ANGULAR_Z` ranges differ or are asymmetric the larger one is used
148
+ and a warning is logged once; cones are capped at 90 degrees. Limit `stiffness`/`damping` become Box3D's per joint
149
+ constraint softness, an approximation that applies to the whole joint. Other combinations warn once and use the closest
150
+ joint above.
151
+
152
+ `setActivationControl` follows Havok: `ALWAYS_ACTIVE` turns Box3D sleeping off for the body, `ALWAYS_INACTIVE` parks it
153
+ (it ignores impulses and velocity changes, is not woken by anything touching it and still blocks other bodies) and
154
+ `SIMULATION_CONTROLLED` hands it back to the solver, asleep until something wakes it.
155
+
156
+ Box3D extras on the plugin: `explode`, `createWheelJoint` (suspension, steering, spin motor), `createParallelJoint`,
157
+ `setShapeFilterGroup`, `setShapeRollingResistance`, `setAllowFastRotation`, `getStats`. `PhysicsCharacterController`
158
+ is not supported yet, it depends on Havok internals.
159
+
160
+ Box3D creates shapes on bodies while Babylon creates shapes standalone, so a `PhysicsShape` is a description that
161
+ is instantiated on every body it is set on. Changing its filter masks, material or density is applied to the live
162
+ Box3D shapes in place; only geometry, children and the trigger flag rebuild them. Hull data is copied into Box3D's world database, mesh and height field
163
+ data are shared and reference counted. Only bodies reported by Box3D's move events are synced each step. Box3D is
164
+ right handed and Babylon left handed by default; no conversion is done, the simulation runs in the mirrored frame
165
+ and mesh winding is flipped, exactly like the Havok plugin.
166
+
167
+ ## Layout
27
168
 
28
169
  | path | what |
29
170
  | --- | --- |
30
- | `lib/esm/box3d.js`, `box3d.wasm` | ES module for bundlers and browsers (default export is the factory) |
31
- | `lib/umd/box3d.umd.js`, `box3d.umd.wasm` | script tag / CommonJS build, global `Box3D` |
32
- | `lib/node/box3d.mjs`, `box3d.wasm` | ES module for node (`import Box3D from "babylon-box3d/node"`) |
33
- | `box3d.d.ts` | typings for the shim API |
34
- | `src/box3d_shim.c` | the C shim: flat, handle based API over box3d (`bx_*` functions) |
35
- | `UPSTREAM_COMMIT` | the box3d commit that was built |
36
-
37
- The shim keeps integer slots for bodies, shape descriptions and joints, returns multi-value results through a
38
- scratch float buffer, and copies box3d's per step events (body moves, contacts, sensors) into flat buffers so the
39
- plugin reads them with one typed array view. Hull data is copied into box3d's world database, mesh and height
40
- field data are shared and reference counted.
171
+ | `src/box3dPlugin.ts` | the plugin (`IPhysicsEnginePluginV2`) |
172
+ | `src/index.ts` | package entry: `Box3DPlugin`, `Box3DWheelJoint`, `Box3D` (wasm factory) |
173
+ | `wasm/box3d_shim.c` | C shim: flat, handle based API over box3d (`bx_*` functions) |
174
+ | `wasm/build.mjs` | emcc build for `lib/esm`, `lib/umd` (global `Box3D`) and `lib/node` |
175
+ | `lib/` | committed wasm builds and typings, `UPSTREAM_COMMIT` is the box3d commit |
176
+ | `umd/` | plugin bundle for script tags (global `BABYLONBOX3D`), built by `npm run build:umd` |
177
+ | `demo/` | vite showcase: `npm run demo`, then `http://localhost:5178/?demo=pyramid` |
178
+ | `docs/` | the community extension page for the Babylon.js documentation |
179
+ | `test/` | vitest unit tests with a mocked wasm module, tests against the real wasm, node smoke test, and `packaged.test.ts` over the built `dist` and every loader in `lib` |
180
+ | `bench/` | Box3D vs Havok vs Oimo benchmark: `npm run bench` (node) or `demo/bench.html` (browser), results in `bench/results` |
181
+
182
+ Demos: `pyramid` (Box3D's Large Pyramid benchmark, thin instances, `&rows=100` for 5050 boxes, click to explode),
183
+ `ragdolls` (Erin's human ragdoll sliding down a chute), `car` (wheel joints, WASD), `destruction` (brick tower and
184
+ wrecking ball), plus `stack`, `joints`, `terrain`, `compound` feature tests. Add `&ui=0` to hide the overlay.
185
+
186
+ ## Benchmarks
187
+
188
+ Box3D, Havok and Oimo build the same scenes through Babylon's regular physics API (v2 for Box3D and Havok, v1 for
189
+ Oimo), with engine defaults, a fixed 1/60 s step and nothing rendered. "Step" is Babylon's whole physics step,
190
+ "engine" is only the engine's own world step. Median of 3 interleaved runs after a warm up, AMD Ryzen 9 5900X,
191
+ Babylon.js 8.56.2, @babylonjs/havok 1.3.14, oimo 1.0.9. Mean milliseconds per step, sleep on:
192
+
193
+ | Scene | Box3D | Havok | Oimo |
194
+ | --- | ---: | ---: | ---: |
195
+ | Pyramid, 20 rows (210 boxes) | 0.09 | 0.47 | 5.94 |
196
+ | Pyramid, 50 rows (1275 boxes) | 0.89 | 7.05, collapses | 69.4, collapses |
197
+ | Pyramid, 100 rows (5050 boxes) | 31.7 | 29.9, collapses | 219, collapses |
198
+ | Pile, 1000 boxes and spheres | 3.45 | 4.26 | 33.3 |
199
+ | Pile, 4000 boxes and spheres | 22.0 | 23.8 | 196 |
200
+
201
+ - Box3D keeps every pyramid standing for 30 s of simulated time, up to 100 rows. With Babylon's default Havok setup
202
+ a 30 row pyramid is flat within 30 s and a 50 row one within 10 s (`bench/results/pyramid-stability-*.md`).
203
+ - In the piles Havok's own world step is faster (17.7 ms vs 20.8 ms at 4000 bodies). Box3D's full Babylon step is
204
+ faster because the plugin only syncs bodies that Box3D reports as moved.
205
+ - Chrome gives the same picture as node (`bench/results/chrome-*.md`). Full tables, including sleep off, p95 and
206
+ max step times, are in `bench/results`. Run `npm run bench` or open `bench.html` from `npm run demo` to reproduce.
41
207
 
42
208
  ## Building
43
209
 
44
210
  ```
45
- node build.mjs # release, all three targets
46
- node build.mjs --debug # assertions and symbols
47
- npm test # node smoke test against lib/node
211
+ npm install
212
+ npm run build # dist (ESM + d.ts) and umd bundles
213
+ npm test # wasm smoke test + unit and real wasm tests
214
+ npm run build:wasm # rebuild the wasm, needs the Emscripten SDK (EMSDK or ../emsdk) and a box3d checkout (BOX3D_DIR or ../box3d)
48
215
  ```
49
216
 
50
- Needs the [Emscripten SDK](https://emscripten.org/docs/getting_started/downloads.html) (`EMSDK` env var, or a
51
- checkout at `../emsdk`) and a Box3D checkout (`BOX3D_DIR`, or `../box3d`). The build is single threaded with wasm
52
- SIMD128. Box3D's task scheduler could run on wasm threads later, that needs `SharedArrayBuffer` and cross origin
53
- isolation, so it is not the default. Outputs are committed so the package installs without a toolchain.
217
+ `npm test` runs the node smoke test against the shim, unit tests with a mocked wasm module, and tests that drive the
218
+ plugin through Babylon's own classes against the real wasm: constraints, events, shapes, filtering, mass properties,
219
+ activation and a zombie ragdoll (18 jointed boxes dropped and stepped for 10 s). Several of them build the same scene
220
+ with Havok and compare, which is what pins the Havok compatible behaviour down.
221
+
222
+ The wasm build is single threaded with wasm SIMD128. Box3D's task scheduler could run on wasm threads later, that
223
+ needs `SharedArrayBuffer` and cross origin isolation.
54
224
 
55
225
  ## License
56
226
 
57
- MIT. Box3D is MIT licensed by Erin Catto, see `LICENSE-box3d.txt`.
227
+ MIT. Box3D is MIT licensed by Erin Catto, see `LICENSE-box3d.txt`. The ragdoll data in the demo is ported from
228
+ box3d's `shared/human.c`.