UPrimitiveComponent::ComputePenetration() returns incorrect FMTDResult - Engine bug?

If a UPrimitiveComponent is defined by more than one simple collision shape (for example, a skeletal mesh who’s collision geometry is defined by two capsules in the physics asset) then UPrimitiveComponent::ComputePenetration() will compute the FMTDResult using only the first capsule that has an overlap (ignoring the other capsule even if it has an overlap, too).
This results in an incorrect FMTDResult being returned because only the FMTDResult from the first capsule is returned.
Practically, what this means is that if you move an actor, call UPrimitiveComponent::ComputePenetration(), and then pull back the actor by the FMTDResult in order to remove any overlap with other actors, it doesn’t work reliably at all.

I consider this a bug. The proper version of UPrimitiveComponent::ComputePenetration() should sum the FMTDResult from all overlaps. The code fix should be fairly simple to implement inside FBodyInstance::OverlapPhysX_AssumesLocked(). That’s the function that loops over all physics shapes and (erroneously) returns once a single overlap is found.

Somewhere in engine code I believe it hints that this is actually a PhysX limitation and not an engine one - and that ComputePenetration can only compute for the root collision primitive. I could be wrong there, but I remember seeing something like that. I also don’t believe it works with non-simple collision shapes either (Box, Capsule and Sphere only).

Unless I’m missing a trick here then summing the overlaps probably wouldn’t be a solution - as IIRC the MTD result contains a normal and a ‘time’ to move the object back along that normal, so the only valid thing to return for multiple penetrations would be an array of normals and times to move the object, and which primitives it involves etc. Even then, that wouldn’t necessarily get it out of the collision in some cases, (especially when trapped between two walls for example). Suddenly moving objects out of penetration becomes very difficult…

I think in general it’s a performance vs accuracy problem. I can’t visualize a reliable way to move an object out of penetration if it has more than one colliding primitive.

I agree that moving out of penetration is hard. However, ignoring other penetrations to move out of a single penetration doesn’t seem ideal. Imagine a sphere that has been pushed into an overlapping condition with both walls in a right-angle corner. Summing the overlaps will at least give you the correct direction to move, even if the magnitude is slightly less than the full pullback required. Similarly, if a sphere gets sandwiched between two parallel walls, you don’t want to pick one wall and move the sphere into the other. You’d want to move into the middle to minimize the maximum penetration. Summing the overlaps would give you this behavior.

This is the UE4 engine code inside FBodyInstance::OverlapPhysX_AssumesLocked() which performs the PhysX calls. You can see that it’s returning only one overlap (whichever overlap happens to be first in the list. It’s not even the first overlap that occurs in time). I agree it could be a performance vs. accuracy problem, however, there should at least be an option to sum the overlaps if desired. As it stands, I can’t even use the routine because it straight up fails to remove simple overlaps about 5% of the time. I had to write my own version using PhysX directly (It’s not a PhysX problem - the Physx routine just does a geometry overlap test for which the algorithms are well documented in most real-time graphics books.)

// Iterate over each shape
for(int32 ShapeIdx=0; ShapeIdx<NumShapes; ++ShapeIdx)
{
    const PxShape* PShape = PShapes[ShapeIdx];
    check(PShape);

    if (IsShapeBoundToBody(PShape) == true)
    {
        PxVec3 POutDirection;
        float OutDistance;

        if(OutMTD)
        {
            if (PxGeometryQuery::computePenetration(POutDirection, OutDistance, PGeom, ShapePose, PShape->getGeometry().any(), GetPxTransform_AssumesLocked(PShape, RigidBody)))
            {
                //TODO: there are some edge cases that give us nan results. In these cases we skip
                if (!POutDirection.isFinite())
                {
                    POutDirection.x = 0.f;
                    POutDirection.y = 0.f;
                    POutDirection.z = 0.f;
                }

                OutMTD->Direction = P2UVector(POutDirection);
                OutMTD->Distance = FMath::Abs(OutDistance);

                if (GHillClimbError)
                {
                    LogHillClimbError(this, PGeom, ShapePose);
                }

                return true;    <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< This is the early return that simply returns one overlap result.
            }
        }
        else
        {
            if(PxGeometryQuery::overlap(PGeom, ShapePose, PShape->getGeometry().any(), GetPxTransform_AssumesLocked(PShape, RigidBody)))
            {
                return true;
            }
        }
    }
}