GImpactShape Test Added (#1117)

* GImpactShape Test Added

* GImpactShape Test Updates (WIP)

* Minor tweaks based on feedback

* Minor corrections + documentation added

* Final tweaks
accellbaker
Lou H 5 years ago committed by Stephen Gold
parent cff4dec57d
commit 8856ba7d25
  1. 158
      jme3-examples/src/main/java/jme3test/bullet/PhysicsTestHelper.java
  2. 342
      jme3-examples/src/main/java/jme3test/bullet/shape/TestGimpactShape.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;
}
}

@ -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);
}
}
}
Loading…
Cancel
Save