Include gap in body_margin, fixing mid-phase pruning of in-gap contacts.
The mid-phase BVH descent filter prunes body pairs using body_margin, which was compiled as the max over geom margins, excluding gap. Broadphase and the leaf-level test both use margin+gap, so any multi-geom body relying on gap could silently lose its in-gap contacts when raw AABBs don't overlap. Single-geom bodies take the leaf-leaf path and were unaffected. PiperOrigin-RevId: 946967548 Change-Id: I6d92baa296f1a83be68b4dbfd64a96d1c7efd3c4
This commit is contained in:
committed by
Copybara-Service
parent
f5f9d9efb7
commit
dddb2767c6
@@ -38,6 +38,8 @@ General
|
|||||||
|
|
||||||
Bug fixes
|
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.
|
- Fixed a bug in the mesh compiler where normals were scaled as vectors rather than covectors.
|
||||||
|
|
||||||
Version 3.10.0 (June 22, 2026)
|
Version 3.10.0 (June 22, 2026)
|
||||||
|
|||||||
@@ -690,7 +690,7 @@ typedef struct mjModel_ {
|
|||||||
mjtNum* body_inertia; // diagonal inertia in ipos/iquat frame (nbody x 3)
|
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_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_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)
|
mjtNum* body_user; // user data (nbody x nuser_body)
|
||||||
int* body_plugin; // plugin instance id; -1: not in use (nbody x 1)
|
int* body_plugin; // plugin instance id; -1: not in use (nbody x 1)
|
||||||
int* body_contype; // OR over all geom contypes (nbody x 1)
|
int* body_contype; // OR over all geom contypes (nbody x 1)
|
||||||
|
|||||||
@@ -378,7 +378,7 @@ typedef struct mjModel_ {
|
|||||||
mjtNum* body_inertia; // diagonal inertia in ipos/iquat frame (nbody x 3)
|
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_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_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)
|
mjtNum* body_user; // user data (nbody x nuser_body)
|
||||||
int* body_plugin; // plugin instance id; -1: not in use (nbody x 1)
|
int* body_plugin; // plugin instance id; -1: not in use (nbody x 1)
|
||||||
int* body_contype; // OR over all geom contypes (nbody x 1)
|
int* body_contype; // OR over all geom contypes (nbody x 1)
|
||||||
|
|||||||
@@ -1612,7 +1612,7 @@ STRUCTS: Mapping[str, StructDecl] = dict([
|
|||||||
type=PointerType(
|
type=PointerType(
|
||||||
inner_type=ValueType(name='mjtNum'),
|
inner_type=ValueType(name='mjtNum'),
|
||||||
),
|
),
|
||||||
doc='MAX over all geom margins',
|
doc='MAX over all geom margins+gaps',
|
||||||
array_extent=('nbody',),
|
array_extent=('nbody',),
|
||||||
),
|
),
|
||||||
StructFieldDecl(
|
StructFieldDecl(
|
||||||
|
|||||||
@@ -4539,7 +4539,7 @@ void mjCModel::FuseStatic(void) {
|
|||||||
for (const auto& geom : par->geoms) {
|
for (const auto& geom : par->geoms) {
|
||||||
par->contype |= geom->contype;
|
par->contype |= geom->contype;
|
||||||
par->conaffinity |= geom->conaffinity;
|
par->conaffinity |= geom->conaffinity;
|
||||||
par->margin = std::max(par->margin, geom->margin);
|
par->margin = std::max(par->margin, geom->margin + geom->gap);
|
||||||
}
|
}
|
||||||
|
|
||||||
// recompute BVH
|
// recompute BVH
|
||||||
|
|||||||
@@ -2780,7 +2780,7 @@ void mjCBody::Compile(void) {
|
|||||||
for (int i=0; i < geoms.size(); i++) {
|
for (int i=0; i < geoms.size(); i++) {
|
||||||
contype |= geoms[i]->contype;
|
contype |= geoms[i]->contype;
|
||||||
conaffinity |= geoms[i]->conaffinity;
|
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
|
// check conditions for free-joint alignment
|
||||||
|
|||||||
@@ -128,6 +128,44 @@ TEST_F(MjCollisionTest, ContactCount) {
|
|||||||
EXPECT_EQ(d->ncon, 8);
|
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"(
|
||||||
|
<mujoco>
|
||||||
|
<worldbody>
|
||||||
|
<body pos="0 0 1">
|
||||||
|
<freejoint/>
|
||||||
|
<geom name="A" type="box" size=".005 .01 .3" pos="-.015 0 0" gap=".004"/>
|
||||||
|
<geom name="far1" type="box" size=".005 .01 .3" pos="-.2 0 0"/>
|
||||||
|
</body>
|
||||||
|
<body pos="0 0 1">
|
||||||
|
<freejoint/>
|
||||||
|
<geom name="B" type="box" size=".005 .01 .3" pos="-.0022 0 0" gap=".004"/>
|
||||||
|
<geom name="far2" type="box" size=".005 .01 .3" pos=".1 0 0"/>
|
||||||
|
</body>
|
||||||
|
</worldbody>
|
||||||
|
</mujoco>
|
||||||
|
)";
|
||||||
|
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) {
|
TEST_F(MjCollisionTest, FilterParent) {
|
||||||
constexpr char xml[] = R"(
|
constexpr char xml[] = R"(
|
||||||
<mujoco>
|
<mujoco>
|
||||||
|
|||||||
Reference in New Issue
Block a user