@driftengine/physics 3.61.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/LICENSE +202 -0
- package/NOTICE +9 -0
- package/README.md +503 -0
- package/dist/bodies.d.ts +121 -0
- package/dist/bodies.js +298 -0
- package/dist/buoyancy.d.ts +46 -0
- package/dist/buoyancy.js +45 -0
- package/dist/cloth.d.ts +226 -0
- package/dist/cloth.js +587 -0
- package/dist/collide/aabb.d.ts +10 -0
- package/dist/collide/aabb.js +11 -0
- package/dist/collide/body.d.ts +202 -0
- package/dist/collide/body.js +142 -0
- package/dist/collide/eject.d.ts +39 -0
- package/dist/collide/eject.js +142 -0
- package/dist/collide/index.d.ts +18 -0
- package/dist/collide/index.js +5 -0
- package/dist/collide/scratch.d.ts +8 -0
- package/dist/collide/scratch.js +9 -0
- package/dist/collide/segment.d.ts +12 -0
- package/dist/collide/segment.js +109 -0
- package/dist/collide/sweep.d.ts +3 -0
- package/dist/collide/sweep.js +1036 -0
- package/dist/colliderSet.d.ts +239 -0
- package/dist/colliderSet.js +546 -0
- package/dist/compoundContact.d.ts +70 -0
- package/dist/compoundContact.js +221 -0
- package/dist/controller.d.ts +351 -0
- package/dist/controller.js +641 -0
- package/dist/cylinder.d.ts +78 -0
- package/dist/cylinder.js +294 -0
- package/dist/cylinderContact.d.ts +7 -0
- package/dist/cylinderContact.js +704 -0
- package/dist/decompose.d.ts +105 -0
- package/dist/decompose.js +979 -0
- package/dist/events.d.ts +43 -0
- package/dist/events.js +95 -0
- package/dist/executor.d.ts +147 -0
- package/dist/executor.js +173 -0
- package/dist/faces.d.ts +26 -0
- package/dist/faces.js +34 -0
- package/dist/fingerprint.d.ts +61 -0
- package/dist/fingerprint.js +139 -0
- package/dist/groundMotor.d.ts +34 -0
- package/dist/groundMotor.js +41 -0
- package/dist/heightfieldShape.d.ts +128 -0
- package/dist/heightfieldShape.js +328 -0
- package/dist/index.d.ts +73 -0
- package/dist/index.js +47 -0
- package/dist/island.d.ts +55 -0
- package/dist/island.js +227 -0
- package/dist/islandSolve.d.ts +70 -0
- package/dist/islandSolve.js +171 -0
- package/dist/islandWorker.d.ts +18 -0
- package/dist/islandWorker.js +125 -0
- package/dist/joints.d.ts +180 -0
- package/dist/joints.js +1094 -0
- package/dist/manifold.d.ts +89 -0
- package/dist/manifold.js +608 -0
- package/dist/mass.d.ts +53 -0
- package/dist/mass.js +353 -0
- package/dist/meshContact.d.ts +57 -0
- package/dist/meshContact.js +393 -0
- package/dist/meshShape.d.ts +76 -0
- package/dist/meshShape.js +175 -0
- package/dist/pairs.d.ts +62 -0
- package/dist/pairs.js +154 -0
- package/dist/query.d.ts +71 -0
- package/dist/query.js +683 -0
- package/dist/ragdoll.d.ts +135 -0
- package/dist/ragdoll.js +489 -0
- package/dist/shape.d.ts +177 -0
- package/dist/shape.js +533 -0
- package/dist/solver.d.ts +136 -0
- package/dist/solver.js +601 -0
- package/dist/stage.d.ts +167 -0
- package/dist/stage.js +350 -0
- package/dist/stallEscape.d.ts +141 -0
- package/dist/stallEscape.js +254 -0
- package/dist/sweptStep.d.ts +173 -0
- package/dist/sweptStep.js +472 -0
- package/dist/tree.d.ts +61 -0
- package/dist/tree.js +348 -0
- package/dist/vehicle.d.ts +154 -0
- package/dist/vehicle.js +343 -0
- package/dist/workerPool.d.ts +107 -0
- package/dist/workerPool.js +239 -0
- package/dist/workers.d.ts +37 -0
- package/dist/workers.js +35 -0
- package/dist/world.d.ts +298 -0
- package/dist/world.js +712 -0
- package/package.json +55 -0
- package/src/bodies.ts +369 -0
- package/src/buoyancy.ts +78 -0
- package/src/cloth.ts +734 -0
- package/src/collide/aabb.ts +28 -0
- package/src/collide/body.ts +336 -0
- package/src/collide/eject.ts +156 -0
- package/src/collide/index.ts +26 -0
- package/src/collide/scratch.ts +10 -0
- package/src/collide/segment.ts +127 -0
- package/src/collide/sweep.ts +1132 -0
- package/src/colliderSet.ts +637 -0
- package/src/compoundContact.ts +284 -0
- package/src/controller.ts +817 -0
- package/src/cylinder.ts +340 -0
- package/src/cylinderContact.ts +825 -0
- package/src/decompose.ts +1198 -0
- package/src/events.ts +108 -0
- package/src/executor.ts +224 -0
- package/src/faces.ts +37 -0
- package/src/fingerprint.ts +146 -0
- package/src/groundMotor.ts +53 -0
- package/src/heightfieldShape.ts +395 -0
- package/src/index.ts +164 -0
- package/src/island.ts +219 -0
- package/src/islandSolve.ts +218 -0
- package/src/islandWorker.ts +141 -0
- package/src/joints.ts +1367 -0
- package/src/manifold.ts +798 -0
- package/src/mass.ts +424 -0
- package/src/meshContact.ts +443 -0
- package/src/meshShape.ts +261 -0
- package/src/pairs.ts +169 -0
- package/src/query.ts +844 -0
- package/src/ragdoll.ts +662 -0
- package/src/shape.ts +656 -0
- package/src/solver.ts +873 -0
- package/src/stage.ts +406 -0
- package/src/stallEscape.ts +362 -0
- package/src/sweptStep.ts +609 -0
- package/src/tree.ts +366 -0
- package/src/vehicle.ts +489 -0
- package/src/workerPool.ts +283 -0
- package/src/workers.ts +38 -0
- package/src/world.ts +961 -0
package/src/meshShape.ts
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import type { Aabb } from './collide/index.ts';
|
|
2
|
+
import type { ConvexShape } from './shape.ts';
|
|
3
|
+
import type { Heightfield } from './heightfieldShape.ts';
|
|
4
|
+
import { DynamicTree } from './tree.ts';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A static triangle mesh as a collision shape: the triangles, their planes, and the tree over them.
|
|
8
|
+
*
|
|
9
|
+
* **Static only, and the limit is enforced rather than documented.** `PhysicsWorld.addBody` refuses
|
|
10
|
+
* one on anything but a static body and `colliderFromShape` refuses one outright. A moving concave
|
|
11
|
+
* mesh needs its tree rebuilt or refitted every tick and needs a mass tensor a triangle soup does
|
|
12
|
+
* not have — and the usual answer to a moving concave thing is convex decomposition, which is a row
|
|
13
|
+
* of its own. **Most consumers want exactly this**: the level is a mesh and everything in it is a
|
|
14
|
+
* hull, a box or a sphere.
|
|
15
|
+
*
|
|
16
|
+
* **The tree is `DynamicTree`, built once and never moved.** A second acceleration structure would
|
|
17
|
+
* be a second thing to get right, and the one this package already has is an AABB tree with a
|
|
18
|
+
* tested query — its incremental machinery simply goes unused. *What that costs* is the 10 cm fat
|
|
19
|
+
* margin every proxy carries, which for a small triangle means a query returns a few candidates it
|
|
20
|
+
* did not need; the narrow phase rejects them in one separating-axis test each. *What would make it
|
|
21
|
+
* wrong* is a mesh of millimetre triangles, where the margin dwarfs the geometry and every query
|
|
22
|
+
* returns most of the mesh.
|
|
23
|
+
*
|
|
24
|
+
* **The interior-edge data is the half that decides whether it feels right.** A box sliding across
|
|
25
|
+
* two triangles catches on their shared edge, because at the seam the narrow phase finds the edge
|
|
26
|
+
* rather than the face and pushes the box back along it — the classic symptom is a character
|
|
27
|
+
* stumbling on a perfectly flat floor every metre. Each edge is therefore classified once, here,
|
|
28
|
+
* as convex or not, and `meshContact.ts` uses that to replace an edge normal with the face's.
|
|
29
|
+
*/
|
|
30
|
+
export interface TriangleMesh {
|
|
31
|
+
/** xyz per vertex, in the mesh's own space. */
|
|
32
|
+
readonly positions: Float32Array;
|
|
33
|
+
/** Three vertex indices per triangle. */
|
|
34
|
+
readonly indices: Uint32Array;
|
|
35
|
+
readonly triangleCount: number;
|
|
36
|
+
/** `nx, ny, nz, d` per triangle: the outward plane, with `d = n · v0`. */
|
|
37
|
+
readonly planes: Float32Array;
|
|
38
|
+
/**
|
|
39
|
+
* Three bits per triangle, one per edge: set where the fold at that edge turns **away** from the
|
|
40
|
+
* front face, or where the edge has no neighbour at all.
|
|
41
|
+
*
|
|
42
|
+
* Edge `i` runs from local vertex `i` to `(i + 1) % 3`. A boundary edge counts as convex because
|
|
43
|
+
* there is nothing on the other side to be caught between; a coplanar neighbour counts as *not*
|
|
44
|
+
* convex, because a flat seam is exactly the case a contact must never come off.
|
|
45
|
+
*/
|
|
46
|
+
readonly convexEdges: Uint8Array;
|
|
47
|
+
/**
|
|
48
|
+
* Over triangle indices, built once — and **null for a heightfield**, which needs none.
|
|
49
|
+
*
|
|
50
|
+
* A field's candidates are an index range rather than a tree query: the cells a box covers are
|
|
51
|
+
* arithmetic on its own bounds. See `heightfieldShape.ts`, which is the other producer of this
|
|
52
|
+
* shape and the reason this is nullable.
|
|
53
|
+
*/
|
|
54
|
+
readonly tree: DynamicTree | null;
|
|
55
|
+
/**
|
|
56
|
+
* Set where the triangles are generated from a heightfield rather than stored.
|
|
57
|
+
*
|
|
58
|
+
* The arrays above are then empty and nothing reads them: `meshContact.ts` and `query.ts` take
|
|
59
|
+
* their corners, planes and edge classifications from the field instead. Everything *else* about
|
|
60
|
+
* a mesh — the one-sided rule, the many manifolds, the interior-edge filter — is shared, which is
|
|
61
|
+
* the whole design.
|
|
62
|
+
*/
|
|
63
|
+
readonly field?: Heightfield;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** How far a neighbour's opposite vertex must sit off a plane before the fold has a direction. */
|
|
67
|
+
const COPLANAR_M = 1e-5;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Build a static mesh shape.
|
|
71
|
+
*
|
|
72
|
+
* Returns a `ConvexShape` whose `triangles` names the mesh, whose `vertices` are the eight corners
|
|
73
|
+
* of its bounding box — so `shapeBounds`, `boundRadius` and the broad phase need no special case —
|
|
74
|
+
* and whose `faceNormals` and `edgeDirs` are **empty**. That last part is deliberate: a mesh has no
|
|
75
|
+
* separating axes worth enumerating, and handing the kinematic sweep the box's three would make it
|
|
76
|
+
* treat a hollow level as a solid brick.
|
|
77
|
+
*
|
|
78
|
+
* **`positions` and `indices` are kept, not copied**, the same bargain `packSplats` makes: a level
|
|
79
|
+
* mesh is megabytes and duplicating it serves nobody. A caller must not go on mutating them, which
|
|
80
|
+
* is stated here because nothing enforces it.
|
|
81
|
+
*/
|
|
82
|
+
export function meshShape(positions: Float32Array, indices: Uint32Array): ConvexShape {
|
|
83
|
+
if (indices.length === 0 || indices.length % 3 !== 0) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
`meshShape: ${indices.length} indices is not a whole number of triangles, or is none at all`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
const triangleCount = indices.length / 3;
|
|
89
|
+
const planes = new Float32Array(triangleCount * 4);
|
|
90
|
+
const convexEdges = new Uint8Array(triangleCount);
|
|
91
|
+
const tree = new DynamicTree(Math.max(64, triangleCount * 2));
|
|
92
|
+
|
|
93
|
+
let minX = Infinity;
|
|
94
|
+
let minY = Infinity;
|
|
95
|
+
let minZ = Infinity;
|
|
96
|
+
let maxX = -Infinity;
|
|
97
|
+
let maxY = -Infinity;
|
|
98
|
+
let maxZ = -Infinity;
|
|
99
|
+
const box: Aabb = { minX: 0, minY: 0, minZ: 0, maxX: 0, maxY: 0, maxZ: 0 };
|
|
100
|
+
|
|
101
|
+
/*
|
|
102
|
+
* Every undirected edge to the one triangle that has claimed it, so the second triangle to meet
|
|
103
|
+
* it finds the first. A `Map` rather than a sorted array because this is build-time and the key
|
|
104
|
+
* is a pair of indices; the value packs the triangle and which of its three edges it was.
|
|
105
|
+
*/
|
|
106
|
+
const edges = new Map<number, number>();
|
|
107
|
+
const vertexCount = positions.length / 3;
|
|
108
|
+
const edgeKey = (a: number, b: number): number => (a < b ? a : b) * vertexCount + (a < b ? b : a);
|
|
109
|
+
|
|
110
|
+
for (let t = 0; t < triangleCount; t++) {
|
|
111
|
+
const i0 = indices[t * 3] ?? 0;
|
|
112
|
+
const i1 = indices[t * 3 + 1] ?? 0;
|
|
113
|
+
const i2 = indices[t * 3 + 2] ?? 0;
|
|
114
|
+
const ax = positions[i0 * 3] ?? 0;
|
|
115
|
+
const ay = positions[i0 * 3 + 1] ?? 0;
|
|
116
|
+
const az = positions[i0 * 3 + 2] ?? 0;
|
|
117
|
+
const bx = positions[i1 * 3] ?? 0;
|
|
118
|
+
const by = positions[i1 * 3 + 1] ?? 0;
|
|
119
|
+
const bz = positions[i1 * 3 + 2] ?? 0;
|
|
120
|
+
const cx = positions[i2 * 3] ?? 0;
|
|
121
|
+
const cy = positions[i2 * 3 + 1] ?? 0;
|
|
122
|
+
const cz = positions[i2 * 3 + 2] ?? 0;
|
|
123
|
+
|
|
124
|
+
let nx = (by - ay) * (cz - az) - (bz - az) * (cy - ay);
|
|
125
|
+
let ny = (bz - az) * (cx - ax) - (bx - ax) * (cz - az);
|
|
126
|
+
let nz = (bx - ax) * (cy - ay) - (by - ay) * (cx - ax);
|
|
127
|
+
const length = Math.sqrt(nx * nx + ny * ny + nz * nz);
|
|
128
|
+
/*
|
|
129
|
+
* A degenerate triangle has no normal and is dropped rather than refused: a mesh exported from
|
|
130
|
+
* a modelling tool routinely carries a few, and rejecting the whole level for three slivers
|
|
131
|
+
* would be a refusal a consumer cannot act on. It keeps its slot so triangle indices still
|
|
132
|
+
* name the caller's own triangles, and its plane is a zero the contact code skips.
|
|
133
|
+
*/
|
|
134
|
+
if (length > 1e-12) {
|
|
135
|
+
nx /= length;
|
|
136
|
+
ny /= length;
|
|
137
|
+
nz /= length;
|
|
138
|
+
} else {
|
|
139
|
+
nx = 0;
|
|
140
|
+
ny = 0;
|
|
141
|
+
nz = 0;
|
|
142
|
+
}
|
|
143
|
+
planes[t * 4] = nx;
|
|
144
|
+
planes[t * 4 + 1] = ny;
|
|
145
|
+
planes[t * 4 + 2] = nz;
|
|
146
|
+
planes[t * 4 + 3] = nx * ax + ny * ay + nz * az;
|
|
147
|
+
|
|
148
|
+
box.minX = Math.min(ax, bx, cx);
|
|
149
|
+
box.minY = Math.min(ay, by, cy);
|
|
150
|
+
box.minZ = Math.min(az, bz, cz);
|
|
151
|
+
box.maxX = Math.max(ax, bx, cx);
|
|
152
|
+
box.maxY = Math.max(ay, by, cy);
|
|
153
|
+
box.maxZ = Math.max(az, bz, cz);
|
|
154
|
+
tree.insert(t, box);
|
|
155
|
+
minX = Math.min(minX, box.minX);
|
|
156
|
+
minY = Math.min(minY, box.minY);
|
|
157
|
+
minZ = Math.min(minZ, box.minZ);
|
|
158
|
+
maxX = Math.max(maxX, box.maxX);
|
|
159
|
+
maxY = Math.max(maxY, box.maxY);
|
|
160
|
+
maxZ = Math.max(maxZ, box.maxZ);
|
|
161
|
+
|
|
162
|
+
/* Every edge starts convex; meeting a neighbour is what can take that away. */
|
|
163
|
+
convexEdges[t] = 0b111;
|
|
164
|
+
for (let e = 0; e < 3; e++) {
|
|
165
|
+
const from = indices[t * 3 + e] ?? 0;
|
|
166
|
+
const to = indices[t * 3 + ((e + 1) % 3)] ?? 0;
|
|
167
|
+
const key = edgeKey(from, to);
|
|
168
|
+
const seen = edges.get(key);
|
|
169
|
+
if (seen === undefined) {
|
|
170
|
+
edges.set(key, t * 3 + e);
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
classify(positions, indices, planes, convexEdges, t, e, seen);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
/* The eight corners of the bounds, so everything that measures a shape measures the right box. */
|
|
179
|
+
vertices: new Float32Array([
|
|
180
|
+
minX,
|
|
181
|
+
minY,
|
|
182
|
+
minZ,
|
|
183
|
+
maxX,
|
|
184
|
+
minY,
|
|
185
|
+
minZ,
|
|
186
|
+
minX,
|
|
187
|
+
maxY,
|
|
188
|
+
minZ,
|
|
189
|
+
maxX,
|
|
190
|
+
maxY,
|
|
191
|
+
minZ,
|
|
192
|
+
minX,
|
|
193
|
+
minY,
|
|
194
|
+
maxZ,
|
|
195
|
+
maxX,
|
|
196
|
+
minY,
|
|
197
|
+
maxZ,
|
|
198
|
+
minX,
|
|
199
|
+
maxY,
|
|
200
|
+
maxZ,
|
|
201
|
+
maxX,
|
|
202
|
+
maxY,
|
|
203
|
+
maxZ,
|
|
204
|
+
]),
|
|
205
|
+
faceNormals: new Float32Array(0),
|
|
206
|
+
edgeDirs: new Float32Array(0),
|
|
207
|
+
radius: 0,
|
|
208
|
+
sideRadius: 0,
|
|
209
|
+
boundRadius: Math.sqrt(
|
|
210
|
+
Math.max(minX * minX, maxX * maxX) +
|
|
211
|
+
Math.max(minY * minY, maxY * maxY) +
|
|
212
|
+
Math.max(minZ * minZ, maxZ * maxZ),
|
|
213
|
+
),
|
|
214
|
+
facePlanes: new Float32Array(0),
|
|
215
|
+
faceVertexStart: new Uint16Array(0),
|
|
216
|
+
faceVertexIndices: new Uint16Array(0),
|
|
217
|
+
triangles: { positions, indices, triangleCount, planes, convexEdges, tree },
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Decide, for one shared edge, whether either side of it may produce a contact normal of its own.
|
|
223
|
+
*
|
|
224
|
+
* The test is where the *other* triangle's opposite vertex sits relative to this one's plane, and
|
|
225
|
+
* both sides are answered at once because the answer is symmetric in what it forbids: a fold that
|
|
226
|
+
* turns away from one face turns toward the other.
|
|
227
|
+
*/
|
|
228
|
+
function classify(
|
|
229
|
+
positions: Float32Array,
|
|
230
|
+
indices: Uint32Array,
|
|
231
|
+
planes: Float32Array,
|
|
232
|
+
convexEdges: Uint8Array,
|
|
233
|
+
triangle: number,
|
|
234
|
+
edge: number,
|
|
235
|
+
other: number,
|
|
236
|
+
): void {
|
|
237
|
+
const otherTriangle = Math.floor(other / 3);
|
|
238
|
+
const otherEdge = other % 3;
|
|
239
|
+
/* The vertex neither triangle shares: the one opposite the edge. */
|
|
240
|
+
const opposite = indices[otherTriangle * 3 + ((otherEdge + 2) % 3)] ?? 0;
|
|
241
|
+
const px = positions[opposite * 3] ?? 0;
|
|
242
|
+
const py = positions[opposite * 3 + 1] ?? 0;
|
|
243
|
+
const pz = positions[opposite * 3 + 2] ?? 0;
|
|
244
|
+
const height =
|
|
245
|
+
px * (planes[triangle * 4] ?? 0) +
|
|
246
|
+
py * (planes[triangle * 4 + 1] ?? 0) +
|
|
247
|
+
pz * (planes[triangle * 4 + 2] ?? 0) -
|
|
248
|
+
(planes[triangle * 4 + 3] ?? 0);
|
|
249
|
+
|
|
250
|
+
/*
|
|
251
|
+
* **Below the plane is a convex fold and everything else is not.** A neighbour that rises above
|
|
252
|
+
* this triangle's face is a concave corner, where a contact normal off the edge is a normal
|
|
253
|
+
* pointing into a wall that is really there; a neighbour lying in the plane is a flat seam, where
|
|
254
|
+
* an edge normal is the interior-edge artefact this exists to remove.
|
|
255
|
+
*/
|
|
256
|
+
const convex = height < -COPLANAR_M;
|
|
257
|
+
if (!convex) {
|
|
258
|
+
convexEdges[triangle] = (convexEdges[triangle] ?? 0) & ~(1 << edge);
|
|
259
|
+
convexEdges[otherTriangle] = (convexEdges[otherTriangle] ?? 0) & ~(1 << otherEdge);
|
|
260
|
+
}
|
|
261
|
+
}
|
package/src/pairs.ts
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { BODY_STATIC } from './bodies.ts';
|
|
2
|
+
import type { BodySet } from './bodies.ts';
|
|
3
|
+
import type { Aabb } from './collide/index.ts';
|
|
4
|
+
import { DynamicTree } from './tree.ts';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Candidate body pairs, ordered by body index and never by tree shape.
|
|
8
|
+
*
|
|
9
|
+
* **This is the file that makes the broadphase safe to change.** A dynamic tree's shape depends on
|
|
10
|
+
* the order proxies were inserted in, and its traversal order depends on its shape — so a pair list
|
|
11
|
+
* taken straight from the tree carries insertion history in it. Sequential impulses are
|
|
12
|
+
* order-dependent, so that history would reach the simulation's *result*, and a scene rebuilt in a
|
|
13
|
+
* different order would diverge while looking identical.
|
|
14
|
+
*
|
|
15
|
+
* So every pair is packed into one 32-bit key as `min * 2^16 + max`, radix-sorted, and nothing
|
|
16
|
+
* downstream ever learns there was a tree. **What this costs** is a two-pass sort over preallocated
|
|
17
|
+
* scratch each tick. **What would make it wrong** is more than 65,536 bodies, which the key cannot
|
|
18
|
+
* express; `MAX_BODIES` names that and the builder refuses past it rather than aliasing two pairs
|
|
19
|
+
* onto one key.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** The key packs two indices into 32 bits, so neither may reach this. */
|
|
23
|
+
export const MAX_BODIES = 1 << 16;
|
|
24
|
+
|
|
25
|
+
export class PairSet {
|
|
26
|
+
/** Packed `(min, max)` keys, sorted ascending. Read `count` of them. */
|
|
27
|
+
keys: Uint32Array;
|
|
28
|
+
count = 0;
|
|
29
|
+
|
|
30
|
+
private scratch: Uint32Array;
|
|
31
|
+
private histogram = new Uint32Array(256);
|
|
32
|
+
private hits: Int32Array;
|
|
33
|
+
private queryBox: Aabb = { minX: 0, minY: 0, minZ: 0, maxX: 0, maxY: 0, maxZ: 0 };
|
|
34
|
+
|
|
35
|
+
constructor(capacity = 256) {
|
|
36
|
+
this.keys = new Uint32Array(capacity);
|
|
37
|
+
this.scratch = new Uint32Array(capacity);
|
|
38
|
+
this.hits = new Int32Array(256);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Collect every candidate pair among the tree's proxies, sorted.
|
|
43
|
+
*
|
|
44
|
+
* `leafOf` maps a body index to its tree leaf, because a query answers with proxy ids and the
|
|
45
|
+
* tree needs the leaf to read a body's stored bounds back.
|
|
46
|
+
*/
|
|
47
|
+
build(
|
|
48
|
+
bodies: BodySet,
|
|
49
|
+
tree: DynamicTree,
|
|
50
|
+
leafOf: Int32Array,
|
|
51
|
+
ignored: ReadonlySet<number> | null = null,
|
|
52
|
+
): number {
|
|
53
|
+
this.count = 0;
|
|
54
|
+
if (bodies.count >= MAX_BODIES) {
|
|
55
|
+
throw new Error(`PairSet: ${bodies.count} bodies exceeds the ${MAX_BODIES} a pair key holds`);
|
|
56
|
+
}
|
|
57
|
+
for (let a = 0; a < bodies.count; a++) {
|
|
58
|
+
const leaf = leafOf[a] ?? -1;
|
|
59
|
+
if (leaf < 0) continue;
|
|
60
|
+
const at = leaf * 6;
|
|
61
|
+
this.queryBox.minX = tree.bounds[at] ?? 0;
|
|
62
|
+
this.queryBox.minY = tree.bounds[at + 1] ?? 0;
|
|
63
|
+
this.queryBox.minZ = tree.bounds[at + 2] ?? 0;
|
|
64
|
+
this.queryBox.maxX = tree.bounds[at + 3] ?? 0;
|
|
65
|
+
this.queryBox.maxY = tree.bounds[at + 4] ?? 0;
|
|
66
|
+
this.queryBox.maxZ = tree.bounds[at + 5] ?? 0;
|
|
67
|
+
|
|
68
|
+
let found = tree.query(this.queryBox, this.hits);
|
|
69
|
+
while (found === this.hits.length) {
|
|
70
|
+
// The query truncated, so it cannot be trusted to have reported everything.
|
|
71
|
+
this.hits = new Int32Array(this.hits.length * 2);
|
|
72
|
+
found = tree.query(this.queryBox, this.hits);
|
|
73
|
+
}
|
|
74
|
+
for (let k = 0; k < found; k++) {
|
|
75
|
+
const b = this.hits[k] ?? 0;
|
|
76
|
+
// Each unordered pair once, and never a body against itself.
|
|
77
|
+
if (b <= a) continue;
|
|
78
|
+
if (!layersInteract(bodies, a, b)) continue;
|
|
79
|
+
/* `b > a` by the test above, so this is already the order `pairKey` writes. */
|
|
80
|
+
const key = a * MAX_BODIES + b;
|
|
81
|
+
/* The size test rather than the lookup: a world that never excluded a pair pays one
|
|
82
|
+
integer compare per candidate, which is what keeps this out of everybody else's way. */
|
|
83
|
+
if (ignored !== null && ignored.size !== 0 && ignored.has(key)) continue;
|
|
84
|
+
this.push(key);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
this.sort();
|
|
88
|
+
return this.count;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private push(key: number): void {
|
|
92
|
+
if (this.count === this.keys.length) {
|
|
93
|
+
const wider = new Uint32Array(this.keys.length * 2);
|
|
94
|
+
wider.set(this.keys);
|
|
95
|
+
this.keys = wider;
|
|
96
|
+
this.scratch = new Uint32Array(wider.length);
|
|
97
|
+
}
|
|
98
|
+
this.keys[this.count++] = key;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Four passes of an 8-bit radix sort, which is stable and touches no comparator.
|
|
103
|
+
*
|
|
104
|
+
* A comparison sort would also be deterministic, and this is here for the hot path rather than
|
|
105
|
+
* for correctness: the pair list is rebuilt every tick and is the one array whose length grows
|
|
106
|
+
* with the square of local density.
|
|
107
|
+
*/
|
|
108
|
+
private sort(): void {
|
|
109
|
+
let from = this.keys;
|
|
110
|
+
let into = this.scratch;
|
|
111
|
+
for (let shift = 0; shift < 32; shift += 8) {
|
|
112
|
+
this.histogram.fill(0);
|
|
113
|
+
for (let i = 0; i < this.count; i++) {
|
|
114
|
+
this.histogram[((from[i] ?? 0) >>> shift) & 255]++;
|
|
115
|
+
}
|
|
116
|
+
let sum = 0;
|
|
117
|
+
for (let d = 0; d < 256; d++) {
|
|
118
|
+
const c = this.histogram[d] ?? 0;
|
|
119
|
+
this.histogram[d] = sum;
|
|
120
|
+
sum += c;
|
|
121
|
+
}
|
|
122
|
+
for (let i = 0; i < this.count; i++) {
|
|
123
|
+
const key = from[i] ?? 0;
|
|
124
|
+
const d = (key >>> shift) & 255;
|
|
125
|
+
into[this.histogram[d]++] = key;
|
|
126
|
+
}
|
|
127
|
+
const swap = from;
|
|
128
|
+
from = into;
|
|
129
|
+
into = swap;
|
|
130
|
+
}
|
|
131
|
+
// Four passes is even, so the result is back where it started.
|
|
132
|
+
this.keys = from;
|
|
133
|
+
this.scratch = into;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** The 32-layer mask, both ways. A pair layersInteract only if each side admits the other. */
|
|
138
|
+
export function layersInteract(bodies: BodySet, a: number, b: number): boolean {
|
|
139
|
+
if (bodies.type[a] === BODY_STATIC && bodies.type[b] === BODY_STATIC) return false;
|
|
140
|
+
const la = bodies.layer[a] ?? 0;
|
|
141
|
+
const lb = bodies.layer[b] ?? 0;
|
|
142
|
+
const ma = bodies.mask[a] ?? 0;
|
|
143
|
+
const mb = bodies.mask[b] ?? 0;
|
|
144
|
+
return (la & mb) !== 0 && (lb & ma) !== 0;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The key a pair of bodies is stored under, in either order.
|
|
149
|
+
*
|
|
150
|
+
* **Two bodies that must never collide cannot always be said with layers.** The mask is a property
|
|
151
|
+
* of each body on its own, so it can express "this kind does not meet that kind" and cannot express
|
|
152
|
+
* "these two in particular" — and a ragdoll is entirely the second thing: every bone overlaps the
|
|
153
|
+
* one it hangs from, because that is what having a radius means, while the same two *kinds* of bone
|
|
154
|
+
* elsewhere in the body must still collide. A consumer that tried it with layers spent a bit per
|
|
155
|
+
* bone and most of the 32 on one doll.
|
|
156
|
+
*/
|
|
157
|
+
export function pairKey(a: number, b: number): number {
|
|
158
|
+
return a < b ? a * MAX_BODIES + b : b * MAX_BODIES + a;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Unpack a key's lower body index. */
|
|
162
|
+
export function pairA(key: number): number {
|
|
163
|
+
return Math.floor(key / MAX_BODIES);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Unpack a key's higher body index. */
|
|
167
|
+
export function pairB(key: number): number {
|
|
168
|
+
return key % MAX_BODIES;
|
|
169
|
+
}
|