Mesh to mesh collision

Hi!

I’m implementing a level editor in which I need to avoid the user to place objects overlapping other objects. There’re objects of really different sizes so user could, for example, place a bottle below a table so I need detailed collision checks so some objects could be inside the gaps of other.

I’ve tried using Node.collideWith(Collidable, CollisionResults) which ends up with an UnsupportedCollisionException if using any spatial as collidable parameter (it expects Ray or BoundingVolume). For this use case it’s not an option to use the bounding volume.

Also I’ve tried setting up physics (with minie) setting ghost controls with mesh collision shapes based on simplified low-res models and using GhostControl.getOverlappingObjects() but in the end uses AABBs so same here, not a good resolution collision check for this case

So here’s the question… Is there any way to do a mesh to mesh collision check in jme3 nowadays (or MeshCollisionShape to MeshCollisionShape in minie)? I know this could be a performance killer but I’m not using this in all frames, just in a callback when the user moves the object and not for all nodes in the scene, just a subset and also if a prior BB to BB collision is detected

Thanks

1 Like

JME doesn’t have anything like this built in and it’s a fairly complicated topic. (I have a book on my shelf that is devoted solely to mesh collisions… a whole book.)

You are probably better off representing your objects with simpler compound shapes in the physics engine and not using ghost controls.

OR when the small object is trying to intersect with the big one, model it as a handful of spheres that you use to collide with the scene with standard JME mesh stuff.

Thanks for your answer @pspeed

Yes, I know it’s a complicated topic, so I hoped there was something I could use already implemented in the engine.

I could use compound shapes or even simpler basic shapes for most (if not all) the objects, there’s no problem with that. What I don’t understand is how could I check the collisions among them. Also, should I use RigidBodyControl instead of ghost and fully use the physics?

My idea is to allow the user move everything freely but not allowing him to place the object in an inadequate way (colliding with stuff the object shouldn’t).

Current implementation is based on spatial.collideWith(bounding) works for most of the cases because I’m using as bounding parameter the smaller object between them but not as accurate as I would like it to be

Nah, the engine doesn’t have anything. (I’m not sure any game engine does, actually.) I guess games don’t usually have a use for an operation that would potentially take several seconds.

Your example of putting something on a table made me think of using a sphere to represent the bottom.

I’m not sure, but this might work for you:

CollisionResults bottleResults = new CollisionResults();
bottle.collideWith(table.getWorldBound(), bottleResults);

CollisionResults tableResults = new CollisionResults();
table.collideWith(bottle.getWorldBound(), tableResults);

int mult = bottleResults.size() * tableResults.size();
boolean collided = mult > 0;

In this case, mult will be greater than zero only if both CollisionResults detect something.

Minie implements mesh-mesh collisions. As you discovered, however, ghost overlap tests use AABB. Perhaps you could get exact collisions from ghosts, but I believe the best approach would be to use a contact test:

https://stephengold.github.io/Minie/javadoc/master/com/jme3/bullet/CollisionSpace.html#contactTest(com.jme3.bullet.collision.PhysicsCollisionObject,com.jme3.bullet.collision.PhysicsCollisionListener)

Not sure which would work best: GImpactCollisionShape or MeshCollisionShape.

Thank you all for your answers and proposals

@pspeed honestly, I don’t get your point :confused:

@fba I tried your approach and although it looks good, it’s giving me same collisions than my current method of using the smaller object bounding volume as parameter, let’s say always doing table.collideWith(bottle.getWorldBound())

@sgold I’ve tried the contactTest method but it’s returning 0 and the listener is not being called although there’s an obvious collision when having the collision shapes created as CollisionShapeFactory.createMeshShape (which in the end creates MeshCollisionShape objects). If I change it to CollisionShapeFactory.createDynamicMeshShape (which creates HullCollisionShape) it’s detected but again I’m losing resolution as the hull is bigger than the mesh. I’m using minie 4.0.2. Maybe mesh to mesh is implemented in a latter release?

1 Like

No, there haven’t been any major changes like that. Try using a GImpactCollisionShape; it has a constructor that takes JME meshes.

Thanks for the advice! I just tried it and GImpactCollisionShape is really detailed (almost same as MeshCollisionShape). Now that I’m using gimpact, the PhysicsCollisionListener.contact callback is called but contactTest always returns 0. Maybe an issue in minie?

If you think you’ve found a MInie issue, please send me a simple test that demonstrates the issue.

2 Likes

Here’s a simple test case for this:

package test;

import com.jme3.app.SimpleApplication;
import com.jme3.bullet.BulletAppState;
import com.jme3.bullet.collision.PhysicsCollisionEvent;
import com.jme3.bullet.collision.PhysicsCollisionListener;
import com.jme3.bullet.collision.shapes.CollisionShape;
import com.jme3.bullet.collision.shapes.GImpactCollisionShape;
import com.jme3.bullet.control.GhostControl;
import com.jme3.bullet.util.CollisionShapeFactory;
import com.jme3.input.KeyInput;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.KeyTrigger;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector3f;
import com.jme3.scene.Geometry;
import com.jme3.scene.Mesh;
import com.jme3.scene.Spatial;
import com.jme3.scene.shape.Box;

/**
 *
 * @author joliver82
 */
public class MinieContactTestSimpleTest extends SimpleApplication implements PhysicsCollisionListener, ActionListener
{
    private enum CollisionShapeMode
    {
        MESH, GIMPACT, DYNAMIC
    };
    
    private BulletAppState bullet;
    private int collisions=0;
    private GhostControl collisionTester;

    // Test stuff
    CollisionShapeMode collShapeMode=CollisionShapeMode.GIMPACT; // Change this to test other collision shapes
    
    public static void main(String[] args) {
        MinieContactTestSimpleTest app = new MinieContactTestSimpleTest();
        app.start();
    }
    
    @Override
    public void simpleInitApp()
    {
        flyCam.setDragToRotate(true);
        flyCam.setMoveSpeed(10);
        cam.setLocation(new Vector3f(0,10,10));
        cam.lookAt(Vector3f.ZERO, Vector3f.UNIT_Y);
        
        // Physics
        bullet=new BulletAppState();
        bullet.setDebugEnabled(true);
        bullet.setDebugCamera(cam);
        getStateManager().attach(bullet);
        //bullet.getPhysicsSpace().addCollisionListener(this);
        
        // Materials
        Material grayMat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
        grayMat.setColor("Color", ColorRGBA.LightGray);
        Material redMat=grayMat.clone();
        redMat.setColor("Color", ColorRGBA.Red);
        Material greenMat=grayMat.clone();
        greenMat.setColor("Color", ColorRGBA.Green);
        Material blueMat=grayMat.clone();
        blueMat.setColor("Color", ColorRGBA.Blue);
        
        // Scene
        Geometry floor=new Geometry("Floor");
        floor.setMesh(new Box(Vector3f.ZERO, 50, 0.1f, 50));
        floor.setMaterial(grayMat);
        rootNode.attachChild(floor);
        
        Spatial tmpSpat;
        tmpSpat=createCollidableObject(new Box(Vector3f.ZERO, 1, 1, 1), redMat, new Vector3f(-1.9f,1,0), collShapeMode);
        bullet.getPhysicsSpace().addCollisionObject(tmpSpat.getControl(GhostControl.class));
        rootNode.attachChild(tmpSpat);
        
        tmpSpat=createCollidableObject(new Box(Vector3f.ZERO, 1, 1, 1), greenMat, new Vector3f(0,1,0), collShapeMode);
        bullet.getPhysicsSpace().addCollisionObject(tmpSpat.getControl(GhostControl.class));
        collisionTester=tmpSpat.getControl(GhostControl.class);
        rootNode.attachChild(tmpSpat);
        
        tmpSpat=createCollidableObject(new Box(Vector3f.ZERO, 1, 1, 1), blueMat, new Vector3f(1.9f,1,0), collShapeMode);
        bullet.getPhysicsSpace().addCollisionObject(tmpSpat.getControl(GhostControl.class));
        rootNode.attachChild(tmpSpat);
        
        // setup keys
        getInputManager().addMapping("Overlap", new KeyTrigger(KeyInput.KEY_O));
        getInputManager().addListener(this,"Overlap");
    }
    
    private Spatial createCollidableObject(Mesh mesh, Material mat, Vector3f position, CollisionShapeMode mode)
    {
        Geometry geom=new Geometry("");
        geom.setMesh(mesh);
        geom.setMaterial(mat);
        geom.setLocalTranslation(position);
        
        CollisionShape collShape;
        switch(mode)
        {
            case MESH:
                collShape=CollisionShapeFactory.createMeshShape(geom);
                break;
            case GIMPACT:
                collShape=new GImpactCollisionShape(mesh);
                break;
            case DYNAMIC:
            default:
                collShape=CollisionShapeFactory.createDynamicMeshShape(geom);
                break;
        }
        
        GhostControl gc=new GhostControl(collShape);
        geom.addControl(gc);
        
        return geom;
    }

    @Override
    public void collision(PhysicsCollisionEvent event)
    {
        ++collisions;
        System.out.println("collision callback was run " + collisions + " times");
    }

    @Override
    public void onAction(String name, boolean isPressed, float tpf)
    {
        if("Overlap".equals(name) && isPressed)
        {
            // Run contact test over a predefined object
            collisions=0;
            if(collisionTester!=null)
            {
                int contactTests=bullet.getPhysicsSpace().contactTest(collisionTester, this);
                System.out.println("Got " + contactTests + " from contactTest");
            }
        }
    }
}

To test with different collision shapes, change the field collShapeMode to either MESH, GIMPACT or DYNAMIC

Notice the line //bullet.getPhysicsSpace().addCollisionListener(this); which is commented on purpose as I don’t want it to be reacting to all events but just after I’ve called contactTest

These are my results:

  • For dynamic (hull collision shape) → contactTest returns 2 and the collision callback is called twice (correct)
  • For mesh → contactTest returns 0 and the collision callback is not called (issue)
  • For gimpact → same behaviour as mesh

If I add the collision listener to the physics space (bullet.getPhysicsSpace().addCollisionListener(this);) the collision callback is called continuously (because there are collisions) but returned value of contactTest keep the same (0 for both mesh and gimpact)

Thanks :wink:

1 Like

Sorry about the long silence. I started investigating this but didn’t get very far. I think your code is okay. The issue is probably in native Bullet, but I haven’t found the root cause yet.

Edit: with the help of GDB, I identified the root cause for GImpact shapes. When GImpact registers itself with the collision dispatcher, it registers a contact-point algorithm but not a closest-point algorithm. The contact-test method request a closest-point algorithm (counterintuitive!) from the dispatcher. The dispatcher, finding none for GImpact shapes, dispatches btEmptyAlgorithm, which does nothing.

Modifying GImpact to register CreateFunc as a closest-point algorithm caused the contact test to return a positive count for GImpact shapes. Before committing this fix, I plan to investigate the issue with MeshCollisionShape.

Edit^2: GDB helped uncover the root cause for MeshCollisionShape, but it’s harder to address. Even though page 16 of the Bullet 2.83 Physics SDK Manual says that “the entire matrix is filled”, the actual code in btDefaultCollisionConfiguration::getClosestPointsAlgorithmCreateFunc() fails to find an algorithm when both shapes are “concave” (as MeshCollisionShape is).

I’ve no desire to implement a “concaveconcave” algorithm in C++, so I’ll probably just document the limitation of MeshCollisionShape and move on.

Edit^3: I opened Libbulletjme issue 7, which should be fixed in Minie v4.2 (soon).

Also, investigation revealed that collisions between HeightfieldCollisionShape, MeshCollisionShape, and PlaneCollisionShape objects are never detected, since Bullet doesn’t supply collision algorithms for them. But as far as I know, all shapes (including those 3) do collide with compound shapes, convex shapes, and GImpact shapes—or will once I fix issue 7.

4 Likes

Don’t worry about the delay, I understand it’s a really complex topic and requires time.

Thanks for investigating it and giving so many details about. I’m glad you found the cause of the issue.

I’ll update to Minie 4.2 as soon as it’s released :wink:

1 Like

v4.2 has been released. Contact tests involving GImpact shape should work now.

Note that for contact tests, the test object needn’t be added to the space.

As always, I have concerns about the performance of GImpact. If your modeling workflow permits, I think it might be best to decompose your 3-D models into convex meshes, either manually or else (as Paul suggested) using V-HACD. For many 3-D models, manual decomposition (in Blender) is fairly simple.

I’ve just tried latest 4.2.0 release and the gimpact collision is detected but for the testcase I wrote the callback is being called 379 times for gimpact while for dynamic and/or vhacd shapes it’s being called just twice. Is it a correct behaviour?

1 Like

Is it a correct behaviour?

Probably. This is yet another reason why Gimpact should be avoided if possible.

I’ve been thinking about creating a mechanism to report just the first detected contact, but that might involve extensive changes to Bullet.

In general, I guess the first detected hit is not always the best one… and determining the best one can be pretty subjective.

The nice thing about the simpler convex solids is that the number and types of contact points is going to be predictable or at least useful. With sphere being the best of all because from the sphere’s perspective you can always boil things down to one useful “contact point”.

Things get out of hand really quick otherwise…

image

2 Likes

OK, I’ll try changing it to v-hacd in my project and check the results. At a performance level is similar to any other convex shape and the only drawback is the time it takes to create the collision shape, right?

1 Like

the first detected hit is not always the best one

That’s very true when simulating contact forces. However, there are some situations (and I think joliver’s is one) where the application only cares about whether two collision objects are in contact or not. In which case, the test might as well return after detecting a single contact.

At a performance level is similar to any other convex shape and the only drawback is the time it takes to create the collision shape, right?

One nice thing about V-HACD is that you can generate the shape statically. You can do it once (when you convert the model to J3O, say) and then store the resulting shape in the J3O. So you should only worry about the dynamic performance, not how long it takes the generate the shape.

That said, V-HACD produces a compound shape composed of hulls. Dynamic performance will depend on the number of hulls and their complexity. I can’t guarantee good performance in all situations.

Yes, that’s my scenario, I just need to know if there’s collision or not as I’m not applying forces or simulating anything, just positioning assets in the scene

Thanks for the tip. I did the same for other stuff like scene lightprobes and similar but not for collision shapes as I was creating mesh shapes instead.

I’ll integrate this changes into my project and get back to you with the results (including performance related)

Thanks

1 Like