Browse Source

GImpactShape Test Added (#1117)

* GImpactShape Test Added

* GImpactShape Test Updates (WIP)

* Minor tweaks based on feedback

* Minor corrections + documentation added

* Final tweaks
Lou H 6 years ago
parent
commit
8856ba7d25

+ 137 - 21
jme3-examples/src/main/java/jme3test/bullet/PhysicsTestHelper.java

@@ -36,6 +36,7 @@ import com.jme3.asset.AssetManager;
 import com.jme3.asset.TextureKey;
 import com.jme3.bullet.PhysicsSpace;
 import com.jme3.bullet.collision.shapes.CollisionShape;
+import com.jme3.bullet.collision.shapes.GImpactCollisionShape;
 import com.jme3.bullet.collision.shapes.MeshCollisionShape;
 import com.jme3.bullet.control.RigidBodyControl;
 import com.jme3.input.MouseInput;
@@ -44,13 +45,18 @@ import com.jme3.input.controls.MouseButtonTrigger;
 import com.jme3.light.AmbientLight;
 import com.jme3.material.Material;
 import com.jme3.math.ColorRGBA;
+import com.jme3.math.FastMath;
+import com.jme3.math.Vector3f;
 import com.jme3.renderer.queue.RenderQueue.ShadowMode;
 import com.jme3.scene.Geometry;
+import com.jme3.scene.Mesh;
 import com.jme3.scene.Node;
+import com.jme3.scene.VertexBuffer;
 import com.jme3.scene.shape.Box;
 import com.jme3.scene.shape.Sphere;
 import com.jme3.scene.shape.Sphere.TextureMode;
 import com.jme3.texture.Texture;
+import com.jme3.util.BufferUtils;
 
 /**
  *
@@ -59,8 +65,7 @@ import com.jme3.texture.Texture;
 public class PhysicsTestHelper {
 
     /**
-     * creates a simple physics test world with a floor, an obstacle and some
-     * test boxes
+     * creates a simple physics test world with a floor, an obstacle and some test boxes
      *
      * @param rootNode where lights and geometries should be added
      * @param assetManager for loading assets
@@ -107,7 +112,7 @@ public class PhysicsTestHelper {
         space.add(sphereGeometry);
 
     }
-    
+
     public static void createPhysicsTestWorldSoccer(Node rootNode, AssetManager assetManager, PhysicsSpace space) {
         AmbientLight light = new AmbientLight();
         light.setColor(ColorRGBA.LightGray);
@@ -140,24 +145,24 @@ public class PhysicsTestHelper {
             space.add(ballGeometry);
         }
         {
-        //immovable Box with mesh collision shape
-        Box box = new Box(1, 1, 1);
-        Geometry boxGeometry = new Geometry("Box", box);
-        boxGeometry.setMaterial(material);
-        boxGeometry.setLocalTranslation(4, 1, 2);
-        boxGeometry.addControl(new RigidBodyControl(new MeshCollisionShape(box), 0));
-        rootNode.attachChild(boxGeometry);
-        space.add(boxGeometry);
+            //immovable Box with mesh collision shape
+            Box box = new Box(1, 1, 1);
+            Geometry boxGeometry = new Geometry("Box", box);
+            boxGeometry.setMaterial(material);
+            boxGeometry.setLocalTranslation(4, 1, 2);
+            boxGeometry.addControl(new RigidBodyControl(new MeshCollisionShape(box), 0));
+            rootNode.attachChild(boxGeometry);
+            space.add(boxGeometry);
         }
         {
-        //immovable Box with mesh collision shape
-        Box box = new Box(1, 1, 1);
-        Geometry boxGeometry = new Geometry("Box", box);
-        boxGeometry.setMaterial(material);
-        boxGeometry.setLocalTranslation(4, 3, 4);
-        boxGeometry.addControl(new RigidBodyControl(new MeshCollisionShape(box), 0));
-        rootNode.attachChild(boxGeometry);
-        space.add(boxGeometry);
+            //immovable Box with mesh collision shape
+            Box box = new Box(1, 1, 1);
+            Geometry boxGeometry = new Geometry("Box", box);
+            boxGeometry.setMaterial(material);
+            boxGeometry.setLocalTranslation(4, 3, 4);
+            boxGeometry.addControl(new RigidBodyControl(new MeshCollisionShape(box), 0));
+            rootNode.attachChild(boxGeometry);
+            space.add(boxGeometry);
         }
     }
 
@@ -211,8 +216,7 @@ public class PhysicsTestHelper {
     }
 
     /**
-     * creates the necessary inputlistener and action to shoot balls from the
-     * camera
+     * creates the necessary inputlistener and action to shoot balls from the camera
      *
      * @param app the application that's running
      * @param rootNode where ball geometries should be added
@@ -246,4 +250,116 @@ public class PhysicsTestHelper {
         app.getInputManager().addMapping("shoot", new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
         app.getInputManager().addListener(actionListener, "shoot");
     }
+
+    /**
+     * Creates a curved "floor" with a GImpactCollisionShape provided as the RigidBodyControl's collision
+     * shape. Surface has four slightly concave corners to allow for multiple tests and minimize falling off
+     * the edge of the floor.
+     *
+     * @param assetManager for loading assets
+     * @param floorDimensions width/depth of the "floor" (X/Z)
+     * @param position sets the floor's local translation
+     * @return
+     */
+    public static Geometry createGImpactTestFloor(AssetManager assetManager, float floorDimensions, Vector3f position) {
+        Geometry floor = createTestFloor(assetManager, floorDimensions, position, ColorRGBA.Red);
+        RigidBodyControl floorControl = new RigidBodyControl(new GImpactCollisionShape(floor.getMesh()), 0);
+        floor.addControl(floorControl);
+        return floor;
+    }
+
+    /**
+     * Creates a curved "floor" with a MeshCollisionShape provided as the RigidBodyControl's collision shape.
+     * Surface has four slightly concave corners to allow for multiple tests and minimize falling off the edge
+     * of the floor.
+     *
+     * @param assetManager for loading assets
+     * @param floorDimensions width/depth of the "floor" (X/Z)
+     * @param position sets the floor's local translation
+     * @return
+     */
+    public static Geometry createMeshTestFloor(AssetManager assetManager, float floorDimensions, Vector3f position) {
+        Geometry floor = createTestFloor(assetManager, floorDimensions, position, new ColorRGBA(0.5f, 0.5f, 0.9f, 1));
+        RigidBodyControl floorControl = new RigidBodyControl(new MeshCollisionShape(floor.getMesh()), 0);
+        floor.addControl(floorControl);
+        return floor;
+    }
+
+    private static Geometry createTestFloor(AssetManager assetManager, float floorDimensions, Vector3f position, ColorRGBA color) {
+        Geometry floor = new Geometry("floor", createFloorMesh(20, floorDimensions));
+        Material material = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+        material.getAdditionalRenderState().setWireframe(true);
+        material.setColor("Color", color);
+        floor.setMaterial(material);
+        floor.setLocalTranslation(position);
+        return floor;
+    }
+
+    private static Mesh createFloorMesh(int meshDetail, float floorDimensions) {
+        if (meshDetail < 10) {
+            meshDetail = 10;
+        }
+        int numVertices = meshDetail * meshDetail * 2 * 3;//width * depth * two tris * 3 verts per tri
+
+        int[] indexBuf = new int[numVertices];
+        int i = 0;
+        for (int x = 0; x < meshDetail; x++) {
+            for (int z = 0; z < meshDetail; z++) {
+                indexBuf[i] = i++;
+                indexBuf[i] = i++;
+                indexBuf[i] = i++;
+                indexBuf[i] = i++;
+                indexBuf[i] = i++;
+                indexBuf[i] = i++;
+            }
+        }
+
+        float[] vertBuf = new float[numVertices * 3];
+        float xIncrement = floorDimensions / meshDetail;
+        float zIncrement = floorDimensions / meshDetail;
+        int j = 0;
+        for (int x = 0; x < meshDetail; x++) {
+            float xPos = x * xIncrement;
+            for (int z = 0; z < meshDetail; z++) {
+                float zPos = z * zIncrement;
+                //First tri
+                vertBuf[j++] = xPos;
+                vertBuf[j++] = getY(xPos, zPos, floorDimensions);
+                vertBuf[j++] = zPos;
+                vertBuf[j++] = xPos;
+                vertBuf[j++] = getY(xPos, zPos + zIncrement, floorDimensions);
+                vertBuf[j++] = zPos + zIncrement;
+                vertBuf[j++] = xPos + xIncrement;
+                vertBuf[j++] = getY(xPos + xIncrement, zPos, floorDimensions);
+                vertBuf[j++] = zPos;
+                //Second tri
+                vertBuf[j++] = xPos;
+                vertBuf[j++] = getY(xPos, zPos + zIncrement, floorDimensions);
+                vertBuf[j++] = zPos + zIncrement;
+                vertBuf[j++] = xPos + xIncrement;
+                vertBuf[j++] = getY(xPos + xIncrement, zPos + zIncrement, floorDimensions);
+                vertBuf[j++] = zPos + zIncrement;
+                vertBuf[j++] = xPos + xIncrement;
+                vertBuf[j++] = getY(xPos + xIncrement, zPos, floorDimensions);
+                vertBuf[j++] = zPos;
+            }
+        }
+
+        Mesh m = new Mesh();
+        m.setBuffer(VertexBuffer.Type.Index, 1, BufferUtils.createIntBuffer(indexBuf));
+        m.setBuffer(VertexBuffer.Type.Position, 3, BufferUtils.createFloatBuffer(vertBuf));
+        m.updateBound();
+        return m;
+    }
+
+    private static float getY(float x, float z, float max) {
+        float yMaxHeight = 8;
+        float xv = FastMath.unInterpolateLinear(FastMath.abs(x - (max / 2)), 0, max) * FastMath.TWO_PI;
+        float zv = FastMath.unInterpolateLinear(FastMath.abs(z - (max / 2)), 0, max) * FastMath.TWO_PI;
+
+        float xComp = (FastMath.sin(xv) + 1) * 0.5f;
+        float zComp = (FastMath.sin(zv) + 1) * 0.5f;
+
+        return -yMaxHeight * xComp * zComp;
+    }
 }

+ 342 - 0
jme3-examples/src/main/java/jme3test/bullet/shape/TestGimpactShape.java

@@ -0,0 +1,342 @@
+/*
+ * Copyright (c) 2009-2019 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+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.input.KeyInput;
+import com.jme3.input.controls.ActionListener;
+import com.jme3.input.controls.KeyTrigger;
+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.PQTorus;
+import com.jme3.scene.shape.Torus;
+import com.jme3.system.AppSettings;
+import java.util.ArrayList;
+import java.util.List;
+import jme3test.bullet.PhysicsTestHelper;
+
+/**
+ * This test demonstrates various GImpactCollisionShapes colliding against two identical curved surfaces. The
+ * left surface is a MeshCollisionShape, right surface is another GImpactCollisionShape. An ideal result is
+ * for all objects to land and change to a blue colored mesh indicating they are inactive. Falling through the
+ * floor, or never going inactive (bouncing forever) are failure conditions.
+ * <p>
+ * Observations as of June 2019 (JME v3.3.0-alpha2):
+ * <ol>
+ * <li>
+ * With default starting parameters, Native Bullet should pass the test parameters above. JBullet fails due to
+ * the rocket/MeshCollisionShape never going inactive.
+ * </li>
+ * <li>
+ * Native Bullet behaves better than JBullet. JBullet sometimes allows objects to "gain too much energy" after
+ * a collision, such as the rocket or teapot. Native also does this, to a lesser degree. This generally
+ * appears to happen at larger object scales.
+ * </li>
+ * <li>
+ * JBullet allows some objects to get "stuck" inside the floor, which usually results in a fall-through
+ * eventually, generally a larger scales for this test.
+ * </li>
+ * <li>
+ * Some shapes such as PQTorus & signpost never go inactive at larger scales for both Native and JBullet (test
+ * at 1.5 and 1.9 scale)
+ * </li>
+ * </ol>
+ *
+ * @author lou
+ */
+public class TestGimpactShape extends SimpleApplication {
+
+    private static TestGimpactShape test;
+    private BulletAppState bulletAppState;
+    private int solverNumIterations = 10;
+    private BitmapFont font;
+    private final BitmapText[] testInfo = new BitmapText[2];
+    private BitmapText timeElapsedTxt;
+    private BitmapText solverNumIterationsTxt;
+    private BitmapText testScale;
+    private final List<Spatial> testObjects = new ArrayList<>();
+    private float testTimer = 0;
+    private float scaleMod = 1;
+    private boolean restart = true;
+    private static final boolean SKIP_SETTINGS = false;//Used for repeated runs of this test during dev
+
+    public static void main(String[] args) {
+        test = new TestGimpactShape();
+        test.setSettings(new AppSettings(true));
+        test.settings.setFrameRate(60);
+        if (SKIP_SETTINGS) {
+            test.settings.setWidth(1920);
+            test.settings.setHeight(1150);
+            test.showSettings = !SKIP_SETTINGS;
+        }
+        test.start();
+    }
+
+    @Override
+    public void simpleInitApp() {
+        getCamera().setLocation(new Vector3f(40, 30, 160));
+        getCamera().lookAt(new Vector3f(40, -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);
+
+        //Setup test instructions
+        guiNode = getGuiNode();
+        font = assetManager.loadFont("Interface/Fonts/Default.fnt");
+        testInfo[0] = new BitmapText(font);
+        testInfo[1] = new BitmapText(font);
+        timeElapsedTxt = new BitmapText(font);
+        solverNumIterationsTxt = new BitmapText(font);
+        testScale = new BitmapText(font);
+
+        float lineHeight = testInfo[0].getLineHeight();
+        testInfo[0].setText("Camera move:W/A/S/D/Q/Z     Solver iterations: 1=10, 2=20, 3=30");
+        testInfo[0].setLocalTranslation(5, test.settings.getHeight(), 0);
+        guiNode.attachChild(testInfo[0]);
+        testInfo[1].setText("P: Toggle pause     Inc/Dec object scale: +, -     Space: Restart test");
+        testInfo[1].setLocalTranslation(5, test.settings.getHeight() - lineHeight, 0);
+        guiNode.attachChild(testInfo[1]);
+
+        timeElapsedTxt.setLocalTranslation(202, lineHeight * 1, 0);
+        guiNode.attachChild(timeElapsedTxt);
+        solverNumIterationsTxt.setLocalTranslation(202, lineHeight * 2, 0);
+        guiNode.attachChild(solverNumIterationsTxt);
+        testScale.setLocalTranslation(202, lineHeight * 3, 0);
+        guiNode.attachChild(testScale);
+
+        //Setup interactive test controls
+        inputManager.addMapping("restart", new KeyTrigger(KeyInput.KEY_SPACE));
+        inputManager.addListener((ActionListener) (String name, boolean isPressed, float tpf) -> {
+            restart = true;
+        }, "restart");
+
+        inputManager.addMapping("pause", new KeyTrigger(KeyInput.KEY_P));
+        inputManager.addListener((ActionListener) (String name, boolean isPressed, float tpf) -> {
+            if (!isPressed) {
+                return;
+            }
+            bulletAppState.setSpeed(bulletAppState.getSpeed() > 0.1 ? 0 : 1);
+        }, "pause");
+
+        inputManager.addMapping("1", new KeyTrigger(KeyInput.KEY_1));
+        inputManager.addMapping("2", new KeyTrigger(KeyInput.KEY_2));
+        inputManager.addMapping("3", new KeyTrigger(KeyInput.KEY_3));
+        inputManager.addMapping("+", new KeyTrigger(KeyInput.KEY_ADD), new KeyTrigger(KeyInput.KEY_EQUALS));
+        inputManager.addMapping("-", new KeyTrigger(KeyInput.KEY_SUBTRACT), new KeyTrigger(KeyInput.KEY_MINUS));
+        inputManager.addListener((ActionListener) (String name, boolean isPressed, float tpf) -> {
+            if (!isPressed) {
+                return;
+            }
+            switch (name) {
+                case "1":
+                    solverNumIterations = 10;
+                    break;
+                case "2":
+                    solverNumIterations = 20;
+                    break;
+                case "3":
+                    solverNumIterations = 30;
+                    break;
+                case "+":
+                    scaleMod += scaleMod < 1.9f ? 0.1f : 0;
+                    break;
+                case "-":
+                    scaleMod -= scaleMod > 0.5f ? 0.1f : 0;
+                    break;
+            }
+            restart = true;
+        }, "1", "2", "3", "+", "-");
+
+        initializeNewTest();
+    }
+
+    private void initializeNewTest() {
+        testScale.setText("Object scale: " + String.format("%.1f", scaleMod));
+        solverNumIterationsTxt.setText("Solver Iterations: " + solverNumIterations);
+
+        bulletAppState = new BulletAppState();
+        bulletAppState.setDebugEnabled(true);
+        stateManager.attach(bulletAppState);
+        bulletAppState.getPhysicsSpace().setSolverNumIterations(solverNumIterations);
+
+        float floorSize = 80;
+        //Left side test - GImpact objects collide with MeshCollisionShape floor
+        Vector3f leftFloorPos = new Vector3f(-41, -5, -10);
+        Vector3f leftFloorCenter = leftFloorPos.add(floorSize / 2, 0, floorSize / 2);
+
+        dropTest1(leftFloorCenter);
+        dropTest2(leftFloorCenter);
+        dropPot(leftFloorCenter);
+        dropSword(leftFloorCenter);
+        dropSign(leftFloorCenter);
+        dropRocket(leftFloorCenter);
+
+        Geometry leftFloor = PhysicsTestHelper.createMeshTestFloor(assetManager, floorSize, leftFloorPos);
+        addObject(leftFloor);
+
+        //Right side test - GImpact objects collide with GImpact floor
+        Vector3f rightFloorPos = new Vector3f(41, -5, -10);
+        Vector3f rightFloorCenter = rightFloorPos.add(floorSize / 2, 0, floorSize / 2);
+
+        dropTest1(rightFloorCenter);
+        dropTest2(rightFloorCenter);
+        dropPot(rightFloorCenter);
+        dropSword(rightFloorCenter);
+        dropSign(rightFloorCenter);
+        dropRocket(rightFloorCenter);
+
+        Geometry rightFloor = PhysicsTestHelper.createGImpactTestFloor(assetManager, floorSize, rightFloorPos);
+        addObject(rightFloor);
+
+        //Hide physics debug visualization for floors
+        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 dropTest1(Vector3f offset) {
+        offset = offset.add(-18, 6, -18);
+        attachTestObject(new Torus(16, 16, 0.15f, 0.5f), new Vector3f(-12f, 0f, 5f).add(offset), 1);
+        attachTestObject(new PQTorus(2f, 3f, 0.6f, 0.2f, 48, 16), new Vector3f(0, 0, 0).add(offset), 5);
+
+    }
+
+    private void dropTest2(Vector3f offset) {
+        offset = offset.add(18, 6, -18);
+        attachTestObject(new Torus(16, 16, 0.3f, 0.8f), new Vector3f(12f, 0f, 5f).add(offset), 3);
+        attachTestObject(new PQTorus(3f, 5f, 0.8f, 0.2f, 96, 16), new Vector3f(0, 0, 0).add(offset), 10);
+    }
+
+    private void dropPot(Vector3f offset) {
+        drop(offset.add(-12, 7, 15), "Models/Teapot/Teapot.mesh.xml", 1.0f, 2);
+    }
+
+    private void dropSword(Vector3f offset) {
+        drop(offset.add(-10, 5, 3), "Models/Sinbad/Sword.mesh.xml", 1.0f, 2);
+    }
+
+    private void dropSign(Vector3f offset) {
+        drop(offset.add(9, 15, 5), "Models/Sign Post/Sign Post.mesh.xml", 1.0f, 1);
+    }
+
+    private void dropRocket(Vector3f offset) {
+        RigidBodyControl c = drop(offset.add(26, 4, 7), "Models/SpaceCraft/Rocket.mesh.xml", 4.0f, 3);
+        c.setAngularDamping(0.5f);
+        c.setLinearDamping(0.5f);
+    }
+
+    private RigidBodyControl drop(Vector3f offset, String model, float scale, float mass) {
+        scale *= scaleMod;
+        Node n = (Node) assetManager.loadModel(model);
+        n.setLocalTranslation(offset);
+        n.rotate(0, 0, -FastMath.HALF_PI);
+
+        Geometry tp = ((Geometry) n.getChild(0));
+        tp.scale(scale);
+        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(scale, scale, scale));
+
+        RigidBodyControl control = new RigidBodyControl(shape, mass);
+        n.addControl(control);
+        addObject(n);
+        return control;
+    }
+
+    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.scale(scaleMod);
+        g.setLocalTranslation(position);
+        g.setMaterial(material);
+
+        GImpactCollisionShape shape = new GImpactCollisionShape(mesh);
+        shape.setScale(new Vector3f(scaleMod, scaleMod, scaleMod));
+        RigidBodyControl control = new RigidBodyControl(shape, mass);
+        g.addControl(control);
+        addObject(g);
+    }
+
+    private PhysicsSpace physicsSpace() {
+        return bulletAppState.getPhysicsSpace();
+    }
+
+    @Override
+    public void simpleUpdate(float tpf) {
+        testTimer += tpf * bulletAppState.getSpeed();
+
+        if (restart) {
+            cleanup();
+            initializeNewTest();
+            restart = false;
+            testTimer = 0;
+        }
+        timeElapsedTxt.setText("Time Elapsed: " + String.format("%.3f", testTimer));
+    }
+
+    private void cleanup() {
+        stateManager.detach(bulletAppState);
+        stateManager.detach(stateManager.getState(BulletDebugAppState.class));
+        for (Spatial s : testObjects) {
+            rootNode.detachChild(s);
+        }
+    }
+}