From d4fc9bbb91c91eed8293a11ee0e5ca61f8618b84 Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Sun, 27 Sep 2026 00:08:36 +0300 Subject: [PATCH] fix(physics): push keeps the old collider contract exactly - a thing wholly under the feet or over the head is not in the way, anything else pushes out sideways (Jolt's axis when side-on, else radially by the overlap across) Co-Authored-By: Claude Opus 5.5 --- packages/ludic.physics/README.md | 2 +- .../lib/macos-arm64/libludicjolt.dylib | 2 +- .../ludic.physics/native/shim/jph_queries.inl | 33 +++++++++++++++---- .../ludic.physics/native/shim/jph_shim.cpp | 2 ++ packages/ludic.physics/queries.ludic | 3 +- .../ludic.physics/tests/physics_test.ludic | 9 +++-- 6 files changed, 36 insertions(+), 15 deletions(-) diff --git a/packages/ludic.physics/README.md b/packages/ludic.physics/README.md index 97c49abb..754fca53 100644 --- a/packages/ludic.physics/README.md +++ b/packages/ludic.physics/README.md @@ -39,7 +39,7 @@ import "ludic.physics" | `phys_ray(st, o, d, mask) -> PhysHit` | the first thing along a ray | | `phys_top_at(st, x, z, r, from_y, depth, mask) -> float` | what a foot would land on (`CharacterGround.top_at`), `PHYS_NONE` if nothing | | `phys_push(st, x, z, r, y0, y1, mask) -> PhysPush` | the move that stands an upright body clear (`CharacterGround.push`) | -| `phys_resolve(st, x, z, r, y0, y1, mask) -> bool`, `phys_resolved_x` / `_z` | pushed clear in two passes, the place kept (`CharacterGround.push`, `pushed_x` / `_z`); a contact more up than across is ground under the feet, not a wall | +| `phys_resolve(st, x, z, r, y0, y1, mask) -> bool`, `phys_resolved_x` / `_z` | pushed clear in two passes, the place kept (`CharacterGround.push`, `pushed_x` / `_z`); a thing wholly under the feet or over the head is not in the way | | `phys_step_top(st, x, z, r, feet, reach, mask) -> float` | the highest top a foot could step up to, anything taller than `feet + reach` left out as a wall | | `phys_overlap(st, x, y, z, r, mask) -> []int` | the bodies a ball touches | | `phys_float(st, id, buoyancy, linear_drag, angular_drag)`, `phys_sink`, `phys_floating` | a body buoyed each step against `PhysWater` and carried by its current | diff --git a/packages/ludic.physics/lib/macos-arm64/libludicjolt.dylib b/packages/ludic.physics/lib/macos-arm64/libludicjolt.dylib index ab8757a4..3a12b6ae 100755 --- a/packages/ludic.physics/lib/macos-arm64/libludicjolt.dylib +++ b/packages/ludic.physics/lib/macos-arm64/libludicjolt.dylib @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c84347121c46557ad468b6a4a86d9d7590e305c3982cd2f2d17d7b9747b0fe3a +oid sha256:fadb571f9e934ff8a9ba3407b6396d55cf1389f14e78af0a25dbc14f464c3289 size 1099936 diff --git a/packages/ludic.physics/native/shim/jph_queries.inl b/packages/ludic.physics/native/shim/jph_queries.inl index 180781c7..0009a312 100644 --- a/packages/ludic.physics/native/shim/jph_queries.inl +++ b/packages/ludic.physics/native/shim/jph_queries.inl @@ -66,18 +66,37 @@ JPH_SHIM int jph_push(void *w, float x, float z, float r, float y0, float y1, in ww->sys.GetNarrowPhaseQuery().CollideShape(&body, Vec3::sOne(), RMat44::sTranslation(RVec3(x, y0 + half, z)), cs, RVec3::sZero(), hits, {}, f); hits.Sort(); - // a contact more up than across is what the feet stand on (a rock's top, a dome under them), - // not a wall, and a touch with nothing in it is no contact + // a thing wholly below the feet (stood on) or above the head is not in the way; anything else + // pushes out sideways - along Jolt's own axis when the contact is side-on, else straight away + // from the thing's middle, by as much as the two overlap across (exact for an upright post) float px = 0.0f, pz = 0.0f; int n = 0; for (const CollideShapeResult &h : hits.mHits) { - if (h.mPenetrationDepth <= 1.0e-4f) continue; + BodyLockRead lock(ww->sys.GetBodyLockInterface(), h.mBodyID2); + if (!lock.Succeeded()) continue; + AABox bb = lock.GetBody().GetWorldSpaceBounds(); + if (float(bb.mMax.GetY()) <= y0 + 1.0e-3f || float(bb.mMin.GetY()) >= y1 - 1.0e-3f) continue; Vec3 a = h.mPenetrationAxis.NormalizedOr(Vec3::sAxisY()); Vec3 flat(a.GetX(), 0.0f, a.GetZ()); - if (flat.Length() < 0.5f) continue; - flat = flat.Normalized() * h.mPenetrationDepth; - px -= flat.GetX(); - pz -= flat.GetZ(); + float dx, dz; + if (flat.Length() >= 0.5f && h.mPenetrationDepth > 1.0e-4f) { + flat = flat.Normalized() * h.mPenetrationDepth; + dx = -flat.GetX(); + dz = -flat.GetZ(); + } else { + Vec3 c = bb.GetCenter(); + float ex = x - float(c.GetX()), ez = z - float(c.GetZ()); + float d = std::sqrt(ex * ex + ez * ez); + float half = std::max(float(bb.GetExtent().GetX()), float(bb.GetExtent().GetZ())); + float push = half + r - d; + if (push <= 1.0e-4f) continue; + float ux = 1.0f, uz = 0.0f; + if (d > 1.0e-6f) { ux = ex / d; uz = ez / d; } + dx = ux * push; + dz = uz * push; + } + px += dx; + pz += dz; n++; } out[0] = px; diff --git a/packages/ludic.physics/native/shim/jph_shim.cpp b/packages/ludic.physics/native/shim/jph_shim.cpp index 09d1857a..31897e90 100644 --- a/packages/ludic.physics/native/shim/jph_shim.cpp +++ b/packages/ludic.physics/native/shim/jph_shim.cpp @@ -25,6 +25,8 @@ #include #include #include +#include +#include #if defined(_WIN32) #define JPH_SHIM extern "C" __declspec(dllexport) diff --git a/packages/ludic.physics/queries.ludic b/packages/ludic.physics/queries.ludic index 7c2ce493..ca588f87 100644 --- a/packages/ludic.physics/queries.ludic +++ b/packages/ludic.physics/queries.ludic @@ -75,7 +75,8 @@ export function phys_push(physics_st: mut PhysicsState, x: float, z: float, r: f # a body r wide from y0 to y1 at (x, z) moved clear of what it stands in, two passes over (a push out # of one thing can put it into another); true when it moved, the place in phys_resolved_x / _z - -# what CharacterGround.push and pushed_x / _z ask +# what CharacterGround.push and pushed_x / _z ask. A thing wholly below y0 or above y1 is not in +# the way export function phys_resolve(physics_st: mut PhysicsState, x: float, z: float, r: float, y0: float, y1: float, mask: int) -> bool { var px = x var pz = z diff --git a/packages/ludic.physics/tests/physics_test.ludic b/packages/ludic.physics/tests/physics_test.ludic index ab19eab4..70672fb1 100644 --- a/packages/ludic.physics/tests/physics_test.ludic +++ b/packages/ludic.physics/tests/physics_test.ludic @@ -111,25 +111,24 @@ program PhysicsTest { phys_close(physics_st) } - test "a body is resolved clear of a post, and standing on a rock is not being in it" (physics_st: mut PhysicsState) { + test "a body is resolved clear of a post; a low rock blocks from the ground and not once stood on" (physics_st: mut PhysicsState) { ground(physics_st) let post = phys_offset(physics_st, phys_cylinder(physics_st, 1.0, 0.3), 0.0, 1.0, 0.0, 0.0) phys_static_add(physics_st, post, 0.0, 0.0, 0.0, 0.0) let rock = phys_offset(physics_st, phys_cylinder(physics_st, 0.2, 1.0), 0.0, 0.2, 0.0, 0.0) phys_static_add(physics_st, rock, 5.0, 0.0, 0.0, 0.0) - phys_static_add(physics_st, phys_dome(physics_st, 1.5, 0.6), -5.0, 0.0, 0.0, 0.0) phys_settle(physics_st) expect(phys_resolve(physics_st, 0.4, 0.1, 0.35, 0.0, 1.8, PHYS_M_STATIC)) let dx = phys_resolved_x(physics_st) let dz = phys_resolved_z(physics_st) expect(Math.sqrt(dx * dx + dz * dz) > 0.64) + expect(phys_resolve(physics_st, 5.2, 0.0, 0.35, 0.0, 1.8, PHYS_M_STATIC)) + expect(phys_resolved_x(physics_st) > 6.34) expect(not phys_resolve(physics_st, 5.2, 0.0, 0.35, 0.4, 2.2, PHYS_M_STATIC)) expect_eq(phys_resolved_x(physics_st), 5.2) expect(phys_resolve(physics_st, 6.3, 0.0, 0.35, 0.0, 1.8, PHYS_M_STATIC)) expect(phys_resolved_x(physics_st) > 6.34) - let top = phys_step_top(physics_st, -5.3, 0.0, 0.35, 0.0, 0.7, PHYS_M_STATIC) - expect(top > 0.4) - expect(not phys_resolve(physics_st, -5.3, 0.0, 0.35, top, top + 1.8, PHYS_M_STATIC)) + expect(not phys_resolve(physics_st, 5.0, 3.0, 0.35, 0.0, 1.8, PHYS_M_STATIC)) phys_close(physics_st) } }