@driftengine/physics 4.8.0 → 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.d.ts +2 -0
- package/dist/controller.js +81 -28
- package/dist/manifold.js +132 -58
- package/dist/query.js +28 -2
- package/dist/ragdoll.d.ts +9 -1
- package/dist/ragdoll.js +143 -37
- package/package.json +1 -1
- package/src/controller.ts +91 -26
- package/src/manifold.ts +154 -55
- package/src/query.ts +25 -2
- package/src/ragdoll.ts +163 -38
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
|
@@ -170,6 +170,8 @@ const SLIDE_PASSES = 4;
|
|
|
170
170
|
* own stop margin already leaves a tenth of a millimetre of clearance, which is what a skin was for.
|
|
171
171
|
*/
|
|
172
172
|
const SNAP_EPS = 0.001;
|
|
173
|
+
/** How far past a step's edge a step that landed on the edge carries the body's axis. */
|
|
174
|
+
const STEP_OVER = 0.01;
|
|
173
175
|
|
|
174
176
|
export class CharacterController {
|
|
175
177
|
x = 0;
|
|
@@ -451,9 +453,10 @@ export class CharacterController {
|
|
|
451
453
|
* the same clamp and air control — only the plane is given up, not the feel.
|
|
452
454
|
*/
|
|
453
455
|
if (input.projectMove === false) {
|
|
454
|
-
|
|
455
|
-
this.
|
|
456
|
-
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;
|
|
457
460
|
} else {
|
|
458
461
|
const alongUp = this.speedAlongUp;
|
|
459
462
|
let tangentX = this.velX - this.upX * alongUp;
|
|
@@ -463,9 +466,15 @@ export class CharacterController {
|
|
|
463
466
|
const wantTangentX = wantX - this.upX * wantIntoUp;
|
|
464
467
|
const wantTangentY = wantY - this.upY * wantIntoUp;
|
|
465
468
|
const wantTangentZ = wantZ - this.upZ * wantIntoUp;
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
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;
|
|
469
478
|
this.velX = tangentX + this.upX * alongUp;
|
|
470
479
|
this.velY = tangentY + this.upY * alongUp;
|
|
471
480
|
this.velZ = tangentZ + this.upZ * alongUp;
|
|
@@ -603,26 +612,74 @@ export class CharacterController {
|
|
|
603
612
|
this.z += liftZ;
|
|
604
613
|
this.readPose();
|
|
605
614
|
if (world.shapecast(this.shape, this.pose, flatX, flatY, flatZ, this.stepHit, this.filter)) {
|
|
606
|
-
this.
|
|
607
|
-
this.y = startY;
|
|
608
|
-
this.z = startZ;
|
|
609
|
-
return false;
|
|
615
|
+
return this.stepBack(startX, startY, startZ);
|
|
610
616
|
}
|
|
611
617
|
this.x += flatX;
|
|
612
618
|
this.y += flatY;
|
|
613
619
|
this.z += flatZ;
|
|
614
620
|
this.readPose();
|
|
615
|
-
if (
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
return true;
|
|
621
|
+
if (
|
|
622
|
+
!world.shapecast(this.shape, this.pose, -liftX, -liftY, -liftZ, this.stepHit, this.filter)
|
|
623
|
+
) {
|
|
624
|
+
// Nothing beneath the lifted position, so this was a gap rather than a step.
|
|
625
|
+
return this.stepBack(startX, startY, startZ);
|
|
621
626
|
}
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
this.
|
|
625
|
-
|
|
627
|
+
let landing =
|
|
628
|
+
this.stepHit.nx * this.upX + this.stepHit.ny * this.upY + this.stepHit.nz * this.upZ;
|
|
629
|
+
if (landing < this.slopeCos) {
|
|
630
|
+
/*
|
|
631
|
+
* **Landed on the lip, so go on over it.** The forward part of a step is what was left of
|
|
632
|
+
* the tick's move when the body met the step, and at a tick's length that can be millimetres:
|
|
633
|
+
* the foot ball then comes down on the step's edge, a contact far too steep to stand on, and
|
|
634
|
+
* the body was set there airborne, slid off, and never stepped again. Measured with a 0.32 m
|
|
635
|
+
* radius and a 0.35 m step height: a 0.25 m rise climbed and a 0.28 m one did not.
|
|
636
|
+
*
|
|
637
|
+
* The edge's normal points from the edge at the ball's centre, so its part across up is how
|
|
638
|
+
* far the axis stands short of the edge. Moving that far again, plus a centimetre, puts the
|
|
639
|
+
* ball over the step's top, where the drop meets a face. The body moves up to one radius
|
|
640
|
+
* further in that tick than it was going to, which is the price of a step taken in one tick.
|
|
641
|
+
*/
|
|
642
|
+
const lipX = this.stepHit.nx - this.upX * landing;
|
|
643
|
+
const lipY = this.stepHit.ny - this.upY * landing;
|
|
644
|
+
const lipZ = this.stepHit.nz - this.upZ * landing;
|
|
645
|
+
const lip = Math.sqrt(lipX * lipX + lipY * lipY + lipZ * lipZ);
|
|
646
|
+
// An edge behind the move, or beside it, is not one this step was climbing.
|
|
647
|
+
if (lip < 1e-6 || lipX * flatX + lipY * flatY + lipZ * flatZ >= 0) {
|
|
648
|
+
return this.stepBack(startX, startY, startZ);
|
|
649
|
+
}
|
|
650
|
+
const reach = (this.radius * lip + STEP_OVER) / lip;
|
|
651
|
+
const overX = -lipX * reach;
|
|
652
|
+
const overY = -lipY * reach;
|
|
653
|
+
const overZ = -lipZ * reach;
|
|
654
|
+
this.readPose();
|
|
655
|
+
if (world.shapecast(this.shape, this.pose, overX, overY, overZ, this.stepHit, this.filter)) {
|
|
656
|
+
return this.stepBack(startX, startY, startZ);
|
|
657
|
+
}
|
|
658
|
+
this.x += overX;
|
|
659
|
+
this.y += overY;
|
|
660
|
+
this.z += overZ;
|
|
661
|
+
this.readPose();
|
|
662
|
+
if (
|
|
663
|
+
!world.shapecast(this.shape, this.pose, -liftX, -liftY, -liftZ, this.stepHit, this.filter)
|
|
664
|
+
) {
|
|
665
|
+
return this.stepBack(startX, startY, startZ);
|
|
666
|
+
}
|
|
667
|
+
landing =
|
|
668
|
+
this.stepHit.nx * this.upX + this.stepHit.ny * this.upY + this.stepHit.nz * this.upZ;
|
|
669
|
+
if (landing < this.slopeCos) return this.stepBack(startX, startY, startZ);
|
|
670
|
+
}
|
|
671
|
+
const drop = this.stepHeight * this.stepHit.fraction - SNAP_EPS;
|
|
672
|
+
this.x -= this.upX * drop;
|
|
673
|
+
this.y -= this.upY * drop;
|
|
674
|
+
this.z -= this.upZ * drop;
|
|
675
|
+
return true;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/** Put the body back where a step began, and say the step was not taken. */
|
|
679
|
+
private stepBack(x: number, y: number, z: number): false {
|
|
680
|
+
this.x = x;
|
|
681
|
+
this.y = y;
|
|
682
|
+
this.z = z;
|
|
626
683
|
return false;
|
|
627
684
|
}
|
|
628
685
|
|
|
@@ -808,10 +865,18 @@ export class CharacterController {
|
|
|
808
865
|
}
|
|
809
866
|
}
|
|
810
867
|
|
|
811
|
-
/**
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
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;
|
|
817
882
|
}
|
package/src/manifold.ts
CHANGED
|
@@ -207,6 +207,76 @@ function roundRound(a: ConvexShape, b: ConvexShape, margin: number, out: Manifol
|
|
|
207
207
|
|
|
208
208
|
/* ---------- sphere or capsule against a polytope ---------- */
|
|
209
209
|
|
|
210
|
+
/** Where along a capsule's segment the search settles: 0.618 to the 30th of 1.2 m is under a micron. */
|
|
211
|
+
const SEGMENT_STEPS = 30;
|
|
212
|
+
const INV_PHI = 0.6180339887498949;
|
|
213
|
+
/** Each end's direction away from the polytope, and its separation, read by the segment search. */
|
|
214
|
+
const END_DIR = new Float64Array(9);
|
|
215
|
+
const END_SEP = new Float64Array(2);
|
|
216
|
+
/** The manifold being filled by `roundPolytope`, kept here so adding a contact allocates nothing. */
|
|
217
|
+
const ROUND = { count: 0, bestSep: Infinity, nx: 0, ny: 1, nz: 0, margin: 0, flipped: false };
|
|
218
|
+
|
|
219
|
+
/** Add the contact `CLOSEST` and `END_DIR[slot]` describe, at `separation`, as `feature`. */
|
|
220
|
+
function emitRound(out: Manifold, slot: number, separation: number, feature: number): void {
|
|
221
|
+
if (separation > ROUND.margin || ROUND.count >= MAX_CONTACTS) return;
|
|
222
|
+
// Normal points from the round shape toward the polytope, then flipped if A was the polytope.
|
|
223
|
+
const ux = END_DIR[slot * 3] ?? 0;
|
|
224
|
+
const uy = END_DIR[slot * 3 + 1] ?? 0;
|
|
225
|
+
const uz = END_DIR[slot * 3 + 2] ?? 0;
|
|
226
|
+
if (separation < ROUND.bestSep) {
|
|
227
|
+
ROUND.bestSep = separation;
|
|
228
|
+
ROUND.nx = ROUND.flipped ? ux : -ux;
|
|
229
|
+
ROUND.ny = ROUND.flipped ? uy : -uy;
|
|
230
|
+
ROUND.nz = ROUND.flipped ? uz : -uz;
|
|
231
|
+
}
|
|
232
|
+
const at = ROUND.count * 3;
|
|
233
|
+
out.points[at] = CLOSEST[0];
|
|
234
|
+
out.points[at + 1] = CLOSEST[1];
|
|
235
|
+
out.points[at + 2] = CLOSEST[2];
|
|
236
|
+
out.separations[ROUND.count] = separation;
|
|
237
|
+
out.featureIds[ROUND.count] = feature;
|
|
238
|
+
ROUND.count++;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* The signed distance from a point to the polytope, with the direction away from it left in
|
|
243
|
+
* `END_DIR[slot]`: positive outside, the deepest face's plane distance inside.
|
|
244
|
+
*/
|
|
245
|
+
function signedDistance(
|
|
246
|
+
poly: ConvexShape,
|
|
247
|
+
polyWorld: Float32Array,
|
|
248
|
+
faces: number,
|
|
249
|
+
px: number,
|
|
250
|
+
py: number,
|
|
251
|
+
pz: number,
|
|
252
|
+
slot: number,
|
|
253
|
+
): number {
|
|
254
|
+
closestOnPolytope(poly, polyWorld, planesB, faces, px, py, pz);
|
|
255
|
+
let ux = px - CLOSEST[0];
|
|
256
|
+
let uy = py - CLOSEST[1];
|
|
257
|
+
let uz = pz - CLOSEST[2];
|
|
258
|
+
let d = Math.sqrt(ux * ux + uy * uy + uz * uz);
|
|
259
|
+
if (CLOSEST[6] < 0) {
|
|
260
|
+
// Inside: the closest face's outward normal is the direction, and depth is its plane distance.
|
|
261
|
+
ux = CLOSEST[3];
|
|
262
|
+
uy = CLOSEST[4];
|
|
263
|
+
uz = CLOSEST[5];
|
|
264
|
+
d = CLOSEST[6];
|
|
265
|
+
} else if (d === 0) {
|
|
266
|
+
ux = CLOSEST[3];
|
|
267
|
+
uy = CLOSEST[4];
|
|
268
|
+
uz = CLOSEST[5];
|
|
269
|
+
} else {
|
|
270
|
+
ux /= d;
|
|
271
|
+
uy /= d;
|
|
272
|
+
uz /= d;
|
|
273
|
+
}
|
|
274
|
+
END_DIR[slot * 3] = ux;
|
|
275
|
+
END_DIR[slot * 3 + 1] = uy;
|
|
276
|
+
END_DIR[slot * 3 + 2] = uz;
|
|
277
|
+
return d;
|
|
278
|
+
}
|
|
279
|
+
|
|
210
280
|
function roundPolytope(
|
|
211
281
|
round: ConvexShape,
|
|
212
282
|
poly: ConvexShape,
|
|
@@ -222,63 +292,92 @@ function roundPolytope(
|
|
|
222
292
|
|
|
223
293
|
const points = round.vertices.length / 3;
|
|
224
294
|
const faces = faceCount(poly);
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
} else if (d === 0) {
|
|
247
|
-
ux = CLOSEST[3];
|
|
248
|
-
uy = CLOSEST[4];
|
|
249
|
-
uz = CLOSEST[5];
|
|
250
|
-
} else {
|
|
251
|
-
ux /= d;
|
|
252
|
-
uy /= d;
|
|
253
|
-
uz /= d;
|
|
254
|
-
}
|
|
255
|
-
const separation = d - round.radius - poly.radius;
|
|
256
|
-
if (separation > margin) continue;
|
|
257
|
-
if (count >= MAX_CONTACTS) break;
|
|
258
|
-
// Normal points from the round shape toward the polytope, then flipped if A was the polytope.
|
|
259
|
-
const sx = flipped ? ux : -ux;
|
|
260
|
-
const sy = flipped ? uy : -uy;
|
|
261
|
-
const sz = flipped ? uz : -uz;
|
|
262
|
-
if (separation < bestSep) {
|
|
263
|
-
bestSep = separation;
|
|
264
|
-
nx = sx;
|
|
265
|
-
ny = sy;
|
|
266
|
-
nz = sz;
|
|
267
|
-
}
|
|
268
|
-
const at = count * 3;
|
|
269
|
-
out.points[at] = CLOSEST[0];
|
|
270
|
-
out.points[at + 1] = CLOSEST[1];
|
|
271
|
-
out.points[at + 2] = CLOSEST[2];
|
|
272
|
-
out.separations[count] = separation;
|
|
295
|
+
const radii = round.radius + poly.radius;
|
|
296
|
+
ROUND.count = 0;
|
|
297
|
+
ROUND.bestSep = Infinity;
|
|
298
|
+
ROUND.nx = 0;
|
|
299
|
+
ROUND.ny = 1;
|
|
300
|
+
ROUND.nz = 0;
|
|
301
|
+
ROUND.margin = margin;
|
|
302
|
+
ROUND.flipped = flipped;
|
|
303
|
+
|
|
304
|
+
for (let p = 0; p < points && p < 2; p++) {
|
|
305
|
+
const separation =
|
|
306
|
+
signedDistance(
|
|
307
|
+
poly,
|
|
308
|
+
polyWorld,
|
|
309
|
+
faces,
|
|
310
|
+
roundWorld[p * 3] ?? 0,
|
|
311
|
+
roundWorld[p * 3 + 1] ?? 0,
|
|
312
|
+
roundWorld[p * 3 + 2] ?? 0,
|
|
313
|
+
p,
|
|
314
|
+
) - radii;
|
|
315
|
+
END_SEP[p] = separation;
|
|
273
316
|
// The endpoint that produced it, which is stable while the capsule keeps its orientation.
|
|
274
|
-
out
|
|
275
|
-
count++;
|
|
317
|
+
emitRound(out, p, separation, 0x40000000 | p);
|
|
276
318
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
319
|
+
|
|
320
|
+
/*
|
|
321
|
+
* **A capsule is a segment, and its middle can be the part that touches.** This used to ask only
|
|
322
|
+
* its two ends, which is exact for a ball and for a capsule lying on a face, and wrong wherever the
|
|
323
|
+
* segment crosses an edge: a standing capsule walked through a slab across its waist, because
|
|
324
|
+
* neither end ball came within reach of it, and a capsule lying across a rail fell through it.
|
|
325
|
+
*
|
|
326
|
+
* The distance from a point on the segment to a convex polytope is convex along the segment, so
|
|
327
|
+
* its least value is at an end exactly when the slope there points away from the other end. Each
|
|
328
|
+
* end's direction away from the polytope is that slope, so a capsule whose ends are the answer, as
|
|
329
|
+
* one standing on a floor always is, costs nothing more. Otherwise a fixed number of golden-section
|
|
330
|
+
* steps finds the interior point, and it is kept only where it is nearer than both ends.
|
|
331
|
+
*/
|
|
332
|
+
if (points === 2) {
|
|
333
|
+
const ax = roundWorld[0] ?? 0;
|
|
334
|
+
const ay = roundWorld[1] ?? 0;
|
|
335
|
+
const az = roundWorld[2] ?? 0;
|
|
336
|
+
const sx = (roundWorld[3] ?? 0) - ax;
|
|
337
|
+
const sy = (roundWorld[4] ?? 0) - ay;
|
|
338
|
+
const sz = (roundWorld[5] ?? 0) - az;
|
|
339
|
+
const leavesFirst = sx * (END_DIR[0] ?? 0) + sy * (END_DIR[1] ?? 0) + sz * (END_DIR[2] ?? 0);
|
|
340
|
+
const leavesSecond = -(
|
|
341
|
+
sx * (END_DIR[3] ?? 0) +
|
|
342
|
+
sy * (END_DIR[4] ?? 0) +
|
|
343
|
+
sz * (END_DIR[5] ?? 0)
|
|
344
|
+
);
|
|
345
|
+
if (leavesFirst < 0 && leavesSecond < 0) {
|
|
346
|
+
let lo = 0;
|
|
347
|
+
let hi = 1;
|
|
348
|
+
let m1 = hi - (hi - lo) * INV_PHI;
|
|
349
|
+
let m2 = lo + (hi - lo) * INV_PHI;
|
|
350
|
+
let f1 = signedDistance(poly, polyWorld, faces, ax + sx * m1, ay + sy * m1, az + sz * m1, 2);
|
|
351
|
+
let f2 = signedDistance(poly, polyWorld, faces, ax + sx * m2, ay + sy * m2, az + sz * m2, 2);
|
|
352
|
+
for (let step = 0; step < SEGMENT_STEPS; step++) {
|
|
353
|
+
if (f1 <= f2) {
|
|
354
|
+
hi = m2;
|
|
355
|
+
m2 = m1;
|
|
356
|
+
f2 = f1;
|
|
357
|
+
m1 = hi - (hi - lo) * INV_PHI;
|
|
358
|
+
f1 = signedDistance(poly, polyWorld, faces, ax + sx * m1, ay + sy * m1, az + sz * m1, 2);
|
|
359
|
+
} else {
|
|
360
|
+
lo = m1;
|
|
361
|
+
m1 = m2;
|
|
362
|
+
f1 = f2;
|
|
363
|
+
m2 = lo + (hi - lo) * INV_PHI;
|
|
364
|
+
f2 = signedDistance(poly, polyWorld, faces, ax + sx * m2, ay + sy * m2, az + sz * m2, 2);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
const at = (lo + hi) / 2;
|
|
368
|
+
const separation =
|
|
369
|
+
signedDistance(poly, polyWorld, faces, ax + sx * at, ay + sy * at, az + sz * at, 2) - radii;
|
|
370
|
+
if (separation < Math.min(END_SEP[0] ?? Infinity, END_SEP[1] ?? Infinity) - 1e-7) {
|
|
371
|
+
emitRound(out, 2, separation, 0x40000000 | 2);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (ROUND.count === 0) return false;
|
|
377
|
+
out.nx = ROUND.nx;
|
|
378
|
+
out.ny = ROUND.ny;
|
|
379
|
+
out.nz = ROUND.nz;
|
|
380
|
+
out.count = ROUND.count;
|
|
282
381
|
return true;
|
|
283
382
|
}
|
|
284
383
|
|