feat(physics): phys_step_top - the highest top a foot could step up to, walls left out (what CharacterGround.top_at asks)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-26 23:43:40 +03:00
parent 204ec93c5a
commit 1c814464c5
5 changed files with 66 additions and 2 deletions

Binary file not shown.

View file

@ -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"

View file

@ -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<World *>(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<CollideShapeCollector> 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<CastShapeCollector> 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;
}

View file

@ -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

View file

@ -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)
}
}