Trying to ”split” a physics controlled BatchNode

I’m trying to learn JME and try out different ideas in the engine. I’m currently trying to do simple voxel-style object deformation/destruction by removing children from a BatchNode that is using physics. As an early proof of concept of this idea, what I’m doing is to split a BatchNode into multiple BatchNodes, and then attempt to remap the RigidBodyControl for the original BatchNode onto the two new BatchNodes. With a few tweaks to BatchNode, I actually got this to mostly work, but it seems to confuse JBullet.



Here’s a rough outline of what my code is doing to split a BatchNode (assuming it only has one merged geometry, for the moment):



  1. On a "shoot" event, realize that the target geometry will split the merged BatchNode geometry into two separate meshes.

  2. Remove the target BatchNode from the scene and the physics space.

  3. Distribute the children of the BatchNode across two new BatchNodes, exculding the original target of the "shoot" event--it gets removed.

  4. Add the new BatchNodes to the physics space.

  5. Copy the location/rotation of the original BatchNode to the two children.

  6. Add the new BatchNodes to the scene.



With physics debug enabled, I can see that all the collision shapes appear to be correct. Further, I can visually confirm that no objects are overlapping when the RigidBodyControls are being added. However, after I apply this algorithm, the new BatchNodes behave erratically in the physics simulation. They jitter, bounce, swivel upright, and otherwise go crazy. My guess is that I'm not allowed to manipulate the physics space in this way, or that I'm not correctly applying all the transforms to the physics controller.

My question is: what can I do to make the algorithm work, or is there another way in the JME engine to do voxel-like deformation? I'm also wiling to accept that this is outside of JME's current default capabilities and that I'll need to do some heavy lifting on my own.

I read code better than text descriptions, so I've included a test case below. Hopefully I included enough comments to follow what's going on. The test case renders a tower and each time you hit space, it splits the tower using the algorithm described above. As the segments of the tower hit the ground plane, you can see them go crazy.

[java]import java.util.ArrayList;
import java.util.List;

import com.jme3.app.SimpleApplication;
import com.jme3.bounding.BoundingVolume;
import com.jme3.bullet.BulletAppState;
import com.jme3.bullet.collision.shapes.CollisionShape;
import com.jme3.bullet.control.RigidBodyControl;
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.Quaternion;
import com.jme3.math.Vector3f;
import com.jme3.scene.BatchNode;
import com.jme3.scene.Geometry;
import com.jme3.scene.Spatial;
import com.jme3.scene.shape.Box;

public class BatchPhysicsSplit extends SimpleApplication {

/**
* List of currently active batch nodes--there may be ways to do
* this that work better performance-wise, but this makes the
* example easier.
*/
List<MyBatchNode> batches = new ArrayList<MyBatchNode>();

// Make the tower bigger or smaller, height = 2 * HALF_TOWER_HEIGHT + 1
static final int HALF_TOWER_HEIGHT = 10;

BulletAppState bulletAppState;

/**
* We assume that all geos can be merged into one mesh for the purposes of this example.
* Needed to make some tweaks to BatchNode to get this logic to work.
*/
class MyBatchNode extends BatchNode {
/**
* The bounding volume of the BatchNode's merged geometry doesn't seem to be quite right--it's always equal to
* the unit cube in this example. See in-line comments for how I worked around that.
*/
protected void updateWorldBound() {
super.updateWorldBound(); // seems to be necessary...?

BoundingVolume resultBound = null;
for (Spatial child : children) {
/**
* Do NOT include the merged geometry in the bounding volume,
* because the volume of the merged geo is wrong! Instead,
* merge all the spatials under this batch node--this seems
* to give us the right answer for our simple case.
*/
if (batchesByGeom.containsKey(child)) {
if (resultBound != null) {
resultBound.mergeLocal(child.getWorldBound());
} else if (child.getWorldBound() != null) {
resultBound = child.getWorldBound().clone(this.worldBound);
}
}
}

worldBound = resultBound;
}

// This just makes the example easier to read -- wouldn't work in real code
List<Spatial> getBoxes() {
List<Spatial> results = new ArrayList<Spatial>(getChildren());
results.remove(results.size() - 1);
return results;
}
}

@Override
public void simpleInitApp() {
// init physics
bulletAppState = new BulletAppState();
stateManager.attach(bulletAppState);
bulletAppState.getPhysicsSpace().enableDebug(assetManager);

// draw a ground plane
Material brown = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
brown.setColor("Color", ColorRGBA.Brown);
Geometry ground = new Geometry("ground", new Box(Vector3f.ZERO, 25, 0.5f, 25));
ground.setLocalTranslation(new Vector3f(3, -1, 3));
ground.setMaterial(brown);
RigidBodyControl rbc = new RigidBodyControl(0f);
ground.addControl(rbc);
bulletAppState.getPhysicsSpace().add(rbc);
rootNode.attachChild(ground);

// draw the initial tower around 0,0,0 -- group into a batch node
Material red = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
red.setColor("Color", ColorRGBA.Red);
MyBatchNode batchNode = new MyBatchNode();
for (int y = -HALF_TOWER_HEIGHT; y <= HALF_TOWER_HEIGHT; y++) {
Geometry geo = new Geometry("box" + y, new Box(null, 0.5f, 0.5f, 0.5f));
geo.setLocalTranslation(new Vector3f(0, y, 0));
geo.setMaterial(red);
batchNode.attachChild(geo);
}
batchNode.batch();
// Now attach physics and move the tower above the ground plane.
applyPhysics(batchNode).setPhysicsLocation(new Vector3f(0, HALF_TOWER_HEIGHT, 0));
rootNode.attachChild(batchNode);
batches.add(batchNode);

// When the user hits SPACE, split all the batch nodes in space in half
inputManager.addMapping("split", new KeyTrigger(KeyInput.KEY_SPACE));
inputManager.addListener(new ActionListener() {
public void onAction(String name, boolean keyPressed, float tpf) {
if ("split".equals(name) && !keyPressed) {
List<MyBatchNode> oldBatches = batches;
batches = new ArrayList<MyBatchNode>();
for (MyBatchNode node : oldBatches) {
split(node);
}
}
}
}, "split");
}

/**
* Apply physics to this batch node
*/
RigidBodyControl applyPhysics(BatchNode node) {
node.updateGeometricState(); // do this to force bounding volume calculation

// Create a custom collision shape based on the mesh of the merged mesh
Geometry mergedGeo = (Geometry) node.getChild("null-batch0");
CollisionShape mcs = CollisionShapeFactory.createDynamicMeshShape(mergedGeo);
RigidBodyControl rbc = new RigidBodyControl(mcs, 300f);
node.addControl(rbc);
bulletAppState.getPhysicsSpace().add(rbc);

return rbc;
}

/**
* Split a batch node in two, removing the box in the middle from the world.
*/
void split(MyBatchNode node) {
List<Spatial> boxes = node.getBoxes();
if (boxes.size() > 1) {
// Remove the old physics controller; remove from scene
RigidBodyControl oldRbc = node.getControl(RigidBodyControl.class);
bulletAppState.getPhysicsSpace().remove(oldRbc);
rootNode.detachChild(node);

// Split the boxes across two new batch nodes, leaving one box out
MyBatchNode[] splits = { new MyBatchNode(), new MyBatchNode() };
int nodeToRemove = boxes.size() / 2;
for (int i = 0; i < boxes.size(); i++) {
if (i == nodeToRemove) {
continue;
}
splits.attachChild(boxes.get(i));
}

// OK. Here's were we assign the physics across the split nodes
Quaternion rotation = oldRbc.getPhysicsRotation();
Vector3f location = oldRbc.getPhysicsLocation();
for (MyBatchNode split : splits) {
if (split.getChildren().size() > 0) {
split.batch(); // batch it up first, this gens the mesh
RigidBodyControl rbc = applyPhysics(split);
// The location appears to be adjusted by local transform
rbc.setPhysicsLocation(location);
// Should be able to just apply the same rotation
rbc.setPhysicsRotation(rotation);
rootNode.attachChild(split);
batches.add(split);
}
}
}
}

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

}
[/java]

Thanks!

Not sure it’s related but an issue was reported recently about the BatchNode not updating geometry bounds correctly when rebatched.

Not sure it can have implication on physics though.

nehon, I guess it could be related? If I go straight to unit cubes, everything works. So I changed split(MyBatchNode) to keep half the stack static and break the other half into dynamic unit cubes, the dynamic cubes react like you’d expect. My guess it must have something to do with one of the bits of metadata calculated by the batch node, but I’m not familiar enough with JME/JBullet to get a good handle on what that is. My guess is something in center of mass, but I’m not sure.



After playing with it for a bit, this is sort of the effect I was going for anyway. The updated code is below, in case it helps anyone.



[java]void split(MyBatchNode node) {

List<Spatial> boxes = node.getBoxes();

if (boxes.size() > 1) {

// Remove the old physics controller

RigidBodyControl oldRbc = node.getControl(RigidBodyControl.class);

bulletAppState.getPhysicsSpace().remove(oldRbc);

rootNode.detachChild(node);



MyBatchNode mbn = new MyBatchNode();

int nodeToRemove = boxes.size() / 2;

for (int i = 0; i < boxes.size(); i++) {

if (i > nodeToRemove) {

mbn.attachChild(boxes.get(i));

} else {

Spatial spatial = boxes.get(i);

RigidBodyControl rbc = new RigidBodyControl(100f);

spatial.addControl(rbc);

bulletAppState.getPhysicsSpace().add(rbc);

rootNode.attachChild(spatial);

}

}

mbn.batch();

RigidBodyControl rbc = applyPhysics(mbn);

rbc.setPhysicsLocation(oldRbc.getPhysicsLocation());

rbc.setPhysicsRotation(oldRbc.getPhysicsRotation());

batches.add(mbn);

rootNode.attachChild(mbn);

}

}[/java]