diff --git a/doc/changelog.rst b/doc/changelog.rst index 903133b3..de44a0cb 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -38,6 +38,8 @@ General Bug fixes ^^^^^^^^^ +- Fixed a bug where ``body_margin`` excluded ``gap``, causing the mid-phase collision filter to incorrectly prune + in-gap contacts on multi-geom bodies. - Fixed a bug in the mesh compiler where normals were scaled as vectors rather than covectors. Version 3.10.0 (June 22, 2026) diff --git a/doc/includes/references.h b/doc/includes/references.h index 2ce25f06..faed9cd1 100644 --- a/doc/includes/references.h +++ b/doc/includes/references.h @@ -690,7 +690,7 @@ typedef struct mjModel_ { mjtNum* body_inertia; // diagonal inertia in ipos/iquat frame (nbody x 3) mjtNum* body_invweight0; // mean inv inert in qpos0 (trn, rot) (nbody x 2) mjtNum* body_gravcomp; // antigravity force, units of body weight (nbody x 1) - mjtNum* body_margin; // MAX over all geom margins (nbody x 1) + mjtNum* body_margin; // MAX over all geom margins+gaps (nbody x 1) mjtNum* body_user; // user data (nbody x nuser_body) int* body_plugin; // plugin instance id; -1: not in use (nbody x 1) int* body_contype; // OR over all geom contypes (nbody x 1) diff --git a/include/mujoco/mjmodel.h b/include/mujoco/mjmodel.h index 6dbd3789..1467e39e 100644 --- a/include/mujoco/mjmodel.h +++ b/include/mujoco/mjmodel.h @@ -378,7 +378,7 @@ typedef struct mjModel_ { mjtNum* body_inertia; // diagonal inertia in ipos/iquat frame (nbody x 3) mjtNum* body_invweight0; // mean inv inert in qpos0 (trn, rot) (nbody x 2) mjtNum* body_gravcomp; // antigravity force, units of body weight (nbody x 1) - mjtNum* body_margin; // MAX over all geom margins (nbody x 1) + mjtNum* body_margin; // MAX over all geom margins+gaps (nbody x 1) mjtNum* body_user; // user data (nbody x nuser_body) int* body_plugin; // plugin instance id; -1: not in use (nbody x 1) int* body_contype; // OR over all geom contypes (nbody x 1) diff --git a/python/mujoco/introspect/structs.py b/python/mujoco/introspect/structs.py index c71b461f..a9dfe2fc 100644 --- a/python/mujoco/introspect/structs.py +++ b/python/mujoco/introspect/structs.py @@ -1612,7 +1612,7 @@ STRUCTS: Mapping[str, StructDecl] = dict([ type=PointerType( inner_type=ValueType(name='mjtNum'), ), - doc='MAX over all geom margins', + doc='MAX over all geom margins+gaps', array_extent=('nbody',), ), StructFieldDecl( diff --git a/src/user/user_model.cc b/src/user/user_model.cc index add51056..6337f9cd 100644 --- a/src/user/user_model.cc +++ b/src/user/user_model.cc @@ -4539,7 +4539,7 @@ void mjCModel::FuseStatic(void) { for (const auto& geom : par->geoms) { par->contype |= geom->contype; par->conaffinity |= geom->conaffinity; - par->margin = std::max(par->margin, geom->margin); + par->margin = std::max(par->margin, geom->margin + geom->gap); } // recompute BVH diff --git a/src/user/user_objects.cc b/src/user/user_objects.cc index 297775a8..a61f35be 100644 --- a/src/user/user_objects.cc +++ b/src/user/user_objects.cc @@ -2780,7 +2780,7 @@ void mjCBody::Compile(void) { for (int i=0; i < geoms.size(); i++) { contype |= geoms[i]->contype; conaffinity |= geoms[i]->conaffinity; - margin = std::max(margin, geoms[i]->margin); + margin = std::max(margin, geoms[i]->margin + geoms[i]->gap); } // check conditions for free-joint alignment diff --git a/test/engine/engine_collision_driver_test.cc b/test/engine/engine_collision_driver_test.cc index 5aa9600e..949a99ca 100644 --- a/test/engine/engine_collision_driver_test.cc +++ b/test/engine/engine_collision_driver_test.cc @@ -128,6 +128,44 @@ TEST_F(MjCollisionTest, ContactCount) { EXPECT_EQ(d->ncon, 8); } +TEST_F(MjCollisionTest, InGapContactsMultiGeomBody) { + // In-gap contacts survive mid-phase BVH pruning. Both bodies + // have a "far" geom that pulls the BVH root box away, so detecting the A-B + // pair requires the descent filter to account for gap (body_margin). + constexpr char xml[] = R"( + + + + + + + + + + + + + + + )"; + char error[1024]; + MjModelPtr m = LoadModelFromString(xml, error, sizeof(error)); + ASSERT_THAT(m.get(), NotNull()) << error; + MjDataPtr d = MakeData(m); + ASSERT_THAT(d, NotNull()); + + mj_forward(m.get(), d.get()); + + // geoms A and B are separated by 2.8mm, within the 8mm combined gap: + // inactive contacts are expected + EXPECT_GT(d->ncon, 0); + for (int i = 0; i < d->ncon; i++) { + EXPECT_GT(d->contact[i].dist, 0); + EXPECT_LT(d->contact[i].dist, 0.008); + EXPECT_EQ(d->contact[i].efc_address, -1); + } +} + TEST_F(MjCollisionTest, FilterParent) { constexpr char xml[] = R"(