diff --git a/packages/ludic.physics/lib/macos-arm64/libludicjolt.dylib b/packages/ludic.physics/lib/macos-arm64/libludicjolt.dylib index d6266968..47888bcc 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:8cc67c97be16787659f781337f0c80551aab3663b6a216e96ac43bcb3eecda47 -size 1099872 +oid sha256:7e7360edbd7cdec8cb8a7084914ddac37fcbb6d34c89947db0959477b25fbec5 +size 1099936 diff --git a/packages/ludic.physics/native.ludic b/packages/ludic.physics/native.ludic index 2b5a5ec7..abef00ef 100644 --- a/packages/ludic.physics/native.ludic +++ b/packages/ludic.physics/native.ludic @@ -30,3 +30,4 @@ extern function jph_ray(w: pointer, ox: float, oy: float, oz: float, dx: float, extern function jph_top_at(w: pointer, x: float, z: float, r: float, from_y: float, depth: float, mask: int) -> float = "jph_top_at" extern function jph_overlap(w: pointer, x: float, y: float, z: float, r: float, mask: int, ids: pointer, cap: int) -> int = "jph_overlap" extern function jph_push(w: pointer, x: float, z: float, r: float, y0: float, y1: float, mask: int, out: pointer) -> int = "jph_push" +extern function jph_step_top(w: pointer, x: float, z: float, r: float, feet: float, reach: float, mask: int) -> float = "jph_step_top" diff --git a/packages/ludic.physics/native/shim/jph_queries.inl b/packages/ludic.physics/native/shim/jph_queries.inl index 76642b22..e4ef2c75 100644 --- a/packages/ludic.physics/native/shim/jph_queries.inl +++ b/packages/ludic.physics/native/shim/jph_queries.inl @@ -79,3 +79,40 @@ JPH_SHIM int jph_push(void *w, float x, float z, float r, float y0, float y1, in out[1] = pz; return int(hits.mHits.size()); } + +// the highest top under an upright body r wide at (x, z) that a foot at `feet` could stand on or +// step up to: a thing whose top is above feet + reach is a wall and is left out, whatever else is +// there. Each candidate's surface is found by letting a ball r across down onto it alone. +// -FLT_MAX when there is nothing. +JPH_SHIM float jph_step_top(void *w, float x, float z, float r, float feet, float reach, int mask) { + World *ww = static_cast(w); + float limit = feet + reach; + float y0 = feet - 1.0f, y1 = limit + 0.05f; + float half = (y1 - y0) * 0.5f; + CylinderShape column(half, r, 0.0f); + column.SetEmbedded(); + AllHitCollisionCollector hits; + MaskFilter f(mask); + ww->sys.GetNarrowPhaseQuery().CollideShape(&column, Vec3::sOne(), RMat44::sTranslation(RVec3(x, y0 + half, z)), + CollideShapeSettings(), RVec3::sZero(), hits, {}, f); + hits.Sort(); + float best = -FLT_MAX; + SphereShape ball(r); + ball.SetEmbedded(); + float from = limit + r + 0.01f; + RShapeCast cast = RShapeCast::sFromWorldTransform(&ball, Vec3::sOne(), RMat44::sTranslation(RVec3(x, from, z)), + Vec3(0.0f, y0 - from, 0.0f)); + for (const CollideShapeResult &h : hits.mHits) { + BodyLockRead lock(ww->sys.GetBodyLockInterface(), h.mBodyID2); + if (!lock.Succeeded()) continue; + const Body &b = lock.GetBody(); + float top = float(b.GetWorldSpaceBounds().mMax.GetY()); + if (top > limit + 1.0e-4f) continue; + ClosestHitCollisionCollector c; + b.GetTransformedShape().CastShape(cast, ShapeCastSettings(), RVec3::sZero(), c); + float y = c.HadHit() ? float(c.mHit.mContactPointOn2.GetY()) : top; + if (y > top) y = top; + if (y > best) best = y; + } + return best; +} diff --git a/packages/ludic.physics/queries.ludic b/packages/ludic.physics/queries.ludic index 5febb98f..42e251da 100644 --- a/packages/ludic.physics/queries.ludic +++ b/packages/ludic.physics/queries.ludic @@ -35,6 +35,16 @@ export function phys_top_at(physics_st: PhysicsState, x: float, z: float, r: flo return y } +# the highest top under an upright body r wide that a foot at `feet` could stand on or step up +# to, or PHYS_NONE: anything whose top is above feet + reach is a wall and left out - what +# CharacterGround.top_at asks when a boulder may be climbed and a cliff may not +export function phys_step_top(physics_st: PhysicsState, x: float, z: float, r: float, feet: float, reach: float, mask: int) -> float { + if physics_st.ph_world == null { return PHYS_NONE } + let y = jph_step_top(physics_st.ph_world, x, z, r, feet, reach, mask) + if y < PHYS_NONE { return PHYS_NONE } + return y +} + # the bodies a ball at (x, y, z) touches, in the order Jolt sorts them export function phys_overlap(physics_st: mut PhysicsState, x: float, y: float, z: float, r: float, mask: int) -> []int { let out = new []int diff --git a/packages/ludic.physics/tests/physics_test.ludic b/packages/ludic.physics/tests/physics_test.ludic index 5accf4fe..c183bdf7 100644 --- a/packages/ludic.physics/tests/physics_test.ludic +++ b/packages/ludic.physics/tests/physics_test.ludic @@ -94,4 +94,20 @@ program PhysicsTest { expect_eq(clear.n, 0) phys_close(physics_st) } + + test "a step's top counts, a wall's does not, and the step beside a wall still does" (physics_st: mut PhysicsState) { + ground(physics_st) + let step = phys_offset(physics_st, phys_cylinder(physics_st, 0.15, 0.8), 0.0, 0.15, 0.0, 0.0) + let wall = phys_offset(physics_st, phys_cylinder(physics_st, 1.5, 0.8), 0.0, 1.5, 0.0, 0.0) + phys_static_add(physics_st, step, 0.0, 0.0, 0.0, 0.0) + phys_static_add(physics_st, wall, 1.5, 0.0, 0.0, 0.0) + phys_static_add(physics_st, wall, 5.0, 0.0, 0.0, 0.0) + phys_settle(physics_st) + expect(Math.abs(phys_step_top(physics_st, 0.0, 0.0, 0.35, 0.0, 0.55, PHYS_M_STATIC) - 0.3) < 0.02) + expect(Math.abs(phys_step_top(physics_st, 0.6, 0.0, 0.35, 0.0, 0.55, PHYS_M_STATIC) - 0.3) < 0.02) + expect(phys_step_top(physics_st, 5.0, 0.0, 0.35, 0.0, 0.55, PHYS_M_STATIC) == PHYS_NONE) + expect(phys_step_top(physics_st, -5.0, 0.0, 0.35, 0.0, 0.55, PHYS_M_STATIC) == PHYS_NONE) + expect(Math.abs(phys_step_top(physics_st, 5.0, 0.0, 0.35, 2.9, 0.55, PHYS_M_STATIC) - 3.0) < 0.02) + phys_close(physics_st) + } }