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 +200 -29
- package/dist/box3dPlugin.d.ts +445 -0
- package/dist/box3dPlugin.d.ts.map +1 -0
- package/dist/box3dPlugin.js +2432 -0
- package/dist/box3dPlugin.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/{box3d.d.ts → lib/esm/box3d.d.ts} +40 -1
- package/lib/esm/box3d.js +2 -2
- package/lib/esm/box3d.wasm +0 -0
- package/lib/node/box3d.d.ts +221 -0
- package/lib/node/box3d.mjs +2 -2
- package/lib/node/box3d.wasm +0 -0
- package/lib/umd/box3d.umd.js +2 -2
- package/lib/umd/box3d.umd.wasm +0 -0
- package/package.json +58 -16
- package/umd/babylon.box3d.js +2602 -0
- package/umd/babylon.box3d.js.map +7 -0
- package/umd/babylon.box3d.min.js +3 -0
- package/umd/babylon.box3d.min.js.map +7 -0
- package/{src → wasm}/box3d_shim.c +444 -96
package/README.md
CHANGED
|
@@ -1,57 +1,228 @@
|
|
|
1
1
|
# babylon-box3d
|
|
2
2
|
|
|
3
|
-
[Box3D](https://github.com/erincatto/box3d)
|
|
4
|
-
|
|
5
|
-
|
|
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
|
|
9
|
-
import { Box3DPlugin } from "
|
|
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
|
-
|
|
16
|
+
Script tags (Playground, plain HTML):
|
|
16
17
|
|
|
17
18
|
```html
|
|
18
|
-
<script src="https://cdn.
|
|
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(
|
|
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
|
|
28
|
+
The wasm is fetched next to the loader script. With a bundler pass `locateFile`, for example with vite:
|
|
25
29
|
|
|
26
|
-
|
|
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
|
-
| `
|
|
31
|
-
| `
|
|
32
|
-
| `
|
|
33
|
-
| `
|
|
34
|
-
| `
|
|
35
|
-
| `
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
npm test
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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`.
|