Skip to content

Commit c32242f

Browse files
committed
toi/ccd: bug 9 fix (Liang-Barsky segment-AABB tunnel detection)
- Fix bug 9 (HULL_TET soak 0x2f59c53c): body tunnels PAST a thin wall — post-solve center ends on the far side (not inside any static's AABB). Center-in-AABB overlap check missed because body is no longer inside the wall volume. Fix: test whether the pre_pos -> post_pos center SEGMENT crosses any candidate static AABB (Liang-Barsky slab clip). Covers both "ended inside volume" (tunnel in) and "passed through volume" (tunnel through) in one test. Now 1248 tests pass.
1 parent 351daf0 commit c32242f

2 files changed

Lines changed: 73 additions & 8 deletions

File tree

src/tests.c

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13031,6 +13031,51 @@ static void test_post_ccd_hull_tet_not_inside_beam_5c801e62()
1303113031
destroy_world(w);
1303213032
}
1303313033

13034+
// Soak repro: spawn_rng=0x2f59c53c, HULL_TET scale=0.8853,
13035+
// vel=(110.143,2.834,4.490), omega=(-3.230,-4.335,-4.709). Body tunnels
13036+
// fully past wall (center ends on far side). Post-solve velocity is flipped
13037+
// (-0.93 in x) so velocity-based TOI sweep misses — body ends at (5.125)
13038+
// past wall back face (5.05). Fix: segment-AABB clip tunnel detection
13039+
// samples the pre_pos -> post_pos center trajectory.
13040+
static void test_post_ccd_hull_tet_no_tunnel_2f59c53c()
13041+
{
13042+
WorldParams wp = { .gravity = V3(0, -9.81f, 0), .broadphase = BROADPHASE_BVH };
13043+
World w = create_world(wp);
13044+
WorldInternal* wi = (WorldInternal*)w.id;
13045+
wi->sleep_enabled = 0;
13046+
13047+
Body floor_b = create_body(w, (BodyParams){ .position = V3(0, -1, 0), .rotation = quat_identity(), .mass = 0, .friction = 0.5f });
13048+
body_add_shape(w, floor_b, (ShapeParams){ .type = SHAPE_BOX, .box.half_extents = V3(20, 1, 20) });
13049+
Body wall = create_body(w, (BodyParams){ .position = V3(5.0f, 2.0f, 0), .rotation = quat_identity(), .mass = 0 });
13050+
body_add_shape(w, wall, (ShapeParams){ .type = SHAPE_BOX, .box.half_extents = V3(0.05f, 3.0f, 3.0f) });
13051+
Body beam = create_body(w, (BodyParams){ .position = V3(5.0f, 2.0f, 0), .rotation = quat_identity(), .mass = 0 });
13052+
body_add_shape(w, beam, (ShapeParams){ .type = SHAPE_BOX, .box.half_extents = V3(0.35f, 0.15f, 3.0f) });
13053+
13054+
v3 tet[] = { {0, 0.6f, 0}, {0.5f, -0.3f, 0.3f}, {-0.5f, -0.3f, 0.3f}, {0, -0.3f, -0.5f} };
13055+
Hull* h_tet = quickhull(tet, 4);
13056+
float s = 0.8853f;
13057+
Body b = create_body(w, (BodyParams){
13058+
.position = V3(-8.5f, 1.5f, 0), .rotation = quat_identity(),
13059+
.mass = 0.15f, .restitution = 0.25f, .friction = 0.4f,
13060+
});
13061+
body_add_shape(w, b, (ShapeParams){ .type = SHAPE_HULL, .hull = { .hull = h_tet, .scale = V3(0.18f*s, 0.18f*s, 0.18f*s) } });
13062+
body_set_velocity(w, b, V3(110.143f, 2.834f, 4.490f));
13063+
body_set_angular_velocity(w, b, V3(-3.230f, -4.335f, -4.709f));
13064+
13065+
float dt = 1.0f / 60.0f;
13066+
int tunneled = 0;
13067+
for (int frame = 0; frame < 30; frame++) {
13068+
world_step(w, dt);
13069+
v3 pos = body_get_position(w, b);
13070+
int in_wall_yz = fabsf(pos.y - 2.0f) < 3.0f && fabsf(pos.z) < 3.0f;
13071+
if (pos.x > 5.10f && in_wall_yz) { tunneled = 1; break; }
13072+
}
13073+
TEST_BEGIN("post-CCD hull_tet (soak 0x2f59c53c): no tunnel past wall");
13074+
TEST_ASSERT(!tunneled);
13075+
hull_free(h_tet);
13076+
destroy_world(w);
13077+
}
13078+
1303413079
static void run_post_ccd_tests()
1303513080
{
1303613081
printf("--- nudge post-CCD motion tests ---\n");
@@ -13041,6 +13086,7 @@ static void run_post_ccd_tests()
1304113086
test_post_ccd_capsule_not_wedged_814d37c7();
1304213087
test_post_ccd_capsule_not_frozen_4c9aeda6();
1304313088
test_post_ccd_hull_tet_not_inside_beam_5c801e62();
13089+
test_post_ccd_hull_tet_no_tunnel_2f59c53c();
1304413090
test_post_ccd_tet_not_stuck_in_beam();
1304513091
test_post_ccd_box_bullet_no_freeze_after_rebound();
1304613092
test_post_ccd_box_bullet_no_tunnel_soak_seed();

src/toi.c

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -730,20 +730,39 @@ static void toi_advance_one_body(WorldInternal* w, int k, float dt)
730730
AABB body_aabb_now = body_aabb(bs, bc);
731731
CK_DYNA int* cands = NULL;
732732
bvh_query_aabb(w->bvh_static, body_aabb_now, &cands);
733-
// "Overlapping" here means body CENTER is inside a static body's AABB.
734-
// This flags tunneling (body ended up INSIDE the volume) while ignoring
735-
// shallow gravity-induced surface penetration on resting bodies (center
736-
// stays outside the static AABB, only surface crosses it).
733+
// Tunnel detection: body CENTER either ends inside a static AABB, or
734+
// the pre→post CENTER path crosses fully through a static AABB (body
735+
// tunneled past a thin static). Sample 8 lerp points along the
736+
// center trajectory and check center-in-AABB at each; also test
737+
// whether the segment pre_pos→post_pos passes through any candidate
738+
// static's AABB (Liang-Barsky slab clip).
737739
int overlapping = 0;
740+
v3 post_pos_tmp = bs->position;
738741
for (int c = 0; c < asize(cands) && !overlapping; c++) {
739742
int sbi = cands[c];
740743
if (sbi == bi || !split_alive(w->body_gen, sbi)) continue;
741744
AABB sbbox = body_aabb(&w->body_state[sbi], &w->body_cold[sbi]);
742-
if (bs->position.x >= sbbox.min.x && bs->position.x <= sbbox.max.x
743-
&& bs->position.y >= sbbox.min.y && bs->position.y <= sbbox.max.y
744-
&& bs->position.z >= sbbox.min.z && bs->position.z <= sbbox.max.z) {
745-
overlapping = 1;
745+
// Liang-Barsky: segment p0 + t*(post-p0), find t-range inside AABB.
746+
v3 d = sub(post_pos_tmp, p0);
747+
float tmin = 0.0f, tmax = 1.0f;
748+
float p_origin[3] = { p0.x, p0.y, p0.z };
749+
float dd[3] = { d.x, d.y, d.z };
750+
float bmin[3] = { sbbox.min.x, sbbox.min.y, sbbox.min.z };
751+
float bmax[3] = { sbbox.max.x, sbbox.max.y, sbbox.max.z };
752+
int hit = 1;
753+
for (int a = 0; a < 3 && hit; a++) {
754+
if (fabsf(dd[a]) < 1e-12f) {
755+
if (p_origin[a] < bmin[a] || p_origin[a] > bmax[a]) hit = 0;
756+
} else {
757+
float t1 = (bmin[a] - p_origin[a]) / dd[a];
758+
float t2 = (bmax[a] - p_origin[a]) / dd[a];
759+
if (t1 > t2) { float tmp = t1; t1 = t2; t2 = tmp; }
760+
if (t1 > tmin) tmin = t1;
761+
if (t2 < tmax) tmax = t2;
762+
if (tmin > tmax) hit = 0;
763+
}
746764
}
765+
if (hit) overlapping = 1;
747766
}
748767
afree(cands);
749768
if (overlapping) {

0 commit comments

Comments
 (0)