avbd-babylon 0.3.0 → 0.4.1

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.
@@ -2,6 +2,18 @@ import type { Vector3, Quaternion } from "@babylonjs/core/Maths/math.vector.js";
2
2
  import type { AbstractMesh } from "@babylonjs/core/Meshes/abstractMesh.js";
3
3
  import type { Scene } from "@babylonjs/core/scene.js";
4
4
  import type { PhysicsShapeType } from "@babylonjs/core/Physics/v2/IPhysicsEnginePlugin.js";
5
+ import type {
6
+ AvbdSleepOptions,
7
+ AvbdHeightfieldOptions,
8
+ AvbdCookedTerrain,
9
+ AvbdTerrainMaterial,
10
+ } from "./nativeScenes.js";
11
+ export interface AvbdTerrain {
12
+ readonly bodies: AvbdPhysicsBody[];
13
+ readonly triangleCount: number;
14
+ readonly kind: "heightfield" | "static-mesh";
15
+ dispose(): void;
16
+ }
5
17
  export const AvbdShapeType: Readonly<{
6
18
  BOX: "box";
7
19
  SPHERE: "sphere";
@@ -31,9 +43,11 @@ export interface AvbdWorldOptions {
31
43
  maxSyncedBodies?: number;
32
44
  broadphase?: "auto" | "grid" | "hploc";
33
45
  solverMode?: AvbdSolverMode;
46
+ /** Automatically batch a closed jointless world of up to 32 bodies. */
47
+ smallGroups?: boolean;
34
48
  bvh?: { rebuildInterval?: number };
35
- /** Optional conservative GPU sleeping. Impacts and edits wake the world. */
36
- sleeping?: boolean | { speedThreshold?: number; timeThreshold?: number };
49
+ /** Optional GPU sleeping. Large worlds wake conservative connected groups on impact. */
50
+ sleeping?: boolean | AvbdSleepOptions;
37
51
  maxContactPairs?: number;
38
52
  maxContactEvents?: number;
39
53
  }
@@ -116,6 +130,17 @@ export class AvbdPhysicsConstraint {
116
130
  dispose(): void;
117
131
  }
118
132
  export class AvbdPhysics {
133
+ /** Register static collision pieces before initialize(); reserve body capacity. */
134
+ addHeightfield(options: AvbdHeightfieldOptions): AvbdTerrain;
135
+ /** Captures the mesh world transform; later mesh edits do not move terrain. */
136
+ addStaticMesh(
137
+ mesh: AbstractMesh,
138
+ options?: AvbdTerrainMaterial & { thickness?: number },
139
+ ): AvbdTerrain;
140
+ addTerrain(
141
+ cooked: AvbdCookedTerrain,
142
+ options?: AvbdTerrainMaterial,
143
+ ): AvbdTerrain;
119
144
  sphereCast(
120
145
  origin: AvbdVector,
121
146
  radius: number,
@@ -151,10 +176,9 @@ export class AvbdPhysics {
151
176
  readonly solverDecision: AvbdSolverDecision;
152
177
  setSolverMode(mode: AvbdSolverMode): AvbdSolverDecision;
153
178
  setGravity(gravity: AvbdVector): void;
154
- enableSleeping(options?: {
155
- speedThreshold?: number;
156
- timeThreshold?: number;
157
- }): this;
179
+ enableSleeping(options?: AvbdSleepOptions): this;
180
+ /** Wake all sleeping bodies and disable automatic sleeping. */
181
+ disableSleeping(): this;
158
182
  initialize(): void;
159
183
  step(): void;
160
184
  flushCommands(): void;
@@ -281,5 +305,6 @@ export class AvbdPhysicsBody {
281
305
  effectiveMass: number;
282
306
  sleeping: boolean;
283
307
  }>;
308
+ /** Mesh-origin position; preserves live rotation and velocities when omitted. */
284
309
  teleport(position: Vector3, rotation?: Quaternion): this;
285
310
  }
package/dist/native.js CHANGED
@@ -1 +1 @@
1
- export { AvbdScene2D, AvbdScene3D, createWebGPUDevice, prepareWebGPUDevice3D, createSolverPipeline } from "./avbd.js";
1
+ export { AvbdScene2D, AvbdScene3D, createWebGPUDevice, prepareWebGPUDevice3D, createSolverPipeline, cookHeightfield, cookStaticMesh } from "./avbd.js";
@@ -1,4 +1,8 @@
1
- import type { AvbdSolverPipeline, AvbdSolver2D, AvbdSolver3D } from "./solverPipeline.js";
1
+ import type {
2
+ AvbdSolverPipeline,
3
+ AvbdSolver2D,
4
+ AvbdSolver3D,
5
+ } from "./solverPipeline.js";
2
6
  export type AvbdTuple3 = readonly [number, number, number];
3
7
  export type AvbdTuple2 = readonly [number, number];
4
8
  export interface AvbdQueryOptions {
@@ -23,6 +27,55 @@ export interface AvbdQueryHit<V> {
23
27
  center: V;
24
28
  }
25
29
  export type AvbdQuaternion = readonly [number, number, number, number];
30
+ export interface AvbdTerrainMaterial {
31
+ friction?: number;
32
+ restitution?: number;
33
+ group?: number;
34
+ collidesWith?: number;
35
+ mass?: 0;
36
+ }
37
+ export interface AvbdHeightfieldOptions extends AvbdTerrainMaterial {
38
+ heights: ArrayLike<number>;
39
+ columns: number;
40
+ rows: number;
41
+ spacing?: number | AvbdTuple2;
42
+ origin?: AvbdTuple3;
43
+ upAxis?: 0 | 1 | 2;
44
+ /** Local height below every sample; defaults to the minimum height minus 2. */
45
+ baseHeight?: number;
46
+ /** Row-major cells; a nonzero entry leaves a hole. */
47
+ holes?: ArrayLike<number>;
48
+ }
49
+ export interface AvbdStaticMeshOptions extends AvbdTerrainMaterial {
50
+ /** World-space xyz triples. */
51
+ positions: ArrayLike<number>;
52
+ indices?: ArrayLike<number>;
53
+ /** Backing behind triangle winding, in world units. Defaults to 0.1. */
54
+ thickness?: number;
55
+ }
56
+ /** Serializable preprocessing output; can be cooked in a separate worker. */
57
+ export interface AvbdCookedTerrain {
58
+ kind: "heightfield" | "static-mesh";
59
+ triangleCount: number;
60
+ pieces: Array<{
61
+ shape: object;
62
+ position: AvbdTuple3;
63
+ rotation: AvbdQuaternion;
64
+ }>;
65
+ }
66
+ export interface AvbdNativeTerrain {
67
+ readonly bodies: AvbdRigid[];
68
+ readonly triangleCount: number;
69
+ readonly kind: "heightfield" | "static-mesh";
70
+ /** Disable collision participation; live indices remain stable. */
71
+ dispose(): void;
72
+ }
73
+ export function cookHeightfield(
74
+ options: AvbdHeightfieldOptions,
75
+ ): AvbdCookedTerrain;
76
+ export function cookStaticMesh(
77
+ options: AvbdStaticMeshOptions,
78
+ ): AvbdCookedTerrain;
26
79
  export interface AvbdNativeState3D {
27
80
  position: [number, number, number];
28
81
  rotation: [number, number, number, number];
@@ -187,6 +240,15 @@ export interface AvbdGpuParameters2D extends AvbdGpuParameters {
187
240
  vbd: boolean;
188
241
  vbdStiffness: number;
189
242
  }
243
+ /** 3D tracks current awake groups and retains sleeping support connections. */
244
+ export interface AvbdSleepOptions {
245
+ speedThreshold?: number;
246
+ timeThreshold?: number;
247
+ /** 3D: automatic for large desktop worlds; false selects whole-world waking. */
248
+ islands?: boolean;
249
+ /** 3D island mode: build contacts once and sleep stationary, supported groups at startup. */
250
+ startAsleep?: boolean;
251
+ }
190
252
  export interface AvbdGpuOptions {
191
253
  bodyBuffer?: GPUBuffer;
192
254
  bodyCapacity?: number;
@@ -194,6 +256,8 @@ export interface AvbdGpuOptions {
194
256
  spatialSort?: boolean;
195
257
  broadphase?: "auto" | "grid" | "hploc";
196
258
  solverMode?: "auto" | "standard" | "optimized" | "points";
259
+ /** 3D: automatically fuse closed jointless worlds of at most 32 bodies. */
260
+ smallGroups?: boolean;
197
261
  bvh?: { rebuildInterval?: number };
198
262
  capacity?: {
199
263
  contacts?: number;
@@ -202,9 +266,10 @@ export interface AvbdGpuOptions {
202
266
  colors?: number;
203
267
  joints?: number;
204
268
  };
269
+ /** Reserve coloring capacity when sleeping bodies wake. */
205
270
  minimumColors?: number;
206
271
  minimumColorRounds?: number;
207
- sleeping?: boolean | { speedThreshold?: number; timeThreshold?: number };
272
+ sleeping?: boolean | AvbdSleepOptions;
208
273
  }
209
274
  export interface AvbdGpuSolver {
210
275
  readonly bodyBuffer: GPUBuffer;
@@ -274,10 +339,7 @@ export interface AvbdGpuSolver3D extends AvbdGpuSolver {
274
339
  rotation?: AvbdQuaternion,
275
340
  ): this;
276
341
  readBodyState(index: number): Promise<AvbdNativeState3D>;
277
- enableSleeping(options?: {
278
- speedThreshold?: number;
279
- timeThreshold?: number;
280
- }): this;
342
+ enableSleeping(options?: AvbdSleepOptions): this;
281
343
  disableSleeping(): this;
282
344
  setSleepEnabled(index: number, enabled: boolean): this;
283
345
  watchContacts(options?: AvbdNativeContactOptions): this;
@@ -509,6 +571,12 @@ export interface AvbdNativeConstraint {
509
571
  destroy(): void;
510
572
  }
511
573
  export class AvbdScene3D {
574
+ addHeightfield(options: AvbdHeightfieldOptions): AvbdNativeTerrain;
575
+ addStaticMesh(options: AvbdStaticMeshOptions): AvbdNativeTerrain;
576
+ addTerrain(
577
+ cooked: AvbdCookedTerrain,
578
+ options?: AvbdTerrainMaterial,
579
+ ): AvbdNativeTerrain;
512
580
  addMotor(
513
581
  a: AvbdRigid | null,
514
582
  b: AvbdRigid,
@@ -621,7 +689,10 @@ export class AvbdScene3D {
621
689
  },
622
690
  ): AvbdNativeConstraint;
623
691
  createSolver(device: GPUDevice, options?: AvbdGpuOptions): AvbdGpuSolver3D;
624
- createSolver(pipeline: AvbdSolverPipeline, options?: AvbdGpuOptions): AvbdSolver3D;
692
+ createSolver(
693
+ pipeline: AvbdSolverPipeline,
694
+ options?: AvbdGpuOptions,
695
+ ): AvbdSolver3D;
625
696
  }
626
697
  export interface AvbdNativeAngularOptions {
627
698
  axisA?: AvbdTuple3;
@@ -731,8 +802,12 @@ export class AvbdScene2D {
731
802
  bodyBuffer?: GPUBuffer;
732
803
  bodyCapacity?: number;
733
804
  colorRounds?: number;
805
+ minimumColors?: number;
734
806
  sleeping?: boolean | { speedThreshold?: number; timeThreshold?: number };
735
807
  },
736
808
  ): AvbdGpuSolver2D;
737
- createSolver(pipeline: AvbdSolverPipeline, options?: AvbdGpuOptions): AvbdSolver2D;
809
+ createSolver(
810
+ pipeline: AvbdSolverPipeline,
811
+ options?: AvbdGpuOptions,
812
+ ): AvbdSolver2D;
738
813
  }
@@ -0,0 +1,161 @@
1
+ /** Nonblocking GPU pass timestamps; null when timestamp-query is unavailable. */
2
+ export class GpuTimer {
3
+ constructor(device: GPUDevice);
4
+ readonly latestMs: number | null;
5
+ readonly samples: number;
6
+ begin(): object | null;
7
+ descriptor(slot: object | null): {
8
+ timestampWrites?: GPUComputePassTimestampWrites;
9
+ };
10
+ encode(encoder: GPUCommandEncoder, slot: object | null): void;
11
+ resolve(slot: object | null): Promise<void> | undefined;
12
+ settled(): Promise<unknown[]>;
13
+ destroy(): void;
14
+ }
15
+ /** Reuse released buffers with compatible usage, without CPU pose transfers. */
16
+ export class GPUBufferRecycler {
17
+ constructor(device: GPUDevice, options?: { maxFreeBytes?: number });
18
+ readonly freeBytes: number;
19
+ acquire(
20
+ byteSize: number,
21
+ usage: GPUBufferUsageFlags,
22
+ label?: string,
23
+ ): GPUBuffer;
24
+ release(
25
+ buffer: GPUBuffer,
26
+ byteSize: number,
27
+ usage: GPUBufferUsageFlags,
28
+ ): void;
29
+ trim(maxFreeBytes?: number): void;
30
+ destroy(): void;
31
+ }
32
+ export const PARTICLE_BYTE_STRIDE: 48;
33
+ export interface SurfaceParticleStep {
34
+ dt: number;
35
+ count?: number;
36
+ speed?: number;
37
+ life?: number;
38
+ bulletRadius?: number;
39
+ spawnRadius?: number;
40
+ jitter?: number;
41
+ frame?: number;
42
+ }
43
+ /** Spherical emitter aimed at the origin. Particles hit triangle surfaces and
44
+ * respawn; they ignore each other. Five rays approximate the particle sweep. */
45
+ export class GpuSurfaceParticles {
46
+ constructor(
47
+ device: GPUDevice,
48
+ options: { capacity: number; triangleCount: number; readback?: boolean },
49
+ );
50
+ readonly device: GPUDevice;
51
+ readonly capacity: number;
52
+ readonly triangleCount: number;
53
+ /** 48 bytes: position/life vec4f, velocity/radius vec4f, seed vec4u. */
54
+ readonly particleBuffer: GPUBuffer;
55
+ readonly triangleBuffer: GPUBuffer;
56
+ readonly counterBuffer: GPUBuffer;
57
+ readonly timer: GpuTimer;
58
+ /** 12 float32 values per triangle: [ax,ay,az,0,bx,by,bz,0,cx,cy,cz,0]. */
59
+ writeTriangles(vertices: Float32Array): void;
60
+ /** Returns the latest asynchronous counters, or null with readback disabled. */
61
+ step(options: SurfaceParticleStep): { hits: number; alive: number } | null;
62
+ clear(): void;
63
+ destroy(): void;
64
+ }
65
+ export const sampledContactShader: string;
66
+ /** Gather a bounded GPU contact overlay, without reading the full contact set. */
67
+ export class GpuContactSample {
68
+ constructor(device: GPUDevice, options?: { count?: number });
69
+ readonly count: number;
70
+ readonly buffer: GPUBuffer;
71
+ encode(
72
+ encoder: GPUCommandEncoder,
73
+ resources: {
74
+ bodyBuffer: GPUBuffer;
75
+ /** Native 3D camera uniform: mat4 vp, vec4 eye, vec4 light/viewport. */
76
+ cameraBuffer: GPUBuffer;
77
+ contactStorage: {
78
+ manifolds: GPUBuffer;
79
+ contacts: GPUBuffer;
80
+ counters: GPUBuffer;
81
+ };
82
+ },
83
+ ): void;
84
+ dispose(): void;
85
+ }
86
+ /** Optional one-time native constraint-graph seed; live GPU coloring still checks contacts. */
87
+ export function seedConstraintColoring(
88
+ gpu: {
89
+ device: GPUDevice;
90
+ bodyCapacity: number;
91
+ colorBuffer: GPUBuffer;
92
+ colorCap: number;
93
+ gpuIndex(index: number): number;
94
+ },
95
+ world: {
96
+ bodies: { mass: number }[];
97
+ forces: { bodyA: object | null; bodyB: object | null }[];
98
+ },
99
+ minimum?: number,
100
+ ): void;
101
+ export const INPUT_WORDS: 9;
102
+ /** Custom compute/copy work in the solver's existing command encoder. Callbacks
103
+ * are synchronous, end their own passes, and must not submit or finish the encoder.
104
+ * Dispose the registration before destroying its buffers. CPU solvers are rejected. */
105
+ export function addGpuStepPass<
106
+ T extends { bodyBuffer: GPUBuffer; step(): void },
107
+ >(
108
+ solver: T,
109
+ hooks: {
110
+ before?: (encoder: GPUCommandEncoder, solver: T) => void;
111
+ after?: (encoder: GPUCommandEncoder, solver: T) => void;
112
+ },
113
+ ): () => void;
114
+ /** DOM-free sizing and event adapter around a caller-owned canvas. */
115
+ export class WorkerCanvas {
116
+ constructor(
117
+ canvas: OffscreenCanvas | HTMLCanvasElement,
118
+ width: number,
119
+ height: number,
120
+ );
121
+ readonly offscreenCanvas: OffscreenCanvas | HTMLCanvasElement;
122
+ readonly clientWidth: number;
123
+ readonly clientHeight: number;
124
+ readonly revision: number;
125
+ width: number;
126
+ height: number;
127
+ resize(width: number, height: number): void;
128
+ getContext(type: "webgpu"): GPUCanvasContext | null;
129
+ getContext(type: string, options?: object): unknown;
130
+ getBoundingClientRect(): {
131
+ left: number;
132
+ top: number;
133
+ width: number;
134
+ height: number;
135
+ };
136
+ addEventListener(type: string, callback: (event: any) => void): void;
137
+ removeEventListener(type: string, callback: (event: any) => void): void;
138
+ setPointerCapture(): void;
139
+ dispatch(type: string, data: object): void;
140
+ }
141
+ export interface SharedPointerState {
142
+ clientX: number;
143
+ clientY: number;
144
+ pointerId?: number;
145
+ buttons?: number;
146
+ altKey?: boolean;
147
+ shiftKey?: boolean;
148
+ ctrlKey?: boolean;
149
+ }
150
+ /** Returns null without cross-origin isolation / SharedArrayBuffer support. */
151
+ export function createSharedInput(): Int32Array<SharedArrayBuffer> | null;
152
+ /** One producer; continuous state only. Send discrete events as ordered messages. */
153
+ export function writeSharedInput(
154
+ view: Int32Array<SharedArrayBuffer>,
155
+ event: SharedPointerState,
156
+ wheel?: number,
157
+ ): void;
158
+ export function readSharedInput(
159
+ view: Int32Array<SharedArrayBuffer>,
160
+ previous?: number,
161
+ ): { sequence: number; event: SharedPointerState; wheel: number } | null;
@@ -0,0 +1 @@
1
+ export { GpuTimer, GPUBufferRecycler, GpuSurfaceParticles, PARTICLE_BYTE_STRIDE, GpuContactSample, sampledContactShader, seedConstraintColoring, addGpuStepPass, WorkerCanvas, INPUT_WORDS, createSharedInput, writeSharedInput, readSharedInput } from "./avbd.js";
@@ -1,5 +1,21 @@
1
1
  # Babylon.js integration
2
2
 
3
+ Static terrain can be added with `physics.addHeightfield(options)` or
4
+ `physics.addStaticMesh(mesh, { thickness, friction, group, collidesWith })`.
5
+ The mesh helper captures current world-space triangles, including parent transforms.
6
+ Both return `{ bodies, triangleCount, kind, dispose() }`. Add terrain before
7
+ `initialize()` or the first render, and reserve body capacity for its collision
8
+ pieces. These pieces do not synchronize meshes each frame. `dispose()` disables
9
+ their collisions while leaving the caller's mesh intact.
10
+
11
+ Heightfields use two solid prisms per cell. Mesh terrain gives each triangle
12
+ finite backing behind its winding; concavities and overhangs remain open. This
13
+ uses the existing convex contact and grid/HPLOC pipelines. Internal tile edges
14
+ can affect sliding, and a rendering mesh should usually have a simpler collision
15
+ mesh. Terrain is static and does not track subsequent mesh transformations.
16
+ See [terrain examples](../README.md#static-terrain-hills-caves-and-overhangs)
17
+ for the native API, holes, coordinate conventions and worker cooking helpers.
18
+
3
19
  Start with the [integration README](README.md) for a complete scene, bundler setup,
4
20
  instance rendering examples and reproducible collision/solver benchmarks.
5
21
 
@@ -79,8 +95,10 @@ impacts below 1 m/s do not bounce. Use `body.setRestitution()` for live changes.
79
95
  `isTrigger:true` detects dynamic-body overlaps without contact forces;
80
96
  `body.setTrigger()` changes the mode. Events include `isTrigger`, and masks
81
97
  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.
98
+ dynamic concave meshes and the full Physics V2 plugin
99
+ interface are not implemented. Static triangle meshes and heightfields use
100
+ `addStaticMesh()` and `addHeightfield()` terrain cooking, including caves and
101
+ overhangs. Static bodies given velocity move kinematically.
84
102
 
85
103
  `addHinge(a,b,{axisA,axisB,anchorA,anchorB,minAngle,maxAngle,motor})` constrains
86
104
  rotation to one axis, with optional angle stops and a torque-limited motor.
@@ -129,18 +147,43 @@ reserve storage; `readContactEvents()` reports dropped events or a full pair
129
147
  table. End events do not retain an old position. Callbacks run asynchronously.
130
148
 
131
149
  `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
150
+ optional GPU policy. Supported quiet bodies sleep. Large desktop 3D worlds
151
+ automatically wake conservative connected groups on impact, keeping unrelated
152
+ piles asleep; the shared static floor does not join them. `islands:true` opts
153
+ into group waking in smaller worlds, and `islands:false` selects whole-world
154
+ waking. Awake groups rebuild their connections from current contacts; sleeping
155
+ groups retain the support connections omitted by collision detection. A group
156
+ settles together after its eligible members stay quiet, so sleeping one course
157
+ does not repeatedly disturb the courses above it. Contact waking checks closing
158
+ speed at each contact point, including rotation. Updating a grab anchor wakes
159
+ the held body; ordinary edits and changes to supports wake the world.
160
+ Unsupported bodies
134
161
  under gravity stay awake. `wakeAll()`, `body.wakeUp()` and `readSleepStats()` are
135
162
  available. Suspended jointed objects are not guaranteed to sleep. Defaults and
136
163
  paper-comparison benchmarks keep sleeping disabled.
137
164
 
165
+ For a 3D assembly placed at rest, use
166
+ `{ islands: true, startAsleep: true }`. Native solver initialization takes one
167
+ support-validation physics step and sleeps stationary groups connected to
168
+ stationary solid support. Moving, floating and sleep-excluded groups stay awake.
169
+ Babylon applies this check on its first physics step. This option is intended
170
+ for assembled stacks, rather than objects already in motion. Interactive
171
+ half-million walls use it; timing comparisons with the paper keep sleeping off.
172
+
173
+ Closed jointless 3D worlds with at most 32 bodies automatically batch movement
174
+ iterations in one GPU workgroup. Body poses stay in shared memory, with the
175
+ same contact order, reductions and equations. Adding a joint or exceeding the
176
+ body limit selects the ordinary pipeline. Qualified mobile compatibility paths
177
+ retain their existing execution policy. `smallGroups:false` disables batching
178
+ for controlled comparisons.
179
+
138
180
  `body.setLinearVelocity()`, `setAngularVelocity()`, `applyImpulse()`,
139
181
  `applyAngularImpulse()` and `teleport(position, quaternion)` enqueue compact
140
182
  GPU commands. Commands for the same body are combined before submission;
141
183
  impulses use the current GPU mass, inertia and position. They never replace a
142
184
  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
185
+ is also called. Omitting its quaternion preserves the live GPU rotation and
186
+ places the mesh origin at the requested position. New aggregates can be added within reserved capacity after
144
187
  initialization; disposed body slots are not reused. For repeated spawning,
145
188
  reserve projectile bodies and teleport them. `applyForce()` and `applyTorque()`
146
189
  apply forces for one fixed step, using impulses on live GPU state.