Error Colliding with BoundingBox & Terrains

Hello, I’m trying to use a BoundingBox for collisions the same way I would use a ray, and I’m running into trouble… So I created a single class test case, and I’ve found I’m getting a null error when the terrain collides with the bounding box. Regular geometries are working, but collisions with any part of the terrain throws an error.

Here is my test case. I’m loading a simple scene that contains a terrain, and pressing space bar triggers a collision:

public class Main extends SimpleApplication {

private Vector3f spawn = new Vector3f(0,-7,0);
private Node terrain;

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

@Override
public void simpleInitApp() {
    
//load a test terrain     
    terrain = (Node) getAssetManager().loadModel("Scenes/newScene.j3o");
    getRootNode().attachChild(terrain);
    terrain.setLocalTranslation(spawn);
    
    
 // add light and input
    flyCam.setMoveSpeed(25);
    DirectionalLight sun = new DirectionalLight();
    sun.setDirection((new Vector3f(-0.5f, -0.5f, -0.5f)).normalizeLocal());
    sun.setColor(ColorRGBA.White);
    rootNode.addLight(sun); 
    inputManager.addMapping("Interact", new KeyTrigger(KeyInput.KEY_SPACE));
    inputManager.addListener(actionListener, "Interact");
         
}

@Override
public void simpleUpdate(float tpf) {
}



private ActionListener actionListener = new ActionListener() {
    @Override
    public void onAction(String binding, boolean keyPressed, float tpf) {
          testCollisionAndOutput();
    }
};
   
private void testCollisionAndOutput(){
       
       
    BoundingBox bounds = new BoundingBox(spawn, 15f, 15f, 15f);
    Ray ray = new Ray(spawn, new Vector3f(0, -1, 0));
       
    CollisionResults results = new CollisionResults();
       
//   rootNode.collideWith(ray, results);   //rays work
    terrain.collideWith(bounds, results);  //bounding box doesnt  :(
       
      
    for(int i =0; i < results.size(); i ++){
        System.out.println("Collision with:  " + results.getCollision(i).getGeometry().getClass()); //Error is thrown on this line
    }
 }

 }

And the error:

java.lang.NullPointerException
	at mygame.Main.testCollisionAndOutput(Main.java:89)
	at mygame.Main.access$000(Main.java:27)
	at mygame.Main$1.onAction(Main.java:70)
	at com.jme3.input.InputManager.invokeActions(InputManager.java:169)
	at com.jme3.input.InputManager.onKeyEventQueued(InputManager.java:469)
	at com.jme3.input.InputManager.processQueue(InputManager.java:862)
	at com.jme3.input.InputManager.update(InputManager.java:914)
	at com.jme3.app.LegacyApplication.update(LegacyApplication.java:725)
	at com.jme3.app.SimpleApplication.update(SimpleApplication.java:227)
	at com.jme3.system.lwjgl.LwjglAbstractDisplay.runLoop(LwjglAbstractDisplay.java:151)
	at com.jme3.system.lwjgl.LwjglDisplay.runLoop(LwjglDisplay.java:197)
	at com.jme3.system.lwjgl.LwjglAbstractDisplay.run(LwjglAbstractDisplay.java:232)
	at java.lang.Thread.run(Thread.java:748)

Could this be a bug, or am I possibly trying to do this collision incorrectly?

In the past I have never needed to retrieve the CollisionResults list unless I was using a ray… and I’ve even had success using BoundingBoxes (or spheres) to intersect with a terrain using the BoundingVolume’s .intersect() method many times…

But in this specific case I need to use .collideWith()to retrieve the CollisionResults list, because I’m using all of the ‘hit’ triangles to reconstruct a decal mesh

(If it helps to give more context, I am (re)-working on a decal / texture projection system that is based on this article)

Thanks :slightly_smiling_face:

Edit: I forgot to note, I’m running this on v3.2.1-stable-sdk1

1 Like

Thanks for the test case, gonna look into it

1 Like

I can’t repro the issue. It could help if you include your newScene.j3o. or do a test case based ont he TestTerrain test case.

No problem, I just copied the TestTerrainAdvanced and added the testCollisionAndOutput() method from my original test case to a key trigger, and the issue is still there. It looks like the collision results list gets filled with a null entry for each Terrain Patch.

Here’s the AdvancedTerrainTest case, with the only change being my collision method getting called when you press the ‘P’ key

/**
 * This is the Main Class of your Game. You should only do initialization here.
 * Move your Logic into AppStates or Controls
 * @author normenhansen
 */
public class Main extends SimpleApplication {

    private TerrainQuad terrain;
    Material matRock;
    Material matWire;
    boolean wireframe = false;
    boolean triPlanar = false;
    protected BitmapText hintText;
    PointLight pl;
    Geometry lightMdl;
    private float grassScale = 64;
    private float dirtScale = 16;
    private float rockScale = 128;

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

    @Override
    public void initialize() {
        super.initialize();

        loadHintText();
    }

    @Override
    public void simpleInitApp() {
        setupKeys();

        // First, we load up our textures and the heightmap texture for the terrain

        // TERRAIN TEXTURE material
        matRock = new Material(assetManager, "Common/MatDefs/Terrain/Terrain.j3md");
        matRock.setBoolean("useTriPlanarMapping", false);

        // ALPHA map (for splat textures)
        matRock.setTexture("Alpha", assetManager.loadTexture("Textures/Terrain/splat/alphamap.png"));

        // HEIGHTMAP image (for the terrain heightmap)
        Texture heightMapImage = assetManager.loadTexture("Textures/Terrain/splat/mountains512.png");

        // GRASS texture
        Texture grass = assetManager.loadTexture("Textures/Terrain/splat/grass.jpg");
        grass.setWrap(Texture.WrapMode.Repeat);
        matRock.setTexture("Tex1", grass);
        matRock.setFloat("Tex1Scale", grassScale);

        // DIRT texture
        Texture dirt = assetManager.loadTexture("Textures/Terrain/splat/dirt.jpg");
        dirt.setWrap(Texture.WrapMode.Repeat);
        matRock.setTexture("Tex2", dirt);
        matRock.setFloat("Tex2Scale", dirtScale);

        // ROCK texture
        Texture rock = assetManager.loadTexture("Textures/Terrain/splat/road.jpg");
        rock.setWrap(Texture.WrapMode.Repeat);
        matRock.setTexture("Tex3", rock);
        matRock.setFloat("Tex3Scale", rockScale);

        // WIREFRAME material
        matWire = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
        matWire.getAdditionalRenderState().setWireframe(true);
        matWire.setColor("Color", ColorRGBA.Green);

        // CREATE HEIGHTMAP
        AbstractHeightMap heightmap = null;
        try {
            //heightmap = new HillHeightMap(1025, 1000, 50, 100, (byte) 3);

            heightmap = new ImageBasedHeightMap(heightMapImage.getImage(), 1f);
            heightmap.load();

        } catch (Exception e) {
            e.printStackTrace();
        }

        /*
         * Here we create the actual terrain. The tiles will be 65x65, and the total size of the
         * terrain will be 513x513. It uses the heightmap we created to generate the height values.
         */
        /**
         * Optimal terrain patch size is 65 (64x64).
         * The total size is up to you. At 1025 it ran fine for me (200+FPS), however at
         * size=2049, it got really slow. But that is a jump from 2 million to 8 million triangles...
         */
        terrain = new TerrainQuad("terrain", 65, 513, heightmap.getHeightMap());
        TerrainLodControl control = new TerrainLodControl(terrain, getCamera());
        control.setLodCalculator( new DistanceLodCalculator(65, 2.7f) ); // patch size, and a multiplier
        terrain.addControl(control);
        terrain.setMaterial(matRock);
        terrain.setLocalTranslation(0, -100, 0);
        terrain.setLocalScale(2f, 0.5f, 2f);
        rootNode.attachChild(terrain);

        DirectionalLight light = new DirectionalLight();
        light.setDirection((new Vector3f(-0.5f, -1f, -0.5f)).normalize());
        rootNode.addLight(light);

        cam.setLocation(new Vector3f(0, 10, -10));
        cam.lookAtDirection(new Vector3f(0, -1.5f, -1).normalizeLocal(), Vector3f.UNIT_Y);
    }

    public void loadHintText() {
        hintText = new BitmapText(guiFont, false);
        hintText.setSize(guiFont.getCharSet().getRenderedSize());
        hintText.setLocalTranslation(0, getCamera().getHeight(), 0);
        hintText.setText("Hit T to switch to wireframe,  P to switch to tri-planar texturing");
        guiNode.attachChild(hintText);
    }

    private void setupKeys() {
        flyCam.setMoveSpeed(50);
        inputManager.addMapping("wireframe", new KeyTrigger(KeyInput.KEY_T));
        inputManager.addListener(actionListener, "wireframe");
        inputManager.addMapping("triPlanar", new KeyTrigger(KeyInput.KEY_P));
        inputManager.addListener(actionListener, "triPlanar");
    }
    private ActionListener actionListener = new ActionListener() {

        public void onAction(String name, boolean pressed, float tpf) {
            if (name.equals("wireframe") && !pressed) {
                wireframe = !wireframe;
                if (!wireframe) {
                    terrain.setMaterial(matWire);
                } else {
                    terrain.setMaterial(matRock);
                }
            } else if (name.equals("triPlanar") && !pressed) {
                triPlanar = !triPlanar;
                if (triPlanar) {
                    matRock.setBoolean("useTriPlanarMapping", true);
                    // planar textures don't use the mesh's texture coordinates but real world coordinates,
                    // so we need to convert these texture coordinate scales into real world scales so it looks
                    // the same when we switch to/from tri-planar mode
                    matRock.setFloat("Tex1Scale", 1f / (float) (512f / grassScale));
                    matRock.setFloat("Tex2Scale", 1f / (float) (512f / dirtScale));
                    matRock.setFloat("Tex3Scale", 1f / (float) (512f / rockScale));
                } else {
                    matRock.setBoolean("useTriPlanarMapping", false);
                    matRock.setFloat("Tex1Scale", grassScale);
                    matRock.setFloat("Tex2Scale", dirtScale);
                    matRock.setFloat("Tex3Scale", rockScale);
                }
                
                testCollisionAndOutput();
                
            }
            
        }
    };

Collision method (called when you press the P key that also toggles triplanar mode in the terrain test):

      private void testCollisionAndOutput(){
           CollisionResults results = new CollisionResults();
           Vector3f spawn = cam.getLocation();
           BoundingBox bounds = new BoundingBox(spawn,150f,150f,150f);
           Ray ray = new Ray(spawn, new Vector3f(0,-1,0));
           
    //      terrain.collideWith(bounds, results); 
           rootNode.collideWith(bounds, results);
           
           
           System.out.println("# of collisions: " + results.size());
           for(int i =0; i < results.size(); i ++){
               System.out.println("Collision with:  " + results.getCollision(i).getGeometry());

               //  System.out.println("Collision with:  " + results.getCollision(i).getGeometry().getName());  <- throws a null pointer
           }
           
           
           
       }
    
}

The crash no longer occurs because I’m no longer calling .getName() on the geometry that is null. When the bounding box collides with the terrain, here is the output.

 # of collisions: 4
 Collision with:  null
 Collision with:  null
 Collision with:  null
 Collision with:  null

I’ll also try to update to a newer SDK version soon and see if I have the same results.

mhhh the terrainPatch collision never get past the bound check

mhh ok terrain patch doesn’t fill the collision information.
@javasabr how did you do for your terrain painting tool? I guess you are using this aren’t you?

Oh ok so you only do a ray test.
Well I guess terrain patch doesn’t handle this properly…

Would it be safe to change the bound check, or is it important to keep it there before calling collideWithBoundingVolume() ?

I guess I could also make a custom method to check the positions of verts in the terrain without using bounding volumes for collision, although I’m not sure if that would be as efficient.

No I have to dig into that. The collision actually happens, but the geom/ triangle etc are not set on the CollisionResult.
I’m quite surprised. The whole collision process is different than with classic geometry for some reason.
Never really used the terrain for this.

After taking a long break from working on some loose ends with my decal system (mostly due to my lack of knowledge in that area at the time) I’ve finally gotten back to work on it, and I’m working on trying to fix this error so that I can properly project decals onto TerrainPatches the same way I project decals onto a classic Geometry.

The problem is in the CollideWithBoundingBox method of the TerrainPatch

The collision does occur, but the CollisionResults are not filled with important data such as the ContactPoint or ContactNormal whenever there’s a collision with a bounding box.

I created a fix on my own branch of jme3-terrain and it appears to work, but I am not quite sure that this fix would be considered the best solution, since it still does not address why collisions with the TerrainPatch’s mesh create a CollisionResult without filling in any of the important data such as contactPoint or contactNormal.

Even with the changes I’ve made in my custom branch, I am still noticing that the triangle is not being properly set on the CollisionResult that I am manually creating for each triangle that intersects the BoundingBox - the method Triangle.getIndex() always returns an index for the same triangle in the TerrainPatch

I would greatly appreciate any input or guidance on fixing this error, as it is the final thing preventing me from cleaning up and finishing the Decal System, before I can finally share it with others :slightly_smiling_face:

You will find that a lot of the collision stuff won’t fill out all of the information. It’s really meant for “yes/no” types of questions and when the answer is easy then it will include it.

For example, though, a box to terrain collision may have a large number of contact points and contact normals. So there is no way to fill in a correct answer.

1 Like

I guess that also answers another question I was going to ask - which is why the box collision against a standard geometry returns a list of all collided triangles, but returns null for all other CollisionResult data such as the contact point/normal/distance.

For some reason it seems like the TerrainPatch is the exact opposite , and finding the contacted triangles isn’t as easy - I’ve been able to fill in a contact point/normal/dist for each vertex that collides or intersects with the box, but the .setTriangle(int index)method for an individual CollisionResult doesn’t take in a Triangle object. Instead it wants the index of the triangle in relation its Geometry, which seems to be impossible to find when I try to use the same code for colliding with TerrainPatches

It would be optimal if I could get the Terrain collision with a bounding box to work the same way as the box vs geometry collision, where it fills a list of all triangles that intersect with the bounding box to the CollisionResults. But It seems like finding the triangle isn’t as easy with the TerrainPatch as it is with the classic Geometry