@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/controller.d.ts
CHANGED
|
@@ -303,6 +303,8 @@ export declare class CharacterController {
|
|
|
303
303
|
* would climb twice.
|
|
304
304
|
*/
|
|
305
305
|
private tryStep;
|
|
306
|
+
/** Put the body back where a step began, and say the step was not taken. */
|
|
307
|
+
private stepBack;
|
|
306
308
|
/**
|
|
307
309
|
* Sweep the foot ball down, and fall back to a ray when what it finds is not walkable.
|
|
308
310
|
*
|
package/dist/controller.js
CHANGED
|
@@ -36,6 +36,8 @@ const SLIDE_PASSES = 4;
|
|
|
36
36
|
* own stop margin already leaves a tenth of a millimetre of clearance, which is what a skin was for.
|
|
37
37
|
*/
|
|
38
38
|
const SNAP_EPS = 0.001;
|
|
39
|
+
/** How far past a step's edge a step that landed on the edge carries the body's axis. */
|
|
40
|
+
const STEP_OVER = 0.01;
|
|
39
41
|
export class CharacterController {
|
|
40
42
|
x = 0;
|
|
41
43
|
y = 0;
|
|
@@ -311,9 +313,10 @@ export class CharacterController {
|
|
|
311
313
|
* the same clamp and air control — only the plane is given up, not the feel.
|
|
312
314
|
*/
|
|
313
315
|
if (input.projectMove === false) {
|
|
314
|
-
|
|
315
|
-
this.
|
|
316
|
-
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;
|
|
317
320
|
}
|
|
318
321
|
else {
|
|
319
322
|
const alongUp = this.speedAlongUp;
|
|
@@ -324,9 +327,10 @@ export class CharacterController {
|
|
|
324
327
|
const wantTangentX = wantX - this.upX * wantIntoUp;
|
|
325
328
|
const wantTangentY = wantY - this.upY * wantIntoUp;
|
|
326
329
|
const wantTangentZ = wantZ - this.upZ * wantIntoUp;
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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;
|
|
330
334
|
this.velX = tangentX + this.upX * alongUp;
|
|
331
335
|
this.velY = tangentY + this.upY * alongUp;
|
|
332
336
|
this.velZ = tangentZ + this.upZ * alongUp;
|
|
@@ -452,26 +456,69 @@ export class CharacterController {
|
|
|
452
456
|
this.z += liftZ;
|
|
453
457
|
this.readPose();
|
|
454
458
|
if (world.shapecast(this.shape, this.pose, flatX, flatY, flatZ, this.stepHit, this.filter)) {
|
|
455
|
-
this.
|
|
456
|
-
this.y = startY;
|
|
457
|
-
this.z = startZ;
|
|
458
|
-
return false;
|
|
459
|
+
return this.stepBack(startX, startY, startZ);
|
|
459
460
|
}
|
|
460
461
|
this.x += flatX;
|
|
461
462
|
this.y += flatY;
|
|
462
463
|
this.z += flatZ;
|
|
463
464
|
this.readPose();
|
|
464
|
-
if (world.shapecast(this.shape, this.pose, -liftX, -liftY, -liftZ, this.stepHit, this.filter)) {
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
this.y -= this.upY * drop;
|
|
468
|
-
this.z -= this.upZ * drop;
|
|
469
|
-
return true;
|
|
465
|
+
if (!world.shapecast(this.shape, this.pose, -liftX, -liftY, -liftZ, this.stepHit, this.filter)) {
|
|
466
|
+
// Nothing beneath the lifted position, so this was a gap rather than a step.
|
|
467
|
+
return this.stepBack(startX, startY, startZ);
|
|
470
468
|
}
|
|
471
|
-
|
|
472
|
-
this.
|
|
473
|
-
|
|
474
|
-
|
|
469
|
+
let landing = this.stepHit.nx * this.upX + this.stepHit.ny * this.upY + this.stepHit.nz * this.upZ;
|
|
470
|
+
if (landing < this.slopeCos) {
|
|
471
|
+
/*
|
|
472
|
+
* **Landed on the lip, so go on over it.** The forward part of a step is what was left of
|
|
473
|
+
* the tick's move when the body met the step, and at a tick's length that can be millimetres:
|
|
474
|
+
* the foot ball then comes down on the step's edge, a contact far too steep to stand on, and
|
|
475
|
+
* the body was set there airborne, slid off, and never stepped again. Measured with a 0.32 m
|
|
476
|
+
* radius and a 0.35 m step height: a 0.25 m rise climbed and a 0.28 m one did not.
|
|
477
|
+
*
|
|
478
|
+
* The edge's normal points from the edge at the ball's centre, so its part across up is how
|
|
479
|
+
* far the axis stands short of the edge. Moving that far again, plus a centimetre, puts the
|
|
480
|
+
* ball over the step's top, where the drop meets a face. The body moves up to one radius
|
|
481
|
+
* further in that tick than it was going to, which is the price of a step taken in one tick.
|
|
482
|
+
*/
|
|
483
|
+
const lipX = this.stepHit.nx - this.upX * landing;
|
|
484
|
+
const lipY = this.stepHit.ny - this.upY * landing;
|
|
485
|
+
const lipZ = this.stepHit.nz - this.upZ * landing;
|
|
486
|
+
const lip = Math.sqrt(lipX * lipX + lipY * lipY + lipZ * lipZ);
|
|
487
|
+
// An edge behind the move, or beside it, is not one this step was climbing.
|
|
488
|
+
if (lip < 1e-6 || lipX * flatX + lipY * flatY + lipZ * flatZ >= 0) {
|
|
489
|
+
return this.stepBack(startX, startY, startZ);
|
|
490
|
+
}
|
|
491
|
+
const reach = (this.radius * lip + STEP_OVER) / lip;
|
|
492
|
+
const overX = -lipX * reach;
|
|
493
|
+
const overY = -lipY * reach;
|
|
494
|
+
const overZ = -lipZ * reach;
|
|
495
|
+
this.readPose();
|
|
496
|
+
if (world.shapecast(this.shape, this.pose, overX, overY, overZ, this.stepHit, this.filter)) {
|
|
497
|
+
return this.stepBack(startX, startY, startZ);
|
|
498
|
+
}
|
|
499
|
+
this.x += overX;
|
|
500
|
+
this.y += overY;
|
|
501
|
+
this.z += overZ;
|
|
502
|
+
this.readPose();
|
|
503
|
+
if (!world.shapecast(this.shape, this.pose, -liftX, -liftY, -liftZ, this.stepHit, this.filter)) {
|
|
504
|
+
return this.stepBack(startX, startY, startZ);
|
|
505
|
+
}
|
|
506
|
+
landing =
|
|
507
|
+
this.stepHit.nx * this.upX + this.stepHit.ny * this.upY + this.stepHit.nz * this.upZ;
|
|
508
|
+
if (landing < this.slopeCos)
|
|
509
|
+
return this.stepBack(startX, startY, startZ);
|
|
510
|
+
}
|
|
511
|
+
const drop = this.stepHeight * this.stepHit.fraction - SNAP_EPS;
|
|
512
|
+
this.x -= this.upX * drop;
|
|
513
|
+
this.y -= this.upY * drop;
|
|
514
|
+
this.z -= this.upZ * drop;
|
|
515
|
+
return true;
|
|
516
|
+
}
|
|
517
|
+
/** Put the body back where a step began, and say the step was not taken. */
|
|
518
|
+
stepBack(x, y, z) {
|
|
519
|
+
this.x = x;
|
|
520
|
+
this.y = y;
|
|
521
|
+
this.z = z;
|
|
475
522
|
return false;
|
|
476
523
|
}
|
|
477
524
|
/**
|
|
@@ -630,12 +677,18 @@ export class CharacterController {
|
|
|
630
677
|
this.pose.qw = w * inverse;
|
|
631
678
|
}
|
|
632
679
|
}
|
|
633
|
-
/**
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
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;
|
|
641
694
|
}
|
package/dist/manifold.js
CHANGED
|
@@ -113,6 +113,68 @@ function roundRound(a, b, margin, out) {
|
|
|
113
113
|
return true;
|
|
114
114
|
}
|
|
115
115
|
/* ---------- sphere or capsule against a polytope ---------- */
|
|
116
|
+
/** Where along a capsule's segment the search settles: 0.618 to the 30th of 1.2 m is under a micron. */
|
|
117
|
+
const SEGMENT_STEPS = 30;
|
|
118
|
+
const INV_PHI = 0.6180339887498949;
|
|
119
|
+
/** Each end's direction away from the polytope, and its separation, read by the segment search. */
|
|
120
|
+
const END_DIR = new Float64Array(9);
|
|
121
|
+
const END_SEP = new Float64Array(2);
|
|
122
|
+
/** The manifold being filled by `roundPolytope`, kept here so adding a contact allocates nothing. */
|
|
123
|
+
const ROUND = { count: 0, bestSep: Infinity, nx: 0, ny: 1, nz: 0, margin: 0, flipped: false };
|
|
124
|
+
/** Add the contact `CLOSEST` and `END_DIR[slot]` describe, at `separation`, as `feature`. */
|
|
125
|
+
function emitRound(out, slot, separation, feature) {
|
|
126
|
+
if (separation > ROUND.margin || ROUND.count >= MAX_CONTACTS)
|
|
127
|
+
return;
|
|
128
|
+
// Normal points from the round shape toward the polytope, then flipped if A was the polytope.
|
|
129
|
+
const ux = END_DIR[slot * 3] ?? 0;
|
|
130
|
+
const uy = END_DIR[slot * 3 + 1] ?? 0;
|
|
131
|
+
const uz = END_DIR[slot * 3 + 2] ?? 0;
|
|
132
|
+
if (separation < ROUND.bestSep) {
|
|
133
|
+
ROUND.bestSep = separation;
|
|
134
|
+
ROUND.nx = ROUND.flipped ? ux : -ux;
|
|
135
|
+
ROUND.ny = ROUND.flipped ? uy : -uy;
|
|
136
|
+
ROUND.nz = ROUND.flipped ? uz : -uz;
|
|
137
|
+
}
|
|
138
|
+
const at = ROUND.count * 3;
|
|
139
|
+
out.points[at] = CLOSEST[0];
|
|
140
|
+
out.points[at + 1] = CLOSEST[1];
|
|
141
|
+
out.points[at + 2] = CLOSEST[2];
|
|
142
|
+
out.separations[ROUND.count] = separation;
|
|
143
|
+
out.featureIds[ROUND.count] = feature;
|
|
144
|
+
ROUND.count++;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* The signed distance from a point to the polytope, with the direction away from it left in
|
|
148
|
+
* `END_DIR[slot]`: positive outside, the deepest face's plane distance inside.
|
|
149
|
+
*/
|
|
150
|
+
function signedDistance(poly, polyWorld, faces, px, py, pz, slot) {
|
|
151
|
+
closestOnPolytope(poly, polyWorld, planesB, faces, px, py, pz);
|
|
152
|
+
let ux = px - CLOSEST[0];
|
|
153
|
+
let uy = py - CLOSEST[1];
|
|
154
|
+
let uz = pz - CLOSEST[2];
|
|
155
|
+
let d = Math.sqrt(ux * ux + uy * uy + uz * uz);
|
|
156
|
+
if (CLOSEST[6] < 0) {
|
|
157
|
+
// Inside: the closest face's outward normal is the direction, and depth is its plane distance.
|
|
158
|
+
ux = CLOSEST[3];
|
|
159
|
+
uy = CLOSEST[4];
|
|
160
|
+
uz = CLOSEST[5];
|
|
161
|
+
d = CLOSEST[6];
|
|
162
|
+
}
|
|
163
|
+
else if (d === 0) {
|
|
164
|
+
ux = CLOSEST[3];
|
|
165
|
+
uy = CLOSEST[4];
|
|
166
|
+
uz = CLOSEST[5];
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
ux /= d;
|
|
170
|
+
uy /= d;
|
|
171
|
+
uz /= d;
|
|
172
|
+
}
|
|
173
|
+
END_DIR[slot * 3] = ux;
|
|
174
|
+
END_DIR[slot * 3 + 1] = uy;
|
|
175
|
+
END_DIR[slot * 3 + 2] = uz;
|
|
176
|
+
return d;
|
|
177
|
+
}
|
|
116
178
|
function roundPolytope(round, poly, polyPose, margin, out, flipped) {
|
|
117
179
|
// `worldA` holds whichever shape was passed first, so read the round one from the right buffer.
|
|
118
180
|
const roundWorld = flipped ? worldB : worldA;
|
|
@@ -120,67 +182,79 @@ function roundPolytope(round, poly, polyPose, margin, out, flipped) {
|
|
|
120
182
|
planesToWorld(poly, polyPose, polyWorld, planesB);
|
|
121
183
|
const points = round.vertices.length / 3;
|
|
122
184
|
const faces = faceCount(poly);
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
let uy = py - CLOSEST[1];
|
|
135
|
-
let uz = pz - CLOSEST[2];
|
|
136
|
-
let d = Math.sqrt(ux * ux + uy * uy + uz * uz);
|
|
137
|
-
if (CLOSEST[6] < 0) {
|
|
138
|
-
// Inside: the closest face's outward normal is the direction, and depth is its plane distance.
|
|
139
|
-
ux = CLOSEST[3];
|
|
140
|
-
uy = CLOSEST[4];
|
|
141
|
-
uz = CLOSEST[5];
|
|
142
|
-
d = CLOSEST[6];
|
|
143
|
-
}
|
|
144
|
-
else if (d === 0) {
|
|
145
|
-
ux = CLOSEST[3];
|
|
146
|
-
uy = CLOSEST[4];
|
|
147
|
-
uz = CLOSEST[5];
|
|
148
|
-
}
|
|
149
|
-
else {
|
|
150
|
-
ux /= d;
|
|
151
|
-
uy /= d;
|
|
152
|
-
uz /= d;
|
|
153
|
-
}
|
|
154
|
-
const separation = d - round.radius - poly.radius;
|
|
155
|
-
if (separation > margin)
|
|
156
|
-
continue;
|
|
157
|
-
if (count >= MAX_CONTACTS)
|
|
158
|
-
break;
|
|
159
|
-
// Normal points from the round shape toward the polytope, then flipped if A was the polytope.
|
|
160
|
-
const sx = flipped ? ux : -ux;
|
|
161
|
-
const sy = flipped ? uy : -uy;
|
|
162
|
-
const sz = flipped ? uz : -uz;
|
|
163
|
-
if (separation < bestSep) {
|
|
164
|
-
bestSep = separation;
|
|
165
|
-
nx = sx;
|
|
166
|
-
ny = sy;
|
|
167
|
-
nz = sz;
|
|
168
|
-
}
|
|
169
|
-
const at = count * 3;
|
|
170
|
-
out.points[at] = CLOSEST[0];
|
|
171
|
-
out.points[at + 1] = CLOSEST[1];
|
|
172
|
-
out.points[at + 2] = CLOSEST[2];
|
|
173
|
-
out.separations[count] = separation;
|
|
185
|
+
const radii = round.radius + poly.radius;
|
|
186
|
+
ROUND.count = 0;
|
|
187
|
+
ROUND.bestSep = Infinity;
|
|
188
|
+
ROUND.nx = 0;
|
|
189
|
+
ROUND.ny = 1;
|
|
190
|
+
ROUND.nz = 0;
|
|
191
|
+
ROUND.margin = margin;
|
|
192
|
+
ROUND.flipped = flipped;
|
|
193
|
+
for (let p = 0; p < points && p < 2; p++) {
|
|
194
|
+
const separation = signedDistance(poly, polyWorld, faces, roundWorld[p * 3] ?? 0, roundWorld[p * 3 + 1] ?? 0, roundWorld[p * 3 + 2] ?? 0, p) - radii;
|
|
195
|
+
END_SEP[p] = separation;
|
|
174
196
|
// The endpoint that produced it, which is stable while the capsule keeps its orientation.
|
|
175
|
-
out
|
|
176
|
-
count++;
|
|
197
|
+
emitRound(out, p, separation, 0x40000000 | p);
|
|
177
198
|
}
|
|
178
|
-
|
|
199
|
+
/*
|
|
200
|
+
* **A capsule is a segment, and its middle can be the part that touches.** This used to ask only
|
|
201
|
+
* its two ends, which is exact for a ball and for a capsule lying on a face, and wrong wherever the
|
|
202
|
+
* segment crosses an edge: a standing capsule walked through a slab across its waist, because
|
|
203
|
+
* neither end ball came within reach of it, and a capsule lying across a rail fell through it.
|
|
204
|
+
*
|
|
205
|
+
* The distance from a point on the segment to a convex polytope is convex along the segment, so
|
|
206
|
+
* its least value is at an end exactly when the slope there points away from the other end. Each
|
|
207
|
+
* end's direction away from the polytope is that slope, so a capsule whose ends are the answer, as
|
|
208
|
+
* one standing on a floor always is, costs nothing more. Otherwise a fixed number of golden-section
|
|
209
|
+
* steps finds the interior point, and it is kept only where it is nearer than both ends.
|
|
210
|
+
*/
|
|
211
|
+
if (points === 2) {
|
|
212
|
+
const ax = roundWorld[0] ?? 0;
|
|
213
|
+
const ay = roundWorld[1] ?? 0;
|
|
214
|
+
const az = roundWorld[2] ?? 0;
|
|
215
|
+
const sx = (roundWorld[3] ?? 0) - ax;
|
|
216
|
+
const sy = (roundWorld[4] ?? 0) - ay;
|
|
217
|
+
const sz = (roundWorld[5] ?? 0) - az;
|
|
218
|
+
const leavesFirst = sx * (END_DIR[0] ?? 0) + sy * (END_DIR[1] ?? 0) + sz * (END_DIR[2] ?? 0);
|
|
219
|
+
const leavesSecond = -(sx * (END_DIR[3] ?? 0) +
|
|
220
|
+
sy * (END_DIR[4] ?? 0) +
|
|
221
|
+
sz * (END_DIR[5] ?? 0));
|
|
222
|
+
if (leavesFirst < 0 && leavesSecond < 0) {
|
|
223
|
+
let lo = 0;
|
|
224
|
+
let hi = 1;
|
|
225
|
+
let m1 = hi - (hi - lo) * INV_PHI;
|
|
226
|
+
let m2 = lo + (hi - lo) * INV_PHI;
|
|
227
|
+
let f1 = signedDistance(poly, polyWorld, faces, ax + sx * m1, ay + sy * m1, az + sz * m1, 2);
|
|
228
|
+
let f2 = signedDistance(poly, polyWorld, faces, ax + sx * m2, ay + sy * m2, az + sz * m2, 2);
|
|
229
|
+
for (let step = 0; step < SEGMENT_STEPS; step++) {
|
|
230
|
+
if (f1 <= f2) {
|
|
231
|
+
hi = m2;
|
|
232
|
+
m2 = m1;
|
|
233
|
+
f2 = f1;
|
|
234
|
+
m1 = hi - (hi - lo) * INV_PHI;
|
|
235
|
+
f1 = signedDistance(poly, polyWorld, faces, ax + sx * m1, ay + sy * m1, az + sz * m1, 2);
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
lo = m1;
|
|
239
|
+
m1 = m2;
|
|
240
|
+
f1 = f2;
|
|
241
|
+
m2 = lo + (hi - lo) * INV_PHI;
|
|
242
|
+
f2 = signedDistance(poly, polyWorld, faces, ax + sx * m2, ay + sy * m2, az + sz * m2, 2);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
const at = (lo + hi) / 2;
|
|
246
|
+
const separation = signedDistance(poly, polyWorld, faces, ax + sx * at, ay + sy * at, az + sz * at, 2) - radii;
|
|
247
|
+
if (separation < Math.min(END_SEP[0] ?? Infinity, END_SEP[1] ?? Infinity) - 1e-7) {
|
|
248
|
+
emitRound(out, 2, separation, 0x40000000 | 2);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (ROUND.count === 0)
|
|
179
253
|
return false;
|
|
180
|
-
out.nx = nx;
|
|
181
|
-
out.ny = ny;
|
|
182
|
-
out.nz = nz;
|
|
183
|
-
out.count = count;
|
|
254
|
+
out.nx = ROUND.nx;
|
|
255
|
+
out.ny = ROUND.ny;
|
|
256
|
+
out.nz = ROUND.nz;
|
|
257
|
+
out.count = ROUND.count;
|
|
184
258
|
return true;
|
|
185
259
|
}
|
|
186
260
|
/* ---------- polytope against polytope ---------- */
|
package/dist/query.js
CHANGED
|
@@ -697,7 +697,15 @@ export function shapecastWorld(bodies, tree, shape, pose, dx, dy, dz, out, filte
|
|
|
697
697
|
}
|
|
698
698
|
else if (!collideShapes(shape, poseA, other, poseB, 1e9, manifold))
|
|
699
699
|
break;
|
|
700
|
-
|
|
700
|
+
/*
|
|
701
|
+
* **The nearest contact, not the first one written.** A capsule's manifold holds a point per
|
|
702
|
+
* end, and the first is its lower end: reading only that advanced the sweep by the distance
|
|
703
|
+
* from the lower ball while the upper one was the nearer, so a standing capsule swept clean
|
|
704
|
+
* through a beam at head height.
|
|
705
|
+
*/
|
|
706
|
+
let gap = Infinity;
|
|
707
|
+
for (let c = 0; c < manifold.count; c++)
|
|
708
|
+
gap = Math.min(gap, manifold.separations[c] ?? gap);
|
|
701
709
|
if (gap < 1e-4) {
|
|
702
710
|
/*
|
|
703
711
|
* A shape already touching a surface it is travelling *along* has not run into it.
|
|
@@ -713,7 +721,25 @@ export function shapecastWorld(bodies, tree, shape, pose, dx, dy, dz, out, filte
|
|
|
713
721
|
touched = true;
|
|
714
722
|
break;
|
|
715
723
|
}
|
|
716
|
-
|
|
724
|
+
/*
|
|
725
|
+
* **How far the shapes may close along the contact normal, not along the travel.** The
|
|
726
|
+
* manifold's normal separates the two by `gap`, so they cannot meet before the sweep has
|
|
727
|
+
* closed that much along it. Dividing by the whole travel was always safe and crawled
|
|
728
|
+
* wherever the travel grazes the surface: a ball dropping past the lip of a step closes on it
|
|
729
|
+
* at a sixth of its speed, and twenty-four steps ended before it arrived, so the sweep said
|
|
730
|
+
* nothing was there. A mesh keeps the old step, since one triangle's normal does not separate
|
|
731
|
+
* the whole mesh.
|
|
732
|
+
*/
|
|
733
|
+
if (isMesh(shape) || isMesh(other)) {
|
|
734
|
+
t += gap / travel;
|
|
735
|
+
}
|
|
736
|
+
else {
|
|
737
|
+
const closing = dx * manifold.nx + dy * manifold.ny + dz * manifold.nz;
|
|
738
|
+
// Not closing along the normal that separates them, so they never meet on this travel.
|
|
739
|
+
if (closing <= 1e-12)
|
|
740
|
+
break;
|
|
741
|
+
t += gap / closing;
|
|
742
|
+
}
|
|
717
743
|
if (t > 1)
|
|
718
744
|
break;
|
|
719
745
|
}
|
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;
|