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 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-27 00:08:36 +03:00
parent 28c54e4b39
commit d4fc9bbb91
6 changed files with 36 additions and 15 deletions

View file

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

Binary file not shown.

View file

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

View file

@ -25,6 +25,8 @@
#include <mutex>
#include <thread>
#include <cfloat>
#include <cmath>
#include <algorithm>
#if defined(_WIN32)
#define JPH_SHIM extern "C" __declspec(dllexport)

View file

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

View file

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