@driftengine/physics 4.8.1 → 4.8.2
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/dist/controller.js +22 -14
- package/dist/ragdoll.d.ts +9 -1
- package/dist/ragdoll.js +143 -37
- package/package.json +1 -1
- package/src/controller.ts +27 -12
- package/src/ragdoll.ts +163 -38
package/dist/controller.js
CHANGED
|
@@ -313,9 +313,10 @@ export class CharacterController {
|
|
|
313
313
|
* the same clamp and air control — only the plane is given up, not the feel.
|
|
314
314
|
*/
|
|
315
315
|
if (input.projectMove === false) {
|
|
316
|
-
|
|
317
|
-
this.
|
|
318
|
-
this.
|
|
316
|
+
const t = approachFraction(wantX - this.velX, wantY - this.velY, wantZ - this.velZ, step);
|
|
317
|
+
this.velX = t === 1 ? wantX : this.velX + (wantX - this.velX) * t;
|
|
318
|
+
this.velY = t === 1 ? wantY : this.velY + (wantY - this.velY) * t;
|
|
319
|
+
this.velZ = t === 1 ? wantZ : this.velZ + (wantZ - this.velZ) * t;
|
|
319
320
|
}
|
|
320
321
|
else {
|
|
321
322
|
const alongUp = this.speedAlongUp;
|
|
@@ -326,9 +327,10 @@ export class CharacterController {
|
|
|
326
327
|
const wantTangentX = wantX - this.upX * wantIntoUp;
|
|
327
328
|
const wantTangentY = wantY - this.upY * wantIntoUp;
|
|
328
329
|
const wantTangentZ = wantZ - this.upZ * wantIntoUp;
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
330
|
+
const t = approachFraction(wantTangentX - tangentX, wantTangentY - tangentY, wantTangentZ - tangentZ, step);
|
|
331
|
+
tangentX = t === 1 ? wantTangentX : tangentX + (wantTangentX - tangentX) * t;
|
|
332
|
+
tangentY = t === 1 ? wantTangentY : tangentY + (wantTangentY - tangentY) * t;
|
|
333
|
+
tangentZ = t === 1 ? wantTangentZ : tangentZ + (wantTangentZ - tangentZ) * t;
|
|
332
334
|
this.velX = tangentX + this.upX * alongUp;
|
|
333
335
|
this.velY = tangentY + this.upY * alongUp;
|
|
334
336
|
this.velZ = tangentZ + this.upZ * alongUp;
|
|
@@ -675,12 +677,18 @@ export class CharacterController {
|
|
|
675
677
|
this.pose.qw = w * inverse;
|
|
676
678
|
}
|
|
677
679
|
}
|
|
678
|
-
/**
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
680
|
+
/**
|
|
681
|
+
* How much of the way to close a gap of `(dx, dy, dz)` this tick: exactly 1 when the gap is no
|
|
682
|
+
* longer than `step`, which the caller reads as "land on the target", and `step` of its length
|
|
683
|
+
* otherwise.
|
|
684
|
+
*
|
|
685
|
+
* **Toward the wanted velocity as a vector, by at most `step` in length.** It was one world axis at
|
|
686
|
+
* a time, each by the whole step, so a diagonal gained and lost speed √2 times as fast as a straight
|
|
687
|
+
* run while the clamp on the wanted vector kept its top speed honest — the faster diagonal lived in
|
|
688
|
+
* the acceleration. What it gives up is that an axis no longer reaches its target on its own
|
|
689
|
+
* schedule: turning from one direction to another now curves rather than squaring the corner.
|
|
690
|
+
*/
|
|
691
|
+
function approachFraction(dx, dy, dz, step) {
|
|
692
|
+
const gap = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
693
|
+
return gap <= step ? 1 : step / gap;
|
|
686
694
|
}
|
package/dist/ragdoll.d.ts
CHANGED
|
@@ -88,7 +88,15 @@ export interface Ragdoll {
|
|
|
88
88
|
readonly bodyOf: Int32Array;
|
|
89
89
|
/** How many bones were built. */
|
|
90
90
|
readonly boneCount: number;
|
|
91
|
-
/**
|
|
91
|
+
/**
|
|
92
|
+
* Write the ragdoll's current shape into a pose.
|
|
93
|
+
*
|
|
94
|
+
* Each joint is turned by the bone that **starts** at it, which is the joint a skinned limb
|
|
95
|
+
* follows — the forearm's body turns the elbow — and a joint with no bone below it rides the one
|
|
96
|
+
* it hangs off. Translations are the rig's own, in each parent's frame. A root's rotation is
|
|
97
|
+
* written in the frame the character's node is placed in, at `rootX`, `rootY` and `rootZ`, with
|
|
98
|
+
* no rotation of its own; a joint no bone reaches is left as the pose had it.
|
|
99
|
+
*/
|
|
92
100
|
writePose(out: PoseTarget): void;
|
|
93
101
|
/** Steer the bodies toward a pose. Weight 1 tracks it, weight 0 goes limp. */
|
|
94
102
|
drive(world: PhysicsWorld, pose: PoseTarget, weight: number): void;
|
package/dist/ragdoll.js
CHANGED
|
@@ -45,8 +45,32 @@ export function ragdollFromBones(world, parents, worldMatrices, options = {}) {
|
|
|
45
45
|
return value === undefined || !Number.isFinite(value) ? fallback : value;
|
|
46
46
|
};
|
|
47
47
|
const bodyOf = new Int32Array(jointCount).fill(-1);
|
|
48
|
-
/**
|
|
48
|
+
/**
|
|
49
|
+
* `conj(bodyRotation) · jointWorldRotation` for the joint a bone **ends** at, so a joint with no
|
|
50
|
+
* bone of its own below it — a fingertip, a head's tip — can ride the bone it hangs off.
|
|
51
|
+
*/
|
|
49
52
|
const bodyToJoint = new Float32Array(jointCount * 4);
|
|
53
|
+
/**
|
|
54
|
+
* `conj(bodyRotation) · jointWorldRotation` for the joint a bone **starts** at, indexed like
|
|
55
|
+
* `bodyOf` — by the bone's own joint — and naming its parent's rotation.
|
|
56
|
+
*
|
|
57
|
+
* **This is the joint a skinned limb follows.** A forearm's vertices are weighted to the elbow,
|
|
58
|
+
* so the elbow's rotation is what points the forearm, and the body standing along the forearm is
|
|
59
|
+
* what carries it. Reading each joint off the bone that *ends* at it — the upper arm for the
|
|
60
|
+
* elbow — drew every limb one bone late: a hand 0.47 m from its body in a doll synced and never
|
|
61
|
+
* stepped, reported from a game. A bone is rigid in its start joint's frame, so the relation
|
|
62
|
+
* captured at rest holds for as long as the doll does.
|
|
63
|
+
*/
|
|
64
|
+
const bodyToStart = new Float32Array(jointCount * 4);
|
|
65
|
+
/** The first bone starting at each joint, by that bone's own index, or −1 where none does. */
|
|
66
|
+
const startBone = new Int32Array(jointCount).fill(-1);
|
|
67
|
+
/**
|
|
68
|
+
* Each joint's translation in its parent's frame at rest, which is what a pose holds.
|
|
69
|
+
*
|
|
70
|
+
* **In the parent's frame, not the world's.** It was the world offset between the two, which is
|
|
71
|
+
* the same number only where the parent is unrotated at rest — true of every chain this module's
|
|
72
|
+
* tests were built from, and of no rig a game loads.
|
|
73
|
+
*/
|
|
50
74
|
const localRest = new Float32Array(jointCount * 3);
|
|
51
75
|
const restRotation = new Float32Array(jointCount * 4);
|
|
52
76
|
/**
|
|
@@ -73,9 +97,7 @@ export function ragdollFromBones(world, parents, worldMatrices, options = {}) {
|
|
|
73
97
|
const tx = worldMatrices[j * 16 + 12] ?? 0;
|
|
74
98
|
const ty = worldMatrices[j * 16 + 13] ?? 0;
|
|
75
99
|
const tz = worldMatrices[j * 16 + 14] ?? 0;
|
|
76
|
-
|
|
77
|
-
localRest[j * 3 + 1] = ty - hy;
|
|
78
|
-
localRest[j * 3 + 2] = tz - hz;
|
|
100
|
+
intoParentFrame(worldMatrices, p, tx - hx, ty - hy, tz - hz, localRest, j * 3);
|
|
79
101
|
const dx = tx - hx;
|
|
80
102
|
const dy = ty - hy;
|
|
81
103
|
const dz = tz - hz;
|
|
@@ -101,8 +123,14 @@ export function ragdollFromBones(world, parents, worldMatrices, options = {}) {
|
|
|
101
123
|
});
|
|
102
124
|
bodyOf[j] = body;
|
|
103
125
|
halfOf[j] = length / 2;
|
|
104
|
-
// What takes the body's rotation back to
|
|
126
|
+
// What takes the body's rotation back to each of its two joints', captured at rest. Parents
|
|
127
|
+
// come before children in a rig, so the start joint's rotation is already read.
|
|
105
128
|
multiplyConjugate(TMP, restRotation, j * 4, bodyToJoint, j * 4);
|
|
129
|
+
if (p >= j)
|
|
130
|
+
matrixRotation(worldMatrices, p, restRotation, p * 4);
|
|
131
|
+
multiplyConjugate(TMP, restRotation, p * 4, bodyToStart, j * 4);
|
|
132
|
+
if ((startBone[p] ?? -1) < 0)
|
|
133
|
+
startBone[p] = j;
|
|
106
134
|
boneCount++;
|
|
107
135
|
if (firstBone < 0) {
|
|
108
136
|
firstBone = j;
|
|
@@ -210,28 +238,52 @@ export function ragdollFromBones(world, parents, worldMatrices, options = {}) {
|
|
|
210
238
|
world.ignorePair(body, other);
|
|
211
239
|
}
|
|
212
240
|
}
|
|
241
|
+
/**
|
|
242
|
+
* One joint's world rotation, from the body that speaks for it: the first bone starting at the
|
|
243
|
+
* joint, or — for a joint with none below it, a fingertip — the bone it hangs off, which it then
|
|
244
|
+
* rides rigidly. False where neither exists and nothing can be said.
|
|
245
|
+
*/
|
|
246
|
+
const recoverJoint = (w, j, out, at) => {
|
|
247
|
+
const start = startBone[j] ?? -1;
|
|
248
|
+
if (start >= 0) {
|
|
249
|
+
readBodyRotation(w, bodyOf[start] ?? 0, TMP);
|
|
250
|
+
multiplyInto(TMP, bodyToStart, start * 4, out, at);
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
const own = bodyOf[j] ?? -1;
|
|
254
|
+
if (own < 0)
|
|
255
|
+
return false;
|
|
256
|
+
readBodyRotation(w, own, TMP);
|
|
257
|
+
multiplyInto(TMP, bodyToJoint, j * 4, out, at);
|
|
258
|
+
return true;
|
|
259
|
+
};
|
|
213
260
|
const root = firstBone;
|
|
214
261
|
return {
|
|
215
262
|
bodyOf,
|
|
216
263
|
boneCount,
|
|
217
264
|
writePose(out) {
|
|
265
|
+
/*
|
|
266
|
+
* Two passes, because a joint's local rotation needs its parent's world rotation and a rig
|
|
267
|
+
* is not obliged to list parents first. The first recovers every joint a body speaks for.
|
|
268
|
+
*/
|
|
269
|
+
for (let j = 0; j < jointCount; j++)
|
|
270
|
+
KNOWN[j] = recoverJoint(world, j, WORLD_ROT, j * 4) ? 1 : 0;
|
|
218
271
|
for (let j = 0; j < jointCount; j++) {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
if (body < 0) {
|
|
222
|
-
// No bone: leave the joint as the pose already had it.
|
|
272
|
+
if (KNOWN[j] !== 1) {
|
|
273
|
+
// No bone at either end: leave the joint as the pose already had it.
|
|
223
274
|
continue;
|
|
224
275
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
multiplyInto(TMP, bodyToJoint, j * 4, WORLD_ROT, j * 4);
|
|
228
|
-
const parentBody = p >= 0 ? (bodyOf[p] ?? -1) : -1;
|
|
229
|
-
if (parentBody >= 0) {
|
|
276
|
+
const p = parents[j] ?? -1;
|
|
277
|
+
if (p >= 0 && KNOWN[p] === 1) {
|
|
230
278
|
conjugateMultiply(WORLD_ROT, p * 4, WORLD_ROT, j * 4, out.rotation, j * 4);
|
|
231
279
|
}
|
|
232
280
|
else {
|
|
281
|
+
/* A root, or a joint whose parent no bone speaks for: its rotation in the frame the
|
|
282
|
+
caller places the character's node in, which `rootX` and its two siblings answer. */
|
|
233
283
|
copy4(WORLD_ROT, j * 4, out.rotation, j * 4);
|
|
234
284
|
}
|
|
285
|
+
if (p < 0)
|
|
286
|
+
continue;
|
|
235
287
|
// Bones are rigid, so translation stays what the rig was built with.
|
|
236
288
|
out.translation[j * 3] = localRest[j * 3] ?? 0;
|
|
237
289
|
out.translation[j * 3 + 1] = localRest[j * 3 + 1] ?? 0;
|
|
@@ -245,27 +297,32 @@ export function ragdollFromBones(world, parents, worldMatrices, options = {}) {
|
|
|
245
297
|
if (weight <= 0)
|
|
246
298
|
return;
|
|
247
299
|
const k = weight > 1 ? 1 : weight;
|
|
248
|
-
for (let
|
|
249
|
-
const body = bodyOf[
|
|
300
|
+
for (let c = 0; c < jointCount; c++) {
|
|
301
|
+
const body = bodyOf[c] ?? -1;
|
|
250
302
|
if (body < 0)
|
|
251
303
|
continue;
|
|
252
|
-
|
|
253
|
-
|
|
304
|
+
/*
|
|
305
|
+
* **A bone is steered by the joint it starts at**, the one whose rotation points it — the
|
|
306
|
+
* forearm by the elbow — which is the same joint `writePose` reads it back as. Steered by
|
|
307
|
+
* the joint it ends at, a pose's elbow bend went into the upper arm.
|
|
308
|
+
*/
|
|
309
|
+
const j = parents[c] ?? -1;
|
|
310
|
+
if (j < 0)
|
|
311
|
+
continue;
|
|
254
312
|
/*
|
|
255
313
|
* The target is the pose's local rotation composed onto the parent's *current* world
|
|
256
314
|
* rotation, so a driven limb follows the animation relative to a body that may itself have
|
|
257
315
|
* been knocked aside — which is what a partial ragdoll is.
|
|
258
316
|
*/
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
multiplyInto(TMP, bodyToJoint, p * 4, PARENT_ROT, 0);
|
|
317
|
+
const p = parents[j] ?? -1;
|
|
318
|
+
if (p >= 0 && recoverJoint(w, p, PARENT_ROT, 0)) {
|
|
262
319
|
multiplyRaw(PARENT_ROT, 0, pose.rotation, j * 4, TARGET_ROT, 0);
|
|
263
320
|
}
|
|
264
321
|
else {
|
|
265
322
|
copy4(pose.rotation, j * 4, TARGET_ROT, 0);
|
|
266
323
|
}
|
|
267
324
|
readBodyRotation(w, body, TMP);
|
|
268
|
-
multiplyInto(TMP,
|
|
325
|
+
multiplyInto(TMP, bodyToStart, c * 4, CURRENT_ROT, 0);
|
|
269
326
|
// Twice the vector part of target · conj(current) is the rotation that closes the gap.
|
|
270
327
|
conjugateMultiply(CURRENT_ROT, 0, TARGET_ROT, 0, ERROR_ROT, 0);
|
|
271
328
|
let ex = ERROR_ROT[0] ?? 0;
|
|
@@ -301,22 +358,23 @@ export function ragdollFromBones(world, parents, worldMatrices, options = {}) {
|
|
|
301
358
|
w.bodies.posY[body] = (hy + ty) / 2;
|
|
302
359
|
w.bodies.posZ[body] = (hz + tz) / 2;
|
|
303
360
|
/*
|
|
304
|
-
* The rotation is
|
|
305
|
-
*
|
|
306
|
-
*
|
|
307
|
-
* the bone
|
|
361
|
+
* **The rotation is the start joint's, carried through the relation captured at rest**,
|
|
362
|
+
* which is the inverse of what `writePose` reads: `jointRotation · conj(bodyToStart)`.
|
|
363
|
+
*
|
|
364
|
+
* It was the shortest arc from +y onto the bone, which points the capsule correctly and
|
|
365
|
+
* throws away the bone's twist about its own length — so a joint with two bones below it, a
|
|
366
|
+
* wrist with a thumb beside the fingers, came back turned about the first and put the
|
|
367
|
+
* second somewhere else. The joint's world matrix alone would be wrong too, wherever a
|
|
368
|
+
* rig's bind orientation is not the bone direction, which is most rigs; the captured
|
|
369
|
+
* relation is what absorbs that. **What would make it wrong** is a pose that stretches a
|
|
370
|
+
* bone, which a ragdoll's rigid bones cannot follow either way.
|
|
308
371
|
*/
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
w.bodies.rotX[body] = TMP[0] ?? 0;
|
|
316
|
-
w.bodies.rotY[body] = TMP[1] ?? 0;
|
|
317
|
-
w.bodies.rotZ[body] = TMP[2] ?? 0;
|
|
318
|
-
w.bodies.rotW[body] = TMP[3] ?? 1;
|
|
319
|
-
}
|
|
372
|
+
matrixRotation(worldMatrices, p, START_ROT, 0);
|
|
373
|
+
multiplyByConjugate(START_ROT, bodyToStart, j * 4, TMP);
|
|
374
|
+
w.bodies.rotX[body] = TMP[0] ?? 0;
|
|
375
|
+
w.bodies.rotY[body] = TMP[1] ?? 0;
|
|
376
|
+
w.bodies.rotZ[body] = TMP[2] ?? 0;
|
|
377
|
+
w.bodies.rotW[body] = TMP[3] ?? 1;
|
|
320
378
|
/* At rest, which is the half that makes it a reaction rather than a continuation: a body
|
|
321
379
|
carrying the speed of a two-second fall is thrown across the character the instant it
|
|
322
380
|
is written into the pose. */
|
|
@@ -341,6 +399,9 @@ export function ragdollFromBones(world, parents, worldMatrices, options = {}) {
|
|
|
341
399
|
};
|
|
342
400
|
}
|
|
343
401
|
const WORLD_ROT = new Float32Array(256 * 4);
|
|
402
|
+
/** Whether `writePose` recovered each joint's rotation this call: 1 if a body speaks for it. */
|
|
403
|
+
const KNOWN = new Uint8Array(256);
|
|
404
|
+
const START_ROT = new Float32Array(4);
|
|
344
405
|
const PARENT_ROT = new Float32Array(4);
|
|
345
406
|
const TARGET_ROT = new Float32Array(4);
|
|
346
407
|
const CURRENT_ROT = new Float32Array(4);
|
|
@@ -454,6 +515,51 @@ function alignYTo(dx, dy, dz, out) {
|
|
|
454
515
|
out[2] = az / len;
|
|
455
516
|
out[3] = w / len;
|
|
456
517
|
}
|
|
518
|
+
/**
|
|
519
|
+
* A world-space offset from joint `p`, expressed in `p`'s own frame: the inverse of the matrix's
|
|
520
|
+
* upper 3x3 applied to it, so a scaled joint is undone as well as a rotated one.
|
|
521
|
+
*/
|
|
522
|
+
function intoParentFrame(m, p, dx, dy, dz, out, at) {
|
|
523
|
+
const o = p * 16;
|
|
524
|
+
const a = m[o] ?? 1;
|
|
525
|
+
const b = m[o + 4] ?? 0;
|
|
526
|
+
const c = m[o + 8] ?? 0;
|
|
527
|
+
const d = m[o + 1] ?? 0;
|
|
528
|
+
const e = m[o + 5] ?? 1;
|
|
529
|
+
const f = m[o + 9] ?? 0;
|
|
530
|
+
const g = m[o + 2] ?? 0;
|
|
531
|
+
const h = m[o + 6] ?? 0;
|
|
532
|
+
const i = m[o + 10] ?? 1;
|
|
533
|
+
const ei = e * i - f * h;
|
|
534
|
+
const fg = f * g - d * i;
|
|
535
|
+
const dh = d * h - e * g;
|
|
536
|
+
const det = a * ei + b * fg + c * dh;
|
|
537
|
+
if (Math.abs(det) < 1e-12) {
|
|
538
|
+
out[at] = dx;
|
|
539
|
+
out[at + 1] = dy;
|
|
540
|
+
out[at + 2] = dz;
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
const inv = 1 / det;
|
|
544
|
+
out[at] = (ei * dx + (c * h - b * i) * dy + (b * f - c * e) * dz) * inv;
|
|
545
|
+
out[at + 1] = (fg * dx + (a * i - c * g) * dy + (c * d - a * f) * dz) * inv;
|
|
546
|
+
out[at + 2] = (dh * dx + (b * g - a * h) * dy + (a * e - b * d) * dz) * inv;
|
|
547
|
+
}
|
|
548
|
+
/** `a · conj(b)` into a Float64 quaternion, for turning a joint's rotation back into a body's. */
|
|
549
|
+
function multiplyByConjugate(a, b, bAt, out) {
|
|
550
|
+
const ax = a[0] ?? 0;
|
|
551
|
+
const ay = a[1] ?? 0;
|
|
552
|
+
const az = a[2] ?? 0;
|
|
553
|
+
const aw = a[3] ?? 1;
|
|
554
|
+
const bx = -(b[bAt] ?? 0);
|
|
555
|
+
const by = -(b[bAt + 1] ?? 0);
|
|
556
|
+
const bz = -(b[bAt + 2] ?? 0);
|
|
557
|
+
const bw = b[bAt + 3] ?? 1;
|
|
558
|
+
out[0] = aw * bx + ax * bw + ay * bz - az * by;
|
|
559
|
+
out[1] = aw * by - ax * bz + ay * bw + az * bx;
|
|
560
|
+
out[2] = aw * bz + ax * by - ay * bx + az * bw;
|
|
561
|
+
out[3] = aw * bw - ax * bx - ay * by - az * bz;
|
|
562
|
+
}
|
|
457
563
|
/** `conj(a) · b` into `out`. */
|
|
458
564
|
function multiplyConjugate(a, b, bAt, out, at) {
|
|
459
565
|
const ax = -a[0];
|
package/package.json
CHANGED
package/src/controller.ts
CHANGED
|
@@ -453,9 +453,10 @@ export class CharacterController {
|
|
|
453
453
|
* the same clamp and air control — only the plane is given up, not the feel.
|
|
454
454
|
*/
|
|
455
455
|
if (input.projectMove === false) {
|
|
456
|
-
|
|
457
|
-
this.
|
|
458
|
-
this.
|
|
456
|
+
const t = approachFraction(wantX - this.velX, wantY - this.velY, wantZ - this.velZ, step);
|
|
457
|
+
this.velX = t === 1 ? wantX : this.velX + (wantX - this.velX) * t;
|
|
458
|
+
this.velY = t === 1 ? wantY : this.velY + (wantY - this.velY) * t;
|
|
459
|
+
this.velZ = t === 1 ? wantZ : this.velZ + (wantZ - this.velZ) * t;
|
|
459
460
|
} else {
|
|
460
461
|
const alongUp = this.speedAlongUp;
|
|
461
462
|
let tangentX = this.velX - this.upX * alongUp;
|
|
@@ -465,9 +466,15 @@ export class CharacterController {
|
|
|
465
466
|
const wantTangentX = wantX - this.upX * wantIntoUp;
|
|
466
467
|
const wantTangentY = wantY - this.upY * wantIntoUp;
|
|
467
468
|
const wantTangentZ = wantZ - this.upZ * wantIntoUp;
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
469
|
+
const t = approachFraction(
|
|
470
|
+
wantTangentX - tangentX,
|
|
471
|
+
wantTangentY - tangentY,
|
|
472
|
+
wantTangentZ - tangentZ,
|
|
473
|
+
step,
|
|
474
|
+
);
|
|
475
|
+
tangentX = t === 1 ? wantTangentX : tangentX + (wantTangentX - tangentX) * t;
|
|
476
|
+
tangentY = t === 1 ? wantTangentY : tangentY + (wantTangentY - tangentY) * t;
|
|
477
|
+
tangentZ = t === 1 ? wantTangentZ : tangentZ + (wantTangentZ - tangentZ) * t;
|
|
471
478
|
this.velX = tangentX + this.upX * alongUp;
|
|
472
479
|
this.velY = tangentY + this.upY * alongUp;
|
|
473
480
|
this.velZ = tangentZ + this.upZ * alongUp;
|
|
@@ -858,10 +865,18 @@ export class CharacterController {
|
|
|
858
865
|
}
|
|
859
866
|
}
|
|
860
867
|
|
|
861
|
-
/**
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
868
|
+
/**
|
|
869
|
+
* How much of the way to close a gap of `(dx, dy, dz)` this tick: exactly 1 when the gap is no
|
|
870
|
+
* longer than `step`, which the caller reads as "land on the target", and `step` of its length
|
|
871
|
+
* otherwise.
|
|
872
|
+
*
|
|
873
|
+
* **Toward the wanted velocity as a vector, by at most `step` in length.** It was one world axis at
|
|
874
|
+
* a time, each by the whole step, so a diagonal gained and lost speed √2 times as fast as a straight
|
|
875
|
+
* run while the clamp on the wanted vector kept its top speed honest — the faster diagonal lived in
|
|
876
|
+
* the acceleration. What it gives up is that an axis no longer reaches its target on its own
|
|
877
|
+
* schedule: turning from one direction to another now curves rather than squaring the corner.
|
|
878
|
+
*/
|
|
879
|
+
function approachFraction(dx: number, dy: number, dz: number, step: number): number {
|
|
880
|
+
const gap = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
881
|
+
return gap <= step ? 1 : step / gap;
|
|
867
882
|
}
|
package/src/ragdoll.ts
CHANGED
|
@@ -95,7 +95,15 @@ export interface Ragdoll {
|
|
|
95
95
|
readonly bodyOf: Int32Array;
|
|
96
96
|
/** How many bones were built. */
|
|
97
97
|
readonly boneCount: number;
|
|
98
|
-
/**
|
|
98
|
+
/**
|
|
99
|
+
* Write the ragdoll's current shape into a pose.
|
|
100
|
+
*
|
|
101
|
+
* Each joint is turned by the bone that **starts** at it, which is the joint a skinned limb
|
|
102
|
+
* follows — the forearm's body turns the elbow — and a joint with no bone below it rides the one
|
|
103
|
+
* it hangs off. Translations are the rig's own, in each parent's frame. A root's rotation is
|
|
104
|
+
* written in the frame the character's node is placed in, at `rootX`, `rootY` and `rootZ`, with
|
|
105
|
+
* no rotation of its own; a joint no bone reaches is left as the pose had it.
|
|
106
|
+
*/
|
|
99
107
|
writePose(out: PoseTarget): void;
|
|
100
108
|
/** Steer the bodies toward a pose. Weight 1 tracks it, weight 0 goes limp. */
|
|
101
109
|
drive(world: PhysicsWorld, pose: PoseTarget, weight: number): void;
|
|
@@ -185,8 +193,32 @@ export function ragdollFromBones(
|
|
|
185
193
|
};
|
|
186
194
|
|
|
187
195
|
const bodyOf = new Int32Array(jointCount).fill(-1);
|
|
188
|
-
/**
|
|
196
|
+
/**
|
|
197
|
+
* `conj(bodyRotation) · jointWorldRotation` for the joint a bone **ends** at, so a joint with no
|
|
198
|
+
* bone of its own below it — a fingertip, a head's tip — can ride the bone it hangs off.
|
|
199
|
+
*/
|
|
189
200
|
const bodyToJoint = new Float32Array(jointCount * 4);
|
|
201
|
+
/**
|
|
202
|
+
* `conj(bodyRotation) · jointWorldRotation` for the joint a bone **starts** at, indexed like
|
|
203
|
+
* `bodyOf` — by the bone's own joint — and naming its parent's rotation.
|
|
204
|
+
*
|
|
205
|
+
* **This is the joint a skinned limb follows.** A forearm's vertices are weighted to the elbow,
|
|
206
|
+
* so the elbow's rotation is what points the forearm, and the body standing along the forearm is
|
|
207
|
+
* what carries it. Reading each joint off the bone that *ends* at it — the upper arm for the
|
|
208
|
+
* elbow — drew every limb one bone late: a hand 0.47 m from its body in a doll synced and never
|
|
209
|
+
* stepped, reported from a game. A bone is rigid in its start joint's frame, so the relation
|
|
210
|
+
* captured at rest holds for as long as the doll does.
|
|
211
|
+
*/
|
|
212
|
+
const bodyToStart = new Float32Array(jointCount * 4);
|
|
213
|
+
/** The first bone starting at each joint, by that bone's own index, or −1 where none does. */
|
|
214
|
+
const startBone = new Int32Array(jointCount).fill(-1);
|
|
215
|
+
/**
|
|
216
|
+
* Each joint's translation in its parent's frame at rest, which is what a pose holds.
|
|
217
|
+
*
|
|
218
|
+
* **In the parent's frame, not the world's.** It was the world offset between the two, which is
|
|
219
|
+
* the same number only where the parent is unrotated at rest — true of every chain this module's
|
|
220
|
+
* tests were built from, and of no rig a game loads.
|
|
221
|
+
*/
|
|
190
222
|
const localRest = new Float32Array(jointCount * 3);
|
|
191
223
|
const restRotation = new Float32Array(jointCount * 4);
|
|
192
224
|
/**
|
|
@@ -214,9 +246,7 @@ export function ragdollFromBones(
|
|
|
214
246
|
const tx = worldMatrices[j * 16 + 12] ?? 0;
|
|
215
247
|
const ty = worldMatrices[j * 16 + 13] ?? 0;
|
|
216
248
|
const tz = worldMatrices[j * 16 + 14] ?? 0;
|
|
217
|
-
|
|
218
|
-
localRest[j * 3 + 1] = ty - hy;
|
|
219
|
-
localRest[j * 3 + 2] = tz - hz;
|
|
249
|
+
intoParentFrame(worldMatrices, p, tx - hx, ty - hy, tz - hz, localRest, j * 3);
|
|
220
250
|
|
|
221
251
|
const dx = tx - hx;
|
|
222
252
|
const dy = ty - hy;
|
|
@@ -243,8 +273,12 @@ export function ragdollFromBones(
|
|
|
243
273
|
});
|
|
244
274
|
bodyOf[j] = body;
|
|
245
275
|
halfOf[j] = length / 2;
|
|
246
|
-
// What takes the body's rotation back to
|
|
276
|
+
// What takes the body's rotation back to each of its two joints', captured at rest. Parents
|
|
277
|
+
// come before children in a rig, so the start joint's rotation is already read.
|
|
247
278
|
multiplyConjugate(TMP, restRotation, j * 4, bodyToJoint, j * 4);
|
|
279
|
+
if (p >= j) matrixRotation(worldMatrices, p, restRotation, p * 4);
|
|
280
|
+
multiplyConjugate(TMP, restRotation, p * 4, bodyToStart, j * 4);
|
|
281
|
+
if ((startBone[p] ?? -1) < 0) startBone[p] = j;
|
|
248
282
|
boneCount++;
|
|
249
283
|
if (firstBone < 0) {
|
|
250
284
|
firstBone = j;
|
|
@@ -349,27 +383,50 @@ export function ragdollFromBones(
|
|
|
349
383
|
}
|
|
350
384
|
}
|
|
351
385
|
|
|
386
|
+
/**
|
|
387
|
+
* One joint's world rotation, from the body that speaks for it: the first bone starting at the
|
|
388
|
+
* joint, or — for a joint with none below it, a fingertip — the bone it hangs off, which it then
|
|
389
|
+
* rides rigidly. False where neither exists and nothing can be said.
|
|
390
|
+
*/
|
|
391
|
+
const recoverJoint = (w: PhysicsWorld, j: number, out: Float32Array, at: number): boolean => {
|
|
392
|
+
const start = startBone[j] ?? -1;
|
|
393
|
+
if (start >= 0) {
|
|
394
|
+
readBodyRotation(w, bodyOf[start] ?? 0, TMP);
|
|
395
|
+
multiplyInto(TMP, bodyToStart, start * 4, out, at);
|
|
396
|
+
return true;
|
|
397
|
+
}
|
|
398
|
+
const own = bodyOf[j] ?? -1;
|
|
399
|
+
if (own < 0) return false;
|
|
400
|
+
readBodyRotation(w, own, TMP);
|
|
401
|
+
multiplyInto(TMP, bodyToJoint, j * 4, out, at);
|
|
402
|
+
return true;
|
|
403
|
+
};
|
|
404
|
+
|
|
352
405
|
const root = firstBone;
|
|
353
406
|
return {
|
|
354
407
|
bodyOf,
|
|
355
408
|
boneCount,
|
|
356
409
|
writePose(out: PoseTarget): void {
|
|
410
|
+
/*
|
|
411
|
+
* Two passes, because a joint's local rotation needs its parent's world rotation and a rig
|
|
412
|
+
* is not obliged to list parents first. The first recovers every joint a body speaks for.
|
|
413
|
+
*/
|
|
414
|
+
for (let j = 0; j < jointCount; j++)
|
|
415
|
+
KNOWN[j] = recoverJoint(world, j, WORLD_ROT, j * 4) ? 1 : 0;
|
|
357
416
|
for (let j = 0; j < jointCount; j++) {
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
if (body < 0) {
|
|
361
|
-
// No bone: leave the joint as the pose already had it.
|
|
417
|
+
if (KNOWN[j] !== 1) {
|
|
418
|
+
// No bone at either end: leave the joint as the pose already had it.
|
|
362
419
|
continue;
|
|
363
420
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
multiplyInto(TMP, bodyToJoint, j * 4, WORLD_ROT, j * 4);
|
|
367
|
-
const parentBody = p >= 0 ? (bodyOf[p] ?? -1) : -1;
|
|
368
|
-
if (parentBody >= 0) {
|
|
421
|
+
const p = parents[j] ?? -1;
|
|
422
|
+
if (p >= 0 && KNOWN[p] === 1) {
|
|
369
423
|
conjugateMultiply(WORLD_ROT, p * 4, WORLD_ROT, j * 4, out.rotation, j * 4);
|
|
370
424
|
} else {
|
|
425
|
+
/* A root, or a joint whose parent no bone speaks for: its rotation in the frame the
|
|
426
|
+
caller places the character's node in, which `rootX` and its two siblings answer. */
|
|
371
427
|
copy4(WORLD_ROT, j * 4, out.rotation, j * 4);
|
|
372
428
|
}
|
|
429
|
+
if (p < 0) continue;
|
|
373
430
|
// Bones are rigid, so translation stays what the rig was built with.
|
|
374
431
|
out.translation[j * 3] = localRest[j * 3] ?? 0;
|
|
375
432
|
out.translation[j * 3 + 1] = localRest[j * 3 + 1] ?? 0;
|
|
@@ -382,25 +439,29 @@ export function ragdollFromBones(
|
|
|
382
439
|
drive(w: PhysicsWorld, pose: PoseTarget, weight: number): void {
|
|
383
440
|
if (weight <= 0) return;
|
|
384
441
|
const k = weight > 1 ? 1 : weight;
|
|
385
|
-
for (let
|
|
386
|
-
const body = bodyOf[
|
|
442
|
+
for (let c = 0; c < jointCount; c++) {
|
|
443
|
+
const body = bodyOf[c] ?? -1;
|
|
387
444
|
if (body < 0) continue;
|
|
388
|
-
|
|
389
|
-
|
|
445
|
+
/*
|
|
446
|
+
* **A bone is steered by the joint it starts at**, the one whose rotation points it — the
|
|
447
|
+
* forearm by the elbow — which is the same joint `writePose` reads it back as. Steered by
|
|
448
|
+
* the joint it ends at, a pose's elbow bend went into the upper arm.
|
|
449
|
+
*/
|
|
450
|
+
const j = parents[c] ?? -1;
|
|
451
|
+
if (j < 0) continue;
|
|
390
452
|
/*
|
|
391
453
|
* The target is the pose's local rotation composed onto the parent's *current* world
|
|
392
454
|
* rotation, so a driven limb follows the animation relative to a body that may itself have
|
|
393
455
|
* been knocked aside — which is what a partial ragdoll is.
|
|
394
456
|
*/
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
multiplyInto(TMP, bodyToJoint, p * 4, PARENT_ROT, 0);
|
|
457
|
+
const p = parents[j] ?? -1;
|
|
458
|
+
if (p >= 0 && recoverJoint(w, p, PARENT_ROT, 0)) {
|
|
398
459
|
multiplyRaw(PARENT_ROT, 0, pose.rotation, j * 4, TARGET_ROT, 0);
|
|
399
460
|
} else {
|
|
400
461
|
copy4(pose.rotation, j * 4, TARGET_ROT, 0);
|
|
401
462
|
}
|
|
402
463
|
readBodyRotation(w, body, TMP);
|
|
403
|
-
multiplyInto(TMP,
|
|
464
|
+
multiplyInto(TMP, bodyToStart, c * 4, CURRENT_ROT, 0);
|
|
404
465
|
// Twice the vector part of target · conj(current) is the rotation that closes the gap.
|
|
405
466
|
conjugateMultiply(CURRENT_ROT, 0, TARGET_ROT, 0, ERROR_ROT, 0);
|
|
406
467
|
let ex = ERROR_ROT[0] ?? 0;
|
|
@@ -437,22 +498,23 @@ export function ragdollFromBones(
|
|
|
437
498
|
w.bodies.posZ[body] = (hz + tz) / 2;
|
|
438
499
|
|
|
439
500
|
/*
|
|
440
|
-
* The rotation is
|
|
441
|
-
*
|
|
442
|
-
*
|
|
443
|
-
* the bone
|
|
501
|
+
* **The rotation is the start joint's, carried through the relation captured at rest**,
|
|
502
|
+
* which is the inverse of what `writePose` reads: `jointRotation · conj(bodyToStart)`.
|
|
503
|
+
*
|
|
504
|
+
* It was the shortest arc from +y onto the bone, which points the capsule correctly and
|
|
505
|
+
* throws away the bone's twist about its own length — so a joint with two bones below it, a
|
|
506
|
+
* wrist with a thumb beside the fingers, came back turned about the first and put the
|
|
507
|
+
* second somewhere else. The joint's world matrix alone would be wrong too, wherever a
|
|
508
|
+
* rig's bind orientation is not the bone direction, which is most rigs; the captured
|
|
509
|
+
* relation is what absorbs that. **What would make it wrong** is a pose that stretches a
|
|
510
|
+
* bone, which a ragdoll's rigid bones cannot follow either way.
|
|
444
511
|
*/
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
w.bodies.rotX[body] = TMP[0] ?? 0;
|
|
452
|
-
w.bodies.rotY[body] = TMP[1] ?? 0;
|
|
453
|
-
w.bodies.rotZ[body] = TMP[2] ?? 0;
|
|
454
|
-
w.bodies.rotW[body] = TMP[3] ?? 1;
|
|
455
|
-
}
|
|
512
|
+
matrixRotation(worldMatrices, p, START_ROT, 0);
|
|
513
|
+
multiplyByConjugate(START_ROT, bodyToStart, j * 4, TMP);
|
|
514
|
+
w.bodies.rotX[body] = TMP[0] ?? 0;
|
|
515
|
+
w.bodies.rotY[body] = TMP[1] ?? 0;
|
|
516
|
+
w.bodies.rotZ[body] = TMP[2] ?? 0;
|
|
517
|
+
w.bodies.rotW[body] = TMP[3] ?? 1;
|
|
456
518
|
|
|
457
519
|
/* At rest, which is the half that makes it a reaction rather than a continuation: a body
|
|
458
520
|
carrying the speed of a two-second fall is thrown across the character the instant it
|
|
@@ -479,6 +541,9 @@ export function ragdollFromBones(
|
|
|
479
541
|
}
|
|
480
542
|
|
|
481
543
|
const WORLD_ROT = new Float32Array(256 * 4);
|
|
544
|
+
/** Whether `writePose` recovered each joint's rotation this call: 1 if a body speaks for it. */
|
|
545
|
+
const KNOWN = new Uint8Array(256);
|
|
546
|
+
const START_ROT = new Float32Array(4);
|
|
482
547
|
const PARENT_ROT = new Float32Array(4);
|
|
483
548
|
const TARGET_ROT = new Float32Array(4);
|
|
484
549
|
const CURRENT_ROT = new Float32Array(4);
|
|
@@ -596,6 +661,66 @@ function alignYTo(dx: number, dy: number, dz: number, out: Float64Array): void {
|
|
|
596
661
|
out[3] = w / len;
|
|
597
662
|
}
|
|
598
663
|
|
|
664
|
+
/**
|
|
665
|
+
* A world-space offset from joint `p`, expressed in `p`'s own frame: the inverse of the matrix's
|
|
666
|
+
* upper 3x3 applied to it, so a scaled joint is undone as well as a rotated one.
|
|
667
|
+
*/
|
|
668
|
+
function intoParentFrame(
|
|
669
|
+
m: Float32Array,
|
|
670
|
+
p: number,
|
|
671
|
+
dx: number,
|
|
672
|
+
dy: number,
|
|
673
|
+
dz: number,
|
|
674
|
+
out: Float32Array,
|
|
675
|
+
at: number,
|
|
676
|
+
): void {
|
|
677
|
+
const o = p * 16;
|
|
678
|
+
const a = m[o] ?? 1;
|
|
679
|
+
const b = m[o + 4] ?? 0;
|
|
680
|
+
const c = m[o + 8] ?? 0;
|
|
681
|
+
const d = m[o + 1] ?? 0;
|
|
682
|
+
const e = m[o + 5] ?? 1;
|
|
683
|
+
const f = m[o + 9] ?? 0;
|
|
684
|
+
const g = m[o + 2] ?? 0;
|
|
685
|
+
const h = m[o + 6] ?? 0;
|
|
686
|
+
const i = m[o + 10] ?? 1;
|
|
687
|
+
const ei = e * i - f * h;
|
|
688
|
+
const fg = f * g - d * i;
|
|
689
|
+
const dh = d * h - e * g;
|
|
690
|
+
const det = a * ei + b * fg + c * dh;
|
|
691
|
+
if (Math.abs(det) < 1e-12) {
|
|
692
|
+
out[at] = dx;
|
|
693
|
+
out[at + 1] = dy;
|
|
694
|
+
out[at + 2] = dz;
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
const inv = 1 / det;
|
|
698
|
+
out[at] = (ei * dx + (c * h - b * i) * dy + (b * f - c * e) * dz) * inv;
|
|
699
|
+
out[at + 1] = (fg * dx + (a * i - c * g) * dy + (c * d - a * f) * dz) * inv;
|
|
700
|
+
out[at + 2] = (dh * dx + (b * g - a * h) * dy + (a * e - b * d) * dz) * inv;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/** `a · conj(b)` into a Float64 quaternion, for turning a joint's rotation back into a body's. */
|
|
704
|
+
function multiplyByConjugate(
|
|
705
|
+
a: Float32Array,
|
|
706
|
+
b: Float32Array,
|
|
707
|
+
bAt: number,
|
|
708
|
+
out: Float64Array,
|
|
709
|
+
): void {
|
|
710
|
+
const ax = a[0] ?? 0;
|
|
711
|
+
const ay = a[1] ?? 0;
|
|
712
|
+
const az = a[2] ?? 0;
|
|
713
|
+
const aw = a[3] ?? 1;
|
|
714
|
+
const bx = -(b[bAt] ?? 0);
|
|
715
|
+
const by = -(b[bAt + 1] ?? 0);
|
|
716
|
+
const bz = -(b[bAt + 2] ?? 0);
|
|
717
|
+
const bw = b[bAt + 3] ?? 1;
|
|
718
|
+
out[0] = aw * bx + ax * bw + ay * bz - az * by;
|
|
719
|
+
out[1] = aw * by - ax * bz + ay * bw + az * bx;
|
|
720
|
+
out[2] = aw * bz + ax * by - ay * bx + az * bw;
|
|
721
|
+
out[3] = aw * bw - ax * bx - ay * by - az * bz;
|
|
722
|
+
}
|
|
723
|
+
|
|
599
724
|
/** `conj(a) · b` into `out`. */
|
|
600
725
|
function multiplyConjugate(
|
|
601
726
|
a: Float64Array,
|