Quick answer: Register a btGhostPairCallback on the broadphase’s overlapping pair cache. Without it, ghost objects never accumulate overlapping pairs.

A trigger volume uses a btGhostObject (or btPairCachingGhostObject). getNumOverlappingObjects() always returns 0 even when bodies are clearly inside.

Register the Ghost Pair Callback

btGhostPairCallback* ghostCallback = new btGhostPairCallback();
dynamicsWorld->getBroadphase()
    ->getOverlappingPairCache()
    ->setInternalGhostPairCallback(ghostCallback);

This is the step everyone misses. Without the callback installed on the broadphase, ghost objects are never told about their overlapping pairs.

Use btPairCachingGhostObject

For querying overlaps, use btPairCachingGhostObject — it caches the pairs so you can iterate them. Plain btGhostObject is more limited.

Add to the World With Flags

ghost->setCollisionFlags(ghost->getCollisionFlags()
    | btCollisionObject::CF_NO_CONTACT_RESPONSE);
dynamicsWorld->addCollisionObject(ghost,
    COL_TRIGGER, COL_PLAYER | COL_ENEMY);

CF_NO_CONTACT_RESPONSE makes it a sensor (no physical push). The group/mask args control what it detects.

Query Overlaps

for (int i = 0; i < ghost->getNumOverlappingObjects(); i++) {
    btCollisionObject* obj = ghost->getOverlappingObject(i);
    // note: AABB overlap — refine with a narrowphase test if needed
}

getOverlappingObjects gives broadphase (AABB) overlaps. For precise shape overlap, run a contactTest.

Verifying

Move a body into the trigger volume. getNumOverlappingObjects becomes non-zero. Leaving the volume drops it back. The ghost never pushes anything.

“Ghost objects need the ghost pair callback on the broadphase. That one registration makes overlaps work.”

getOverlappingObjects is AABB-level — for tight trigger shapes, always follow up with a contactTest or you’ll get false positives at the corners.