@series-inc/rundot-syncplay 5.26.0-beta.4 → 5.26.0-beta.6
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/movement3d.js +93 -6
- package/dist/physics3d.js +11 -1
- package/package.json +1 -1
package/dist/movement3d.js
CHANGED
|
@@ -409,12 +409,34 @@ const KCC_GROUND_CLEARANCE = 1e-4;
|
|
|
409
409
|
* instead of the skyward fling an epsilon probe would give. Per axis it takes
|
|
410
410
|
* the steeper of forward/backward so a sample straddling an edge cannot
|
|
411
411
|
* average its way to an under-lift and back into the freeze.
|
|
412
|
+
*
|
|
413
|
+
* A sample that lands outside the selected body's own footprint falls back to
|
|
414
|
+
* the highest neighbouring static surface at that point. Without this a
|
|
415
|
+
* capsule straddling the seam between two abutting bodies (two heightfield
|
|
416
|
+
* chunks) is lifted only for the chunk under its centre: crossing a CONVEX
|
|
417
|
+
* crease downhill, the shallow chunk's lift collapses while the bottom sphere
|
|
418
|
+
* is still riding the steep chunk behind, the capsule is snapped down into
|
|
419
|
+
* that chunk, and the slide reverts the move — frozen exactly at the seam.
|
|
420
|
+
* Only undefined samples consult the fallback, so a single unbroken body (or
|
|
421
|
+
* a capsule beside a raised prop on continuous ground) behaves as before.
|
|
422
|
+
*
|
|
423
|
+
* The fallback is gated on continuity at the shared edge (see
|
|
424
|
+
* continuousNeighbourTopAt): streamed chunks meet their neighbours there, a
|
|
425
|
+
* wall or prop abutting the ground does not. Without the gate such a prop
|
|
426
|
+
* reads as an extreme slope and the lift snaps the capsule up its side.
|
|
412
427
|
*/
|
|
413
|
-
function kccSlopeClearance(body, x, z, top, geometry) {
|
|
428
|
+
function kccSlopeClearance(body, x, z, top, geometry, neighbourTopAt) {
|
|
414
429
|
const step = geometry.radius + geometry.skinWidth;
|
|
430
|
+
const sampleTopAt = (sampleX, sampleZ) => {
|
|
431
|
+
const own = groundContactTopAt(body, sampleX, sampleZ);
|
|
432
|
+
if (own !== undefined || neighbourTopAt === undefined) {
|
|
433
|
+
return own;
|
|
434
|
+
}
|
|
435
|
+
return continuousNeighbourTopAt(body, x, z, sampleX, sampleZ, neighbourTopAt);
|
|
436
|
+
};
|
|
415
437
|
const axisGradient = (deltaX, deltaZ) => {
|
|
416
|
-
const ahead =
|
|
417
|
-
const behind =
|
|
438
|
+
const ahead = sampleTopAt(x + deltaX, z + deltaZ);
|
|
439
|
+
const behind = sampleTopAt(x - deltaX, z - deltaZ);
|
|
418
440
|
const forward = ahead === undefined ? 0 : Math.abs(ahead - top) / step;
|
|
419
441
|
const backward = behind === undefined ? 0 : Math.abs(top - behind) / step;
|
|
420
442
|
return Math.max(forward, backward);
|
|
@@ -428,6 +450,55 @@ function kccSlopeClearance(body, x, z, top, geometry) {
|
|
|
428
450
|
// radius + skinWidth is the bottom-sphere radius physicsOverlapsKcc probes with.
|
|
429
451
|
return (geometry.radius + geometry.skinWidth) * (solverSqrt(1 + gradientSq) - 1);
|
|
430
452
|
}
|
|
453
|
+
/**
|
|
454
|
+
* How far two abutting bodies' surfaces may differ at their shared edge before
|
|
455
|
+
* the neighbour stops counting as walkable continuation. Fixed rather than
|
|
456
|
+
* derived from skinWidth (which defaults to 0): it only has to swallow
|
|
457
|
+
* triangulation and bisection noise at a streamed-terrain seam — millimetres
|
|
458
|
+
* at any sane gradient — while staying well under anything a game would call
|
|
459
|
+
* a step (`world.stepHeight`, typically 0.5+).
|
|
460
|
+
*/
|
|
461
|
+
const KCC_SEAM_CONTINUITY_TOLERANCE = 0.05;
|
|
462
|
+
/** Ray-bisection resolution for the footprint edge: 8 halvings of a sample step. */
|
|
463
|
+
const KCC_SEAM_EDGE_BISECTIONS = 8;
|
|
464
|
+
/**
|
|
465
|
+
* Neighbour surface at a sample point off the selected body's footprint, or
|
|
466
|
+
* undefined when no neighbour CONTINUES the selected surface there.
|
|
467
|
+
*
|
|
468
|
+
* Continuity is judged at the shared edge, not by extrapolating the selected
|
|
469
|
+
* surface — at a crease the two gradients differ by design, so any
|
|
470
|
+
* extrapolation test rejects exactly the seams this exists for. Bisect the
|
|
471
|
+
* centre→sample ray for the footprint edge, then compare the selected surface
|
|
472
|
+
* just inside it with the neighbour surface just outside: streamed chunks
|
|
473
|
+
* from one source interpolate the same shared edge row, so they match to
|
|
474
|
+
* float noise at ANY gradient, while a wall, prop, or cliff abutting the edge
|
|
475
|
+
* misses by its full height and is rejected. Fixed iteration count and plain
|
|
476
|
+
* double arithmetic keep it bit-identical on every peer.
|
|
477
|
+
*/
|
|
478
|
+
function continuousNeighbourTopAt(body, x, z, sampleX, sampleZ, neighbourTopAt) {
|
|
479
|
+
// The centre is on the selected body (its top is what kccSlopeClearance was
|
|
480
|
+
// given) and the sample is off it, so the ray crosses a footprint edge.
|
|
481
|
+
let inside = 0;
|
|
482
|
+
let outside = 1;
|
|
483
|
+
for (let i = 0; i < KCC_SEAM_EDGE_BISECTIONS; i += 1) {
|
|
484
|
+
const mid = (inside + outside) / 2;
|
|
485
|
+
if (groundContactTopAt(body, x + (sampleX - x) * mid, z + (sampleZ - z) * mid) === undefined) {
|
|
486
|
+
outside = mid;
|
|
487
|
+
}
|
|
488
|
+
else {
|
|
489
|
+
inside = mid;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
const innerTop = groundContactTopAt(body, x + (sampleX - x) * inside, z + (sampleZ - z) * inside);
|
|
493
|
+
const outerTop = neighbourTopAt(x + (sampleX - x) * outside, z + (sampleZ - z) * outside);
|
|
494
|
+
if (innerTop === undefined || outerTop === undefined) {
|
|
495
|
+
return undefined;
|
|
496
|
+
}
|
|
497
|
+
if (Math.abs(innerTop - outerTop) > KCC_SEAM_CONTINUITY_TOLERANCE) {
|
|
498
|
+
return undefined;
|
|
499
|
+
}
|
|
500
|
+
return neighbourTopAt(sampleX, sampleZ);
|
|
501
|
+
}
|
|
431
502
|
/**
|
|
432
503
|
* Distance from the capsule's centre to the bottom of its collision shape.
|
|
433
504
|
* A capsule's vertical extent is halfHeight + radius (see primitiveHalfExtent),
|
|
@@ -461,13 +532,29 @@ function staticGroundUnderKcc(physics, x, y, z, previousFeetY, geometry, ignored
|
|
|
461
532
|
hitKinematics: false,
|
|
462
533
|
};
|
|
463
534
|
const hitIds = new Set(overlapDeterministicPhysics3D(physics, probe));
|
|
464
|
-
const
|
|
465
|
-
.filter((body) => body.kind === 'static' && hitIds.has(body.id) && !ignoredBodyIds.has(body.id))
|
|
535
|
+
const hitBodies = physics.bodies
|
|
536
|
+
.filter((body) => body.kind === 'static' && hitIds.has(body.id) && !ignoredBodyIds.has(body.id));
|
|
537
|
+
const grounds = hitBodies
|
|
466
538
|
.map((body) => ({ body, top: groundContactTopAt(body, x, z) }))
|
|
467
539
|
.filter((candidate) => candidate.top !== undefined)
|
|
468
540
|
.map((candidate) => ({
|
|
469
541
|
...candidate,
|
|
470
|
-
|
|
542
|
+
// Where the selected body has no surface (a sample past its edge), read
|
|
543
|
+
// the highest neighbouring static surface instead — abutting terrain
|
|
544
|
+
// chunks share an edge, and the bottom sphere rides BOTH across a seam.
|
|
545
|
+
lift: kccSlopeClearance(candidate.body, x, z, candidate.top, geometry, (sampleX, sampleZ) => {
|
|
546
|
+
let neighbourTop;
|
|
547
|
+
for (const neighbour of hitBodies) {
|
|
548
|
+
if (neighbour.id === candidate.body.id) {
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
const top = groundContactTopAt(neighbour, sampleX, sampleZ);
|
|
552
|
+
if (top !== undefined) {
|
|
553
|
+
neighbourTop = neighbourTop === undefined ? top : Math.max(neighbourTop, top);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
return neighbourTop;
|
|
557
|
+
}),
|
|
471
558
|
}))
|
|
472
559
|
// The band is measured from where the capsule actually STANDS, so a slope
|
|
473
560
|
// lift bigger than snapDistance cannot drop it out of its own ground the
|
package/dist/physics3d.js
CHANGED
|
@@ -761,7 +761,11 @@ export function raycastDeterministicPhysics3DDetailed(world, query) {
|
|
|
761
761
|
*/
|
|
762
762
|
function bodyOverlapsQueryShape(body, shape, x, y, z, queryBounds) {
|
|
763
763
|
if (body.shape.type === 'heightfield') {
|
|
764
|
-
|
|
764
|
+
const mesh = heightfieldMeshShape(body.shape, localBoundsForHeightfield(body, queryBounds));
|
|
765
|
+
if (mesh.vertices.length === 0) {
|
|
766
|
+
return false;
|
|
767
|
+
}
|
|
768
|
+
return bodyOverlapsQueryShape({ ...body, shape: mesh }, shape, x, y, z, queryBounds);
|
|
765
769
|
}
|
|
766
770
|
if (shape.type === 'heightfield') {
|
|
767
771
|
return bodyOverlapsQueryShape(body, heightfieldMeshShape(shape), x, y, z, queryBounds);
|
|
@@ -3100,6 +3104,9 @@ function rayBodyDistance(body, query) {
|
|
|
3100
3104
|
}
|
|
3101
3105
|
if (body.shape.type === 'heightfield') {
|
|
3102
3106
|
const mesh = heightfieldMeshShape(body.shape, localBoundsForHeightfield(body, raySweepBounds(query)));
|
|
3107
|
+
if (mesh.vertices.length === 0) {
|
|
3108
|
+
return undefined;
|
|
3109
|
+
}
|
|
3103
3110
|
return rayMeshDistance(meshPose({ ...bodyPose(body), shape: mesh }, mesh), query);
|
|
3104
3111
|
}
|
|
3105
3112
|
if (body.shape.type === 'hull') {
|
|
@@ -3163,6 +3170,9 @@ function shapeCastBodyDistance(body, query) {
|
|
|
3163
3170
|
}
|
|
3164
3171
|
if (body.shape.type === 'heightfield') {
|
|
3165
3172
|
const mesh = heightfieldMeshShape(body.shape, localBoundsForHeightfield(body, shapeCastSweepBounds(query)));
|
|
3173
|
+
if (mesh.vertices.length === 0) {
|
|
3174
|
+
return undefined;
|
|
3175
|
+
}
|
|
3166
3176
|
return rayExpandedMeshDistance(meshPose({ ...bodyPose(body), shape: mesh }, mesh), query, query.shape.radius);
|
|
3167
3177
|
}
|
|
3168
3178
|
if (body.shape.type === 'sphere') {
|
package/package.json
CHANGED