avbd-babylon 0.3.0 → 0.4.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.
@@ -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,13 @@ export interface AvbdGpuParameters2D extends AvbdGpuParameters {
187
240
  vbd: boolean;
188
241
  vbdStiffness: number;
189
242
  }
243
+ /** 3D can retain conservative connected groups between explicit world edits. */
244
+ export interface AvbdSleepOptions {
245
+ speedThreshold?: number;
246
+ timeThreshold?: number;
247
+ /** 3D: automatic for large jointless desktop worlds; false selects whole-world waking. */
248
+ islands?: boolean;
249
+ }
190
250
  export interface AvbdGpuOptions {
191
251
  bodyBuffer?: GPUBuffer;
192
252
  bodyCapacity?: number;
@@ -194,6 +254,8 @@ export interface AvbdGpuOptions {
194
254
  spatialSort?: boolean;
195
255
  broadphase?: "auto" | "grid" | "hploc";
196
256
  solverMode?: "auto" | "standard" | "optimized" | "points";
257
+ /** 3D: automatically fuse closed jointless worlds of at most 32 bodies. */
258
+ smallGroups?: boolean;
197
259
  bvh?: { rebuildInterval?: number };
198
260
  capacity?: {
199
261
  contacts?: number;
@@ -202,9 +264,10 @@ export interface AvbdGpuOptions {
202
264
  colors?: number;
203
265
  joints?: number;
204
266
  };
267
+ /** Reserve coloring capacity when sleeping bodies wake. */
205
268
  minimumColors?: number;
206
269
  minimumColorRounds?: number;
207
- sleeping?: boolean | { speedThreshold?: number; timeThreshold?: number };
270
+ sleeping?: boolean | AvbdSleepOptions;
208
271
  }
209
272
  export interface AvbdGpuSolver {
210
273
  readonly bodyBuffer: GPUBuffer;
@@ -274,10 +337,7 @@ export interface AvbdGpuSolver3D extends AvbdGpuSolver {
274
337
  rotation?: AvbdQuaternion,
275
338
  ): this;
276
339
  readBodyState(index: number): Promise<AvbdNativeState3D>;
277
- enableSleeping(options?: {
278
- speedThreshold?: number;
279
- timeThreshold?: number;
280
- }): this;
340
+ enableSleeping(options?: AvbdSleepOptions): this;
281
341
  disableSleeping(): this;
282
342
  setSleepEnabled(index: number, enabled: boolean): this;
283
343
  watchContacts(options?: AvbdNativeContactOptions): this;
@@ -509,6 +569,12 @@ export interface AvbdNativeConstraint {
509
569
  destroy(): void;
510
570
  }
511
571
  export class AvbdScene3D {
572
+ addHeightfield(options: AvbdHeightfieldOptions): AvbdNativeTerrain;
573
+ addStaticMesh(options: AvbdStaticMeshOptions): AvbdNativeTerrain;
574
+ addTerrain(
575
+ cooked: AvbdCookedTerrain,
576
+ options?: AvbdTerrainMaterial,
577
+ ): AvbdNativeTerrain;
512
578
  addMotor(
513
579
  a: AvbdRigid | null,
514
580
  b: AvbdRigid,
@@ -621,7 +687,10 @@ export class AvbdScene3D {
621
687
  },
622
688
  ): AvbdNativeConstraint;
623
689
  createSolver(device: GPUDevice, options?: AvbdGpuOptions): AvbdGpuSolver3D;
624
- createSolver(pipeline: AvbdSolverPipeline, options?: AvbdGpuOptions): AvbdSolver3D;
690
+ createSolver(
691
+ pipeline: AvbdSolverPipeline,
692
+ options?: AvbdGpuOptions,
693
+ ): AvbdSolver3D;
625
694
  }
626
695
  export interface AvbdNativeAngularOptions {
627
696
  axisA?: AvbdTuple3;
@@ -731,8 +800,12 @@ export class AvbdScene2D {
731
800
  bodyBuffer?: GPUBuffer;
732
801
  bodyCapacity?: number;
733
802
  colorRounds?: number;
803
+ minimumColors?: number;
734
804
  sleeping?: boolean | { speedThreshold?: number; timeThreshold?: number };
735
805
  },
736
806
  ): AvbdGpuSolver2D;
737
- createSolver(pipeline: AvbdSolverPipeline, options?: AvbdGpuOptions): AvbdSolver2D;
807
+ createSolver(
808
+ pipeline: AvbdSolverPipeline,
809
+ options?: AvbdGpuOptions,
810
+ ): AvbdSolver2D;
738
811
  }
@@ -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,29 @@ 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 jointless 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. Explicit edits wake the world and reset group history. Unsupported bodies
134
155
  under gravity stay awake. `wakeAll()`, `body.wakeUp()` and `readSleepStats()` are
135
156
  available. Suspended jointed objects are not guaranteed to sleep. Defaults and
136
157
  paper-comparison benchmarks keep sleeping disabled.
137
158
 
159
+ Closed jointless 3D worlds with at most 32 bodies automatically batch movement
160
+ iterations in one GPU workgroup. Body poses stay in shared memory, with the
161
+ same contact order, reductions and equations. Adding a joint or exceeding the
162
+ body limit selects the ordinary pipeline. Qualified mobile compatibility paths
163
+ retain their existing execution policy. `smallGroups:false` disables batching
164
+ for controlled comparisons.
165
+
138
166
  `body.setLinearVelocity()`, `setAngularVelocity()`, `applyImpulse()`,
139
167
  `applyAngularImpulse()` and `teleport(position, quaternion)` enqueue compact
140
168
  GPU commands. Commands for the same body are combined before submission;
141
169
  impulses use the current GPU mass, inertia and position. They never replace a
142
170
  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
171
+ is also called. Omitting its quaternion preserves the live GPU rotation and
172
+ places the mesh origin at the requested position. New aggregates can be added within reserved capacity after
144
173
  initialization; disposed body slots are not reused. For repeated spawning,
145
174
  reserve projectile bodies and teleport them. `applyForce()` and `applyTorque()`
146
175
  apply forces for one fixed step, using impulses on live GPU state.