jMonkeyEngine / jmonkeyengine

A complete 3-D game development suite written in Java.
http://jmonkeyengine.org
BSD 3-Clause "New" or "Revised" License
3.74k stars 1.12k forks source link

scaled GImpactShapes fall through MeshCollisionShape [JBullet] #1120

Open louhy opened 5 years ago

louhy commented 5 years ago

This issue was discovered while putting together the following test draft. Behavior can be reproduced in JBullet only (tests with native bullet work fine).

package jme3test.bullet.shape;

import com.jme3.app.SimpleApplication;
import com.jme3.bullet.BulletAppState;
import com.jme3.bullet.PhysicsSpace;
import com.jme3.bullet.collision.shapes.GImpactCollisionShape;
import com.jme3.bullet.control.RigidBodyControl;
import com.jme3.bullet.debug.BulletDebugAppState;
import com.jme3.font.BitmapFont;
import com.jme3.font.BitmapText;
import com.jme3.light.DirectionalLight;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.FastMath;
import com.jme3.math.Vector3f;
import com.jme3.scene.Geometry;
import com.jme3.scene.Mesh;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.shape.Box;
import com.jme3.scene.shape.Cylinder;
import com.jme3.scene.shape.Sphere;
import java.util.ArrayList;
import java.util.List;
import jme3test.bullet.PhysicsTestHelper;

/**
 * 1st Test: 10 solver iterations, large pot<br>
 * 2nd Test: 20 solver iterations, large pot<br>
 * 3rd Test: 30 solver iterations, large pot<br>
 * 4th Test: 10 solver iterations, small pot<br>
 * 5th Test: 20 solver iterations, small pot<br>
 * 6th Test: 30 solver iterations, small pot
 *
 * @author lou
 */
public class TestGimpactShape extends SimpleApplication {

    private BulletAppState bulletAppState;
    private final boolean physicsDebug = true;
    private int solverNumIterations = 10;
    protected BitmapFont font;
    protected BitmapText timeElapsedTxt;
    protected BitmapText solverNumIterationsTxt;
    private final List<Spatial> testObjects = new ArrayList<>();
    private float testTimer = 0;
    private final float TIME_PER_TEST = 10;
    private float teapotScale = 1;

    public static void main(String[] args) {
        TestGimpactShape a = new TestGimpactShape();
        a.start();
    }

    @Override
    public void simpleInitApp() {
        getCamera().setLocation(new Vector3f(0, 10, 25));
        getCamera().lookAt(new Vector3f(0, -5, 0), Vector3f.UNIT_Y);
        getFlyByCamera().setMoveSpeed(25);

        DirectionalLight dl = new DirectionalLight();
        dl.setDirection(new Vector3f(-1, -1, -1).normalizeLocal());
        dl.setColor(ColorRGBA.Green);
        rootNode.addLight(dl);

        guiNode = getGuiNode();
        font = assetManager.loadFont("Interface/Fonts/Default.fnt");
        timeElapsedTxt = new BitmapText(font, false);
        solverNumIterationsTxt = new BitmapText(font, false);
        float lineHeight = timeElapsedTxt.getLineHeight();

        timeElapsedTxt.setLocalTranslation(202, lineHeight * 1, 0);
        guiNode.attachChild(timeElapsedTxt);
        solverNumIterationsTxt.setLocalTranslation(202, lineHeight * 2, 0);
        guiNode.attachChild(solverNumIterationsTxt);

        init();
    }

    private void init() {
        solverNumIterationsTxt.setText("Solver Iterations: " + solverNumIterations);

        bulletAppState = new BulletAppState();
        bulletAppState.setDebugEnabled(physicsDebug);
        stateManager.attach(bulletAppState);
        bulletAppState.getPhysicsSpace().setSolverNumIterations(solverNumIterations);

        //Left side test - GImpact objects collide with MeshCollisionShape floor
        dropTest(-5, 2, 0);
        dropTest2(-11, 7, 3);

        Geometry leftFloor = PhysicsTestHelper.createMeshTestFloor(assetManager, 20, new Vector3f(-21, -5, -10));
        addObject(leftFloor);

        //Right side test - GImpact objects collide with GImpact floor
        dropTest(10, 2, 0);
        dropTest2(9, 7, 3);

        Geometry rightFloor = PhysicsTestHelper.createGImpactTestFloor(assetManager, 20, new Vector3f(0, -5, -10));
        addObject(rightFloor);

        //Hide physics debug visualization for floors
        if (physicsDebug) {
            BulletDebugAppState bulletDebugAppState = stateManager.getState(BulletDebugAppState.class);
            bulletDebugAppState.setFilter((Object obj) -> {
                return !(obj.equals(rightFloor.getControl(RigidBodyControl.class))
                    || obj.equals(leftFloor.getControl(RigidBodyControl.class)));
            });
        }
    }

    private void addObject(Spatial s) {
        testObjects.add(s);
        rootNode.attachChild(s);
        physicsSpace().add(s);
    }

    private void dropTest(float x, float y, float z) {
        Vector3f offset = new Vector3f(x, y, z);
        attachTestObject(new Sphere(16, 16, 0.5f), new Vector3f(-4f, 2f, 2f).add(offset), 1);
        attachTestObject(new Sphere(16, 16, 0.5f), new Vector3f(-5f, 2f, 0f).add(offset), 1);
        attachTestObject(new Sphere(16, 16, 0.5f), new Vector3f(-6f, 2f, -2f).add(offset), 1);
        attachTestObject(new Box(0.5f, 0.5f, 0.5f), new Vector3f(-8f, 2f, -1f).add(offset), 10);
        attachTestObject(new Box(0.5f, 0.5f, 0.5f), new Vector3f(0f, 2f, -6f).add(offset), 10);
        attachTestObject(new Box(0.5f, 0.5f, 0.5f), new Vector3f(0f, 2f, -3f).add(offset), 10);
        attachTestObject(new Cylinder(2, 16, 0.2f, 2f), new Vector3f(0f, 2f, -5f).add(offset), 2);
        attachTestObject(new Cylinder(2, 16, 0.2f, 2f), new Vector3f(-1f, 2f, -5f).add(offset), 2);
        attachTestObject(new Cylinder(2, 16, 0.2f, 2f), new Vector3f(-2f, 2f, -5f).add(offset), 2);
        attachTestObject(new Cylinder(2, 16, 0.2f, 2f), new Vector3f(-3f, 2f, -5f).add(offset), 2);
    }

    private void dropTest2(float x, float y, float z) {
        Node n = (Node) assetManager.loadModel("Models/Teapot/Teapot.mesh.xml");
        n.setLocalTranslation(x, y, z);
        n.rotate(0, 0, -FastMath.HALF_PI);
        n.scale(teapotScale);

        Geometry tp = ((Geometry) n.getChild(0));
        Material mat = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
        tp.setMaterial(mat);

        Mesh mesh = tp.getMesh();
        GImpactCollisionShape shape = new GImpactCollisionShape(mesh);
        shape.setScale(new Vector3f(teapotScale, teapotScale, teapotScale));

        RigidBodyControl control = new RigidBodyControl(shape, 2);
        n.addControl(control);
        addObject(n);
    }

    private void attachTestObject(Mesh mesh, Vector3f position, float mass) {
        Material material = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
        material.setTexture("ColorMap", assetManager.loadTexture("Interface/Logo/Monkey.jpg"));
        Geometry g = new Geometry("mesh", mesh);
        g.setLocalTranslation(position);
        g.setMaterial(material);

        RigidBodyControl control = new RigidBodyControl(new GImpactCollisionShape(mesh), mass);
        g.addControl(control);
        addObject(g);
    }

    private PhysicsSpace physicsSpace() {
        return bulletAppState.getPhysicsSpace();
    }

    @Override
    public void simpleUpdate(float tpf) {
        testTimer += tpf;

        if (testTimer / TIME_PER_TEST > 1) {
            testTimer = 0;
            switch (solverNumIterations) {
                case 10:
                    solverNumIterations = 20;
                    cleanup();
                    init();
                    break;
                case 20:
                    solverNumIterations = 30;
                    cleanup();
                    init();
                    break;
                case 30:
                    solverNumIterations = 10;
                    teapotScale = teapotScale > 0.9f ? 0.5f : 1;
                    cleanup();
                    init();
                    break;
            }
        }
        timeElapsedTxt.setText("Time Elapsed: " + testTimer);
    }

    private void cleanup() {
        stateManager.detach(bulletAppState);
        stateManager.detach(stateManager.getState(BulletDebugAppState.class));
        for (Spatial s : testObjects) {
            rootNode.detachChild(s);
        }
    }
}
stephengold commented 5 years ago

Thank you for documenting this issue.

louhy commented 5 years ago

No problem. It does bother me a little that the issue goes away when solverNumIterations is increased high enough. It makes me wonder whether it's not technically a bug and more a matter of native being a slightly more accurate simulation (I can't imagine why that would be).

stephengold commented 5 years ago

I believe that invoking updateBound() during setScale() will fix this, for native Bullet at least.

stephengold commented 5 years ago

Looks like jme3-jbullet's GImpactCollisionShape.java has an update issue similar to that in jme3-bullet:

        cShape = new GImpactMeshShape(tiv);
        cShape.setLocalScaling(Converter.convert(worldScale));
        ((GImpactMeshShape)cShape).updateBound();
        cShape.setLocalScaling(Converter.convert(getScale()));
        cShape.setMargin(margin);

I'll try moving the updateBound() into setScale() in jme3-jbullet.

stephengold commented 5 years ago

@louhy please confirm the fix is good. Also, check whether there's a similar issue with GImpactCollisionShape.setMargin().

louhy commented 5 years ago

Nice find! Will double check this fix for you. I think we should make a separate dedicated test for it too (using the code above or a variant), ex "TestIssue1120.java". If there's any way to add me as a second assignee here so I don't forget, feel free.

stephengold commented 5 years ago

GitHub allows me to assign up to 10 people to an issue. You're in!

louhy commented 5 years ago

I think I have bad news, good news, great news:

Bad: One problem with adding a test based on code above is that it's using GImpact shapes for some primitives where technically you wouldn't do that in practice (your setting a good example suggestion in the other issue). It may be hard to replace those with other shapes while still reproducing a fall through. For JBullet those cylinders still fall through the mesh. Native is fine, but I don't believe this particular test showed an issue for native. But it's a 0.2 radius GImpact cylinder... do we really care?

Good: The current GImpactShape test demonstrates fall-through (and crazy bounces) with the kettle when scale is increased to 1.8.

Great: When I run the current GImpactShape test against the current master version, not only is this fixed, but JBullet performs WAY better and the crazy bounciness seems totally gone (from testing so far). JBullet actually passes the test criteria now if you're patient waiting for things to go inactive.

I think this restores my faith in JBullet's usefulness...

stephengold commented 5 years ago

If 0.2-radius GImpact cylinder falls through in jme3-jbullet, I think that's an issue. Not a high-priority issue for me, but it's an issue.

So can we close this issue (for the latest master branch, at least)?

louhy commented 5 years ago

Since this looks way better in master now, I'm fine with considering it closed.

I'd be happy to turn the code here into a slimmed down test PR which demonstrates the GImpact cylinder fall through if you'd like.

stephengold commented 5 years ago

I'd like to play with that slimmed-down test case.

louhy commented 5 years ago

Hmm, maybe we shouldn't have closed this since the OP is specifically for the JBullet fall-through issue (which still remains). Anyway, I've got a smaller test case eliminating the distractions, and fairly minimal code.

Running this one at half speed by default since it moves kind of fast. I've also made the speed variable. (Don't forget to switch to JBullet before testing.) PR coming next...

stephengold commented 5 years ago

Re-opening as suggested.

louhy commented 4 years ago

I keep forgetting what this was referring to (own comments about this being "fixed" above confusing me), so attaching a reminder animation of what this test currently does when you enable JBullet for this test...

jbullet-fall-through

stephengold commented 3 years ago

This issue is still evident in v3.4.0-beta4.