@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/solver.ts
ADDED
|
@@ -0,0 +1,873 @@
|
|
|
1
|
+
import { BODY_DYNAMIC } from './bodies.ts';
|
|
2
|
+
import type { BodySet } from './bodies.ts';
|
|
3
|
+
import type { Manifold } from './manifold.ts';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Contact constraints, and the substepped soft solve over them.
|
|
7
|
+
*
|
|
8
|
+
* **Soft rather than Baumgarte, and the difference is a property rather than a preference.** Every
|
|
9
|
+
* constraint carries a *hertz* and a damping ratio, from which the bias rate, mass scale and
|
|
10
|
+
* impulse scale are derived using the substep length. Stating stiffness as a frequency is what
|
|
11
|
+
* makes the substep count a quality dial: turn it up and nothing moves differently. A Baumgarte
|
|
12
|
+
* bias is a fraction of penetration per step, so it means something different at every step length,
|
|
13
|
+
* and the mitigations for that are all tuning constants — which `AGENTS.md` says never to assert.
|
|
14
|
+
*
|
|
15
|
+
* **Nothing here recomputes a manifold.** Anchors are stored in body-local frame at prepare time
|
|
16
|
+
* along with a separation offset, so the current separation is exact at any point inside the tick
|
|
17
|
+
* without re-running narrowphase per substep. `s = sBase + dot((pB + rB') − (pA + rA'), n)` is that
|
|
18
|
+
* identity, and it holds because `sBase` is defined to make it hold at prepare.
|
|
19
|
+
*
|
|
20
|
+
* **Every entry point takes an order array rather than a contiguous range.** Islands are disjoint
|
|
21
|
+
* by construction, so they can be solved in any order or simultaneously — but a constraint list
|
|
22
|
+
* built in pair order is not grouped by island. Indirecting through an order array groups them
|
|
23
|
+
* without permuting eleven parallel arrays, and it is what lets one `solveIsland` serve both
|
|
24
|
+
* executors.
|
|
25
|
+
*
|
|
26
|
+
* **Friction is solved before the normal**, against the normal impulse accumulated so far. Solving
|
|
27
|
+
* it after means the first iteration has no normal impulse to bound against and applies none, so a
|
|
28
|
+
* resting body slides for one iteration every substep.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/** Contacts start with this much slack allowed before the bias fights them, in metres. */
|
|
32
|
+
export const LINEAR_SLOP = 0.005;
|
|
33
|
+
/** How fast the bias may push overlap out, in metres per second. */
|
|
34
|
+
export const MAX_BIAS_VELOCITY = 4;
|
|
35
|
+
/** Below this approach speed a bounce is not worth having, and jitters if it is applied. */
|
|
36
|
+
export const RESTITUTION_THRESHOLD = 1;
|
|
37
|
+
|
|
38
|
+
export class ContactConstraints {
|
|
39
|
+
/** One entry per manifold. */
|
|
40
|
+
count = 0;
|
|
41
|
+
bodyA: Int32Array;
|
|
42
|
+
bodyB: Int32Array;
|
|
43
|
+
nx: Float32Array;
|
|
44
|
+
ny: Float32Array;
|
|
45
|
+
nz: Float32Array;
|
|
46
|
+
t1x: Float32Array;
|
|
47
|
+
t1y: Float32Array;
|
|
48
|
+
t1z: Float32Array;
|
|
49
|
+
t2x: Float32Array;
|
|
50
|
+
t2y: Float32Array;
|
|
51
|
+
t2z: Float32Array;
|
|
52
|
+
friction: Float32Array;
|
|
53
|
+
restitution: Float32Array;
|
|
54
|
+
pointStart: Int32Array;
|
|
55
|
+
pointCount: Int32Array;
|
|
56
|
+
|
|
57
|
+
/** One entry per contact point. */
|
|
58
|
+
points = 0;
|
|
59
|
+
localA: Float32Array;
|
|
60
|
+
localB: Float32Array;
|
|
61
|
+
sBase: Float32Array;
|
|
62
|
+
normalMass: Float32Array;
|
|
63
|
+
tangent1Mass: Float32Array;
|
|
64
|
+
tangent2Mass: Float32Array;
|
|
65
|
+
normalImpulse: Float32Array;
|
|
66
|
+
tangent1Impulse: Float32Array;
|
|
67
|
+
tangent2Impulse: Float32Array;
|
|
68
|
+
/** Approach speed when the constraint was prepared, which is what restitution answers to. */
|
|
69
|
+
approach: Float32Array;
|
|
70
|
+
featureId: Int32Array;
|
|
71
|
+
/**
|
|
72
|
+
* Whether each side's anchor sits on a curved surface, one byte per point.
|
|
73
|
+
*
|
|
74
|
+
* A curved anchor is held in world frame rather than in the body's, for the reason `Manifold`
|
|
75
|
+
* states at length: the point where a ball touches the floor does not turn with the ball, and an
|
|
76
|
+
* anchor that turns anyway climbs its side and reports a gap that is not there. Per point rather
|
|
77
|
+
* than per manifold so `worldAnchors` needs only the point index, which is all its callers have.
|
|
78
|
+
*/
|
|
79
|
+
curvedA: Uint8Array;
|
|
80
|
+
curvedB: Uint8Array;
|
|
81
|
+
|
|
82
|
+
private cap: number;
|
|
83
|
+
private pcap: number;
|
|
84
|
+
|
|
85
|
+
constructor(capacity = 128) {
|
|
86
|
+
this.cap = capacity;
|
|
87
|
+
this.pcap = capacity * 4;
|
|
88
|
+
this.bodyA = new Int32Array(this.cap);
|
|
89
|
+
this.bodyB = new Int32Array(this.cap);
|
|
90
|
+
this.nx = new Float32Array(this.cap);
|
|
91
|
+
this.ny = new Float32Array(this.cap);
|
|
92
|
+
this.nz = new Float32Array(this.cap);
|
|
93
|
+
this.t1x = new Float32Array(this.cap);
|
|
94
|
+
this.t1y = new Float32Array(this.cap);
|
|
95
|
+
this.t1z = new Float32Array(this.cap);
|
|
96
|
+
this.t2x = new Float32Array(this.cap);
|
|
97
|
+
this.t2y = new Float32Array(this.cap);
|
|
98
|
+
this.t2z = new Float32Array(this.cap);
|
|
99
|
+
this.friction = new Float32Array(this.cap);
|
|
100
|
+
this.restitution = new Float32Array(this.cap);
|
|
101
|
+
this.pointStart = new Int32Array(this.cap);
|
|
102
|
+
this.pointCount = new Int32Array(this.cap);
|
|
103
|
+
this.localA = new Float32Array(this.pcap * 3);
|
|
104
|
+
this.localB = new Float32Array(this.pcap * 3);
|
|
105
|
+
this.sBase = new Float32Array(this.pcap);
|
|
106
|
+
this.normalMass = new Float32Array(this.pcap);
|
|
107
|
+
this.tangent1Mass = new Float32Array(this.pcap);
|
|
108
|
+
this.tangent2Mass = new Float32Array(this.pcap);
|
|
109
|
+
this.normalImpulse = new Float32Array(this.pcap);
|
|
110
|
+
this.tangent1Impulse = new Float32Array(this.pcap);
|
|
111
|
+
this.tangent2Impulse = new Float32Array(this.pcap);
|
|
112
|
+
this.approach = new Float32Array(this.pcap);
|
|
113
|
+
this.featureId = new Int32Array(this.pcap);
|
|
114
|
+
this.curvedA = new Uint8Array(this.pcap);
|
|
115
|
+
this.curvedB = new Uint8Array(this.pcap);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
clear(): void {
|
|
119
|
+
this.count = 0;
|
|
120
|
+
this.points = 0;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
reserve(pointsNeeded: number): void {
|
|
124
|
+
if (this.count === this.cap) this.growConstraints();
|
|
125
|
+
while (this.points + pointsNeeded > this.pcap) this.growPoints();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private growConstraints(): void {
|
|
129
|
+
this.cap *= 2;
|
|
130
|
+
const g = <T extends Int32Array | Float32Array>(a: T): T => {
|
|
131
|
+
const out = new (a.constructor as new (n: number) => T)(this.cap);
|
|
132
|
+
out.set(a);
|
|
133
|
+
return out;
|
|
134
|
+
};
|
|
135
|
+
this.bodyA = g(this.bodyA);
|
|
136
|
+
this.bodyB = g(this.bodyB);
|
|
137
|
+
this.nx = g(this.nx);
|
|
138
|
+
this.ny = g(this.ny);
|
|
139
|
+
this.nz = g(this.nz);
|
|
140
|
+
this.t1x = g(this.t1x);
|
|
141
|
+
this.t1y = g(this.t1y);
|
|
142
|
+
this.t1z = g(this.t1z);
|
|
143
|
+
this.t2x = g(this.t2x);
|
|
144
|
+
this.t2y = g(this.t2y);
|
|
145
|
+
this.t2z = g(this.t2z);
|
|
146
|
+
this.friction = g(this.friction);
|
|
147
|
+
this.restitution = g(this.restitution);
|
|
148
|
+
this.pointStart = g(this.pointStart);
|
|
149
|
+
this.pointCount = g(this.pointCount);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private growPoints(): void {
|
|
153
|
+
this.pcap *= 2;
|
|
154
|
+
const g = <T extends Int32Array | Float32Array | Uint8Array>(a: T, stride = 1): T => {
|
|
155
|
+
const out = new (a.constructor as new (n: number) => T)(this.pcap * stride);
|
|
156
|
+
out.set(a);
|
|
157
|
+
return out;
|
|
158
|
+
};
|
|
159
|
+
this.localA = g(this.localA, 3);
|
|
160
|
+
this.localB = g(this.localB, 3);
|
|
161
|
+
this.sBase = g(this.sBase);
|
|
162
|
+
this.normalMass = g(this.normalMass);
|
|
163
|
+
this.tangent1Mass = g(this.tangent1Mass);
|
|
164
|
+
this.tangent2Mass = g(this.tangent2Mass);
|
|
165
|
+
this.normalImpulse = g(this.normalImpulse);
|
|
166
|
+
this.tangent1Impulse = g(this.tangent1Impulse);
|
|
167
|
+
this.tangent2Impulse = g(this.tangent2Impulse);
|
|
168
|
+
this.approach = g(this.approach);
|
|
169
|
+
this.featureId = g(this.featureId);
|
|
170
|
+
this.curvedA = g(this.curvedA);
|
|
171
|
+
this.curvedB = g(this.curvedB);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const invIA = new Float32Array(6);
|
|
176
|
+
const invIB = new Float32Array(6);
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Turn one manifold into a constraint, with its effective masses and its local anchors.
|
|
180
|
+
*
|
|
181
|
+
* `warm` supplies the previous tick's impulses by feature id, or −1 where there is none. Warm
|
|
182
|
+
* starting is what lets a stack converge in a handful of iterations rather than a hundred.
|
|
183
|
+
*/
|
|
184
|
+
export function prepareContact(
|
|
185
|
+
bodies: BodySet,
|
|
186
|
+
a: number,
|
|
187
|
+
b: number,
|
|
188
|
+
m: Manifold,
|
|
189
|
+
c: ContactConstraints,
|
|
190
|
+
warmNormal: Float32Array | null,
|
|
191
|
+
warmT1: Float32Array | null,
|
|
192
|
+
warmT2: Float32Array | null,
|
|
193
|
+
): void {
|
|
194
|
+
c.reserve(m.count);
|
|
195
|
+
const k = c.count++;
|
|
196
|
+
c.bodyA[k] = a;
|
|
197
|
+
c.bodyB[k] = b;
|
|
198
|
+
c.nx[k] = m.nx;
|
|
199
|
+
c.ny[k] = m.ny;
|
|
200
|
+
c.nz[k] = m.nz;
|
|
201
|
+
// An orthonormal basis around the normal, built from comparisons and one square root.
|
|
202
|
+
let t1x: number;
|
|
203
|
+
let t1y: number;
|
|
204
|
+
let t1z: number;
|
|
205
|
+
if (Math.abs(m.nx) >= 0.57735) {
|
|
206
|
+
t1x = m.ny;
|
|
207
|
+
t1y = -m.nx;
|
|
208
|
+
t1z = 0;
|
|
209
|
+
} else {
|
|
210
|
+
t1x = 0;
|
|
211
|
+
t1y = m.nz;
|
|
212
|
+
t1z = -m.ny;
|
|
213
|
+
}
|
|
214
|
+
const tl = Math.sqrt(t1x * t1x + t1y * t1y + t1z * t1z) || 1;
|
|
215
|
+
t1x /= tl;
|
|
216
|
+
t1y /= tl;
|
|
217
|
+
t1z /= tl;
|
|
218
|
+
c.t1x[k] = t1x;
|
|
219
|
+
c.t1y[k] = t1y;
|
|
220
|
+
c.t1z[k] = t1z;
|
|
221
|
+
c.t2x[k] = m.ny * t1z - m.nz * t1y;
|
|
222
|
+
c.t2y[k] = m.nz * t1x - m.nx * t1z;
|
|
223
|
+
c.t2z[k] = m.nx * t1y - m.ny * t1x;
|
|
224
|
+
|
|
225
|
+
// A pair's friction and bounce are the geometric and the larger mean of the two bodies'.
|
|
226
|
+
c.friction[k] = Math.sqrt((bodies.friction[a] ?? 0) * (bodies.friction[b] ?? 0));
|
|
227
|
+
const ra = bodies.restitution[a] ?? 0;
|
|
228
|
+
const rb = bodies.restitution[b] ?? 0;
|
|
229
|
+
c.restitution[k] = ra > rb ? ra : rb;
|
|
230
|
+
|
|
231
|
+
bodies.worldInverseInertia(a, invIA);
|
|
232
|
+
bodies.worldInverseInertia(b, invIB);
|
|
233
|
+
const imA = bodies.invMass[a] ?? 0;
|
|
234
|
+
const imB = bodies.invMass[b] ?? 0;
|
|
235
|
+
|
|
236
|
+
c.pointStart[k] = c.points;
|
|
237
|
+
c.pointCount[k] = m.count;
|
|
238
|
+
for (let i = 0; i < m.count; i++) {
|
|
239
|
+
const p = c.points++;
|
|
240
|
+
const px = m.points[i * 3] ?? 0;
|
|
241
|
+
const py = m.points[i * 3 + 1] ?? 0;
|
|
242
|
+
const pz = m.points[i * 3 + 2] ?? 0;
|
|
243
|
+
const rax = px - (bodies.posX[a] ?? 0);
|
|
244
|
+
const ray = py - (bodies.posY[a] ?? 0);
|
|
245
|
+
const raz = pz - (bodies.posZ[a] ?? 0);
|
|
246
|
+
const rbx = px - (bodies.posX[b] ?? 0);
|
|
247
|
+
const rby = py - (bodies.posY[b] ?? 0);
|
|
248
|
+
const rbz = pz - (bodies.posZ[b] ?? 0);
|
|
249
|
+
/*
|
|
250
|
+
* A flat anchor is stored in its body's frame and turned forward with it; a curved one is
|
|
251
|
+
* stored in world frame and left alone. Both are read back by `worldAnchors`, which reads the
|
|
252
|
+
* same flag — so the pair of them is one decision written in two places and nowhere else.
|
|
253
|
+
*/
|
|
254
|
+
c.curvedA[p] = m.curvedA ? 1 : 0;
|
|
255
|
+
c.curvedB[p] = m.curvedB ? 1 : 0;
|
|
256
|
+
if (m.curvedA) {
|
|
257
|
+
c.localA[p * 3] = rax;
|
|
258
|
+
c.localA[p * 3 + 1] = ray;
|
|
259
|
+
c.localA[p * 3 + 2] = raz;
|
|
260
|
+
} else {
|
|
261
|
+
unrotate(bodies, a, rax, ray, raz, c.localA, p * 3);
|
|
262
|
+
}
|
|
263
|
+
if (m.curvedB) {
|
|
264
|
+
c.localB[p * 3] = rbx;
|
|
265
|
+
c.localB[p * 3 + 1] = rby;
|
|
266
|
+
c.localB[p * 3 + 2] = rbz;
|
|
267
|
+
} else {
|
|
268
|
+
unrotate(bodies, b, rbx, rby, rbz, c.localB, p * 3);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/*
|
|
272
|
+
* The offset that makes `s = sBase + dot((pB + rB') − (pA + rA'), n)` reproduce this tick's
|
|
273
|
+
* separation exactly, and every later substep's without a second narrowphase.
|
|
274
|
+
*/
|
|
275
|
+
const nx = m.nx;
|
|
276
|
+
const ny = m.ny;
|
|
277
|
+
const nz = m.nz;
|
|
278
|
+
const now =
|
|
279
|
+
((bodies.posX[b] ?? 0) + rbx - (bodies.posX[a] ?? 0) - rax) * nx +
|
|
280
|
+
((bodies.posY[b] ?? 0) + rby - (bodies.posY[a] ?? 0) - ray) * ny +
|
|
281
|
+
((bodies.posZ[b] ?? 0) + rbz - (bodies.posZ[a] ?? 0) - raz) * nz;
|
|
282
|
+
c.sBase[p] = (m.separations[i] ?? 0) - now;
|
|
283
|
+
|
|
284
|
+
c.normalMass[p] = effectiveMass(imA, imB, rax, ray, raz, rbx, rby, rbz, nx, ny, nz);
|
|
285
|
+
c.tangent1Mass[p] = effectiveMass(imA, imB, rax, ray, raz, rbx, rby, rbz, t1x, t1y, t1z);
|
|
286
|
+
c.tangent2Mass[p] = effectiveMass(
|
|
287
|
+
imA,
|
|
288
|
+
imB,
|
|
289
|
+
rax,
|
|
290
|
+
ray,
|
|
291
|
+
raz,
|
|
292
|
+
rbx,
|
|
293
|
+
rby,
|
|
294
|
+
rbz,
|
|
295
|
+
c.t2x[k] ?? 0,
|
|
296
|
+
c.t2y[k] ?? 0,
|
|
297
|
+
c.t2z[k] ?? 0,
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
relativeVelocity(bodies, a, b, rax, ray, raz, rbx, rby, rbz);
|
|
301
|
+
c.approach[p] = REL[0] * nx + REL[1] * ny + REL[2] * nz;
|
|
302
|
+
c.featureId[p] = m.featureIds[i] ?? 0;
|
|
303
|
+
c.normalImpulse[p] = warmNormal ? (warmNormal[i] ?? 0) : 0;
|
|
304
|
+
c.tangent1Impulse[p] = warmT1 ? (warmT1[i] ?? 0) : 0;
|
|
305
|
+
c.tangent2Impulse[p] = warmT2 ? (warmT2[i] ?? 0) : 0;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** Apply the impulses carried over from the previous tick, before the first iteration. */
|
|
310
|
+
export function warmStart(
|
|
311
|
+
bodies: BodySet,
|
|
312
|
+
c: ContactConstraints,
|
|
313
|
+
order: Int32Array,
|
|
314
|
+
from: number,
|
|
315
|
+
to: number,
|
|
316
|
+
): void {
|
|
317
|
+
for (let oi = from; oi < to; oi++) {
|
|
318
|
+
const k = order[oi] ?? 0;
|
|
319
|
+
const a = c.bodyA[k] ?? 0;
|
|
320
|
+
const b = c.bodyB[k] ?? 0;
|
|
321
|
+
bodies.worldInverseInertia(a, invIA);
|
|
322
|
+
bodies.worldInverseInertia(b, invIB);
|
|
323
|
+
const start = c.pointStart[k] ?? 0;
|
|
324
|
+
const end = start + (c.pointCount[k] ?? 0);
|
|
325
|
+
for (let p = start; p < end; p++) {
|
|
326
|
+
worldAnchors(bodies, a, b, c, p);
|
|
327
|
+
const px =
|
|
328
|
+
(c.nx[k] ?? 0) * (c.normalImpulse[p] ?? 0) +
|
|
329
|
+
(c.t1x[k] ?? 0) * (c.tangent1Impulse[p] ?? 0) +
|
|
330
|
+
(c.t2x[k] ?? 0) * (c.tangent2Impulse[p] ?? 0);
|
|
331
|
+
const py =
|
|
332
|
+
(c.ny[k] ?? 0) * (c.normalImpulse[p] ?? 0) +
|
|
333
|
+
(c.t1y[k] ?? 0) * (c.tangent1Impulse[p] ?? 0) +
|
|
334
|
+
(c.t2y[k] ?? 0) * (c.tangent2Impulse[p] ?? 0);
|
|
335
|
+
const pz =
|
|
336
|
+
(c.nz[k] ?? 0) * (c.normalImpulse[p] ?? 0) +
|
|
337
|
+
(c.t1z[k] ?? 0) * (c.tangent1Impulse[p] ?? 0) +
|
|
338
|
+
(c.t2z[k] ?? 0) * (c.tangent2Impulse[p] ?? 0);
|
|
339
|
+
applyImpulse(bodies, a, b, ANCHOR, px, py, pz);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* One iteration over a range of constraints.
|
|
346
|
+
*
|
|
347
|
+
* `useBias` is false on the relax pass, which is what removes the energy the bias added and is why
|
|
348
|
+
* a stack stops breathing.
|
|
349
|
+
*/
|
|
350
|
+
/**
|
|
351
|
+
* How the two tangent rows are bounded against each other.
|
|
352
|
+
*
|
|
353
|
+
* **`box` is two independent clamps and is what this solver has always done.** Each axis is bounded
|
|
354
|
+
* at `mu * N` on its own, so along the diagonal between them the pair reaches `sqrt(2) * mu * N` —
|
|
355
|
+
* forty-one per cent more friction than the same surface offers along an axis. The tangent basis is
|
|
356
|
+
* constructed from the contact normal, so *which* headings are cheap is an artefact of that
|
|
357
|
+
* construction rather than a fact about the world, and a box measurably slides further in some
|
|
358
|
+
* directions than others.
|
|
359
|
+
*
|
|
360
|
+
* **`elliptical` couples them and friction becomes isotropic**, which is what it physically is. It
|
|
361
|
+
* is a correctness fix and it is **not the default**, because every stacking result moves under it:
|
|
362
|
+
* `fingerprintBodies`, both baselines, and any golden fingerprint or stored replay a consumer has
|
|
363
|
+
* kept. A production engine does not silently invalidate those. *What would reverse
|
|
364
|
+
* that:* a major version, where the honest thing is to make the ellipse the default and say so in
|
|
365
|
+
* the porting guide.
|
|
366
|
+
*
|
|
367
|
+
* **One coefficient, not two, and that is a decision rather than an omission.** Anisotropic
|
|
368
|
+
* friction is the natural API for an ellipse — different `mu` along each tangent — and it needs a
|
|
369
|
+
* tangent frame anchored to a *material*, which this solver does not have: `prepareContact` builds
|
|
370
|
+
* the tangents from the normal, so they rotate arbitrarily as a contact moves and a per-axis
|
|
371
|
+
* coefficient would mean something different from one tick to the next. That is a silent wrong
|
|
372
|
+
* answer, so the ellipse here is a circle. *What would reverse it:* a tangent frame carried by the
|
|
373
|
+
* body, which is a larger change than the cone; `vehicle.ts` already needs that and answers it its
|
|
374
|
+
* own way, with a grip table rather than a coefficient.
|
|
375
|
+
*/
|
|
376
|
+
export type FrictionModel = 'box' | 'elliptical';
|
|
377
|
+
|
|
378
|
+
export function solveContacts(
|
|
379
|
+
bodies: BodySet,
|
|
380
|
+
c: ContactConstraints,
|
|
381
|
+
order: Int32Array,
|
|
382
|
+
from: number,
|
|
383
|
+
to: number,
|
|
384
|
+
h: number,
|
|
385
|
+
useBias: boolean,
|
|
386
|
+
hertz: number,
|
|
387
|
+
damping: number,
|
|
388
|
+
/* Defaulted so a consumer calling this directly — it is exported — keeps its old numbers. */
|
|
389
|
+
friction: FrictionModel = 'box',
|
|
390
|
+
): void {
|
|
391
|
+
// The soft-constraint triple, from a frequency rather than from a per-step fraction.
|
|
392
|
+
const omega = 2 * Math.PI * hertz;
|
|
393
|
+
const a1 = 2 * damping + h * omega;
|
|
394
|
+
const a2 = h * omega * a1;
|
|
395
|
+
const a3 = 1 / (1 + a2);
|
|
396
|
+
const biasRate = omega / a1;
|
|
397
|
+
const massScale = a2 * a3;
|
|
398
|
+
const impulseScale = a3;
|
|
399
|
+
const invH = h > 0 ? 1 / h : 0;
|
|
400
|
+
|
|
401
|
+
for (let oi = from; oi < to; oi++) {
|
|
402
|
+
const k = order[oi] ?? 0;
|
|
403
|
+
const a = c.bodyA[k] ?? 0;
|
|
404
|
+
const b = c.bodyB[k] ?? 0;
|
|
405
|
+
bodies.worldInverseInertia(a, invIA);
|
|
406
|
+
bodies.worldInverseInertia(b, invIB);
|
|
407
|
+
const nx = c.nx[k] ?? 0;
|
|
408
|
+
const ny = c.ny[k] ?? 0;
|
|
409
|
+
const nz = c.nz[k] ?? 0;
|
|
410
|
+
const mu = c.friction[k] ?? 0;
|
|
411
|
+
const start = c.pointStart[k] ?? 0;
|
|
412
|
+
const end = start + (c.pointCount[k] ?? 0);
|
|
413
|
+
|
|
414
|
+
/*
|
|
415
|
+
* Friction first, bounded by the normal impulse standing from the previous iteration. Solving
|
|
416
|
+
* it after the normal costs the first iteration of every substep its friction entirely, because
|
|
417
|
+
* there is no accumulated normal impulse to bound against yet.
|
|
418
|
+
*/
|
|
419
|
+
for (let p = start; p < end; p++) {
|
|
420
|
+
worldAnchors(bodies, a, b, c, p);
|
|
421
|
+
relativeVelocity(
|
|
422
|
+
bodies,
|
|
423
|
+
a,
|
|
424
|
+
b,
|
|
425
|
+
ANCHOR[0],
|
|
426
|
+
ANCHOR[1],
|
|
427
|
+
ANCHOR[2],
|
|
428
|
+
ANCHOR[3],
|
|
429
|
+
ANCHOR[4],
|
|
430
|
+
ANCHOR[5],
|
|
431
|
+
);
|
|
432
|
+
const maxF = mu * (c.normalImpulse[p] ?? 0);
|
|
433
|
+
if (friction === 'elliptical') {
|
|
434
|
+
solveFrictionCone(bodies, a, b, k, p, c, maxF);
|
|
435
|
+
} else {
|
|
436
|
+
solveTangent(
|
|
437
|
+
bodies,
|
|
438
|
+
a,
|
|
439
|
+
b,
|
|
440
|
+
p,
|
|
441
|
+
c.t1x[k] ?? 0,
|
|
442
|
+
c.t1y[k] ?? 0,
|
|
443
|
+
c.t1z[k] ?? 0,
|
|
444
|
+
c.tangent1Mass,
|
|
445
|
+
c.tangent1Impulse,
|
|
446
|
+
maxF,
|
|
447
|
+
);
|
|
448
|
+
relativeVelocity(
|
|
449
|
+
bodies,
|
|
450
|
+
a,
|
|
451
|
+
b,
|
|
452
|
+
ANCHOR[0],
|
|
453
|
+
ANCHOR[1],
|
|
454
|
+
ANCHOR[2],
|
|
455
|
+
ANCHOR[3],
|
|
456
|
+
ANCHOR[4],
|
|
457
|
+
ANCHOR[5],
|
|
458
|
+
);
|
|
459
|
+
solveTangent(
|
|
460
|
+
bodies,
|
|
461
|
+
a,
|
|
462
|
+
b,
|
|
463
|
+
p,
|
|
464
|
+
c.t2x[k] ?? 0,
|
|
465
|
+
c.t2y[k] ?? 0,
|
|
466
|
+
c.t2z[k] ?? 0,
|
|
467
|
+
c.tangent2Mass,
|
|
468
|
+
c.tangent2Impulse,
|
|
469
|
+
maxF,
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
for (let p = start; p < end; p++) {
|
|
475
|
+
worldAnchors(bodies, a, b, c, p);
|
|
476
|
+
const s =
|
|
477
|
+
(c.sBase[p] ?? 0) +
|
|
478
|
+
((bodies.posX[b] ?? 0) + ANCHOR[3] - (bodies.posX[a] ?? 0) - ANCHOR[0]) * nx +
|
|
479
|
+
((bodies.posY[b] ?? 0) + ANCHOR[4] - (bodies.posY[a] ?? 0) - ANCHOR[1]) * ny +
|
|
480
|
+
((bodies.posZ[b] ?? 0) + ANCHOR[5] - (bodies.posZ[a] ?? 0) - ANCHOR[2]) * nz;
|
|
481
|
+
|
|
482
|
+
relativeVelocity(
|
|
483
|
+
bodies,
|
|
484
|
+
a,
|
|
485
|
+
b,
|
|
486
|
+
ANCHOR[0],
|
|
487
|
+
ANCHOR[1],
|
|
488
|
+
ANCHOR[2],
|
|
489
|
+
ANCHOR[3],
|
|
490
|
+
ANCHOR[4],
|
|
491
|
+
ANCHOR[5],
|
|
492
|
+
);
|
|
493
|
+
const vn = REL[0] * nx + REL[1] * ny + REL[2] * nz;
|
|
494
|
+
|
|
495
|
+
let bias = 0;
|
|
496
|
+
let ms = 1;
|
|
497
|
+
let is = 0;
|
|
498
|
+
if (s > 0) {
|
|
499
|
+
// Speculative: close exactly the gap this substep and no more.
|
|
500
|
+
bias = s * invH;
|
|
501
|
+
} else if (useBias) {
|
|
502
|
+
const want = biasRate * (s + LINEAR_SLOP);
|
|
503
|
+
bias = want < -MAX_BIAS_VELOCITY ? -MAX_BIAS_VELOCITY : want > 0 ? 0 : want;
|
|
504
|
+
ms = massScale;
|
|
505
|
+
is = impulseScale;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const old = c.normalImpulse[p] ?? 0;
|
|
509
|
+
let impulse = -(c.normalMass[p] ?? 0) * ms * (vn + bias) - is * old;
|
|
510
|
+
const next = old + impulse > 0 ? old + impulse : 0;
|
|
511
|
+
impulse = next - old;
|
|
512
|
+
c.normalImpulse[p] = next;
|
|
513
|
+
applyImpulse(bodies, a, b, ANCHOR, nx * impulse, ny * impulse, nz * impulse);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* One pass after every substep, giving back the bounce the solve suppressed.
|
|
520
|
+
*
|
|
521
|
+
* Applied once against the approach speed captured at prepare, so a drop returns to the same height
|
|
522
|
+
* whatever the substep count. A bounce driven from the *current* velocity instead compounds with
|
|
523
|
+
* every substep and turns the coefficient into a function of the dial.
|
|
524
|
+
*/
|
|
525
|
+
export function applyRestitution(
|
|
526
|
+
bodies: BodySet,
|
|
527
|
+
c: ContactConstraints,
|
|
528
|
+
order: Int32Array,
|
|
529
|
+
from: number,
|
|
530
|
+
to: number,
|
|
531
|
+
): void {
|
|
532
|
+
for (let oi = from; oi < to; oi++) {
|
|
533
|
+
const k = order[oi] ?? 0;
|
|
534
|
+
const e = c.restitution[k] ?? 0;
|
|
535
|
+
if (e === 0) continue;
|
|
536
|
+
const a = c.bodyA[k] ?? 0;
|
|
537
|
+
const b = c.bodyB[k] ?? 0;
|
|
538
|
+
bodies.worldInverseInertia(a, invIA);
|
|
539
|
+
bodies.worldInverseInertia(b, invIB);
|
|
540
|
+
const nx = c.nx[k] ?? 0;
|
|
541
|
+
const ny = c.ny[k] ?? 0;
|
|
542
|
+
const nz = c.nz[k] ?? 0;
|
|
543
|
+
const start = c.pointStart[k] ?? 0;
|
|
544
|
+
const end = start + (c.pointCount[k] ?? 0);
|
|
545
|
+
for (let p = start; p < end; p++) {
|
|
546
|
+
const approach = c.approach[p] ?? 0;
|
|
547
|
+
if (approach > -RESTITUTION_THRESHOLD || (c.normalImpulse[p] ?? 0) === 0) continue;
|
|
548
|
+
worldAnchors(bodies, a, b, c, p);
|
|
549
|
+
relativeVelocity(
|
|
550
|
+
bodies,
|
|
551
|
+
a,
|
|
552
|
+
b,
|
|
553
|
+
ANCHOR[0],
|
|
554
|
+
ANCHOR[1],
|
|
555
|
+
ANCHOR[2],
|
|
556
|
+
ANCHOR[3],
|
|
557
|
+
ANCHOR[4],
|
|
558
|
+
ANCHOR[5],
|
|
559
|
+
);
|
|
560
|
+
const vn = REL[0] * nx + REL[1] * ny + REL[2] * nz;
|
|
561
|
+
const old = c.normalImpulse[p] ?? 0;
|
|
562
|
+
let impulse = -(c.normalMass[p] ?? 0) * (vn + e * approach);
|
|
563
|
+
const next = old + impulse > 0 ? old + impulse : 0;
|
|
564
|
+
impulse = next - old;
|
|
565
|
+
c.normalImpulse[p] = next;
|
|
566
|
+
applyImpulse(bodies, a, b, ANCHOR, nx * impulse, ny * impulse, nz * impulse);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/* ---------- shared scratch, so nothing allocates per contact ---------- */
|
|
572
|
+
|
|
573
|
+
const ANCHOR = new Float64Array(6);
|
|
574
|
+
const REL = new Float64Array(3);
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* One friction axis, solved against the anchors and relative velocity already in scratch.
|
|
578
|
+
*
|
|
579
|
+
* **It takes no `ContactConstraints`, and it used to.** The parameter was dead: everything this
|
|
580
|
+
* needs is either in the module-scope `REL` and `ANCHOR` that the caller filled, or in the two
|
|
581
|
+
* arrays it is handed. A parameter nothing reads is a claim about what a function depends on, and
|
|
582
|
+
* this one said it depended on the whole constraint block.
|
|
583
|
+
*
|
|
584
|
+
* Found by a consumer's typecheck rather than by this repository's: one of them runs
|
|
585
|
+
* `noUnusedParameters` over engine source where the engine's own config does not, on the grounds
|
|
586
|
+
* that the flag costs the engine nothing and catches a real class of mistake. This is it being
|
|
587
|
+
* right, and it arrived as a red CI in a repository that had changed nothing.
|
|
588
|
+
*/
|
|
589
|
+
function solveTangent(
|
|
590
|
+
bodies: BodySet,
|
|
591
|
+
a: number,
|
|
592
|
+
b: number,
|
|
593
|
+
p: number,
|
|
594
|
+
tx: number,
|
|
595
|
+
ty: number,
|
|
596
|
+
tz: number,
|
|
597
|
+
massArray: Float32Array,
|
|
598
|
+
impulseArray: Float32Array,
|
|
599
|
+
maxF: number,
|
|
600
|
+
): void {
|
|
601
|
+
const vt = REL[0] * tx + REL[1] * ty + REL[2] * tz;
|
|
602
|
+
const old = impulseArray[p] ?? 0;
|
|
603
|
+
let impulse = -(massArray[p] ?? 0) * vt;
|
|
604
|
+
let next = old + impulse;
|
|
605
|
+
if (next > maxF) next = maxF;
|
|
606
|
+
else if (next < -maxF) next = -maxF;
|
|
607
|
+
impulse = next - old;
|
|
608
|
+
impulseArray[p] = next;
|
|
609
|
+
applyImpulse(bodies, a, b, ANCHOR, tx * impulse, ty * impulse, tz * impulse);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Both friction axes at once, with the pair projected onto a disc of radius `maxF`.
|
|
614
|
+
*
|
|
615
|
+
* **Two differences from the pair of `solveTangent` calls it replaces, and only one of them is the
|
|
616
|
+
* clamp.** The clamp is the point: the accumulated `(P1, P2)` is scaled onto the circle rather than
|
|
617
|
+
* each component being clipped to a square, so the total friction impulse is `mu * N` whatever the
|
|
618
|
+
* heading. The other is that **both rows read one relative velocity** — the box path re-reads it
|
|
619
|
+
* between the two axes, which makes the second row see the first row's push, and there is no
|
|
620
|
+
* meaningful way to keep that while solving them jointly. That is why this is not a change a stored
|
|
621
|
+
* replay survives, and why it is behind an option.
|
|
622
|
+
*
|
|
623
|
+
* The impulse is applied once, as the sum of the two deltas, rather than twice: two applications of
|
|
624
|
+
* a jointly-solved pair would each disturb the other's velocity between them, which is the sequential
|
|
625
|
+
* behaviour this exists to leave behind.
|
|
626
|
+
*/
|
|
627
|
+
function solveFrictionCone(
|
|
628
|
+
bodies: BodySet,
|
|
629
|
+
a: number,
|
|
630
|
+
b: number,
|
|
631
|
+
k: number,
|
|
632
|
+
p: number,
|
|
633
|
+
c: ContactConstraints,
|
|
634
|
+
maxF: number,
|
|
635
|
+
): void {
|
|
636
|
+
const t1x = c.t1x[k] ?? 0;
|
|
637
|
+
const t1y = c.t1y[k] ?? 0;
|
|
638
|
+
const t1z = c.t1z[k] ?? 0;
|
|
639
|
+
const t2x = c.t2x[k] ?? 0;
|
|
640
|
+
const t2y = c.t2y[k] ?? 0;
|
|
641
|
+
const t2z = c.t2z[k] ?? 0;
|
|
642
|
+
|
|
643
|
+
const vt1 = REL[0] * t1x + REL[1] * t1y + REL[2] * t1z;
|
|
644
|
+
const vt2 = REL[0] * t2x + REL[1] * t2y + REL[2] * t2z;
|
|
645
|
+
const old1 = c.tangent1Impulse[p] ?? 0;
|
|
646
|
+
const old2 = c.tangent2Impulse[p] ?? 0;
|
|
647
|
+
|
|
648
|
+
let next1 = old1 - (c.tangent1Mass[p] ?? 0) * vt1;
|
|
649
|
+
let next2 = old2 - (c.tangent2Mass[p] ?? 0) * vt2;
|
|
650
|
+
|
|
651
|
+
const length = Math.sqrt(next1 * next1 + next2 * next2);
|
|
652
|
+
if (length > maxF) {
|
|
653
|
+
/* `length > maxF` implies `length > 0` for a non-negative bound, so the divide is safe: a zero
|
|
654
|
+
normal impulse gives `maxF = 0` and a zero length fails the compare. */
|
|
655
|
+
const scale = maxF / length;
|
|
656
|
+
next1 *= scale;
|
|
657
|
+
next2 *= scale;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
const delta1 = next1 - old1;
|
|
661
|
+
const delta2 = next2 - old2;
|
|
662
|
+
c.tangent1Impulse[p] = next1;
|
|
663
|
+
c.tangent2Impulse[p] = next2;
|
|
664
|
+
applyImpulse(
|
|
665
|
+
bodies,
|
|
666
|
+
a,
|
|
667
|
+
b,
|
|
668
|
+
ANCHOR,
|
|
669
|
+
t1x * delta1 + t2x * delta2,
|
|
670
|
+
t1y * delta1 + t2y * delta2,
|
|
671
|
+
t1z * delta1 + t2z * delta2,
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/** `1 / (mA + mB + (rA×n)ᵀ IA (rA×n) + (rB×n)ᵀ IB (rB×n))`, or zero where nothing can move. */
|
|
676
|
+
function effectiveMass(
|
|
677
|
+
imA: number,
|
|
678
|
+
imB: number,
|
|
679
|
+
rax: number,
|
|
680
|
+
ray: number,
|
|
681
|
+
raz: number,
|
|
682
|
+
rbx: number,
|
|
683
|
+
rby: number,
|
|
684
|
+
rbz: number,
|
|
685
|
+
nx: number,
|
|
686
|
+
ny: number,
|
|
687
|
+
nz: number,
|
|
688
|
+
): number {
|
|
689
|
+
const ax = ray * nz - raz * ny;
|
|
690
|
+
const ay = raz * nx - rax * nz;
|
|
691
|
+
const az = rax * ny - ray * nx;
|
|
692
|
+
const bx = rby * nz - rbz * ny;
|
|
693
|
+
const by = rbz * nx - rbx * nz;
|
|
694
|
+
const bz = rbx * ny - rby * nx;
|
|
695
|
+
const k = imA + imB + quadratic(invIA, ax, ay, az) + quadratic(invIB, bx, by, bz);
|
|
696
|
+
return k > 0 ? 1 / k : 0;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/** `vᵀ M v` for a symmetric M stored as six terms. */
|
|
700
|
+
function quadratic(m: Float32Array, x: number, y: number, z: number): number {
|
|
701
|
+
return (
|
|
702
|
+
(m[0] ?? 0) * x * x +
|
|
703
|
+
(m[1] ?? 0) * y * y +
|
|
704
|
+
(m[2] ?? 0) * z * z +
|
|
705
|
+
2 * ((m[3] ?? 0) * x * y + (m[4] ?? 0) * x * z + (m[5] ?? 0) * y * z)
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/** Rotate the stored local anchors into world, into `ANCHOR[0..5]`. */
|
|
710
|
+
function worldAnchors(
|
|
711
|
+
bodies: BodySet,
|
|
712
|
+
a: number,
|
|
713
|
+
b: number,
|
|
714
|
+
c: ContactConstraints,
|
|
715
|
+
p: number,
|
|
716
|
+
): void {
|
|
717
|
+
if (c.curvedA[p]) {
|
|
718
|
+
ANCHOR[0] = c.localA[p * 3] ?? 0;
|
|
719
|
+
ANCHOR[1] = c.localA[p * 3 + 1] ?? 0;
|
|
720
|
+
ANCHOR[2] = c.localA[p * 3 + 2] ?? 0;
|
|
721
|
+
} else {
|
|
722
|
+
rotate(
|
|
723
|
+
bodies,
|
|
724
|
+
a,
|
|
725
|
+
c.localA[p * 3] ?? 0,
|
|
726
|
+
c.localA[p * 3 + 1] ?? 0,
|
|
727
|
+
c.localA[p * 3 + 2] ?? 0,
|
|
728
|
+
ANCHOR,
|
|
729
|
+
0,
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
if (c.curvedB[p]) {
|
|
733
|
+
ANCHOR[3] = c.localB[p * 3] ?? 0;
|
|
734
|
+
ANCHOR[4] = c.localB[p * 3 + 1] ?? 0;
|
|
735
|
+
ANCHOR[5] = c.localB[p * 3 + 2] ?? 0;
|
|
736
|
+
} else {
|
|
737
|
+
rotate(
|
|
738
|
+
bodies,
|
|
739
|
+
b,
|
|
740
|
+
c.localB[p * 3] ?? 0,
|
|
741
|
+
c.localB[p * 3 + 1] ?? 0,
|
|
742
|
+
c.localB[p * 3 + 2] ?? 0,
|
|
743
|
+
ANCHOR,
|
|
744
|
+
3,
|
|
745
|
+
);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/** `(vB + wB × rB) − (vA + wA × rA)`, into `REL`. */
|
|
750
|
+
function relativeVelocity(
|
|
751
|
+
bodies: BodySet,
|
|
752
|
+
a: number,
|
|
753
|
+
b: number,
|
|
754
|
+
rax: number,
|
|
755
|
+
ray: number,
|
|
756
|
+
raz: number,
|
|
757
|
+
rbx: number,
|
|
758
|
+
rby: number,
|
|
759
|
+
rbz: number,
|
|
760
|
+
): void {
|
|
761
|
+
const wax = bodies.angX[a] ?? 0;
|
|
762
|
+
const way = bodies.angY[a] ?? 0;
|
|
763
|
+
const waz = bodies.angZ[a] ?? 0;
|
|
764
|
+
const wbx = bodies.angX[b] ?? 0;
|
|
765
|
+
const wby = bodies.angY[b] ?? 0;
|
|
766
|
+
const wbz = bodies.angZ[b] ?? 0;
|
|
767
|
+
REL[0] =
|
|
768
|
+
(bodies.velX[b] ?? 0) +
|
|
769
|
+
(wby * rbz - wbz * rby) -
|
|
770
|
+
(bodies.velX[a] ?? 0) -
|
|
771
|
+
(way * raz - waz * ray);
|
|
772
|
+
REL[1] =
|
|
773
|
+
(bodies.velY[b] ?? 0) +
|
|
774
|
+
(wbz * rbx - wbx * rbz) -
|
|
775
|
+
(bodies.velY[a] ?? 0) -
|
|
776
|
+
(waz * rax - wax * raz);
|
|
777
|
+
REL[2] =
|
|
778
|
+
(bodies.velZ[b] ?? 0) +
|
|
779
|
+
(wbx * rby - wby * rbx) -
|
|
780
|
+
(bodies.velZ[a] ?? 0) -
|
|
781
|
+
(wax * ray - way * rax);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/** Apply `+P` to B at `rB` and `−P` to A at `rA`. */
|
|
785
|
+
function applyImpulse(
|
|
786
|
+
bodies: BodySet,
|
|
787
|
+
a: number,
|
|
788
|
+
b: number,
|
|
789
|
+
anchors: Float64Array,
|
|
790
|
+
px: number,
|
|
791
|
+
py: number,
|
|
792
|
+
pz: number,
|
|
793
|
+
): void {
|
|
794
|
+
const imA = bodies.invMass[a] ?? 0;
|
|
795
|
+
if (imA > 0) {
|
|
796
|
+
bodies.velX[a] = (bodies.velX[a] ?? 0) - px * imA;
|
|
797
|
+
bodies.velY[a] = (bodies.velY[a] ?? 0) - py * imA;
|
|
798
|
+
bodies.velZ[a] = (bodies.velZ[a] ?? 0) - pz * imA;
|
|
799
|
+
const tx = anchors[1] * pz - anchors[2] * py;
|
|
800
|
+
const ty = anchors[2] * px - anchors[0] * pz;
|
|
801
|
+
const tz = anchors[0] * py - anchors[1] * px;
|
|
802
|
+
bodies.angX[a] = (bodies.angX[a] ?? 0) - multiply(invIA, tx, ty, tz, 0);
|
|
803
|
+
bodies.angY[a] = (bodies.angY[a] ?? 0) - multiply(invIA, tx, ty, tz, 1);
|
|
804
|
+
bodies.angZ[a] = (bodies.angZ[a] ?? 0) - multiply(invIA, tx, ty, tz, 2);
|
|
805
|
+
}
|
|
806
|
+
const imB = bodies.invMass[b] ?? 0;
|
|
807
|
+
if (imB > 0) {
|
|
808
|
+
bodies.velX[b] = (bodies.velX[b] ?? 0) + px * imB;
|
|
809
|
+
bodies.velY[b] = (bodies.velY[b] ?? 0) + py * imB;
|
|
810
|
+
bodies.velZ[b] = (bodies.velZ[b] ?? 0) + pz * imB;
|
|
811
|
+
const tx = anchors[4] * pz - anchors[5] * py;
|
|
812
|
+
const ty = anchors[5] * px - anchors[3] * pz;
|
|
813
|
+
const tz = anchors[3] * py - anchors[4] * px;
|
|
814
|
+
bodies.angX[b] = (bodies.angX[b] ?? 0) + multiply(invIB, tx, ty, tz, 0);
|
|
815
|
+
bodies.angY[b] = (bodies.angY[b] ?? 0) + multiply(invIB, tx, ty, tz, 1);
|
|
816
|
+
bodies.angZ[b] = (bodies.angZ[b] ?? 0) + multiply(invIB, tx, ty, tz, 2);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
/** Row `row` of `M v`, for a symmetric M stored as six terms. */
|
|
821
|
+
function multiply(m: Float32Array, x: number, y: number, z: number, row: number): number {
|
|
822
|
+
if (row === 0) return (m[0] ?? 0) * x + (m[3] ?? 0) * y + (m[4] ?? 0) * z;
|
|
823
|
+
if (row === 1) return (m[3] ?? 0) * x + (m[1] ?? 0) * y + (m[5] ?? 0) * z;
|
|
824
|
+
return (m[4] ?? 0) * x + (m[5] ?? 0) * y + (m[2] ?? 0) * z;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
function rotate(
|
|
828
|
+
bodies: BodySet,
|
|
829
|
+
i: number,
|
|
830
|
+
x: number,
|
|
831
|
+
y: number,
|
|
832
|
+
z: number,
|
|
833
|
+
out: Float64Array,
|
|
834
|
+
at: number,
|
|
835
|
+
): void {
|
|
836
|
+
const qx = bodies.rotX[i] ?? 0;
|
|
837
|
+
const qy = bodies.rotY[i] ?? 0;
|
|
838
|
+
const qz = bodies.rotZ[i] ?? 0;
|
|
839
|
+
const qw = bodies.rotW[i] ?? 1;
|
|
840
|
+
// v + 2w(q × v) + 2(q × (q × v)), the standard quaternion rotation without a matrix.
|
|
841
|
+
const tx = 2 * (qy * z - qz * y);
|
|
842
|
+
const ty = 2 * (qz * x - qx * z);
|
|
843
|
+
const tz = 2 * (qx * y - qy * x);
|
|
844
|
+
out[at] = x + qw * tx + (qy * tz - qz * ty);
|
|
845
|
+
out[at + 1] = y + qw * ty + (qz * tx - qx * tz);
|
|
846
|
+
out[at + 2] = z + qw * tz + (qx * ty - qy * tx);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function unrotate(
|
|
850
|
+
bodies: BodySet,
|
|
851
|
+
i: number,
|
|
852
|
+
x: number,
|
|
853
|
+
y: number,
|
|
854
|
+
z: number,
|
|
855
|
+
out: Float32Array,
|
|
856
|
+
at: number,
|
|
857
|
+
): void {
|
|
858
|
+
const qx = -(bodies.rotX[i] ?? 0);
|
|
859
|
+
const qy = -(bodies.rotY[i] ?? 0);
|
|
860
|
+
const qz = -(bodies.rotZ[i] ?? 0);
|
|
861
|
+
const qw = bodies.rotW[i] ?? 1;
|
|
862
|
+
const tx = 2 * (qy * z - qz * y);
|
|
863
|
+
const ty = 2 * (qz * x - qx * z);
|
|
864
|
+
const tz = 2 * (qx * y - qy * x);
|
|
865
|
+
out[at] = x + qw * tx + (qy * tz - qz * ty);
|
|
866
|
+
out[at + 1] = y + qw * ty + (qz * tx - qx * tz);
|
|
867
|
+
out[at + 2] = z + qw * tz + (qx * ty - qy * tx);
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
/** Whether a body is one the solver may move. */
|
|
871
|
+
export function isDynamicBody(bodies: BodySet, i: number): boolean {
|
|
872
|
+
return bodies.type[i] === BODY_DYNAMIC;
|
|
873
|
+
}
|