How to compute the visible spatials?

Hi all,



I'm using jME in a new research project and am deeply amazed at how easy it is to implement a simple 3D game with it. This is a beautiful engine.



However, for my project I need to be able to figure out what Spatials are currently visible. I have experimented with the Camera.contains method, which should tell me whether the bounds of some spatial intersect with the camera frustum. However, this seems to sometimes return objects that are completely hidden by other objects, and it seems that "contains" sometimes continues to return true for objects that were in view before, but are no longer in view because I turned away. This means that the list of spatials that are visible according to "contains" is not accurate.



Is there some official way to get a list of all visible objects?



Thanks!

As occlusion culling is currently not implemented there won't be an easy way to determine the actual visibility, as you want it. But frustum culling should work fine, so contains should give you accurate results. Can you give a short testcase where you think the contains methods gives you wrong results?



To compute if a spatial is completely hidden behind some other geometry have a look at occlusion culling methods, maybe you can find one that fits your needs.

Ok, here's a test case. It's mostly from some tutorial example, but if you press the space key, it will show you the list of all spatials whose bounding boxes are contained in or intersect the frustum. Initially, this will be just the terrain. If you turn far enough to see the grey walls and press space, it will list the terrain plus all three walls (although one of them is hidden; it's a shame that there is no occlusion culling). Then if you turn back to your original orientation, the walls will still be listed, although they are no longer visible.



import javax.swing.ImageIcon;

import com.jme.app.SimpleGame;
import com.jme.bounding.BoundingBox;
import com.jme.image.Texture;
import com.jme.input.KeyInput;
import com.jme.input.action.InputAction;
import com.jme.input.action.InputActionEvent;
import com.jme.math.Vector3f;
import com.jme.scene.Spatial;
import com.jme.scene.shape.Box;
import com.jme.scene.state.TextureState;
import com.jme.util.LoggingSystem;
import com.jme.util.TextureManager;
import com.jmex.terrain.TerrainBlock;
import com.jmex.terrain.util.MidPointHeightMap;
import com.jmex.terrain.util.ProceduralTextureGenerator;

public class VisibleObjectsExample extends SimpleGame {
   private TerrainBlock tb;

   private int wallid = 1;
   

   private class ClickAction extends InputAction {
      public void performAction(InputActionEvent evt) {
            for( Spatial sp : rootNode.getChildren() ) {
               if( cam.contains(sp.getWorldBound()) > 0 ) {
                  System.err.println("Visible: " + sp + " (" + cam.contains(sp.getWorldBound()) + ")");
                  
               }
            }
           
      }
   }

   
   

   @Override
   protected void simpleInitGame() {
      // position camera
      Vector3f left = new Vector3f(-1.0f, 0.0f, 0.0f);
      Vector3f up = new Vector3f(0.0f, 1.0f, 0.0f);
      Vector3f dir = new Vector3f(0.0f, 0.0f, -1.0f);

      cam.setFrame(new Vector3f(500.0f, 150.0f, 500.0f), left, up, dir);
      cam.update();       /** Signal that we've changed our camera's location/frustum. */
      
      
      // make terrain
      buildTerrain();
      rootNode.attachChild(tb);
      
      // put some walls into the scene
      makeWall(300, 500, true);
      makeWall(400, 500, true);
      makeWall(445, 555, false);
      
      // update the scene graph for rendering
      rootNode.updateGeometricState(0.0f, true);
      rootNode.updateRenderState();
      
      input.addAction( new ClickAction(), "click", KeyInput.KEY_SPACE, false );
   }
   
   



   private Box makeWall(float x, float z, boolean orientation) {
      Box wall;
      Vector3f center = new Vector3f(x, 100, z);
      
      if( orientation ) {
         wall = new Box("wall" + (wallid++), center, 50, 100, 5);
      } else {
         wall = new Box("wall" + (wallid++), center, 5, 100, 50);
      }
      
       
      wall.setModelBound(new BoundingBox());
      wall.updateModelBound();
      rootNode.attachChild(wall);
      
      return wall;
   }



   /**
    * build the height map and terrain block.
    */
   private void buildTerrain() {


      // Generate a random terrain data
      MidPointHeightMap heightMap = new MidPointHeightMap(64, 1f);
      // Scale the data
      Vector3f terrainScale = new Vector3f(20, 0.5f, 20);
      // create a terrainblock
      tb = new TerrainBlock("Terrain", heightMap.getSize(), terrainScale,
            heightMap.getHeightMap(), new Vector3f(0, 0, 0), false);

      tb.setModelBound(new BoundingBox());
      tb.updateModelBound();

      // generate a terrain texture with 2 textures
      ProceduralTextureGenerator pt = new ProceduralTextureGenerator(
            heightMap);
      pt.addTexture(new ImageIcon(VisibleObjectsExample.class.getClassLoader()
            .getResource("jmetest/data/texture/grassb.png")), -128, 0, 128);
      pt.addTexture(new ImageIcon(VisibleObjectsExample.class.getClassLoader()
            .getResource("jmetest/data/texture/dirt.jpg")), 0, 128, 255);
      pt.addTexture(new ImageIcon(VisibleObjectsExample.class.getClassLoader()
            .getResource("jmetest/data/texture/highest.jpg")), 128, 255,
            384);
      pt.createTexture(32);

      // assign the texture to the terrain
      TextureState ts = display.getRenderer().createTextureState();
      ts.setEnabled(true);
      Texture t1 = TextureManager.loadTexture(pt.getImageIcon().getImage(),
            Texture.MM_LINEAR_LINEAR, Texture.FM_LINEAR, true);
      ts.setTexture(t1, 0);

      tb.setRenderState(ts);
   }

   /**
    * @param args
    */
   public static void main(String[] args) {
      LoggingSystem.getLogger().setLevel(java.util.logging.Level.WARNING);
      VisibleObjectsExample app = new VisibleObjectsExample();
      app.setDialogBehaviour(FIRSTRUN_OR_NOCONFIGFILE_SHOW_PROPS_DIALOG);
      
      app.start();

   }

}

Here's another thought. I could live with computing the set of visible objects only once a second or so. So I was thinking I could just scan the whole screen and check which object is closest to the camera in each direction. This takes about 10ms, and seems to work okay. Here's the code for doing this; you can just replace the ClickAction class in my previous example with this new version.



private class ClickAction extends InputAction {
      public void performAction(InputActionEvent evt) {
         Set<String> visible = new HashSet<String>();
         Vector3f coordinates = new Vector3f();
         Vector3f direction = new Vector3f();
         Vector3f cameraloc = cam.getLocation();
         
         for( int x = 0; x < display.getWidth(); x += 10 ) {
            for( int y = 0; y < display.getHeight(); y += 10 ) {
               float minDistance = 10000;
               String closestObject = null;
               
               cam.getWorldCoordinates(new Vector2f(x,y), 0.001f, coordinates);
               coordinates.subtract(cameraloc, direction);
               
               Ray ray = new Ray(cameraloc, direction);
               PickResults results = new BoundingPickResults();
               results.setCheckDistance(true);
               rootNode.findPick(ray,results);

               for( int i = 0; i < results.getNumber(); i++ ) {
                  float dist = results.getPickData(i).getDistance();
                  String name = results.getPickData(i).getTargetMesh().getParentGeom().getName();

                  if( dist < minDistance ) {
                     closestObject = name;
                     minDistance = dist;
                  }
               }
               
               if( closestObject != null ) {
                  visible.add(closestObject);
               }
            }
         }
         
         System.err.println("Visible: " + visible);
      }
   }



This seems to work better than the algorithm I looked at first. But this new algorithm now finds fewer visibles than there are. If you look down such that a corner of the walls is in the upper part of the screen, the algorithm will not see the walls. As you raise the camera, the algorithm starts recognizing the walls after they come below a certain y position on the screen.

Am I using the getWorldCoordinates method correctly? The algorithm seems to improve as I reduce the z coordinate parameter to that method (which is why it is now at 0.001), but I don't really understand why and couldn't find documentation.

And also: Am I not supposed to call getWorldCoordinates from another thread? If I create a new thread that calls new ClickAction().performAction(null), I get the following exception:



Exception in thread "Thread-4" java.lang.NullPointerException
   at org.lwjgl.opengl.GL11.glMatrixMode(GL11.java:1737)
   at com.jme.renderer.lwjgl.LWJGLCamera.onFrameChange(Unknown Source)
   at com.jme.renderer.lwjgl.LWJGLCamera.getModelViewMatrix(Unknown Source)
   at com.jme.renderer.AbstractCamera.checkViewProjection(Unknown Source)
   at com.jme.renderer.AbstractCamera.getWorldCoordinates(Unknown Source)
   at give.learning.VisibleObjectsExample$ClickAction.performAction(VisibleObjectsExample.java:58)
   at give.learning.VisibleObjectsExample$1.run(VisibleObjectsExample.java:135)

you're not supposed to do anything with opengl from another thread.

I could be wrong here - but you seem to be using BoundingPickResults(). So I think you should get hits on bounding volumes; if your actual meshes are smaller than the bounding volumes then you will get things being "hidden" by bounding boxes in front of them, but still visible past the actual meshes. You could use triangle accurate picking, but I think there might be issues with that, not sure.

I've your graphics card support them, then occlusion queries would be the perfect tool for this since.

The reason your contains code example doesn't work is because of how camera planestate works.  You have to save the planestate and then reset it after checking bounds.



Change your ClickAction like so and it works as expected:


   private class ClickAction extends InputAction {
      public void performAction(InputActionEvent evt) {
         for (Spatial sp : rootNode.getChildren()) {
            int ps = cam.getPlaneState();
            if (cam.contains(sp.getWorldBound()) > 0) {
               System.err.println("Visible: " + sp + " ("
                     + cam.contains(sp.getWorldBound()) + ")");

            }
            cam.setPlaneState(ps);
         }
      }
   }