Issues with Collision Detection based on TestObjectWalking.java

Hey folks,



I'm trying to make my players & NPCs move appropriately over the terrain and objects that appear on the terrain.  In other words, if a bridge appears on the terrain, a player should walk over the bridge instead of through it.  Searching these forums, I found a couple threads that led me to the code in TestObjectWalking.java.  It looks like it's designed to do what I'm looking for, but when I put it into service I discovered a few issues.



Below is the code from my project, which is adapted from TestObjectWalking.java.  The evaluateRequiredY() method is supposed to calculate the appropriate Y coordinate value for the passed-in Graphic (a class I  defined).



    private float evaluateRequiredY(Graphic g, float x, float z) {
       
        // Assertions.
        if (g == null) {
            String msg = "Argument 'g' [Graphic] cannot be null.";
            throw new RuntimeException(msg);
        }
       
        // Not allowed to go lower than the terrain height...
        TerrainBlock tb = terrainManager.getTerrain();
        float highPoint = tb.getHeightFromWorld(new Vector3f(x, 0.0f, z));

        // Calculate the high point of the sceneGraph using a Ray...
        Vector3f origin = new Vector3f(x, 500.0f, z);
        Ray r = new Ray(origin, new Vector3f(0.0f, -1.0f, 0.0f));
        PickResults pr = new TrianglePickResults();
        pr.setCheckDistance(true);
        sceneGraph.calculatePick(r, pr);

        if (pr.getNumber() > 0) {
            PickData pd = pr.getPickData(0);
            Geometry geo = pd.getTargetMesh();
            Vector3f[] vec = new Vector3f[3];
            if (geo instanceof QuadMesh) {
                // Use the terrain until we can figure out a better solution...
                if (log.isDebugEnabled()) {
                    StringBuilder sb = new StringBuilder();
                    sb.append("Failed to calculate highPoint because the targetMesh was a Quad...")
                                .append("ntg.get(Entity.NAME)=").append(g.get(Entity.NAME))
                                .append("ntgeo.getName()=").append(geo.getName())
                                .append("ntx=").append(x).append(", z=").append(z);
                    log.debug(sb.toString());
                }
            } else if (geo instanceof TriMesh) {
                TriMesh mesh = (TriMesh) geo;
                List<Integer> tris = pd.getTargetTris();
                if(tris.size() > 0) {
                    int triIndex = ((Integer) tris.get(0)).intValue();
                    mesh.getTriangle(triIndex, vec);
                    for(int i=0; i < vec.length; i++) {
                        vec[i].multLocal(mesh.getWorldScale());
                        mesh.getWorldRotation().mult(vec[i], vec[i]);
                        vec[i].addLocal(mesh.getWorldTranslation());
                    }
                    Vector3f loc = new Vector3f();
                    pd.getRay().intersectWhere(vec[0], vec[1], vec[2], loc);
                    if (loc.getY() < highPoint) {
                        // How does this happen?
                        if (log.isErrorEnabled()) {
                            StringBuilder sb = new StringBuilder();
                            sb.append("calculatePick selected a Triangle that's lower than the terrain height...")
                                        .append("ntg.get(Entity.NAME)=").append(g.get(Entity.NAME))
                                        .append("ntgeo.getName()=").append(geo.getName())
                                        .append("ntx=").append(x).append(", z=").append(z);
                            log.error(sb.toString());
                        }
                    } else {
                        highPoint = loc.getY();
                    }
                } else {
                    if (log.isDebugEnabled()) {
                        StringBuilder sb = new StringBuilder();
                        sb.append("Failed to calculate highPoint because the 'tris' collection is empty...")
                                    .append("ntg.get(Entity.NAME)=").append(g.get(Entity.NAME))
                                    .append("ntgeo.getName()=").append(geo.getName())
                                    .append("ntx=").append(x).append(", z=").append(z);
                        log.debug(sb.toString());
                    }
                }
            }           
           
        } else {
            // Use the terrain until we can figure out a better solution...
            if (log.isDebugEnabled()) {
                StringBuilder sb = new StringBuilder();
                sb.append("Failed to calculate highPoint because the PickResults were empty...")
                            .append("ntg.get(Entity.NAME)=").append(g.get(Entity.NAME))
                            .append("ntx=").append(x).append(", z=").append(z);
                log.debug(sb.toString());
            }
        }

        return highPoint + (Float) g.getAttribute(DISTANCE_TO_BOTTOM_FROM_CENTER);
       
    }



(1) To begin with, 3/4 of the log messages that indicate failure to calculate the appropriate Y value are (in fact) occurring in the log.  So I know that...
  - There are QuadMesh objects in my scene graph (imported from Blender using the XMLImporter), which don't work in this code because QuadMesh doesn't extend from TriMesh and doesn't have a getTriangle() method.
  - Sometimes the first entry in the PickResults indicates a Triangle that's *BELOW* the terrain, which shouldn't happen b/c the Ray should be intersecting with the terrain.  (Right?)
  - Sometimes the PickResults is empty even when I'm over the terrain, which (again) shouldn't happen b/c the Ray should be intersecting with the terrain.  (Right?)

(2) What is the purpose of this part of the code (adapted from TestObjectWalking.java)?


    Vector3f[] vec = new Vector3f[3];
    mesh.getTriangle(triIndex, vec);
    for(int i=0; i < vec.length; i++) {
        vec[i].multLocal(mesh.getWorldScale());
        mesh.getWorldRotation().mult(vec[i], vec[i]);
        vec[i].addLocal(mesh.getWorldTranslation());
    }



What's wrong with the triangle (vector array) fresh from the mesh?  What are the methods within the for loop even doing?  multLocal?  mult?  addLocal?

(3) All this code seems pretty low-level and complicated, and yet it seems like something that most jME projects will have to deal with.  Should there be framework API methods that help with these tasks and hide the complexity?  If not within jME, what about a add-on project?

Thanks to anyone who can help me advance my understanding of this tricky problem.

- haruspex
haruspex said:

I'm trying to make my players & NPCs move appropriately over the terrain and objects that appear on the terrain.  In other words, if a bridge appears on the terrain, a player should walk over the bridge instead of through it.  Searching these forums, I found a couple threads that led me to the code in TestObjectWalking.java.  It looks like it's designed to do what I'm looking for, but when I put it into service I discovered a few issues.


As usual, my issues were due to "user error."  :P

Also as usual, I discovered my mistake(s) pretty soon after I finally posted about my issues here.

I'm replying to my own thread in case someone comes along later with the same symptoms and wants to know what the solution was in my case...

The biggest problem by far was this:  the scene graph I was using for collision detection was a server-side, "master" tree that was supposed to duplicate what the user was seeing on the client.  I set it up this way because I want the game server to be the ultimate authority over the placement and movement of objects in the game world. 

Well, I was missing the line of code that places objects where they really are on the server side, so all my objects had a local translation of 0.0/0.0/0.0.  :-o  Given this mistake, it's not surprising at all that I wasn't getting the results I expected.

Once I corrected this issue, I was able to detect collisions with the objects in my game world correctly... with a couple exceptions:
  - (1) My code didn't detect collisions with the terrain (hightmap imported from image file).  I don't know why.  I "fixed" it by simply removing the terrain from my server-side scene graph, and using the getHeightFromWorld() method as a "floor" -- the Y coordinate must be getHeightFromWorld() or greater.
  - (2) One of the objects in my scene graph is a QuadMesh.  Looking at the jME source, the QuadMesh class doesn't actually implement the methods for collision detection:  findCollisions() and hasCollision().  This object came into my world via the XMLImporter.  I have no idea why it's a QuadMesh instead of a TriMesh, nor why QuadMesh can't detect collisions... but I think it needs looking into.

Here is my working evaluateRequiredY() method, for anyone interested:


    private float evaluateRequiredY(Graphic g, float x, float z) {
       
        // Assertions.
        if (g == null) {
            String msg = "Argument 'g' [Graphic] cannot be null.";
            throw new RuntimeException(msg);
        }
       
        // Start with the terrain height as the highPoint
        // b/c we're not allowed to go lower than that...
        TerrainBlock tb = terrainManager.getTerrain();
        float highPoint = tb.getHeightFromWorld(new Vector3f(x, 0.0f, z));

        // Determine if, at the intended X & Z coords, we intersect
        // with any objects in the sceneGraph by using a Ray...
        Vector3f origin = new Vector3f(x, 500.0f, z);
        Ray r = new Ray(origin, new Vector3f(0.0f, -1.0f, 0.0f));
        PickResults pr = new TrianglePickResults();
        pr.setCheckDistance(true);
        sceneGraph.calculatePick(r, pr);
       
        Spatial myself = (Spatial) g.getAttribute(SPATIAL_ATTRIBUTE_NAME);
        for (int i=0; i < pr.getNumber(); i++) {
           
            PickData pd = pr.getPickData(i);
            Geometry geo = pd.getTargetMesh();
            if (geo.equals(myself)) {
                // Make sure we don't pick ourselves...
                continue;
            }
           
            // OK -- we can use this one...
            Vector3f[] vec = new Vector3f[3];
            if (geo instanceof QuadMesh) {
                // Use the terrain until we can figure out a better solution...
                if (log.isErrorEnabled()) {
                    StringBuilder sb = new StringBuilder();
                    sb.append("Failed to calculate highPoint because the targetMesh was a Quad...")
                                .append("ntg.get(Entity.NAME)=").append(g.get(Entity.NAME))
                                .append("ntgeo.getName()=").append(geo.getName())
                                .append("ntx=").append(x).append(", z=").append(z);
                    log.error(sb.toString());
                }
            } else if (geo instanceof TriMesh) {
                TriMesh mesh = (TriMesh) geo;
                List<Integer> tris = pd.getTargetTris();
                if(tris.size() > 0) {
                    int triIndex = ((Integer) tris.get(0)).intValue();
                    mesh.getTriangle(triIndex, vec);
                    for(int j=0; j < vec.length; j++) {
                        vec[j].multLocal(mesh.getWorldScale());
                        mesh.getWorldRotation().mult(vec[j], vec[j]);
                        vec[j].addLocal(mesh.getWorldTranslation());
                    }
                    Vector3f loc = new Vector3f();
                    pd.getRay().intersectWhere(vec[0], vec[1], vec[2], loc);
                   
                    // If the point of intersection is higher than the terrain, use it...
                    highPoint = loc.getY() > highPoint ? loc.getY() : highPoint;
                } else {
                    if (log.isWarnEnabled()) {
                        StringBuilder sb = new StringBuilder();
                        sb.append("Failed to calculate point of collision with a mesh because ")
                                    .append("the 'tris' collection is empty...")
                                    .append("ntg.get(Entity.NAME)=").append(g.get(Entity.NAME))
                                    .append("ntgeo.getName()=").append(geo.getName())
                                    .append("ntx=").append(x).append(", z=").append(z);
                        log.warn(sb.toString());
                    }
                }
            }
           
            // We're done once we examine the top non-self pick...
            break;
           
        }

        return highPoint + (Float) g.getAttribute(DISTANCE_TO_BOTTOM_FROM_CENTER);

    }



Cheers,

- haruspex