Learning jME

I just started a project yesterday with the idea of learning jME. Since I can't model for the life of me, my idea was to create a game that didn't require any models beyond cubes. However, I'm running into an issue where if I render one-thousand textured cubes on the screen, the framerate/performance slow to a crawl. Obviously, my first assumption is that perhaps this is too many cubes to render at once, so I was hoping to pick the communities collective brains on some optimizations I can perform to speed up the rendering of these cubes.



Also, on a related note. Using jME's box object, it seems that if I enable back face culling that it actually is letting me see through the cubes to the back, rather than the expected result of culling the back sides (inside faces) of the cube walls. What am I doing stupid?



Thanks.

A couple optimizations:


  1. Organize your boxes in the tree as efficiently as possible. This is wholly dependant on how your game works, but if you can create something like a quadtree/octree that will speed parsing the tree a great deal.
  2. Depending on how the boxes are you can lock parts of them. You can certainly lock the meshes (creating display lists). If they are static you can continue locking other parts including the full tree (I doubt they are completely static though).
  3. Because they are all boxes, you can use shared meshes and lock the target box (creating the display list as mentioned above).



    There are quite a few other ways to optimize, but that might get you started.

Okay. I didn't know about shared mesh, so that was pretty easy to implement. Since all of my models right now are boxes, this certainly helps things. Now for your other two suggestings.


mojomonk said:

1. Organize your boxes in the tree as efficiently as possible. This is wholly dependant on how your game works, but if you can create something like a quadtree/octree that will speed parsing the tree a great deal.


By this I'm assuming you mean how they're organized in the scene graph? If so, I'm not quite sure how to organize them in a way that would speed up the tree processing. What I'm trying to do at the moment is create a maze from the cubes with an underlying quad to represent the ground underneath the maze. The reason I'm using cubes for the walls, etc, is because eventually I want the player to be able to dig new passages (which I imagined I'd implement by having an action that removed the cubes as nessecary based on player actions).

mojomonk said:

2. Depending on how the boxes are you can lock parts of them. You can certainly lock the meshes (creating display lists). If they are static you can continue locking other parts including the full tree (I doubt they are completely static though).


Mind going into a little more detail here? I'm not really sure what "locking" does (other than a guess based on it's English meaning :) ) and I'm a little hesitant to just go around trying out the various methods with the word "lock" in them. Also, now that I'm using shared meshes for all my boxes, what further benefit do I gain by "locking the meshes"?

Anyone…?



I know these are newbie questions, but it's really frustrating that rendering a scene with so little geometry brings my fairly decent gaming rig to a halt. I know that jME is capable of some amazing stuff, but this simplest of hurdles is preventing me from doing just about anything.



If anyone has any ideas or the time to give more details as to the behaviour experienced above and how to correct it, I'd be most appreciative.

Can you post up your code maybe we can give you some hints/optimizations.



Have you gone through all the jmetest package, and seen some of the code like FlagRushTutorial:

http://www.jmonkeyengine.com/wiki/doku.php?id=flag_rush_tutorial_series_by_mojomonkey



It'll show you useful things like adding backface culling, SharedMesh's, etc.



Also an interesting example of performance gain by locking the meshes and allowing them to be converted to display lists, is the jmetest.stress.graphbrowser.GraphBrowser.java class, and the TestGraphBrowser.java to run it.



Initially the test runs at around 10 to 17 fps on my computer with:

Meshes (2049)

Vertices (49154)

Triangles (24576)

Line(1)  (I turned the lines off)



If I add the following couple lines at the bottom of the simpleInitGame() method:



        //Lock all meshes
        rootNode.lockMeshes();
        // Make sure we remove the back faces
        CullState bfculling = DisplaySystem.getDisplaySystem().getRenderer()
                .createCullState();
        bfculling.setCullMode(CullState.CS_BACK);
        rootNode.setRenderState(bfculling);



Then I get the same geometry statistics, but my FPS is now 49 to 60 fps.

Here's the entire modified GraphBrowser.java replace the one you have from the CVS and run the TestGraphBrowser with it to see the improvement.

At a minimum you can call rootNode.lockMeshes() on your box character to see if that helps.


GraphBrowser.java


/*
 * Copyright (c) 2003-2006 jMonkeyEngine
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 * * Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * * Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 *
 * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
 *   may be used to endorse or promote products derived from this software
 *   without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package jmetest.stress.graphbrowser;

import java.util.HashMap;
import java.util.Map;

import jmetest.stress.StressApp;

import com.jme.input.KeyBindingManager;
import com.jme.input.KeyInput;
import com.jme.math.Vector3f;
import com.jme.renderer.ColorRGBA;
import com.jme.scene.Line;
import com.jme.scene.Node;
import com.jme.scene.SceneElement;
import com.jme.scene.SharedMesh;
import com.jme.scene.Spatial;
import com.jme.scene.Text;
import com.jme.scene.shape.Box;
import com.jme.scene.state.CullState;
import com.jme.scene.state.LightState;
import com.jme.scene.state.MaterialState;
import com.jme.system.DisplaySystem;

/**
 * Many Boxes and lines make up some graph. Graph data is specified by a {@link GraphAccessor} and
 * layout by a {@link GraphLayouter}.
 * @author Irrisor
 * @created 08.07.2005, 16:19:38
 */
public class GraphBrowser extends StressApp {

    /**
     * Map from graph node (Object) to visualization (Spatial)
     */
    Map<Object, Spatial> nodes = new HashMap<Object, Spatial>();
    /**
     * Map from graph edge (Object) to visualization (Line)
     */
    Map<Object, Line> edges = new HashMap<Object, Line>();
    /**
     * Accessor used for reading the graph.
     */
    private GraphAccessor accessor;
    /**
     * Layouter to query positions for graph node visuals.
     */
    private GraphLayouter layouter;
    /**
     * Flag for toggling visibility of all/path edges.
     */
    private boolean pathOnly;
    /**
     * Command for toggling edges.
     */
    private static final String COMMAND_PATH_ONLY = "toggle_path_only";
    private Box box;

    /**
     * Create a graphbrowser app that uses given {@link GraphAccessor} and {@link GraphLayouter}.
     * @param accessor graph data
     * @param layouter graph layout
     */
    public GraphBrowser( GraphAccessor accessor, GraphLayouter layouter ) {
        this.accessor = accessor;
        this.layouter = layouter;
    }

    /**
     * Create all the visual stuff for the graph.
     */
    protected void simpleInitGame() {
        lightState.setGlobalAmbient(new ColorRGBA(0.5f,0.5f,0.5f,1));
        box = new Box( "box", new Vector3f( -1, -1, -1 ), new Vector3f( 1, 1, 1 ) );

        MaterialState material = display.getRenderer().createMaterialState();
        material.setEnabled( true );
        ColorRGBA white = new ColorRGBA( 1, 1, 1, 1 );
        material.setDiffuse( white );
        rootNode.setRenderState( material );

        for ( int i = accessor.getNodeCount() - 1; i >= 0; i-- ) {
            Object node = accessor.getNode( i );
            Spatial nodeVis = new SharedMesh( String.valueOf( node ), box );
            nodeVis.getLocalTranslation().set( layouter.getCoordinates( node ) );

            ColorRGBA color = colorForNode( node );
            if ( !white.equals( color ) )
            {
                material = display.getRenderer().createMaterialState();
                material.setEnabled( true );
                material.setDiffuse( color );
                nodeVis.setRenderState( material );
            }
           
            rootNode.attachChild( nodeVis );
            nodes.put( node, nodeVis );
        }

        Node lines = new Node("lines");
        material = display.getRenderer().createMaterialState();
        material.setEnabled( true );
        material.setDiffuse( white );
        material.setEmissive( white );
        lines.setRenderState( material );
        rootNode.attachChild( lines );

        for ( int i = accessor.getEdgeCount() - 1; i >= 0; i-- ) {
            Object edge = accessor.getEdge( i );

            Spatial fromVis = nodes.get( accessor.getEdgeSource( edge ) );
            Spatial toVis = nodes.get( accessor.getEdgeTarget( edge ) );
           
            Vector3f[] points = {fromVis.getLocalTranslation(), toVis.getLocalTranslation()};
            Line edgeVis = new Line( edge.toString(), points, null, null, null );

            ColorRGBA color = colorForEdge( edge );
            if ( !white.equals( color ) )
            {
                material = display.getRenderer().createMaterialState();
                material.setEnabled( true );
                material.setDiffuse( color );
                material.setEmissive( color );
                edgeVis.setRenderState( material );
            }
            edgeVis.setLightCombineMode( LightState.COMBINE_CLOSEST );

            lines.attachChild( edgeVis );
            edgeVis.updateRenderState();
            edges.put( edge, edgeVis );
        }

        KeyBindingManager.getKeyBindingManager().set( COMMAND_PATH_ONLY, KeyInput.KEY_O );
        //Initial toggle the lines off
        togglePath();
       
       
        //Lock all meshes
        rootNode.lockMeshes();
        // Make sure we remove the back faces
        CullState bfculling = DisplaySystem.getDisplaySystem().getRenderer()
                .createCullState();
        bfculling.setCullMode(CullState.CS_BACK);
        rootNode.setRenderState(bfculling);
       
       
        final Text text = createText( "Press O to toggle edges/path" );
        text.getLocalTranslation().set( 0, 20, 0 );
        fpsNode.attachChild( text );

        cam.getLocation().set( 40, 40, 100 );
        cam.update();
    }

    /**
     * Query color for an edge - could be moved to layouter...
     * @param edge edge of interest
     * @return any color (not null)
     */
    private ColorRGBA colorForEdge( Object edge ) {
        boolean steiner = accessor.isEdgePath( edge );
        return new ColorRGBA( 1, steiner ? 0 : 1, steiner ? 0 : 1, 1 );
    }

    /**
     * Query color for a node - could be moved to layouter...
     * @param node node of interest
     * @return any color (not null)
     */
    private ColorRGBA colorForNode( Object node ) {
        return new ColorRGBA( 1, 1, accessor.isNodeTerminal( node ) ? 0 : 1, 1 );
    }

    /**
     * Process key input.
     */
    protected void simpleUpdate() {
        super.simpleUpdate();

        if ( KeyBindingManager
                .getKeyBindingManager()
                .isValidCommand( COMMAND_PATH_ONLY, false ) ) {
            togglePath();
        }

        // rearrange nodes
//        for ( int i = accessor.getNodeCount() - 1; i >= 0; i-- ) {
//            Object node = accessor.getNode( i );
//
//            Spatial nodeVis = (Spatial) nodes.get( node );
//
//            nodeVis.getLocalTranslation().set( layouter.getCoordinate( node, 0 ),
//                    layouter.getCoordinate( node, 1 ),
//                    layouter.getCoordinate( node, 2 ) );
//        }
    }
    private void togglePath() {
        pathOnly = !pathOnly;
        for ( int i = accessor.getEdgeCount() - 1; i >= 0; i-- ) {
            Object edge = accessor.getEdge( i );
            if ( !accessor.isEdgePath( edge ) ) {
                Spatial spatial = edges.get( edge );
                if ( spatial != null ) {
                    spatial.setCullMode( pathOnly ? SceneElement.CULL_ALWAYS : SceneElement.CULL_DYNAMIC);
                }
            }
        }
    }
}


If the position of the boxes is static (or move in the same way all at once) and if you use just a few textures, then use the GeometryBatch class for all boxes with the same texture. This improves performance dramatically, without much thinking :slight_smile:

The reason is very simple: Your graphic card is able to draw 1000 boxes without problems, the bottleneck is the CPU. So your speed does not so much depend on the  number of triangles, but on the number of "batches". This is similar to JDBC: Once you have a connection to the database, you can pull or push a lot of data very fast. But getting a database connection is so slow that connection pooling is used to avoid the overhead of creating additional connections. So GeometryBatch is "one connection" and 1000 boxes are "1000 connections".

@Landei

The position of the boxes that comprise the level are static after they've been initially positioned. I'll look into the GeometryBatch class and see how it works. If you had a pointer to a good example of it's usage, I would be most appreciative.



@dougnukem



Here's some sample code. Right now, I get 22fps with the following at 800x600 resolution with 32bpp and using LWJGL as my renderer.



TestApp


public class TestApp extends SimpleGame {
   public static void main(String[] args) {
      TestApp app = new TestApp();
      app.setDialogBehaviour(ALWAYS_SHOW_PROPS_DIALOG);   
      app.start();
   }

   @Override
   protected void simpleInitGame() {
      Level b = Level.getLevelForDepth("50");
      rootNode.attachChild(b);
      rootNode.lockMeshes();
      rootNode.lockBounds();
      rootNode.lockShadows();
      rootNode.lockTransforms();
      rootNode.lockBranch();
      

It looks like you aren't using bounding volumes?

Bounding volumes?



<============= complete newbie… :confused:

Woah. What a huge difference. Thank you so much Renanse.

HTH :slight_smile:

I have no example for GeometryBatch at hand, but it's very easy:

  • create your boxes, but without positions, and without RenderStates
  • in the place you would attach the boxes to a parent node, create a GeometryBatch
  • add the boxes to the GeometryBatch at the right positions (AFAIK, rotations are not yet implemented, just translations)
  • set the RenderStates of the GeometryBatch (that's why you need different GeometryBatches for different textures)
  • attach the GeometryBatch to the parent node

    that's it…



    By the way, if I remember right, you need just one box for every size, because the GeometryBatch doesn't reference the box, but really "copies" the geometry.