[Solved] Random exceptions while running multiple physics spaces in parallel

Batch the mesh and then create a collision shape. Well yeah, depending on the size one or the other might be better. If the trees are over a large area then having them as one collision shape will make the broadphase useless, having too many objects will overload the physics… The other thing is the graphics, rendering single spatials is causing much more overhead than having one mesh but the same thing as for the broadphase applies for culling here.

Ok, so I have a test case that reproduces the error:

package test;

import com.jme3.app.SimpleApplication;
import com.jme3.bullet.BulletAppState;
import com.jme3.bullet.PhysicsSpace;
import com.jme3.bullet.collision.shapes.MeshCollisionShape;
import com.jme3.bullet.control.BetterCharacterControl;
import com.jme3.bullet.control.RigidBodyControl;
import com.jme3.light.AmbientLight;
import com.jme3.material.Material;
import com.jme3.math.FastMath;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import com.jme3.scene.Geometry;
import com.jme3.scene.Mesh;
import com.jme3.scene.Node;
import com.jme3.scene.shape.Cylinder;
import com.jme3.system.JmeContext;
import com.jme3.terrain.geomipmap.TerrainQuad;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.logging.Logger;
import jme3tools.optimize.GeometryBatchFactory;

public class ParallelPhysicsTest extends SimpleApplication{
    
    private static final int TILE_SIZE = 128;
    private static final int TILES_PER_PHYSICS_SPACE = (int) FastMath.pow(2, 2);   // Tile grid length at power of 2
    private HashMap<String, PhysicsSpace> physicsSpaces = new HashMap<>();
    
    public static void main(String[] args){
        new ParallelPhysicsTest().start(JmeContext.Type.Headless);
        //new ParallelPhysicsTest().start();
    }

    @Override
    public void simpleInitApp(){
        Logger.getLogger(ParallelPhysicsTest.class.getName()).info("Loading... Please wait");
        flyCam.setMoveSpeed(100);
        rootNode.addLight(new AmbientLight());
        for(int tileY = 0; tileY < 4; tileY++){
            for(int tileX = 0; tileX < 4; tileX++){
                Node tile = createTerrain(tileX * TILE_SIZE, tileY * TILE_SIZE);
                tile.setLocalTranslation(new Vector3f(tileY * TILE_SIZE, tile.getLocalTranslation().y, tileX * TILE_SIZE));
                rootNode.attachChild(tile);
                getPhysicsSpace(tileX, tileY).addAll(tile);
            }
        }
        
        // Add some characters (not adding them doesn't give the error)
        
        for(int i = 0; i < 50; i++){
            BetterCharacterControl character = new BetterCharacterControl(1, 5, 30);
            float randomX = (float) (Math.random() * 512);
            float randomZ = (float) (Math.random() * 512);
            Vector3f randomLocation = new Vector3f(randomX, 0, randomZ);
            character.warp(randomLocation);
            character.setWalkDirection(new Vector3f(512 - randomX, 0, 512 - randomZ).normalizeLocal());
            getPhysicsSpace(randomLocation).add(character);
        }
        
        Logger.getLogger(ParallelPhysicsTest.class.getName()).info("Done, number of physics spaces " + physicsSpaces.size());
    }
    
    public PhysicsSpace createPhysicsSpace(){
        BulletAppState bulletAppState = new BulletAppState(PhysicsSpace.BroadphaseType.AXIS_SWEEP_3);
        bulletAppState.setThreadingType(BulletAppState.ThreadingType.PARALLEL);
        stateManager.attach(bulletAppState);

        bulletAppState.getPhysicsSpace().setMaxSubSteps(16);
        
        bulletAppState.setDebugEnabled(true);
        
        return bulletAppState.getPhysicsSpace();
    }
    
    public PhysicsSpace getPhysicsSpace(int tileX, int tileY){
        int physicsX = (int) (tileX / FastMath.sqrt(TILES_PER_PHYSICS_SPACE));
        int physicsY = (int) (tileY / FastMath.sqrt(TILES_PER_PHYSICS_SPACE));
        if(!physicsSpaces.containsKey(physicsX + ", " + physicsY))
            physicsSpaces.put(physicsX + ", " + physicsY, createPhysicsSpace());
        return physicsSpaces.get(physicsX + ", " + physicsY);
    }
    
    public PhysicsSpace getPhysicsSpace(Vector3f location){
        int tileX, tileY;
        if(location.z < 0)
            tileX = (int) (location.z - (TILE_SIZE / 2)) / TILE_SIZE;
        else
            tileX = (int) (location.z + (TILE_SIZE / 2)) / TILE_SIZE;
        if(location.x < 0)
            tileY = (int) (location.x - (TILE_SIZE / 2)) / TILE_SIZE;
        else
            tileY = (int) (location.x + (TILE_SIZE / 2)) / TILE_SIZE;
        int physicsX = (int) (tileX / FastMath.sqrt(TILES_PER_PHYSICS_SPACE));
        int physicsY = (int) (tileY / FastMath.sqrt(TILES_PER_PHYSICS_SPACE));
        if(!physicsSpaces.containsKey(physicsX + ", " + physicsY))
            physicsSpaces.put(physicsX + ", " + physicsY, createPhysicsSpace());
        return physicsSpaces.get(physicsX + ", " + physicsY);
    }
    
    public Node createTerrain(int tileX, int tileY){
        Node tile = new Node("Tile"+tileX+"_"+tileY);
        tile.setLocalTranslation(tileX, 0, tileY);
        TerrainQuad terrain = new TerrainQuad("terrain", 32, 129, null);
        
        Material mat = new Material(assetManager, "Common/MatDefs/Terrain/TerrainLighting.j3md");
        
        // Setup the material
        
        mat.setBoolean("WardIso", true);
        mat.setFloat("DiffuseMap_0_scale", 64f);
        mat.setTexture("DiffuseMap", assetManager.loadTexture("Textures/Terrain/splat/dirt.jpg"));
        terrain.setMaterial(mat);
        
        // Add physics to the terrain
        
        RigidBodyControl physics = new RigidBodyControl(0);
        terrain.addControl(physics);
        physics.setKinematic(true);
        
        // Create some cylinders to be attached to the tile
        
        List<Geometry> geomList = new ArrayList<>();
        Cylinder cylinder = new Cylinder(16, 16, 0.5f, 2.5f, true);
        for(int x = -64; x < 64; x+=10){
            for(int z = -64; z < 64; z+=10){
                Geometry geom = new Geometry("Tree " + x + ", " + z, cylinder.clone());
                geom.rotate(new Quaternion().fromAngleAxis(FastMath.HALF_PI, Vector3f.UNIT_X));
                geom.setLocalTranslation(x, 0, z);
                geomList.add(geom);
                tile.attachChild(geom);
            }
        }
        
        // Batch the cylinders together and use that batched mesh for the physics
        
        Node treeNode = new Node();
        Mesh batchedMesh = new Mesh();
        GeometryBatchFactory.mergeGeometries(geomList, batchedMesh);
        RigidBodyControl r = new RigidBodyControl(new MeshCollisionShape(batchedMesh), 0);
        r.setKinematic(true);
        treeNode.addControl(r);
        tile.attachChild(treeNode);
        
        tile.attachChild(terrain);
        return tile;
    }
}

Simply wait a few minutes and then the error will pop up.

Some observations that I’ve made: if characters are not added the error doesn’t show up. If you remove the cylinder geometry from the root node (remove line 133), then the error happens faster. It also happens faster if the physics for the cylinders are not kinematic (remove line 143).

Actually you shouldn’t set any of the meshes you create to kinematic, they’re 0 mass mesh shapes and thus can’t be kinematic. But I’ll look at the test case when I have some time.

1 Like

Hey, just checking if you had the time to look at the test case?

EDIT: Here’s a more updated version that doesn’t have the physics as kinematic

package test;

import com.jme3.app.SimpleApplication;
import com.jme3.bullet.BulletAppState;
import com.jme3.bullet.PhysicsSpace;
import com.jme3.bullet.collision.shapes.MeshCollisionShape;
import com.jme3.bullet.control.BetterCharacterControl;
import com.jme3.bullet.control.RigidBodyControl;
import com.jme3.light.AmbientLight;
import com.jme3.material.Material;
import com.jme3.math.FastMath;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import com.jme3.scene.Geometry;
import com.jme3.scene.Mesh;
import com.jme3.scene.Node;
import com.jme3.scene.SceneGraphVisitor;
import com.jme3.scene.Spatial;
import com.jme3.scene.shape.Cylinder;
import com.jme3.system.JmeContext;
import com.jme3.terrain.geomipmap.TerrainQuad;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.logging.Logger;
import jme3tools.optimize.GeometryBatchFactory;

public class ParallelPhysicsTest extends SimpleApplication{
    
    private static final int TILE_SIZE = 128;
    private static final int TILES_PER_PHYSICS_SPACE = (int) FastMath.pow(2, 2);   // Tile grid length at power of 2
    private HashMap<String, PhysicsSpace> physicsSpaces = new HashMap<>();
    
    public static void main(String[] args){
        new ParallelPhysicsTest().start(JmeContext.Type.Headless);
        //new ParallelPhysicsTest().start();
    }

    @Override
    public void simpleInitApp(){
        Logger.getLogger(ParallelPhysicsTest.class.getName()).info("Loading... Please wait");
        flyCam.setMoveSpeed(100);
        rootNode.addLight(new AmbientLight());
        for(int tileY = 0; tileY < 4; tileY++){
            for(int tileX = 0; tileX < 4; tileX++){
                final Node tile = createTerrain(tileX * TILE_SIZE, tileY * TILE_SIZE);
                tile.setLocalTranslation(new Vector3f(tileY * TILE_SIZE, tile.getLocalTranslation().y, tileX * TILE_SIZE));
                rootNode.attachChild(tile);
                final int x = tileX;
                final int y = tileY;
                tile.breadthFirstTraversal(new SceneGraphVisitor() {
                    @Override
                    public void visit(Spatial spatial){
                        if(spatial.getControl(RigidBodyControl.class) != null)
                            spatial.getControl(RigidBodyControl.class).setPhysicsLocation(new Vector3f(y * TILE_SIZE, tile.getLocalTranslation().y, x * TILE_SIZE));
                    }
                });
                getPhysicsSpace(tileX, tileY).addAll(tile);
            }
        }
        
        // Add some characters (not adding them doesn't give the error)
        
        for(int i = 0; i < 50; i++){
            BetterCharacterControl character = new BetterCharacterControl(1, 5, 30);
            float randomX = (float) (Math.random() * 512);
            float randomZ = (float) (Math.random() * 512);
            Vector3f randomLocation = new Vector3f(randomX, 0, randomZ);
            character.warp(randomLocation);
            character.setWalkDirection(new Vector3f(512 - randomX, 0, 512 - randomZ).normalizeLocal());
            getPhysicsSpace(randomLocation).add(character);
        }
        
        Logger.getLogger(ParallelPhysicsTest.class.getName()).info("Done, number of physics spaces " + physicsSpaces.size());
    }
    
    public PhysicsSpace createPhysicsSpace(){
        BulletAppState bulletAppState = new BulletAppState(PhysicsSpace.BroadphaseType.AXIS_SWEEP_3);
        bulletAppState.setThreadingType(BulletAppState.ThreadingType.PARALLEL);
        stateManager.attach(bulletAppState);

        bulletAppState.getPhysicsSpace().setMaxSubSteps(16);
        
        bulletAppState.setDebugEnabled(true);
        
        return bulletAppState.getPhysicsSpace();
    }
    
    public PhysicsSpace getPhysicsSpace(int tileX, int tileY){
        int physicsX = (int) (tileX / FastMath.sqrt(TILES_PER_PHYSICS_SPACE));
        int physicsY = (int) (tileY / FastMath.sqrt(TILES_PER_PHYSICS_SPACE));
        if(!physicsSpaces.containsKey(physicsX + ", " + physicsY))
            physicsSpaces.put(physicsX + ", " + physicsY, createPhysicsSpace());
        return physicsSpaces.get(physicsX + ", " + physicsY);
    }
    
    public PhysicsSpace getPhysicsSpace(Vector3f location){
        int tileX, tileY;
        if(location.z < 0)
            tileX = (int) (location.z - (TILE_SIZE / 2)) / TILE_SIZE;
        else
            tileX = (int) (location.z + (TILE_SIZE / 2)) / TILE_SIZE;
        if(location.x < 0)
            tileY = (int) (location.x - (TILE_SIZE / 2)) / TILE_SIZE;
        else
            tileY = (int) (location.x + (TILE_SIZE / 2)) / TILE_SIZE;
        int physicsX = (int) (tileX / FastMath.sqrt(TILES_PER_PHYSICS_SPACE));
        int physicsY = (int) (tileY / FastMath.sqrt(TILES_PER_PHYSICS_SPACE));
        if(!physicsSpaces.containsKey(physicsX + ", " + physicsY))
            physicsSpaces.put(physicsX + ", " + physicsY, createPhysicsSpace());
        return physicsSpaces.get(physicsX + ", " + physicsY);
    }
    
    public Node createTerrain(int tileX, int tileY){
        Node tile = new Node("Tile"+tileX+"_"+tileY);
        tile.setLocalTranslation(tileX, 0, tileY);
        TerrainQuad terrain = new TerrainQuad("terrain", 32, 129, null);
        
        Material mat = new Material(assetManager, "Common/MatDefs/Terrain/TerrainLighting.j3md");
        
        // Setup the material
        
        mat.setBoolean("WardIso", true);
        mat.setFloat("DiffuseMap_0_scale", 64f);
        mat.setTexture("DiffuseMap", assetManager.loadTexture("Textures/Terrain/splat/dirt.jpg"));
        terrain.setMaterial(mat);
        
        // Add physics to the terrain
        
        RigidBodyControl physics = new RigidBodyControl(0);
        terrain.addControl(physics);
        //physics.setKinematic(true);
        
        // Create some cylinders to be attached to the tile
        
        List<Geometry> geomList = new ArrayList<>();
        Cylinder cylinder = new Cylinder(16, 16, 0.5f, 2.5f, true);
        for(int x = -64; x < 64; x+=10){
            for(int z = -64; z < 64; z+=10){
                Geometry geom = new Geometry("Tree " + x + ", " + z, cylinder.clone());
                geom.rotate(new Quaternion().fromAngleAxis(FastMath.HALF_PI, Vector3f.UNIT_X));
                geom.setLocalTranslation(x, 0, z);
                geomList.add(geom);
                tile.attachChild(geom);
            }
        }
        
        // Batch the cylinders together and use that batched mesh for the physics
        
        Node treeNode = new Node();
        Mesh batchedMesh = new Mesh();
        GeometryBatchFactory.mergeGeometries(geomList, batchedMesh);
        RigidBodyControl r = new RigidBodyControl(new MeshCollisionShape(batchedMesh), 0);
        //r.setKinematic(true);
        treeNode.addControl(r);
        tile.attachChild(treeNode);
        
        tile.attachChild(terrain);
        return tile;
    }
}

No I didn’t

So I’ve managed to solve my issues by shifting to native bullet on version 3.1 alpha 2. The UnsatisfiedLinkError exception above was being caused by bullet not being able to find its native library when the application ran in headless mode (see [Solved] Bullet doesn't find native library when running game in headless mode). Thanks for the help!