@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/mass.ts
ADDED
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
import { faceCount, faceVertices } from './faces.ts';
|
|
2
|
+
import type { ConvexShape } from './shape.ts';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A shape's volume, centre of mass and inertia tensor, at a given density.
|
|
6
|
+
*
|
|
7
|
+
* **Exact for a hull, by tetrahedral decomposition over its face loops** — which is why this sits
|
|
8
|
+
* immediately beside the loops themselves: one build-time addition to `ConvexShape` pays for both
|
|
9
|
+
* contact clipping and this, and neither is a separate piece of work.
|
|
10
|
+
*
|
|
11
|
+
* A rounded hull — `radius > 0` with faces — is the Minkowski sum of a hull and a ball, which has
|
|
12
|
+
* no closed form. Sphere and capsule are handled by the ball case below; anything else with a
|
|
13
|
+
* radius is approximated by the hull's tensor about the same centre. **What that gives up** is
|
|
14
|
+
* accuracy for a heavily rounded hull, where the shell is a large fraction of the volume. **What
|
|
15
|
+
* would make it wrong** is a consumer building such a shape and caring how it tumbles, at which
|
|
16
|
+
* point the answer is a numerical integration over the support function rather than a fudge factor.
|
|
17
|
+
*
|
|
18
|
+
* Build-time only, like everything else in `shape.ts`: nothing here may be called per tick.
|
|
19
|
+
*/
|
|
20
|
+
export interface MassProperties {
|
|
21
|
+
volume: number;
|
|
22
|
+
comX: number;
|
|
23
|
+
comY: number;
|
|
24
|
+
comZ: number;
|
|
25
|
+
/** The tensor about the centre of mass. Six independent terms, symmetric. */
|
|
26
|
+
ixx: number;
|
|
27
|
+
iyy: number;
|
|
28
|
+
izz: number;
|
|
29
|
+
ixy: number;
|
|
30
|
+
ixz: number;
|
|
31
|
+
iyz: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** A target for `shapeMassProperties`, so nothing allocates per call. */
|
|
35
|
+
export function createMassProperties(): MassProperties {
|
|
36
|
+
return { volume: 0, comX: 0, comY: 0, comZ: 0, ixx: 0, iyy: 0, izz: 0, ixy: 0, ixz: 0, iyz: 0 };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Scratch for one face's vertex loop. `MAX_POINTS` caps a hull at 64, so a face cannot exceed it. */
|
|
40
|
+
const FACE = new Uint16Array(64);
|
|
41
|
+
|
|
42
|
+
export function shapeMassProperties(
|
|
43
|
+
shape: ConvexShape,
|
|
44
|
+
density: number,
|
|
45
|
+
out: MassProperties,
|
|
46
|
+
): MassProperties {
|
|
47
|
+
const faces = faceCount(shape);
|
|
48
|
+
/*
|
|
49
|
+
* **A triangle mesh has no mass properties and answers zero rather than guessing.** A surface
|
|
50
|
+
* encloses no volume, and a soup of them may not even be closed; the bounding box's tensor would
|
|
51
|
+
* be a plausible number for a body that `addBody` refuses to make dynamic anyway. Zero volume is
|
|
52
|
+
* what a static body's own inverse mass already is.
|
|
53
|
+
*/
|
|
54
|
+
if (shape.triangles !== undefined) {
|
|
55
|
+
out.volume = 0;
|
|
56
|
+
out.comX = 0;
|
|
57
|
+
out.comY = 0;
|
|
58
|
+
out.comZ = 0;
|
|
59
|
+
out.ixx = 0;
|
|
60
|
+
out.iyy = 0;
|
|
61
|
+
out.izz = 0;
|
|
62
|
+
out.ixy = 0;
|
|
63
|
+
out.ixz = 0;
|
|
64
|
+
out.iyz = 0;
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
// A disc-rounded shape is a cylinder and has its own closed form, which is exact rather than a
|
|
68
|
+
// decomposition — so it is tested before the faceless path, whose arithmetic assumes a ball.
|
|
69
|
+
if (shape.sideRadius > 0) return cylinderProperties(shape, density, out);
|
|
70
|
+
if (faces === 0) return ballProperties(shape, density, out);
|
|
71
|
+
|
|
72
|
+
let volume = 0;
|
|
73
|
+
let centroidX = 0;
|
|
74
|
+
let centroidY = 0;
|
|
75
|
+
let centroidZ = 0;
|
|
76
|
+
// Second moments about the origin, accumulated per tetrahedron and shifted at the end.
|
|
77
|
+
let xx = 0;
|
|
78
|
+
let yy = 0;
|
|
79
|
+
let zz = 0;
|
|
80
|
+
let xy = 0;
|
|
81
|
+
let xz = 0;
|
|
82
|
+
let yz = 0;
|
|
83
|
+
|
|
84
|
+
for (let face = 0; face < faces; face++) {
|
|
85
|
+
const n = faceVertices(shape, face, FACE);
|
|
86
|
+
const a = (FACE[0] ?? 0) * 3;
|
|
87
|
+
const ax = shape.vertices[a] ?? 0;
|
|
88
|
+
const ay = shape.vertices[a + 1] ?? 0;
|
|
89
|
+
const az = shape.vertices[a + 2] ?? 0;
|
|
90
|
+
|
|
91
|
+
// Fan the face into triangles, each closing a tetrahedron against the origin.
|
|
92
|
+
for (let i = 1; i + 1 < n; i++) {
|
|
93
|
+
const b = (FACE[i] ?? 0) * 3;
|
|
94
|
+
const c = (FACE[i + 1] ?? 0) * 3;
|
|
95
|
+
const bx = shape.vertices[b] ?? 0;
|
|
96
|
+
const by = shape.vertices[b + 1] ?? 0;
|
|
97
|
+
const bz = shape.vertices[b + 2] ?? 0;
|
|
98
|
+
const cx = shape.vertices[c] ?? 0;
|
|
99
|
+
const cy = shape.vertices[c + 1] ?? 0;
|
|
100
|
+
const cz = shape.vertices[c + 2] ?? 0;
|
|
101
|
+
|
|
102
|
+
// Six times the signed tetrahedron volume: the scalar triple product.
|
|
103
|
+
const det = ax * (by * cz - bz * cy) - ay * (bx * cz - bz * cx) + az * (bx * cy - by * cx);
|
|
104
|
+
const v = det / 6;
|
|
105
|
+
volume += v;
|
|
106
|
+
|
|
107
|
+
// Centroid of a tetrahedron with one vertex at the origin.
|
|
108
|
+
centroidX += (v * (ax + bx + cx)) / 4;
|
|
109
|
+
centroidY += (v * (ay + by + cy)) / 4;
|
|
110
|
+
centroidZ += (v * (az + bz + cz)) / 4;
|
|
111
|
+
|
|
112
|
+
/*
|
|
113
|
+
* Second moments of a tetrahedron about the origin, in closed form. The diagonal integral
|
|
114
|
+
* reduces to det/60 times the sum of squares and pairwise products of the three vertex
|
|
115
|
+
* coordinates; the products use det/120 with the mixed terms. Both are exact rather than
|
|
116
|
+
* quadrature, which is what makes the box test below a hand-derived literal rather than a
|
|
117
|
+
* tolerance chosen to fit.
|
|
118
|
+
*/
|
|
119
|
+
xx += (det / 60) * (ax * ax + bx * bx + cx * cx + ax * bx + ax * cx + bx * cx);
|
|
120
|
+
yy += (det / 60) * (ay * ay + by * by + cy * cy + ay * by + ay * cy + by * cy);
|
|
121
|
+
zz += (det / 60) * (az * az + bz * bz + cz * cz + az * bz + az * cz + bz * cz);
|
|
122
|
+
xy +=
|
|
123
|
+
(det / 120) *
|
|
124
|
+
(2 * ax * ay +
|
|
125
|
+
2 * bx * by +
|
|
126
|
+
2 * cx * cy +
|
|
127
|
+
ax * by +
|
|
128
|
+
ay * bx +
|
|
129
|
+
ax * cy +
|
|
130
|
+
ay * cx +
|
|
131
|
+
bx * cy +
|
|
132
|
+
by * cx);
|
|
133
|
+
xz +=
|
|
134
|
+
(det / 120) *
|
|
135
|
+
(2 * ax * az +
|
|
136
|
+
2 * bx * bz +
|
|
137
|
+
2 * cx * cz +
|
|
138
|
+
ax * bz +
|
|
139
|
+
az * bx +
|
|
140
|
+
ax * cz +
|
|
141
|
+
az * cx +
|
|
142
|
+
bx * cz +
|
|
143
|
+
bz * cx);
|
|
144
|
+
yz +=
|
|
145
|
+
(det / 120) *
|
|
146
|
+
(2 * ay * az +
|
|
147
|
+
2 * by * bz +
|
|
148
|
+
2 * cy * cz +
|
|
149
|
+
ay * bz +
|
|
150
|
+
az * by +
|
|
151
|
+
ay * cz +
|
|
152
|
+
az * cy +
|
|
153
|
+
by * cz +
|
|
154
|
+
bz * cy);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const mass = volume * density;
|
|
159
|
+
const comX = volume === 0 ? 0 : centroidX / volume;
|
|
160
|
+
const comY = volume === 0 ? 0 : centroidY / volume;
|
|
161
|
+
const comZ = volume === 0 ? 0 : centroidZ / volume;
|
|
162
|
+
|
|
163
|
+
out.volume = volume;
|
|
164
|
+
out.comX = comX;
|
|
165
|
+
out.comY = comY;
|
|
166
|
+
out.comZ = comZ;
|
|
167
|
+
// Parallel-axis shift from the origin to the centre of mass.
|
|
168
|
+
out.ixx = density * (yy + zz) - mass * (comY * comY + comZ * comZ);
|
|
169
|
+
out.iyy = density * (xx + zz) - mass * (comX * comX + comZ * comZ);
|
|
170
|
+
out.izz = density * (xx + yy) - mass * (comX * comX + comY * comY);
|
|
171
|
+
out.ixy = -(density * xy) + mass * comX * comY;
|
|
172
|
+
out.ixz = -(density * xz) + mass * comX * comZ;
|
|
173
|
+
out.iyz = -(density * yz) + mass * comY * comZ;
|
|
174
|
+
return out;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Several shapes as one body: volumes added, centre of mass weighted, tensors moved and summed.
|
|
179
|
+
*
|
|
180
|
+
* **What a compound body weighs, and it is the parallel axis theorem doing the work.** Each part's
|
|
181
|
+
* tensor is about that part's own centre of mass; the body's is about the body's. Moving one costs
|
|
182
|
+
* `m (|d|² δ - d ⊗ d)` where `d` is the offset between the two centres, and skipping that term is
|
|
183
|
+
* not a small error — an L-beam released at rest would rotate, because its inertia would no longer
|
|
184
|
+
* match its shape.
|
|
185
|
+
*
|
|
186
|
+
* **Overlap is counted twice, and that is the decision rather than an oversight.** A decomposition's
|
|
187
|
+
* hulls bulge slightly into each other, so the volumes here sum to a little more than the solid they
|
|
188
|
+
* stand for — measured at 27% against 16% occupied on a torus, and 5% against 2% on a cup. Removing
|
|
189
|
+
* it exactly means a boolean intersection of convex solids, which is a large body of code whose
|
|
190
|
+
* output corrects a number the caller invented when they chose a density.
|
|
191
|
+
* `Decomposition.hullVolume` reports the sum, so a caller who cares can divide it out.
|
|
192
|
+
*
|
|
193
|
+
* The shapes are read twice: once for the centre and once for the tensor about it. That is a bake's
|
|
194
|
+
* cost paid at `addBody`, not a tick's.
|
|
195
|
+
*/
|
|
196
|
+
export function combineMassProperties(
|
|
197
|
+
parts: readonly ConvexShape[],
|
|
198
|
+
density: number,
|
|
199
|
+
out: MassProperties,
|
|
200
|
+
): MassProperties {
|
|
201
|
+
let volume = 0;
|
|
202
|
+
let comX = 0;
|
|
203
|
+
let comY = 0;
|
|
204
|
+
let comZ = 0;
|
|
205
|
+
for (const part of parts) {
|
|
206
|
+
const m = shapeMassProperties(part, density, COMBINE_SCRATCH);
|
|
207
|
+
volume += m.volume;
|
|
208
|
+
comX += m.volume * m.comX;
|
|
209
|
+
comY += m.volume * m.comY;
|
|
210
|
+
comZ += m.volume * m.comZ;
|
|
211
|
+
}
|
|
212
|
+
if (volume > 0) {
|
|
213
|
+
comX /= volume;
|
|
214
|
+
comY /= volume;
|
|
215
|
+
comZ /= volume;
|
|
216
|
+
} else {
|
|
217
|
+
comX = 0;
|
|
218
|
+
comY = 0;
|
|
219
|
+
comZ = 0;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
let ixx = 0;
|
|
223
|
+
let iyy = 0;
|
|
224
|
+
let izz = 0;
|
|
225
|
+
let ixy = 0;
|
|
226
|
+
let ixz = 0;
|
|
227
|
+
let iyz = 0;
|
|
228
|
+
for (const part of parts) {
|
|
229
|
+
const m = shapeMassProperties(part, density, COMBINE_SCRATCH);
|
|
230
|
+
const mass = m.volume * density;
|
|
231
|
+
const dx = m.comX - comX;
|
|
232
|
+
const dy = m.comY - comY;
|
|
233
|
+
const dz = m.comZ - comZ;
|
|
234
|
+
ixx += m.ixx + mass * (dy * dy + dz * dz);
|
|
235
|
+
iyy += m.iyy + mass * (dx * dx + dz * dz);
|
|
236
|
+
izz += m.izz + mass * (dx * dx + dy * dy);
|
|
237
|
+
ixy += m.ixy - mass * dx * dy;
|
|
238
|
+
ixz += m.ixz - mass * dx * dz;
|
|
239
|
+
iyz += m.iyz - mass * dy * dz;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
out.volume = volume;
|
|
243
|
+
out.comX = comX;
|
|
244
|
+
out.comY = comY;
|
|
245
|
+
out.comZ = comZ;
|
|
246
|
+
out.ixx = ixx;
|
|
247
|
+
out.iyy = iyy;
|
|
248
|
+
out.izz = izz;
|
|
249
|
+
out.ixy = ixy;
|
|
250
|
+
out.ixz = ixz;
|
|
251
|
+
out.iyz = iyz;
|
|
252
|
+
return out;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Reused by `combineMassProperties`, which is called once per body rather than once per tick. */
|
|
256
|
+
const COMBINE_SCRATCH = createMassProperties();
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* A cylinder: `sideRadius` about the segment between the shape's two points.
|
|
260
|
+
*
|
|
261
|
+
* Exact and in closed form, like the hull path and unlike the capsule's, which has to sum a
|
|
262
|
+
* cylinder and two hemispheres. `m r² / 2` down the axis and `m (3r² + L²) / 12` across it, where
|
|
263
|
+
* `L` is the full length — the two textbook results, hand-derived in the test rather than checked
|
|
264
|
+
* against this code.
|
|
265
|
+
*
|
|
266
|
+
* **The tensor is built about the axis and then rotated into place**, the same last step
|
|
267
|
+
* `capsuleProperties` takes and for the same reason: a cylinder baked into world space by a
|
|
268
|
+
* collider has an arbitrary axis, and a tensor written in the shape's own frame would be wrong
|
|
269
|
+
* for it in a way nothing throws about.
|
|
270
|
+
*
|
|
271
|
+
* A shape carrying both roundings — a cylinder with a filleted rim — is treated as the plain
|
|
272
|
+
* cylinder its two radii describe at the fillet's outer extent. **What that gives up** is the
|
|
273
|
+
* fillet's own volume, a few per cent at a small radius; **what would make it wrong** is a
|
|
274
|
+
* consumer filleting a rim by a large fraction of the shape, where the answer is the numerical
|
|
275
|
+
* integration §4a already names.
|
|
276
|
+
*/
|
|
277
|
+
function cylinderProperties(
|
|
278
|
+
shape: ConvexShape,
|
|
279
|
+
density: number,
|
|
280
|
+
out: MassProperties,
|
|
281
|
+
): MassProperties {
|
|
282
|
+
const r = shape.sideRadius + shape.radius;
|
|
283
|
+
const ax = (shape.vertices[3] ?? 0) - (shape.vertices[0] ?? 0);
|
|
284
|
+
const ay = (shape.vertices[4] ?? 0) - (shape.vertices[1] ?? 0);
|
|
285
|
+
const az = (shape.vertices[5] ?? 0) - (shape.vertices[2] ?? 0);
|
|
286
|
+
const length = Math.sqrt(ax * ax + ay * ay + az * az) + 2 * shape.radius;
|
|
287
|
+
|
|
288
|
+
out.comX = ((shape.vertices[0] ?? 0) + (shape.vertices[3] ?? 0)) / 2;
|
|
289
|
+
out.comY = ((shape.vertices[1] ?? 0) + (shape.vertices[4] ?? 0)) / 2;
|
|
290
|
+
out.comZ = ((shape.vertices[2] ?? 0) + (shape.vertices[5] ?? 0)) / 2;
|
|
291
|
+
out.ixy = 0;
|
|
292
|
+
out.ixz = 0;
|
|
293
|
+
out.iyz = 0;
|
|
294
|
+
|
|
295
|
+
const volume = Math.PI * r * r * length;
|
|
296
|
+
const mass = volume * density;
|
|
297
|
+
out.volume = volume;
|
|
298
|
+
|
|
299
|
+
const along = (mass * r * r) / 2;
|
|
300
|
+
const across = (mass * (3 * r * r + length * length)) / 12;
|
|
301
|
+
if (length === 0) {
|
|
302
|
+
out.ixx = along;
|
|
303
|
+
out.iyy = along;
|
|
304
|
+
out.izz = along;
|
|
305
|
+
return out;
|
|
306
|
+
}
|
|
307
|
+
const segment = Math.sqrt(ax * ax + ay * ay + az * az) || 1;
|
|
308
|
+
const ux = ax / segment;
|
|
309
|
+
const uy = ay / segment;
|
|
310
|
+
const uz = az / segment;
|
|
311
|
+
const d = along - across;
|
|
312
|
+
out.ixx = across + d * ux * ux;
|
|
313
|
+
out.iyy = across + d * uy * uy;
|
|
314
|
+
out.izz = across + d * uz * uz;
|
|
315
|
+
out.ixy = d * ux * uy;
|
|
316
|
+
out.ixz = d * ux * uz;
|
|
317
|
+
out.iyz = d * uy * uz;
|
|
318
|
+
return out;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* A shape with no faces is a ball or a capsule: all of its volume is the rounding radius.
|
|
323
|
+
*
|
|
324
|
+
* Which one is decided by how many points survived `hullShape`'s dedupe, and that is the whole
|
|
325
|
+
* distinction: one point is a sphere, two are a capsule along the segment between them.
|
|
326
|
+
*
|
|
327
|
+
* `Math.PI` is a constant rather than a call, so the determinism gate has nothing to say about it.
|
|
328
|
+
*/
|
|
329
|
+
function ballProperties(shape: ConvexShape, density: number, out: MassProperties): MassProperties {
|
|
330
|
+
const count = shape.vertices.length / 3;
|
|
331
|
+
let cx = 0;
|
|
332
|
+
let cy = 0;
|
|
333
|
+
let cz = 0;
|
|
334
|
+
for (let i = 0; i < count; i++) {
|
|
335
|
+
cx += shape.vertices[i * 3] ?? 0;
|
|
336
|
+
cy += shape.vertices[i * 3 + 1] ?? 0;
|
|
337
|
+
cz += shape.vertices[i * 3 + 2] ?? 0;
|
|
338
|
+
}
|
|
339
|
+
const n = count === 0 ? 1 : count;
|
|
340
|
+
out.comX = cx / n;
|
|
341
|
+
out.comY = cy / n;
|
|
342
|
+
out.comZ = cz / n;
|
|
343
|
+
out.ixy = 0;
|
|
344
|
+
out.ixz = 0;
|
|
345
|
+
out.iyz = 0;
|
|
346
|
+
|
|
347
|
+
const r = shape.radius;
|
|
348
|
+
if (count !== 2) {
|
|
349
|
+
const volume = (4 / 3) * Math.PI * r * r * r;
|
|
350
|
+
const inertia = 0.4 * volume * density * r * r;
|
|
351
|
+
out.volume = volume;
|
|
352
|
+
out.ixx = inertia;
|
|
353
|
+
out.iyy = inertia;
|
|
354
|
+
out.izz = inertia;
|
|
355
|
+
return out;
|
|
356
|
+
}
|
|
357
|
+
return capsuleProperties(shape, density, out);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* A capsule: a cylinder of length 2h capped by two hemispheres of radius r.
|
|
362
|
+
*
|
|
363
|
+
* **This replaces the sphere tensor the previous version returned for any faceless shape**, which
|
|
364
|
+
* was recorded as wrong where it stood: a wrong inertia does not throw, it produces a body that
|
|
365
|
+
* tumbles unconvincingly, and that reads as a solver bug and gets diagnosed as one.
|
|
366
|
+
*
|
|
367
|
+
* The axis is the segment between the two points, and the tensor is built about it and then
|
|
368
|
+
* rotated into place. **What this gives up:** the arithmetic below assumes a capsule, so a
|
|
369
|
+
* two-point cloud with a radius small enough that the points are effectively a line segment is
|
|
370
|
+
* still treated as a capsule, which it is. **What would make it wrong** is a three-point flat
|
|
371
|
+
* cloud with a radius, which has no closed form and falls to the hull path above instead, where
|
|
372
|
+
* §4a's Minkowski caveat applies.
|
|
373
|
+
*/
|
|
374
|
+
function capsuleProperties(
|
|
375
|
+
shape: ConvexShape,
|
|
376
|
+
density: number,
|
|
377
|
+
out: MassProperties,
|
|
378
|
+
): MassProperties {
|
|
379
|
+
const r = shape.radius;
|
|
380
|
+
const ax = (shape.vertices[3] ?? 0) - (shape.vertices[0] ?? 0);
|
|
381
|
+
const ay = (shape.vertices[4] ?? 0) - (shape.vertices[1] ?? 0);
|
|
382
|
+
const az = (shape.vertices[5] ?? 0) - (shape.vertices[2] ?? 0);
|
|
383
|
+
const length = Math.sqrt(ax * ax + ay * ay + az * az);
|
|
384
|
+
const h = length / 2;
|
|
385
|
+
|
|
386
|
+
const cylinder = 2 * Math.PI * r * r * h * density;
|
|
387
|
+
const cap = ((2 * Math.PI * r * r * r) / 3) * density;
|
|
388
|
+
out.volume = (cylinder + 2 * cap) / density;
|
|
389
|
+
|
|
390
|
+
// About the capsule's own long axis.
|
|
391
|
+
const along = (cylinder * r * r) / 2 + 2 * cap * ((2 * r * r) / 5);
|
|
392
|
+
/*
|
|
393
|
+
* Perpendicular to it. The cylinder term is standard; each hemisphere contributes its own
|
|
394
|
+
* 2mr²/5 about its centre plus the parallel-axis shift to the capsule's centre, whose distance
|
|
395
|
+
* terms reduce to h² + 3hr/4 because a hemisphere's centre of mass sits 3r/8 from its flat face.
|
|
396
|
+
*/
|
|
397
|
+
const across =
|
|
398
|
+
(cylinder * (3 * r * r + 4 * h * h)) / 12 +
|
|
399
|
+
2 * cap * ((2 * r * r) / 5 + h * h + (3 * h * r) / 4);
|
|
400
|
+
|
|
401
|
+
if (length === 0) {
|
|
402
|
+
out.ixx = along;
|
|
403
|
+
out.iyy = along;
|
|
404
|
+
out.izz = along;
|
|
405
|
+
return out;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/*
|
|
409
|
+
* The tensor is `across` in every direction perpendicular to the axis and `along` down it, which
|
|
410
|
+
* is `across·I + (along − across)·(a ⊗ a)` for the unit axis `a`. Written out rather than looped
|
|
411
|
+
* because six terms unrolled read better than a 3×3 with two of nine entries unused.
|
|
412
|
+
*/
|
|
413
|
+
const ux = ax / length;
|
|
414
|
+
const uy = ay / length;
|
|
415
|
+
const uz = az / length;
|
|
416
|
+
const d = along - across;
|
|
417
|
+
out.ixx = across + d * ux * ux;
|
|
418
|
+
out.iyy = across + d * uy * uy;
|
|
419
|
+
out.izz = across + d * uz * uz;
|
|
420
|
+
out.ixy = d * ux * uy;
|
|
421
|
+
out.ixz = d * ux * uz;
|
|
422
|
+
out.iyz = d * uy * uz;
|
|
423
|
+
return out;
|
|
424
|
+
}
|