bepu / bepuphysics2

Pure C# 3D real time physics simulation library, now with a higher version number.
Apache License 2.0
2.39k stars 273 forks source link

Dual-sided Mesh Ray and Collision testing #134

Open Frooxius opened 3 years ago

Frooxius commented 3 years ago

Hello!

Would it be possible to implement support for Mesh colliders to be dual-sided for both ray and collision testing?

The Surprises part in the Q&A mentions that Sweep tests are the odd one out and always being dual sided, but I'd be actually interested in having it the other way around - have all interactions with triangles be dual-sided.

For Raycasts, I managed to implement a simple way to do that myself (although I'm not sure if it's the best approach), for Collisions I haven't dug enough into it and I'm not too sure what hurdles I can run into it there, but I wanted to ask if this would be considered for official functionality.

If it helps, in my use-case I don't need it to be configurable per-mesh, so having it as conditional compilation option would be sufficient for me, although having it configurable per-mesh could give more flexibility (though I'm not certain at what cost to code complexity and performance).

RossNordby commented 3 years ago

No plans for an 'official' double sided implementation at the moment. It was dropped for simplicity, because users would often default to it and get surprised by odd behavior (ragdolls getting partially stuck through the ground), and because it can be emulated by using two triangles with opposing windings.

To implement such a thing, the following pair testers would need to be updated: SphereTriangleTester CapsuleTriangleTester BoxTriangleTester TrianglePairTester TriangleConvexHullTester TriangleCylinderTester

The testers vary widely in complexity. Making all triangles double sided would be relatively easy, if I am remembering correctly- removing any early outs and avoiding the contact count zeroing for normals opposing the triangle normal. But all pairs are fully vectorized, and the more complex pairs are... more complex.

There might be some other hidden things, like in CompoundMeshReduction/MeshReduction, but I don't remember any off the top of my head.

I'd probably just try doubling up triangles where necessary. If that's good enough, it would be a whole lot easier.

Frooxius commented 3 years ago

I see, thanks so much for the info! I'll just go for the doubling for now, if there are problems with that or optimizations are needed (e.g. using less memory), then I could put in the effort to implement it directly. On the plus side it makes it also easier to support both double sided and single sided as a toggle!

Frooxius commented 3 years ago

I've ended up running into an odd issue with such dual-sided meshes in combination with character controller. When the triangles are duplicated only with reverse winding, in some cases this seems to cause the capsule shape to "bump" into a side of the triangle on the opposite side.

I've got simple reproduction case with a quad ramp mesh which exhibits this behavior, walking on it ends up in the user not being able to pass from one triangle to the other. It doesn't happen if it's single sided if the speculative margin is set to 0. Increasing the speculative margin for the mesh collider eventually ends up improving the behavior, where it only causes a small bump:

https://youtu.be/GxQ7jYOtFzU

I've been poking around, I found the MeshReduction.cs and CapsuleTriangleTester.cs and tried blindly adjusting some of the epsilons to see if it does anything, but without success, I'm not quite sure how exactly they work and what would I look for. I'm not entirely sure if that's where the issue comes from either, but it was my guess.

Is there a way to adjust the collision behavior to eliminate this issue completely? The speculative margin helps work around the issue, but I wanted to ask in case it's a simple change, as it would help eliminate even the small bumps with our existing content.

In this particular case it'd be definitely better to use a box collider or single sided-mesh since it's just a ramp, but this issue happens with a lot of other more complex collision geometry as well (and some of our users made the geometry itself dual-sided in their content, we can't change it to a single-sided one).

I've only picked this one since it's the simplest one to reproduce it. I can send some sample code to reproduce it in the Demos if needed too!

RossNordby commented 3 years ago

I've reproduced it locally; that's a bug in the CapsuleTriangleTester. A downward pointing triangle is generating a contact with a downward pointing normal, even though the capsule is clearly behind the triangle plane and should not generate any contact. I'll fix it soon.

RossNordby commented 3 years ago

Should be fixed in dfbb1962230fca0c94e3938f759e1607c05678d5, thanks for the report.

Frooxius commented 3 years ago

Oh you're awesome, thank you so much, this has fixed the issues completely! And here I thought I was doing something nasty :D

Frooxius commented 3 years ago

I did some more testing and I think there might be a bit of a side effect - some triangle sides don't seem to collide at all!

I've added a basic triangle to test:

 BufferPool.Take<Triangle>(1, out var triangleBuffer);

            triangleBuffer[0] = new Triangle(
                new Vector3(0f, 0f, 2f),
                new Vector3(-2f, 0f, -2f),
                new Vector3(2f, 0f, -2f));

            var mesh = new Mesh(triangleBuffer, Vector3.One, BufferPool);
            var meshCollidable = new CollidableDescription(Simulation.Shapes.Add(mesh), 0.1f);

            Simulation.Statics.Add(new StaticDescription(new Vector3(0f, 2f, 5f), Quaternion.Identity, meshCollidable));

The capsule collides with only one of the sides, but passes through others. I've recorded a show video to show it off: https://www.youtube.com/watch?v=PjSH2aCFlPg

From what I could figure out, the other edges end up having a pretty large edgeDepth (e.g. -0.9f) vs the one that colliders (that's around -0.1f in my tests) and end up failing this test:

Vector3Wide.ConditionalSelect(useEdge, edgeNormal, faceNormal, out var localNormal);
            Vector3Wide.Dot(localNormal, faceNormal, out var localNormalDotFaceNormal);
            var collidingWithSolidSide = Vector.GreaterThanOrEqual(localNormalDotFaceNormal, new Vector<float>(SphereTriangleTester.BackfaceNormalDotRejectionThreshold));
            if (Vector.EqualsAll(Vector.BitwiseAnd(collidingWithSolidSide, Vector.GreaterThanOrEqual(depth + a.Radius, -speculativeMargin)), Vector<int>.Zero))
            {
                //All contact normals are on the back of the triangle or the distance is too large for the margin, so we can immediately quit.
                manifold.Contact0Exists = Vector<int>.Zero;
                manifold.Contact1Exists = Vector<int>.Zero;
                return;
            }

What's curious is that when I rotate the vertices of the triangle, I'm still only able to collide with the same edge, so it doesn't seem like it would be any of the individual AB, AC and BC tests that would be problematic.

Right now it's not a big deal that would be causing me some issues (unless there's some indirect way this is causing other problems), but I wanted to share my findings!

RossNordby commented 3 years ago

I was wondering if you were going to run into that before I could improve it :P

That was expected behavior. Triangles, being one sided, didn't guarantee robust collision at 90 degree angles. Something like a 50/50 chance of actually generating contacts, the result being determined by whatever numerical breeze was passing at the moment.

But this behavior is pretty unpleasant- if you put two triangles with opposing winding on top of each other, it's a reasonable expectation that it'll act like a double sided triangle at the 90 degree cases. Instead, there's still a bit of a risk (depending on numerical context and pair type) that it won't generate contacts consistently.

I started prodding at this issue a little bit, since I don't think that behavior is acceptable. There's a new constant that the different pairs use now: https://github.com/bepu/bepuphysics2/blob/master/BepuPhysics/CollisionDetection/CollisionTasks/SphereTriangleTester.cs#L17

The goal is to be able to set that to a slightly negative value like -1e-5f, making perpendicular collisions reliably produce contacts. At the moment, I believe it would already work for spheres, capsules, and boxes. Unfortunately, triangle-triangle, convex hull-triangle, and cylinder-triangle all run into various nasty numerical corner cases related to speculative contact clipping and depths when the local normal is perfectly perpendicular. They either produce really bad depths or NaNs.

I've been working on this in the background. I believe I've got a workable path, but it's going to require some refiddling. I'm not sure if I'm going to extend support to triangle-triangle yet- that one's got its own form of nasty- but triangle-triangle is usually best avoided for other reasons, like triangles being infinitely thin.

(If you're wondering, that tuning constant is a step toward allowing double sided triangles directly, but there will be a few complexities left. Just setting it to -2 would work for a couple of pair types, but others would barf. Not sure if I'm going to push it all the way there or not. It turns out my original comment of "making all triangles double sided would be relatively easy" was relatively optimistic.)

Frooxius commented 3 years ago

Thank you for the explanation, it makes a lot more sense now (well at least conceptually)! I was trying to see what I can figure out myself, but it didn't make much sense to me, a lot of the physics math is currently more of a black magic to me ^^;

It'd definitely help with the consistency of behavior, but it's definitely not a critical issue from my end, just more of an oddity. It was essentially found out by an user pulling themselves halfway into a mesh collider and noticing they will get stopped by the triangle in one way, but not the other, but pulling yourself into a collider like that definitely isn't normal usecase :D

RossNordby commented 3 years ago

Alright, several nightmares of long-hidden bugs later, all triangle pairs should handle edge contacts gracefully.

The relevant tuning constant now lives in TriangleWide.BackfaceNormalDotRejectionThreshold: https://github.com/bepu/bepuphysics2/blob/master/BepuPhysics/Collidables/Triangle.cs#L168