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.
@@ -0,0 +1,356 @@
1
+ # Babylon.js integration
2
+
3
+ Start with the [integration README](README.md) for a complete scene, bundler setup,
4
+ instance rendering examples and reproducible collision/solver benchmarks.
5
+
6
+ `babylonAvbd.js` provides a small aggregate/body API with Babylon's familiar
7
+ mass, friction, velocity, impulse and disposal conventions. It wraps the same
8
+ GPU rigid-body solver used by the laboratory. It is not an implementation of
9
+ Babylon's complete Physics V2 plugin interface and does not replace
10
+ `scene.enablePhysics()` or accept Babylon `PhysicsAggregate` instances.
11
+
12
+ The standalone `avbd-babylon` ESM and `AVBD` global bundles have no Babylon
13
+ runtime imports. Creating a world with a scene attaches fixed-step updates and
14
+ disposal automatically. Aggregates accept that scene, a world, or an omitted
15
+ last argument when their mesh belongs to the world’s scene. Both `AvbdShapeType`
16
+ and the supported Babylon `PhysicsShapeType` enum values work. Pass
17
+ `autoAttach: false` when you manage stepping yourself.
18
+
19
+ `AvbdPhysics.create()` checks 3D floor contacts once per device and automatically
20
+ selects a working GPU path. This covers Android drivers that need compatible
21
+ contact calculations or smaller hull workgroups. Desktop devices that pass
22
+ retain their normal shaders and dispatches. The check adds no per-step readbacks.
23
+ For native 3D solvers, use `createWebGPUDevice({validate3D:true})`, or await
24
+ `prepareWebGPUDevice3D(device)` before using a renderer-owned device. Preparation
25
+ is cached and preserves device ownership. 2D does not require the 3D check.
26
+
27
+ ```js
28
+ import { Vector3 } from "@babylonjs/core/Maths/math.vector.js";
29
+ import { AvbdPhysics, AvbdPhysicsAggregate, AvbdShapeType } from "avbd-babylon";
30
+
31
+ const physics = await AvbdPhysics.create({
32
+ scene,
33
+ gravity: new Vector3(0, -9.81, 0),
34
+ iterations: 10,
35
+ capacity: 2048,
36
+ solverMode: "auto", // default: choose the measured implementation for the load
37
+ });
38
+
39
+ new AvbdPhysicsAggregate(floor, AvbdShapeType.BOX, { mass: 0 }, scene);
40
+ const boxPhysics = new AvbdPhysicsAggregate(
41
+ box,
42
+ AvbdShapeType.BOX,
43
+ { mass: 2, friction: 0.6 },
44
+ scene,
45
+ );
46
+
47
+ // Create the scene in a batch, then compile/allocate the world once.
48
+ physics.initialize();
49
+ console.log(physics.solverDecision); // selected mode and why it was chosen
50
+ // create({ scene }) attaches simulation and disposal automatically.
51
+ boxPhysics.body.setLinearVelocity(new Vector3(0, 0, 5));
52
+ boxPhysics.body.applyImpulse(new Vector3(3, 0, 0), box.position);
53
+
54
+ // Aggregate disposal disables its collisions; the mesh remains caller-owned.
55
+ // boxPhysics.dispose();
56
+ // physics.dispose(); // detaches the observer and releases GPU resources
57
+ ```
58
+
59
+ `solverMode` is `"auto"` by default. AVBD selects the GPU contact calculation
60
+ from body count and active joints, and shares threads based on contact load in
61
+ very large scenes. All layouts retain the same physics settings. The read-only
62
+ `physics.solverDecision` explains the choice. `physics.setSolverMode()` accepts
63
+ `"standard"`, `"optimized"`, `"points"` or `"auto"` for advanced profiling and
64
+ compatibility. These names identify internal implementations, not quality
65
+ levels or paper results. Overrides preserve live GPU state; first use may
66
+ compile pipelines. Start with the automatic default.
67
+
68
+ Box, sphere and capsule collisions are native. Capsules use an exact rounded
69
+ segment along local Y; height includes both caps and X/Z diameters must match.
70
+ `CYLINDER` and `CONVEX_HULL` use a polygonal convex hull, with a 32-vertex limit. Mesh geometry,
71
+ positive scaling, centre of mass and principal inertia are captured at creation;
72
+ the visual transform is reconstructed from the GPU body's principal frame.
73
+ Meshes must have no parent. Bake parent transforms before creating aggregates.
74
+ Nonuniform sphere dimensions and unsupported shape types fail explicitly.
75
+
76
+ AVBD supports restitution from 0 to 1 and non-solving trigger volumes. Zero
77
+ restitution adds no bounce passes. The larger coefficient in a pair wins;
78
+ impacts below 1 m/s do not bounce. Use `body.setRestitution()` for live changes.
79
+ `isTrigger:true` detects dynamic-body overlaps without contact forces;
80
+ `body.setTrigger()` changes the mode. Events include `isTrigger`, and masks
81
+ apply to sensors too. Static–static pairs are not scanned. CCD,
82
+ arbitrary concave meshes and the full Physics V2 plugin
83
+ interface are not implemented. Static bodies given velocity move kinematically.
84
+
85
+ `addHinge(a,b,{axisA,axisB,anchorA,anchorB,minAngle,maxAngle,motor})` constrains
86
+ rotation to one axis, with optional angle stops and a torque-limited motor.
87
+ Axes/anchors use local mesh frames; angle stops are radians relative to creation,
88
+ and motor `speed` is radians/second and `maxTorque` is N·m. Change the drive with
89
+ `hinge.motor.setMotor()`; dispose the complete hinge with `hinge.dispose()`.
90
+ `addMotor()` drives an angular axis independently.
91
+
92
+ `raycast()` / `raycastAll()` query current poses. `sphereCast(origin,radius,direction,options)`
93
+ and `sphereCastAll()` sweep a sphere against rounded target geometry, including
94
+ box and hull corners. Queries accept `maxDistance`, `collidesWith`, `ignore`
95
+ (body handles) and `includeTriggers` (default true). Results include distance,
96
+ body, normal and target contact point; sphere casts also include the center at
97
+ impact. Batched queries return one nearest hit per input. Initial overlaps return
98
+ zero distance and a normal opposite travel. Queries are separate from simulation
99
+ CCD. Portable solvers expose the same queries using numeric ignored body indices;
100
+ 2D uses `circleCast()` and two-component coordinates.
101
+
102
+ `addJoint(a, b, options)` connects ball-socket anchors; a null `a` makes a world
103
+ anchor. `addWeld()` also locks angular motion and captures the relative rotation
104
+ on the GPU. `addSpring()` accepts stiffness in N/m and optional rest length,
105
+ otherwise captured from the live anchors. Anchors use mesh-local coordinates,
106
+ with a world position for a null endpoint. `breakForce` enables fracture;
107
+ `breakOnPull` also tests linear force. `constraint.readState()` reads one record.
108
+ `setWorldAnchor()` moves a world anchor; `dispose()` releases a reusable joint
109
+ slot. Body disposal also disposes connected constraints.
110
+
111
+ Spring `breakStrain` is tensile extension relative to its rest length (0.35
112
+ means 35%). Spring `breakForce` measures tension in newtons. Compression does
113
+ not fracture springs. These settings work before and after GPU initialization.
114
+
115
+ World `sleeping:true` enables the GPU policy. Aggregate `allowSleep:false`
116
+ keeps that body awake; explicit `allowSleep:true` also enables the policy if
117
+ needed. `body.setSleepEnabled(boolean)` changes eligibility and wakes the
118
+ world. Impacts, support removal and edits wake bodies conservatively.
119
+
120
+ `raycast(origin, direction, options)` and batched `raycastAll(rays, options)`
121
+ return asynchronous nearest hits with body handles, distance, point and normal.
122
+ `ignore` and `collidesWith` filter rays. Direction must be nonzero. These queries
123
+ scan bodies on the GPU: use a few interactive casts, rather than large sensor
124
+ fleets. `body.setCollisionGroups(group, mask)` changes solver collision masks.
125
+
126
+ `onContact(callback)` enables GPU begin/end events and returns an unsubscribe
127
+ function. Events do not download poses. `maxContactPairs` and `maxContactEvents`
128
+ reserve storage; `readContactEvents()` reports dropped events or a full pair
129
+ table. End events do not retain an old position. Callbacks run asynchronously.
130
+
131
+ `sleeping: true` or `{ speedThreshold: 0.03, timeThreshold: 0.5 }` enables an
132
+ optional GPU policy. Supported quiet bodies sleep; impacts, moving joint
133
+ endpoints and explicit edits wake the world conservatively. Unsupported bodies
134
+ under gravity stay awake. `wakeAll()`, `body.wakeUp()` and `readSleepStats()` are
135
+ available. Suspended jointed objects are not guaranteed to sleep. Defaults and
136
+ paper-comparison benchmarks keep sleeping disabled.
137
+
138
+ `body.setLinearVelocity()`, `setAngularVelocity()`, `applyImpulse()`,
139
+ `applyAngularImpulse()` and `teleport(position, quaternion)` enqueue compact
140
+ GPU commands. Commands for the same body are combined before submission;
141
+ impulses use the current GPU mass, inertia and position. They never replace a
142
+ body with a stale CPU pose. `teleport` retains velocity unless a velocity setter
143
+ is also called. New aggregates can be added within reserved capacity after
144
+ initialization; disposed body slots are not reused. For repeated spawning,
145
+ reserve projectile bodies and teleport them. `applyForce()` and `applyTorque()`
146
+ apply forces for one fixed step, using impulses on live GPU state.
147
+
148
+ Collision search accepts `broadphase: "auto"` (default), `"grid"` or `"hploc"`
149
+ when creating a world. Automatic mode uses the GPU tree for large mixed-size
150
+ scenes whose grid cells cover many small objects; uniform brick scenes retain
151
+ the grid. `bvh: { rebuildInterval: 64 }` sets how often tree ordering is rebuilt.
152
+ All bounds are still updated every step. The [tree documentation](HPLOC.md)
153
+ describes extra memory, adapter limits and reproducible comparisons.
154
+
155
+ The default bridge mirrors small scenes to Babylon meshes asynchronously. It
156
+ allows one outstanding pose copy and does not wait in the render loop. That
157
+ bridge gathers only the mirrored poses (32 bytes each), rather than the whole
158
+ world. It still costs GPU readback and CPU mesh updates, so large scenes should use
159
+ the direct GPU path:
160
+
161
+ ```js
162
+ const physics = await AvbdPhysics.create({
163
+ device, // optionally share a renderer's publicly provided device
164
+ capacity: 100_001,
165
+ syncMeshes: false, // no automatic pose readbacks or per-mesh updates
166
+ });
167
+ // Build aggregates, with { sync: false } if they need no explicit mesh mirror.
168
+ physics.initialize();
169
+ const bodyBuffer = physics.bodyBuffer;
170
+ const instanceIndex = aggregate.body.gpuIndex;
171
+ physics.step(); // exactly one fixed 1/60 s step; does not await the GPU
172
+ ```
173
+
174
+ The body buffer uses 40 floats / 160 bytes per body: position at 0, quaternion
175
+ (x,y,z,w) at 4, full size at 16, mass at 19, principal moments at 20, linear
176
+ velocity at 32 and angular velocity at 36. Spatial sorting means creation order
177
+ is not GPU order; always use `body.gpuIndex`. Babylon coordinates are used
178
+ directly (y up by default), unlike the canonical lab's z-up scene builders.
179
+
180
+ Use a renderer that reads this buffer directly, such as the laboratory renderer,
181
+ adapted for the world's up axis. Do not create 100,000 automatically synchronized
182
+ Babylon mesh transforms. The bridge rejects more than `maxSyncedBodies` (2048
183
+ by default). `readBodies()` and `syncMeshes()` are explicit asynchronous tools
184
+ for inspection and small-scene interoperability. `readBodies([bodyB, bodyA])`
185
+ gathers only those 160-byte records in requested order. `body.readState()` reads
186
+ one record. `getRenderBinding(device)` returns the buffer and byte offsets and
187
+ checks device identity; it does not bind storage to thin instances automatically.
188
+ `readState()` retains physical `mass` and also reports `effectiveMass` (zero
189
+ while sleeping) and `sleeping`. Raw body buffers store effective mass at offset 19.
190
+
191
+ `AvbdScene3D` provides portable GPU builders including `addPoint`, `addFabric`,
192
+ `addRope` and `addRagdoll`. `AvbdScene2D` includes GPU boxes, circles, capsules,
193
+ convex polygons, finite segments, infinite planes, joints, springs and motors. Both are package exports. Their CPU records prepare metadata; simulation
194
+ uses the GPU object returned by `createSolver(device)`. See the root
195
+ [README](../README.md) for recipes and coordinate conventions.
196
+
197
+ The 2D solver uses numeric body indices. After `createSolver(device)`, use
198
+ `gpu.setLinearVelocity(index,[x,y])`, `setAngularVelocity(index,radiansPerSecond)`,
199
+ `applyImpulse(index,[x,y],worldPoint?)`, `applyForce(index,[x,y],worldPoint?)`
200
+ and `teleport(index,[x,y],angle?)`. Commands execute in order using live GPU
201
+ poses. `applyAngularImpulse(index,impulse)` and `applyTorque(index,torque)`
202
+ use scalar angular values in 2D. All angular velocities are radians/second.
203
+ Teleport retains velocity and preserves angle when omitted. A force is
204
+ applied for one fixed step. `readBodyState(index)` copies a single body and returns
205
+ position, angle, linear/angular velocity, mass and moment. Queries and reads
206
+ include pending commands. These vector arguments also accept Babylon `Vector2`.
207
+
208
+ 2D `addJoint()`, `addSpring()` and `addMotor()` return disposable constraint
209
+ handles. Build constraints before `createSolver()`; disposal is safe before or
210
+ after creation. `gpu.setMotor(handle.slot,{speed,maxTorque})` changes a live drive.
211
+ Motor speed is A's angular velocity minus B's, so positive speed with world A
212
+ (`-1`) drives B clockwise. Zero torque permits free spin. `gpu.appendJoint()`
213
+ exposes the native live-joint operation with explicit local anchors and row
214
+ stiffness. Its angular rest angle uses the initial scene poses; use free angular
215
+ stiffness for dragging. `gpu.setWorldAnchor(slot,x,y)` only accepts world joints.
216
+ `disableConstraint(slot)` releases a live constraint without rebuilding the world.
217
+ Handle `disposed` describes manual removal; inspect GPU joint records for fracture.
218
+
219
+ Native 2D and 3D solvers also provide `gpu.body()`, a renderer-independent live
220
+ body handle. Use a builder body reference in 3D or the builder's numeric index in
221
+ 2D. The handle supports `setLinearVelocity`, `setAngularVelocity`, `applyImpulse`,
222
+ `applyForce`, `applyAngularImpulse`, `applyTorque`, `teleport`, `setTrigger`,
223
+ `setRestitution`, `setCollisionGroups`, `setSleepEnabled`, `wakeUp` and `readState`.
224
+ 3D angular values are xyz vectors and rotations are xyzw quaternions; 2D uses
225
+ scalar angular values. Poses and impulse lever arms come from live GPU state.
226
+
227
+ `gpu.bodyIndex(referenceOrHandle)` returns the GPU slot, including initial spatial
228
+ sorting. Numeric arguments are GPU slots. Handles are created only when requested;
229
+ bulk operations can work directly with references or indices.
230
+
231
+ `gpu.editBodies(iterable)` validates the batch before enqueueing commands. Each
232
+ record has a `body` and optional `position`, `rotation`, `linearVelocity`,
233
+ `angularVelocity`, `impulse`, `force`, `worldPoint`, `angularImpulse`, `torque`,
234
+ `isTrigger`, `restitution`, `group`, `collidesWith` and `allowSleep`.
235
+ Supply `group` when setting `collidesWith`; an omitted mask means all groups.
236
+ Within one record, a teleport precedes velocity setters, followed by impulses and
237
+ forces. Repeated records preserve per-body order. `step`, reads and queries flush
238
+ the queue automatically; `flushEdits()` submits without stepping or waiting.
239
+ One grouped dispatch owns each edited body. Teleports also invalidate previous
240
+ contacts touching those bodies. CPU staging and GPU upload buffers are reused.
241
+ Individual linear/angular setters and velocity-only `editBodies()` batches automatically
242
+ use compact velocity uploads; mixed commands keep their per-body order.
243
+ Adjacent velocity replacement setters coalesce, while impulses remain ordered.
244
+ Native 3D teleport cleanup is encoded in the edit submission. Babylon aggregate
245
+ linear/angular edits also use compact uploads; mixed aggregate edits reuse their
246
+ command storage. No explicit bulk call is required to batch individual setters.
247
+
248
+ `gpu.setLinearVelocities(indices, velocities)` submits packed xy/xyz velocity
249
+ arrays in one dispatch after earlier commands. Indices must be unique ascending
250
+ GPU slots. It reuses GPU buffers and accepts reusable typed arrays without
251
+ building command objects. Other velocity components and body properties remain
252
+ unchanged. Use it for frequent bulk velocity updates; use `editBodies()` for
253
+ mixed commands. Both methods wake sleeping bodies conservatively.
254
+
255
+ `gpu.setAngularVelocities(indices, values)` accepts one radians/sec component per
256
+ 2D body or xyz per 3D body. `gpu.setVelocities(indices,{linear,angular})` replaces
257
+ either or both fields in one dispatch. These methods validate unique ascending
258
+ slots, reuse staging and upload storage, and preserve omitted fields.
259
+
260
+ Body-property replacements, motor/world-anchor targets and 2D limit changes collect
261
+ until a step, body/joint read or geometry query. Repeated edits to one property coalesce. Contiguous
262
+ words share direct uploads; sparse words use a GPU scatter dispatch without
263
+ overwriting neighboring solver state. Constraint removal/reuse and body replacement
264
+ flush earlier patches. `flushEdits()` submits without advancing time.
265
+
266
+ Normal steps combine queued edits, optional sleep/wake checks, optional bounce
267
+ handling and solving into one physics queue submission. Contact-event collection,
268
+ readbacks and rendering can submit additional work. Stable solver pass constants
269
+ are cached; changes to coloring, iterations or stabilization invalidate the cache.
270
+ Ray/shape-cast storage has a separate pool of at most two idle jobs, each about
271
+ 2 MiB or less. Concurrent requests never share mapped storage.
272
+
273
+ `gpu.readSelectedBodies(indices,{posesOnly})` downloads only requested records:
274
+ 40 floats per 3D body or 24 per 2D body; compact poses have 8 or 4 floats.
275
+ Small repeated reads reuse an idle staging pool. Concurrent reads receive separate
276
+ GPU storage and independent returned arrays. At most two buffers per role remain
277
+ idle, each selected-record buffer at most 8 MiB; larger reads release their storage
278
+ on completion. World/solver disposal releases the pool.
279
+ `readState()` downloads just one live body. Normal `step()` and
280
+ `advance(elapsedSeconds,{maxSubSteps,maxFrameTime})` never download poses.
281
+ Both dimensions support optional `attachToScene(scene)` / `detachFromScene()`
282
+ without importing Babylon. The renderer owns mesh synchronization.
283
+
284
+ Native 3D body options include `group` and `collidesWith`, and both solvers expose
285
+ `setFilters(indices,groups,collidesWith)`. Values must be unsigned 32-bit masks,
286
+ one of each per live body. Invalid batches are rejected before applying any edit.
287
+
288
+ In either dimension, `gpu.watchContacts({indices:[projectileSlot,goalSlot]})` restricts entry/exit events
289
+ to pairs touching those bodies, avoiding event traffic for an entire brick pile.
290
+ Omit `indices` on the first call to watch all bodies. Later calls without it
291
+ preserve the selection; a supplied array replaces it. 2D live spawns are watched
292
+ automatically only when watching all bodies. Drain `readContactEvents()` regularly
293
+ and inspect its `dropped` count when choosing event capacities.
294
+
295
+ Both the 2D builder and live solver offer `addBox(size,options)`,
296
+ `addCircle(radius,options)`, `addCapsule(radius,length,options)`,
297
+ `addHull(points,options)`, `addSegment(start,end,options)` and `addPlane(normal?,offset?,options)`.
298
+ Live spawning uses the reserved `bodyCapacity` and throws if full. Capsules run
299
+ along local X and their full length includes both caps. Segment endpoints are
300
+ world coordinates; optional radius makes a rounded segment. Segments default
301
+ to static, and zero-thickness segments cannot be dynamic. Planes are always
302
+ static solid half-spaces: the normalized normal points toward the allowed side,
303
+ `dot(normal,point) >= offset`. Their default is a floor at y=0. Finite segments
304
+ are two-sided. Circles, capsules and segments use analytic contacts and casts;
305
+ dynamic disks and capsules use exact area mass/inertia.
306
+
307
+ 2D hulls accept flat xy values or arrays of `[x,y]` points. The convex boundary
308
+ has 3–32 vertices after duplicate, collinear and interior points are removed.
309
+ Geometry is centered on its area centroid; `position` sets that center of mass,
310
+ and `angle` rotates around it. Mass/inertia use exact polygon integrals. Contacts
311
+ use edge clipping, and GPU rays and disk sweeps use the actual polygon. Live
312
+ hull spawning grows the optional geometry storage within device limits; it does
313
+ not change the body buffer layout or reserve extra storage in box-only worlds.
314
+
315
+ `gpu.attachToScene(scene,{maxSubSteps?,maxFrameTime?,afterStep?})` automatically
316
+ steps 2D physics before Babylon rendering and destroys the solver on scene
317
+ disposal. Defaults: six catch-up steps and at most 0.05 seconds elapsed per
318
+ frame. No poses are downloaded by this loop. `detachFromScene()` removes both
319
+ observers without destroying the solver. Destroying a solver also detaches it;
320
+ the shared device remains owned by the caller. Drawing remains your choice.
321
+
322
+ `AvbdScene2D.addHinge(a,b,anchorA?,anchorB?,{minAngle?,maxAngle?,motor?})`
323
+ returns `{joint,limits,motor,dispose(),destroy()}`. Bounds are radians of
324
+ A-minus-B rotation relative to creation; they are unwrapped and may span multiple
325
+ turns. Missing bounds leave angular motion free. Stops use two unilateral AVBD
326
+ rows rather than pose clamping. `addAngularLimit(a,b,options)` adds only the
327
+ angular rows. Its handle exposes `setLimits(options)` before and after solver
328
+ creation; `gpu.setAngleLimits(slot,options)` also changes them live. Equal bounds
329
+ lock the relative angle. `gpu.setMotor(hinge.motor.slot,options)` changes a motor.
330
+
331
+ 2D `createSolver(device,{sleeping:true})` enables GPU sleeping. Options can set
332
+ positive `speedThreshold` and `timeThreshold`. Body `allowSleep:true` enables the
333
+ policy automatically; `false` excludes that body. `gpu.setSleepEnabled(index,bool)`
334
+ changes eligibility. `enableSleeping(options)` enables or reconfigures the policy;
335
+ `disableSleeping()` wakes all bodies and releases it. `wakeUp(index)` conservatively
336
+ wakes the world. `readSleepStats()` returns counts, and `readBodyState(index)`
337
+ includes `sleeping`. Supported quiet bodies rest; impacts, lost supports, active
338
+ motors and edits wake before movement solving. Sleeping keeps mass/inertia and
339
+ collision detection intact. The GPU body flag is `velocity.w`; renderers must
340
+ not mistake sleeping for zero mass. No additional buffer is required by the
341
+ movement or topology shader; the optional policy owns its state separately.
342
+
343
+ When sharing a device, the caller owns it; world disposal leaves it alive. A
344
+ device created by `AvbdPhysics.create()` belongs to the world and is destroyed
345
+ with it. Direct source imports require the repository's WGSL text loader (or equivalent
346
+ bundler configuration); the built package already bundles its shaders. The mixed gallery is a complete runnable example at
347
+ `/?demo=showcase`.
348
+
349
+ `createWebGPUDevice({requiredLimits,preferredLimits})` treats required limits as
350
+ requirements: unsupported requests fail with the limit name and adapter value.
351
+ Preferred maximum capacities adapt downward to the adapter. Requirements override
352
+ preferences for the same limit. Babylon world creation uses preferred capacities
353
+ so small scenes can run on devices with lower storage limits. Unexpected device
354
+ loss stops automatic Babylon stepping, releases the world and records details in
355
+ `physics.deviceLost` and `physics.errors`; recreate the world with a new device to
356
+ resume. A disposed world does not report its normal device destruction as a failure.
package/docs/HPLOC.md ADDED
@@ -0,0 +1,116 @@
1
+ # GPU tree collision detection
2
+
3
+ `GpuHploc` in `gpuHploc.js` builds and traverses a GPU bounding-volume hierarchy.
4
+ Its pairs feed the existing narrowphase and AVBD solver. Shapes, friction,
5
+ rotation, solving rounds and contact equations stay the same; pinned upstream
6
+ sources remain unchanged.
7
+
8
+ This is a portable hierarchical PLOC treelet variant, based on the clustering
9
+ family described in [H-PLOC](https://gpuopen.com/download/HPLOC.pdf), **not** the
10
+ paper's single-launch wave implementation. `hplocpp.wgsl` builds the tree;
11
+ `hplocSort.wgsl` sorts keys.
12
+
13
+ ## Algorithm
14
+
15
+ 1. Compute world AABBs from live GPU positions, quaternions and collider sizes.
16
+ 2. Reduce scene bounds on the GPU, generate 30-bit Morton keys and sort them
17
+ with stable radix passes. No fixed scene coordinates or lost duplicate keys.
18
+ 3. Merge mutual nearest neighbours by surface-area cost in 16-cluster treelets.
19
+ Construct hierarchy levels until one root remains. Dispatch boundaries
20
+ publish global writes; barriers publish local merges. There are no
21
+ cross-workgroup spin waits or relaxed-atomic publication assumptions.
22
+ 4. Build escape links for stackless traversal. Dynamic queries find static and
23
+ kinematic leaves; giant ground slabs do not traverse the whole tree.
24
+ 5. Refit **every leaf and internal bound every step**. Rebuild ordering and
25
+ clustering every 64 steps by default. Refitting preserves coverage after
26
+ teleports; rebuilding improves traversal quality.
27
+
28
+ Pair acceptance reuses the grid's masks, disabled-joint handling, bounding
29
+ spheres, world AABBs, face-axis rejection and overflow accounting. Dynamic
30
+ pairs are emitted once; dynamic/static pairs are emitted from the dynamic
31
+ body. Hulls retain the canonical hull narrowphase.
32
+
33
+ ## Selection and limits
34
+
35
+ `AppGpuSolver3D` and `AvbdPhysics.create` accept `broadphase: "auto"`, `"grid"`
36
+ or `"hploc"`, and `bvh: { rebuildInterval: 64 }`. The lab and browser benchmark
37
+ have the same choices. Explicit selections let you compare on your GPU.
38
+
39
+ Automatic selection uses a tree for at least 10,000 bodies when either:
40
+
41
+ - Grid cells are at least four times a typical body's bounding-sphere diameter.
42
+ - At least a quarter of dynamic bodies are thin plates, and there is at least
43
+ one active constraint per body. This covers the rigid ragdoll net.
44
+
45
+ For this policy, a thin plate has a shortest dimension no more than 40% of its
46
+ middle dimension, with the middle dimension at least half the longest. Rods,
47
+ spheres and convex hulls do not qualify as plates. Shape metadata is checked
48
+ when bodies or active constraint counts change; positions are not downloaded.
49
+
50
+ Body count alone does not select a tree. Adapter dispatch/storage limits are checked; automatic mode
51
+ keeps the grid when unsupported. Explicit unsupported requests fail clearly.
52
+
53
+ This is a conservative policy based on the measured 1K, 10K and 50K mixed-size workloads and the 24K ragdoll net,
54
+ not a universal crossover point. Scene layout, size distribution and GPU matter.
55
+
56
+ Extra buffers store nodes, links, sorting and clustering. JSON records primary
57
+ buffer bytes, rebuild/refit counts and the first GPU step. Startup GPU time
58
+ excludes CPU scene creation and shader compilation. Warm timing includes
59
+ periodic rebuilds, but excludes the first build; inspect both. Grid buffers
60
+ remain available for selection changes. This path adds no sleeping.
61
+ Reported primary bytes exclude prefix-scan scratch, small uniforms and pipelines.
62
+
63
+ ## Replication and checks
64
+
65
+ Open `/tests/performance.html` and close other GPU workloads. Paired comparison
66
+ alternates execution order every step and preserves each scene's settings.
67
+ Every step checks overflow and solve-group conflicts.
68
+
69
+ ```js
70
+ try {
71
+ const result = await __PERFORMANCE__.runPaired(
72
+ "3d-mixed-sizes-50k",
73
+ ["grid", "hploc"],
74
+ 60,
75
+ 60,
76
+ );
77
+ console.table(
78
+ result.results.map((r) => ({
79
+ method: r.broadphase,
80
+ collisionMs: r.summary.collision.mean,
81
+ physicsMs: r.summary.total.mean,
82
+ extraBytes: r.bvh?.extraBytes ?? 0,
83
+ })),
84
+ );
85
+ } finally {
86
+ await __BENCHMARK_VIEW__.preview.load(document.querySelector("#scene").value);
87
+ }
88
+ ```
89
+
90
+ Use `grid-detail` / `hploc-detail` to separate build/refit, pair search,
91
+ narrowphase, body solving and contact-force updates. Extra pass boundaries
92
+ affect timing, so compare matching detailed modes. Raw data retains rebuild
93
+ spikes and noisy samples. An unchanged solve-stage difference over 10% is flagged.
94
+
95
+ For the ragdoll net, compare `"showcase-ragdolls-on-cloth-24k"` with
96
+ `["grid", "hploc"]`, 1440 warmup steps and 60 samples. This measures the scene
97
+ after six simulated seconds at its full 240 Hz collision rate. Saved paired
98
+ results on the RTX 4070 Laptop GPU use the same timestep and five AVBD iterations;
99
+ see `test-results/solver-ragdoll-settled.json`. The separate
100
+ `node scripts/check-ragdoll-net.mjs` regression checks every quarter-second
101
+ through 30 simulated seconds with automatic selection. It checks whole limbs
102
+ below the sheet and partial capsule/triangle intersections deeper than 5 mm.
103
+ The net uses overlapping plates and excludes its eight immediate neighbors
104
+ from self-collisions; distant folds still collide.
105
+
106
+ Tests compare unique, complete pair sets against the grid for empty and partial
107
+ treelets, duplicate keys, rotated hulls and boxes, spheres, giant statics,
108
+ ignored and released joints, filters, teleports and added bodies. Physical regressions cover floor support,
109
+ joints, sleeping and support removal; browser checks exercise motion and drawing.
110
+
111
+ `node scripts/check-hploc-motion.mjs` checks 360 steps of the 10K/50K mixed-size
112
+ scenes, 110K ring, 100K columns and half-million walls with the tree forced.
113
+ It checks every step's capacity and scheduling counters, measures floor support
114
+ and rotation, and captures four screenshots per scene. `npm run test:web`
115
+ includes this check alongside all scene and benchmark views.
116
+