Contact & ContactData.Validate()

Post and discuss features you'd like to see in the BEPUphysics library.
Post Reply
ecosky
Posts: 69
Joined: Fri Nov 04, 2011 7:13 am
Contact:

Contact & ContactData.Validate()

Post by ecosky »

Hi Norbo,

I recently had a problem with my code that was helped by the addition of Validate methods to the Contact and ContactData classes.

Code: Select all

	    [Conditional("CHECKMATH")]
	    public void Validate()
	    {
		    Position.Validate();
		    Normal.Validate();
		    PenetrationDepth.Validate();
	    }
Same code for both types.
I had to scatter calls to Validate wherever the contact was getting set up, as well as when they were added to the various containers via AddContact (ConvexContactManifoldConstraint/NonConvexContactManifoldConstraint.AddContact). I pretty much went through all of the Bepu code wherever either of these types were configured and added a Validate call, which was around 15 spots or so.

My problem was related to custom collider type setting NaN normals and not having them detected by a Validate until much later during code invoked by BEPUphysics.Constraints.Collision.NonConvexContactManifoldConstraint.ExclusiveUpdate().

This seems to me like a useful thing to keep around, I could give you a patch if you are interested but I suspect it'd be faster/better for you to just do the same search & changes yourself if you think the extra validation is worth having.
Norbo
Site Admin
Posts: 4929
Joined: Tue Jul 04, 2006 4:45 am

Re: Contact & ContactData.Validate()

Post by Norbo »

Done and done!
ecosky
Posts: 69
Joined: Fri Nov 04, 2011 7:13 am
Contact:

Re: Contact & ContactData.Validate()

Post by ecosky »

Awesome, thanks!
ecosky
Posts: 69
Joined: Fri Nov 04, 2011 7:13 am
Contact:

Re: Contact & ContactData.Validate()

Post by ecosky »

BTW I found part of the problem in my code which might possibly be of interest to you.

Down in GJKToolbox.GetClosestPoints, it was somehow coming up with closestA and closestB being the same. This caused the GeneralConvexPairTester.DoShallowContact to have a displacement of zero length which was stored in the contact.Normal and presumably normalized later causing the NaNs (this makes me think the contact.Validate should also require normal length > 0).

Here's a screenshot of a watch window showing (most of) the local variables at the end of GJKToolbox.GetClosestPoints when the error was detected (the one used with simplex caching, "public static bool GetClosestPoints(ConvexShape shapeA, ConvexShape shapeB, ref RigidTransform transformA, ref RigidTransform transformB, ref CachedSimplex cachedSimplex, out Vector3 closestPointA, out Vector3 closestPointB)" ) .
Image Unfortunately I didn't grab the TransformA, hopefully this will be of use.

This state is being reached in the context of a character cylinder colliding with a box. I don't know why closestA & closestB are the same yet, but since it is a legal possibility (at least two of the GetClosestPoints overloads called by the one described above can set them both to Toolbox.ZeroVector) this suggests the code in DoShallowContact needs to account for the possibility of a zero length displacement and handle it accordingly instead of propagating a degenerate normal since it will just turn into a NaN shortly afterward.

I suppose since both the cylinder and the box are axis aligned perhaps it is an edge case not seen too often. Just a guess. Anyway, it does kind of seem like something worth taking a look at sometime. I think for the short term when the displacement turns out to be zero length I'll either use a default normal or simply return false to disregard the contact, probably the latter but I'm a little concerned about breaking something either way. I'm curious what your take on all this might be. Thanks again!
Norbo
Site Admin
Posts: 4929
Joined: Tue Jul 04, 2006 4:45 am

Re: Contact & ContactData.Validate()

Post by Norbo »

this makes me think the contact.Validate should also require normal length > 0
Added.
I don't know why closestA & closestB are the same yet, but since it is a legal possibility (at least two of the GetClosestPoints overloads called by the one described above can set them both to Toolbox.ZeroVector) this suggests the code in DoShallowContact needs to account for the possibility of a zero length displacement and handle it accordingly instead of propagating a degenerate normal since it will just turn into a NaN shortly afterward.
When shapes are intersecting, GJK loses its ability to determine a 'closest point' because there's actually an intersecting volume. It can't converge towards the origin anymore because the extreme points used to refine the simplex are always on the surface of the shape. In this case, it just returns a point known to be in both shapes. This is represented by setting the minkowksi space closest points to zero.

In this case, the method should return true. DoShallowContact listens for this and switches to the DoDeepContact state if needed. This prevents DoShallowContact from having to deal with a zero length displacement. Further, the GJK method actually determines whether or not the shapes are intersecting by checking the displacement distance, so all such instances should be caught.

I would not be terribly surprised to hear of a numerical problem interfering with this logic, but I cannot seem to reproduce the problem. Replicating it is made trickier by the stateful nature of the collision pair; to get the same results, I would need to reproduce the simulation leading up to the failure. If you can get this to happen in the demos semireliably, that would be very helpful.
ecosky
Posts: 69
Joined: Fri Nov 04, 2011 7:13 am
Contact:

Re: Contact & ContactData.Validate()

Post by ecosky »

Thanks for the explanation.

As for providing a repro, this seems to be a pretty rare thing to have happen. I only seem to get it after hours of heavy load testing, and unpredictably at that, because I haven't yet been able to figure out the inputs that cause the problem. I will try to get more information. I did add something that I think might be a usable solution to the problem when it does come up however, which is to change this (in GeneralConvexPairTester.DoShallowContact)

Code: Select all

        if (distanceSquared < margin * margin)
to this:

Code: Select all

	if (distanceSquared < margin * margin && distanceSquared > Toolbox.Epsilon)
Since adding this and doing load tests since yesterday, I haven't had an exception. Does this seem like a reasonable (or correct) thing to have in here to catch the occasional fluke zero/near zero length displacement? I will add a bit of logic to trigger a breakpoint when these conditions hit to try to get more information about what is happening so if there is anything you'd like me to look at when it does please let me know. Unfortunately I am so far behind schedule I have to move on to other things ASAP and the extra check above seems to do the trick so regretfully I can't set up a demo to try to trigger this behavior in a more controlled situation.
Norbo
Site Admin
Posts: 4929
Joined: Tue Jul 04, 2006 4:45 am

Re: Contact & ContactData.Validate()

Post by Norbo »

Since adding this and doing load tests since yesterday, I haven't had an exception. Does this seem like a reasonable (or correct) thing to have in here to catch the occasional fluke zero/near zero length displacement?
That would work, though in the rare case where the failure happens, one frame's collision would be missed. To avoid this, the check could be moved to the DoDeepContact if statement:

Code: Select all

            if (UseSimplexCaching)
                GJKToolbox.GetClosestPoints(collidableA.Shape, collidableB.Shape, ref collidableA.worldTransform, ref collidableB.worldTransform, ref cachedSimplex, out closestA, out closestB);
            else
            {
                //The initialization of the pair creates a pretty decent simplex to start from.
                //Just don't try to update it.
                CachedSimplex preInitializedSimplex = cachedSimplex;
                GJKToolbox.GetClosestPoints(collidableA.Shape, collidableB.Shape, ref collidableA.worldTransform, ref collidableB.worldTransform, ref preInitializedSimplex, out closestA, out closestB);
            }
            
            Vector3 displacement;
            Vector3.Subtract(ref closestB, ref closestA, out displacement);
            float distanceSquared = displacement.LengthSquared();

            if (distanceSquared < Toolbox.Epsilon)
            {
                state = CollisionState.DeepContact;
                return DoDeepContact(out contact);
            }

            localDirection = displacement; //Use this as the direction for future deep contacts.
            float margin = collidableA.Shape.collisionMargin + collidableB.Shape.collisionMargin;


            if (distanceSquared < margin * margin)
            ...
I will add a bit of logic to trigger a breakpoint when these conditions hit to try to get more information about what is happening so if there is anything you'd like me to look at when it does please let me know.
Unfortunately there isn't much specific- I was going to do more of a 'know it when I see it' approach.

My best guess right now is that the final simplex.GetClosestPoints call in the GJKToolbox.GetClosestPoints method differs sufficiently in numerical terms to produce a near-zero offset even when the minkowski space offset is greater than epsilon. This would be exacerbated if the GJK error tolerance epsilons were tuned to be smaller, because the numerical error required to cause the problem would shrink.

If that guess is correct, then inserting a distanceSquared test to protect against it is still a fairly reasonable solution. In the interest of patching it up in the short term, I'm going to tentatively assume that this guess is correct and include the above protection.
ecosky
Posts: 69
Joined: Fri Nov 04, 2011 7:13 am
Contact:

Re: Contact & ContactData.Validate()

Post by ecosky »

Thanks Norbo, that makes sense to me. I appreciate your time as always!
Post Reply