Testing Minie 7.7.1-SNAPSHOT

Hi @sgold , I am doing some testing on the next version of Minie.

  • getter and setter for the contact manager.
    It works fine

  • the 4-argument PhysicsSpace.addContactListener() method.
    It works fine

  • the PhysicsSpace.listManifolds() method.
    It seems to work, but I don’t know if I am using it correctly (see my gist). Could you provide an example to test it with please?

  • the contactTest() and pairTest() methods

I have some doubts about how the pairTest and contactTest methods work. When the application starts it seems to detect collisions between the floor and the ball. When I bounce the ball, the two functions no longer detect collisions when the ball touches the ground again. The two methods are invoked continuously in upate cycle. Is there an automatic passivation mechanism?

Here are the test classes:

1 Like

Thanks for your work. I’ll definitely study those tests and figure out what’s going on.

EDIT: You’ve got good ideas. This is an interesting test.

Here’s what I see when I run Test_Main2.java:

  1. Initially, with the ball is resting on the floor, contactTest() and pairTest() both report 1, and the console output shows a single onContactStarted (for that contact).
  2. When I press the space bar, the ball jumps upward. contactTest() and pairTest() both report 0 because the ball and the floor are no longer in contact. The console output shows onContactEnded, indicating that the initial contact has been broken.
  3. When the ball falls back to the floor, contactTest() and pairTest() briefly report 2 contacts, then one. The console output shows another onContactStarted, indicating the ball and the plane are back in contact.
  4. If I keep triggering jumps, eventually contactTest() and pairTest() will report zero contacts even when the ball appears to be at rest on the floor. The console output implies that there’s a contact.

The most interesting results are the zeroes reported at the end. Why don’t contactTest() and pairTest() report 1 contact, as they did in the beginning?

Well, there’s an important distinction between contactTest()/pairTest() and a ContactListener. A ContactListener reports actual contacts between objects in the space, whereas contactTest() and pairTest() report hypothetical contacts: intersections that would be created if the specified object(s) were added to the space in the specified position(s).

In “Test_Main2.java”, the collision objects being tested are already added to the space, which is not the intended use case for contactTest() and pairTest(). If I wanted to know how many contacts there were for objects already added to the space, I’d use countManifolds().

Is there an automatic passivation mechanism?

There’s an activation/sleep mechanism for rigid bodies, but it shouldn’t interfere with contact reporting. In fact, creation of a new contact is one of the things that will re-activate a sleeping body.

Here’s what I believe is happening …

In its rest position, the ball is roughly tangent with the floor, which for contactTest() and pairTest() is an edge case. Either the ball barely intersects the floor, or it barely doesn’t intersect; it’s difficult to say. Probably the distinction between no contacts and non-zero contacts boils down rounding errors.

A more typical use for contactTest() is when you want to add a collision object to the space, but only if it won’t intersect something that’s already there. (Intersecting rigid bodies tend to exhibit violent motion and/or unrealistic vibration.) So you do a contactTest() and then add the object only if the test returns zero. Perhaps you keep trying different positions at random until you find one that works, or perhaps the obstruction is mobile, and you wait for it to move out of the spawning area.

For an example of contactTest(), see the DropTest app in the MinieExamples project. Before a new “drop” (set of connected bodies) gets added to the space, the Drop.hasHullContacts() method is invoked to make sure the drop (actually its convex hull) won’t be entangled with objects previously added to the space.

I haven’t used pairTest() very much. I think a typical use case would be when you want to simultaneously add multiple collision objects to the space. In addition to a contactTest() for each object to be added, you’d also do a pairTest() for each pair of objects to be added, to see whether they’ll intersect with one another.

Does all this seem plausible to you?

1 Like

Another very useful and informative topic. Thank you Stephen. Yes, it is all clear to me. I have another feature I would like to suggest for Minie, but it is not about collisions. I am gathering information to show you what I have in mind. :wink:

1 Like

You could add a very useful function to the CollisionShapeFactory.makeMergedMesh(Spatial subtree) method that I also used in my library for NavMesh and also used in Unity.

I have a scene with multiple objects. Instead of setting JME_PHYSICSIGNORE on the individual geometries to be discarded, it is easier and faster to discard the entire node to which they belong. This way, I only need to set the property on the parent node instead of all the child geometries. This system is much easier and faster to use, especially in scenes with many geometries where it would be frustrating to add the JME_PHYSICSIGNORE property to each individual geometry.

I must discard all geometries contained in the nodes highlighted in red. I added the JME_PHYSICSIGNORE property to both nodes as you can see in the UserData list in the right panel.

        Node scene = (Node) assetManager.loadModel("Scenes/ChampionsField/rocket-league-stadium-small.j3o");
        scene.setShadowMode(ShadowMode.Receive);
        rootNode.attachChild(scene);
        scene.setLocalScale(glScale);
        
        CollisionShape shape = CollisionShapeFactory.createMergedMeshShape(scene);
        RigidBodyControl rb = new RigidBodyControl(shape, PhysicsBody.massForStatic);
        scene.addControl(rb);
        physics.getPhysicsSpace().add(rb);

Here is the code with the changes: CollisionShapeFactory.makeMergedMesh()

    private static List<Geometry> collectGeometries(Spatial spatial, List<Geometry> results) {
        /*
         * Exclude any Spatial tagged with "JmePhysicsIgnore".
         */
        Boolean ignore = spatial.getUserData(UserData.JME_PHYSICSIGNORE);
        if (ignore != null && ignore) {
            //System.out.println("Skipping.... " + spatial);
            return results;
        }
        if (spatial instanceof Geometry) {
            results.add((Geometry) spatial);

        } else if (spatial instanceof Node) {
            Node node = (Node) spatial;
            for (Spatial child : node.getChildren()) {
                collectGeometries(child, results);
            }
        }
        return results;
    }

    private static Mesh makeMergedMesh(Spatial subtree) {
        //List<Geometry> allGeometries = MySpatial.listGeometries(subtree);
        List<Geometry> allGeometries = collectGeometries(subtree, new ArrayList<Geometry>()); 
        
        Collection<Geometry> includedGeometries = new ArrayList<>(allGeometries.size());
        int totalIndices = 0;
        int totalVertices = 0;
        for (Geometry geometry : allGeometries) {
            /**
             * Exclude any Geometry tagged with "JmePhysicsIgnore"
             * or having a null/empty mesh.
             */
//            Boolean ignore = geometry.getUserData(UserData.JME_PHYSICSIGNORE);
//            if (ignore != null && ignore) {
//                continue;
//            }
            Mesh jmeMesh = geometry.getMesh();
            if (jmeMesh == null) {
                continue;
            }
            IndexBuffer indexBuffer = jmeMesh.getIndicesAsList();
            int numIndices = indexBuffer.size();
            if (numIndices == 0) {
                continue;
            }
            int numVertices = jmeMesh.getVertexCount();
            if (numVertices == 0) {
                continue;
            }

            includedGeometries.add(geometry);
            totalIndices += numIndices;
            totalVertices += numVertices;
        }

        IndexBuffer indexBuffer
                = IndexBuffer.createIndexBuffer(totalVertices, totalIndices);
        int totalFloats = numAxes * totalVertices;
        FloatBuffer positionBuffer = BufferUtils.createFloatBuffer(totalFloats);

        for (Geometry geometry : includedGeometries) {
            appendTriangles(geometry, subtree, positionBuffer, indexBuffer);
        }

        VertexBuffer.Format ibFormat = indexBuffer.getFormat();
        Buffer ibData = indexBuffer.getBuffer();
        Mesh result = new Mesh();
        result.setBuffer(VertexBuffer.Type.Index, MyMesh.vpt, ibFormat, ibData);
        result.setBuffer(VertexBuffer.Type.Position, numAxes, positionBuffer);

        return result;
    }

I modified the code and tested it. It works.

What do you think? :slight_smile:

2 Likes

I don’t see great value in implementing JME_PHYSICSIGNORE for nodes, since it’s trivial to obtain a list of the geometries under a given node.

However, it’s easy to implement and unlikely to break existing code, so I’ll do it.

EDIT: On further investigation, I noticed that createBoxShape(), createDynamicMeshShape(), and createMeshShape() already check for JME_PHYSICSIGNORE on nodes, so making createMergedMeshShape() do so will bring the added benefit of greater consistency.

Ready for testing:

cd Minie
git pull
./gradlew install

then rebuild any projects that depend on Minie-7.7.1-SNAPSHOT .

2 Likes

Hi @sgold ,
I am doing some testing with Minie and I have encountered 2 very strange problems using GhostControl.

  • Problem 1:
    GhostControl.setPhysicsLocation() doesn’t work, the location of the ghost doesn’t change.

  • Problem 2:
    When I change the position of the GhostControl/geom, the texture of the sphere stops rendering after a while. I tried to comment the command geo.setLocalTranslation(new Vector3f(0, 20, 0)); and the problem disappeared. I don’t understand what is causing the problem, JME or Minie? Or am I doing something wrong in the createCube() method?

With geo.setLocalTranslation(new Vector3f(0, 20, 0));

Without geo.setLocalTranslation(new Vector3f(0, 20, 0));

    private void createCube() {
        Box box = new Box(4, 4, 4);
        Geometry geo = new Geometry("Box", box);
        Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
        mat.setColor("Color", ColorRGBA.Red);
        mat.getAdditionalRenderState().setWireframe(true);
        geo.setMaterial(mat);
        rootNode.attachChild(geo);
        
        GhostControl ghost = addGhostControl(geo);
        ...
        geo.setLocalTranslation(new Vector3f(0, 20, 0));
//        ghost.setPhysicsLocation(new Vector3f(0, 20, 0)); //doesn't work
    }


Can you help me understand what is not working or if there is a bug please?
Updated source code in gist.

Edit:
I am using the latest version of Minie 7.8.0

1 Like

GhostControl is a “kinematic” control. As long as the control is added and enabled, the Spatial moves the control, instead of vice versa.

Upshot: use getSpatial().setLocalTranslation() instead of setPhysicsLocation().

When I change the position of the GhostControl/geom, the texture of the sphere stops rendering after a while. I tried to comment the command geo.setLocalTranslation(new Vector3f(0, 20, 0)); and the problem disappeared.

I don’t understand what is causing the problem, JME or Minie? Or am I doing something wrong in the createCube() method?

Without a complete application, it’s hard to tell what’s going on. For instance, I can only guess what addGhostControl(geo) is doing.

Let’s back up a step. What are you trying to accomplish?

1 Like

Okay, got it :wink:

I’m testing collisions with GhostControl with BulletAppState in debug mode. I created a simple floor, a sphere and a box. Running the application brought up this rendering problem. I do not understand whether it depends on the debug mode of BulletAppState, a bug in JME, or an error I made. As I wrote in my previous post, you can find the complete source code on my gist. You can download it and run it on your pc.

Thanks.

1 Like

Hello everyone,
I can’t figure out where the problem is, but there is definitely a bug somewhere. Has anyone tried running the source code I uploaded to my gist? Also on your pc’s the ball is not rendered and becomes invisible?

PS I am using Minie v7.8.1+big3

Here is the test case:

package mygame;

import com.jme3.app.SimpleApplication;
import com.jme3.bullet.BulletAppState;
import com.jme3.bullet.PhysicsSpace;
import com.jme3.bullet.collision.PhysicsCollisionObject;
import com.jme3.bullet.collision.shapes.CollisionShape;
import com.jme3.bullet.collision.shapes.SphereCollisionShape;
import com.jme3.bullet.control.GhostControl;
import com.jme3.bullet.control.RigidBodyControl;
import com.jme3.bullet.objects.PhysicsBody;
import com.jme3.bullet.util.CollisionShapeFactory;
import com.jme3.font.BitmapText;
import com.jme3.input.KeyInput;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.KeyTrigger;
import com.jme3.input.controls.Trigger;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector3f;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.ViewPort;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.control.AbstractControl;
import com.jme3.scene.shape.Box;
import com.jme3.scene.shape.Sphere;
import com.jme3.system.AppSettings;
import com.jme3.util.mikktspace.MikktspaceTangentGenerator;

/**
 * 
 * @author capdevon
 */
public class Test_Main2 extends SimpleApplication implements ActionListener {

    /**
     * 
     * @param args
     */
    public static void main(String[] args) {
        Test_Main2 app = new Test_Main2();
        AppSettings settings = new AppSettings(true);
        settings.setResolution(1280, 720);
        app.setSettings(settings);
        app.setShowSettings(false);
        app.setPauseOnLostFocus(false);
        app.start();
    }

    private BulletAppState physics;
    private RigidBodyControl ball;
    private BitmapText hud;

    @Override
    public void simpleInitApp() {
//        viewPort.setBackgroundColor(new ColorRGBA(0.5f, 0.6f, 0.7f, 1.0f));
        
        hud = makeTextUI(20, 10);
        
        configCamera();
        initPhysics();
        createFloor();
        createSphere();
        createCube();
        setupKeys();
    }

    private void configCamera() {
        float aspect = (float) cam.getWidth() / cam.getHeight();
        cam.setFrustumPerspective(45, aspect, 0.01f, 1000f);
        
        flyCam.setDragToRotate(true);
        flyCam.setMoveSpeed(25f);
        
        cam.setLocation(Vector3f.UNIT_XYZ.mult(32f));
        cam.lookAt(new Vector3f(0, 2, 0), Vector3f.UNIT_Y);
    }

    private void initPhysics() {
        physics = new BulletAppState();
        stateManager.attach(physics);
        physics.setDebugEnabled(true);
    }
    
    private BitmapText makeTextUI(float xPos, float yPos) {
        BitmapText bmp = new BitmapText(guiFont);
        bmp.setColor(ColorRGBA.Red);
        bmp.setSize(guiFont.getCharSet().getRenderedSize());
        bmp.setLocalTranslation(xPos, settings.getHeight() - yPos, 0);
        guiNode.attachChild(bmp);
        return bmp;
    }

    private void createFloor() {
        float boxSize = 140f;
        Box box = new Box(boxSize, 0.4f, boxSize);
        Geometry geo = new Geometry("Floor", box);
        Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
        mat.setColor("Color", ColorRGBA.Green);
        geo.setMaterial(mat);
        rootNode.attachChild(geo);
        
        addStaticRigidBody(geo);
    }
    
    private void createCube() {
        Box box = new Box(4, 4, 4);
        Geometry geo = new Geometry("Box", box);
        Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
        mat.setColor("Color", ColorRGBA.Red);
        mat.getAdditionalRenderState().setWireframe(true);
        geo.setMaterial(mat);
        geo.setLocalTranslation(new Vector3f(0, 20, 0));
        rootNode.attachChild(geo);
        
        GhostControl ghost = addGhostControl(geo);
        
        geo.addControl(new AbstractControl() {

            @Override
            protected void controlUpdate(float tpf) {
                for (PhysicsCollisionObject pco : ghost.getOverlappingObjects()) {
                    System.out.println("ghostOverlapping: " + pco.getUserObject());
                }
            }

            @Override
            protected void controlRender(RenderManager rm, ViewPort vp) {
            }
            
        });
    }
    
    private void createSphere() {
        Sphere sphere = new Sphere(32, 32, 4);
        sphere.setTextureMode(Sphere.TextureMode.Projected);
        Geometry geo = new Geometry("Ball", sphere);
        MikktspaceTangentGenerator.generate(geo);
        Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
//        mat.setColor("Color", ColorRGBA.White);
        mat.setTexture("ColorMap", assetManager.loadTexture("Interface/Logo/Monkey.jpg"));
        geo.setMaterial(mat);
        geo.setLocalTranslation(new Vector3f(0, 5, 0));
        rootNode.attachChild(geo);
        
        ball = addDynamicRigidBody(geo, 1f);
    }
    
    private GhostControl addGhostControl(Spatial sp) {
        CollisionShape shape = CollisionShapeFactory.createBoxShape(sp);
        GhostControl ghost = new GhostControl(shape);
        sp.addControl(ghost);
        physics.getPhysicsSpace().addCollisionObject(ghost);
        return ghost;
    }

    private RigidBodyControl addStaticRigidBody(Spatial sp) {
        CollisionShape shape = CollisionShapeFactory.createMeshShape(sp);
        RigidBodyControl rb = new RigidBodyControl(shape, PhysicsBody.massForStatic);
        sp.addControl(rb);
        physics.getPhysicsSpace().add(rb);
        return rb;
    }

    private RigidBodyControl addDynamicRigidBody(Spatial sp, float mass) {
        CollisionShape shape = CollisionShapeFactory.createDynamicMeshShape(sp);
        RigidBodyControl rb = new RigidBodyControl(shape, mass);
        sp.addControl(rb);
        physics.getPhysicsSpace().add(rb);
        return rb;
    }
    
    @Override
    public void simpleUpdate(float tpf) {
        int m = physics.getPhysicsSpace().countManifolds();
        hud.setText("countManifolds: " + m);
    }
    
    private void setupKeys() {
        addMapping("JUMP", new KeyTrigger(KeyInput.KEY_SPACE));
    }
    
    public void addMapping(String bindingName, Trigger... triggers) {
        inputManager.addMapping(bindingName, triggers);
        inputManager.addListener(this, bindingName);
    }

    @Override
    public void onAction(String name, boolean isPressed, float tpf) {
        if (name.equals("JUMP") && isPressed) {
            ball.applyCentralImpulse(Vector3f.UNIT_Y.mult(10));
        }
    }

}
ext.jmeVersion = '3.6.1-stable'

repositories {
    mavenCentral()
}

dependencies {
    // jMonkeyEngine
    implementation 'org.jmonkeyengine:jme3-core:' + jmeVersion
    implementation 'org.jmonkeyengine:jme3-desktop:' + jmeVersion
    implementation 'org.jmonkeyengine:jme3-effects:' + jmeVersion
    runtimeOnly    'org.jmonkeyengine:jme3-awt-dialogs:' + jmeVersion 
    
    implementation 'com.github.stephengold:Minie:7.8.1+big3'

    // select one version of LWJGL
    //runtimeOnly 'org.jmonkeyengine:jme3-lwjgl:' + jmeVersion  // LWJGL 2.x
    runtimeOnly 'org.jmonkeyengine:jme3-lwjgl3:' + jmeVersion // LWJGL 3.x

    runtimeOnly 'org.jmonkeyengine:jme3-jogg:' + jmeVersion
    runtimeOnly 'org.jmonkeyengine:jme3-plugins:' + jmeVersion
    runtimeOnly 'org.jmonkeyengine:jme3-testdata:' + jmeVersion
}
1 Like

I meant: what was your test supposed to verify? What did you expect would happen when you ran the test?

As I wrote in my previous post, you can find the complete source code on my gist.

I’m sorry. I somehow overlooked the fact that you linked to a complete application.

Has anyone tried running the source code I uploaded to my gist?

I apologize for not responding sooner. I’ve been busy troubleshooting other bugs and publishing releases.

Here is the test case:

I tried running this (Test_Main2) application, and it did what exactly I expected it to.

When the ball is resting on the floor, there is 1 contact manifold: the contact between the ball and the floor.

When I depress the space bar, the ball rises up (but not far enough to reach the red cube) and then falls. The number of contacts goes to zero and then (when the ball lands on the floor) it returns to 1.

Since there are never any AABB overlaps between the red cube and any other physics object, the “ghostOverlapping” message is never printed.

I’m unsure what rendering issues you refer to. I do find it confusing that the red cube is rendered in wireframe mode, because the “wires” mostly coincide with the cube’s physics debug visualization, which is also a wireframe. (In JME, ghost objects are customarily rendered in yellow, while rigid bodies are blue or magenta.)

1 Like

Hi, @sgold , thanks for taking a look at the problem, I really appreciate it. Our friendship is one of the reasons why I still use this engine. The problem is not about collisions or object physics. The problem is about the fact that the ball should be textured, but after a while the texture is no longer rendered, so the ball is invisible (only its CollisionShape is seen because of BulletAppState with debug mode enabled).

The question is: why doesn’t JME render the ball texture?

I see it that way:

instead it should be:

Maybe I should create this post in another Thread, but I don’t know if the issue is about JME or Minie…
Any ideas?

Thanks.

1 Like

I tried to reproduce the issue you mention (the ball geometry disappearing) but was unsuccessful. Does it happen spontaneously, or in response to user input? If it’s spontaneous, how long does it take to occur?

If I were debugging an issue like that, I would dump the scene graph, wait for the ball to disappear, and then dump the scene graph again. If the ball is still in the scene graph, I would compare its world location, bounding volume, cull hints, material, mesh, render queue, and so forth before and after.

1 Like