JME2 Starter 12 - Hello LevelOfDetail


« Previous: Starter Tutorial 11 - Hello Animation (2)
Next: Starter Tutorial 13 - Hello SimpleGame »


(Tip: Up-to-date source files for the tutorials are always in the repository)


This program introduces AreaClodMesh, BezierCurve, CurveController, and CameraNode. You will learn how to use AreaClodMesh to speed up your FPS: You will see two models with and without Continuous Level of Detail (CLOD) side by side, so you can compare. You will also learn how to use the CurveController to make the camera fly along a curved path.

Sample code

import com.jme.app.SimpleGame;
 
import com.jmex.model.converters.FormatConverter;
import com.jmex.model.converters.ObjToJme;
 
import com.jme.util.export.binary.BinaryImporter;
 
import com.jme.scene.Node;
import com.jme.scene.TriMesh;
import com.jme.scene.CameraNode;
import com.jme.scene.Controller;
import com.jme.scene.state.RenderState;
import com.jme.scene.lod.AreaClodMesh;
import com.jme.bounding.BoundingSphere;
import com.jme.math.Vector3f;
import com.jme.math.Matrix3f;
import com.jme.curve.CurveController;
import com.jme.curve.BezierCurve;
import com.jme.input.KeyInput;
import com.jme.input.action.KeyExitAction;
import java.net.URL;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ByteArrayInputStream;
import com.jme.input.InputHandler;
 
/**
 * Started Date: Aug 16, 2004<br><br>
 *
 * This program teaches Continuous Level of Detail mesh objects. To use this program, move
 * the camera backwards and watch the model disappear.
 *
 * @author Jack Lindamood
 */
public class HelloLOD extends SimpleGame {
 
    CameraNode cn;
 
    public static void main(String[] args) {
        HelloLOD app = new HelloLOD();
        app.setConfigShowMode(ConfigShowMode.AlwaysShow);
        app.start();
    }
 
    //If you don't use java 6 remove tag, but why don't you use java 6 anyway?
    @Override
    protected void simpleInitGame() {
        // Point to a URL of my model
        URL model =
                HelloLOD.class.getClassLoader().
                getResource("jmetest/data/model/maggie.obj");
        // Create something to convert .obj format to .jme
        FormatConverter converter = new ObjToJme();
        // Point the converter to where it will find the .mtl file from
        converter.setProperty("mtllib", model);
        // This byte array will hold my .jme file
        ByteArrayOutputStream BO = new ByteArrayOutputStream();
        // This will read the .jme format and convert it into a scene graph
        BinaryImporter jbr = new BinaryImporter();
 
        //// Use an exact BoundingSphere bounds
        //   BoundingSphere.useExactBounds = true; //Note: Deprecated??
 
        Node meshParent = null;
        try {
            // Use the format converter to convert .obj to .jme
            converter.convert(model.openStream(), BO);
            // Load the binary .jme format into a scene graph
            Node maggie = (Node) jbr.load(new ByteArrayInputStream(BO.toByteArray()));
 
//            meshParent = (Node) maggie.getChild(0); //Note: ¿¿¿??? that won't work... Deprecated?
            meshParent = maggie; //This seems ok
        } catch (IOException e) { // Just in case anything happens
 
            System.out.println("Damn exceptions!" + e);
            e.printStackTrace();
            System.exit(0);
        }
        // Create a clod duplicate of meshParent.
        Node clodNode = getClodNodeFromParent(meshParent);
        // Attach the clod mesh at the origin.
        clodNode.setLocalScale(.1f);
        rootNode.attachChild(clodNode);
        // Attach the original at -15,0,0
        meshParent.setLocalScale(.1f);
        meshParent.setLocalTranslation(new Vector3f(-15, 0, 0));
        rootNode.attachChild(meshParent);
        // Clear the keyboard commands that can move the camera.
        input = new InputHandler();
        // Insert a keyboard command that can exit the application.
        input.addAction(new KeyExitAction(this), "exit", KeyInput.KEY_ESCAPE, false);
        // The path the camera will take.
        Vector3f[] cameraPoints = new Vector3f[]{
            new Vector3f(0, 5, 20),
            new Vector3f(0, 20, 90),
            new Vector3f(0, 30, 200),
            new Vector3f(0, 100, 300),
            new Vector3f(0, 150, 400),
        };
        // Create a path for the camera.
        BezierCurve bc = new BezierCurve("camera path", cameraPoints);
        // Create a camera node to move along that path.
        cn = new CameraNode("camera node", cam);
        // Create a curve controller to move the CameraNode along the path
        CurveController cc = new CurveController(bc, cn);
        // Cycle the animation.
        cc.setRepeatType(Controller.RT_CYCLE);
        // Slow down the curve controller a bit
        cc.setSpeed(.25f);
        // Add the controller to the node.
        cn.addController(cc);
        // Attach the node to rootNode
        rootNode.attachChild(cn);
    }
 
    private Node getClodNodeFromParent(Node meshParent) {
        // Create a node to hold my cLOD mesh objects
        Node clodNode = new Node("Clod node");
        // For each mesh in maggie
        for (int i = 0; i < meshParent.getQuantity(); i++) {
            // Create an AreaClodMesh for that mesh. Let it compute
            // records automatically
            AreaClodMesh acm = new AreaClodMesh("part" + i,
                    (TriMesh) meshParent.getChild(i), null);
            acm.setModelBound(new BoundingSphere());
            acm.updateModelBound();
            // Allow 1/2 of a triangle in every pixel on the screen in
            // the bounds.
            acm.setTrisPerPixel(.5f);
            // Force a move of 2 units before updating the mesh geometry
            acm.setDistanceTolerance(2);
            // Give the clodMe sh node the material state that the
            // original had.
//acm.setRenderState(meshParent.getChild(i).getRenderStateList()[RenderState.RS_MATERIAL]); //Note: Deprecated
            acm.setRenderState(meshParent.getChild(i).getRenderState(RenderState.RS_MATERIAL));
            // Attach clod node.
            clodNode.attachChild(acm);
        }
        return clodNode;
    }
 
    Vector3f up = new Vector3f(0, 1, 0);
    Vector3f left = new Vector3f(1, 0, 0);
    private static Vector3f tempVa = new Vector3f();
    private static Vector3f tempVb = new Vector3f();
    private static Vector3f tempVc = new Vector3f();
    private static Vector3f tempVd = new Vector3f();
    private static Matrix3f tempMa = new Matrix3f();
 
    @Override
    protected void simpleUpdate() {
        // Get the center of root's bound.
        Vector3f objectCenter = rootNode.getWorldBound().getCenter(tempVa);
        // My direction is the place I want to look minus the location
        // of the camera.
        Vector3f lookAtObject = tempVb.set(objectCenter).subtractLocal(cam.getLocation()).normalizeLocal();
        // Left vector
        tempMa.setColumn(0, up.cross(lookAtObject, tempVc).normalizeLocal());
        // Up vector
        tempMa.setColumn(1, left.cross(lookAtObject, tempVd).normalizeLocal());
        // Direction vector
        tempMa.setColumn(2, lookAtObject);
        cn.setLocalRotation(tempMa);
    }
}

Loading the Model

This program loads a model then goes through the model’s children and creates a Continuous Level of Detail (CLOD) mesh for each child and puts the two versions side by side so you can compare. The camera’s movement is controlled by the program through a CameraNode.

The beginning is simply a copy from HelloModelLoading, so I’m not going to go over it. The first new part is when I create a clod duplicate from my original Maggie model:

private Node getClodNodeFromParent(Node meshParent) {
    // Create a node to hold my cLOD mesh objects
    Node clodNode = new Node("Clod node");
    // For each mesh in maggie
    for (int i = 0; i < meshParent.getQuantity(); i++) {
        // Create an AreaClodMesh for that mesh. Let it compute
        // records automatically
        AreaClodMesh acm = new AreaClodMesh("part" + i,
                (TriMesh) meshParent.getChild(i), null);
        acm.setModelBound(new BoundingSphere());
        acm.updateModelBound();
        // Allow 1/2 of a triangle in every pixel on the screen in
        // the bounds.
        acm.setTrisPerPixel(.5f);
        // Force a move of 2 units before updating the mesh geometry
        acm.setDistanceTolerance(2);
        // Give the clodMe sh node the material state that the
        // original had.
        //acm.setRenderState(meshParent.getChild(i).getRenderStateList()[RenderState.RS_MATERIAL]); //Note: Deprecated
        acm.setRenderState(meshParent.getChild(i).getRenderState(RenderState.RS_MATERIAL));
        // Attach clod node.
        clodNode.attachChild(acm);
    }
    return clodNode;
}

Continuous Level of Detail (CLOD) Mesh

A ClodMesh is a “continuous level of detail" mesh. It allows users to trim away a few triangles at a time from a mesh, resulting in a figure that looks similar to the original but uses less information. This makes rendering quicker. For example, characters in your game need to look really good so you may put 5,000 polygons in them but those polygons are wasted when the character is 1,000ft away. From 1,000ft the character may as well have 400 polygons. The difference won’t be too noticeable to the user playing your game, but will be very big for their computer. As a general rule objects that take up more space on the screen should have more polygons.

A ClodMesh can only be created from a TriMesh. Because Maggie is actually multiple TriMesh objects inside one model (one trimesh is yellow, another is blue, and so on), I have to create new ClodMesh objects for each TriMesh. The type of ClodMesh I create is called an AreaClodMesh. AreaClodMesh uses the area on the screen the object’s bounding volume occupies to determine how many triangles the mesh should have. Close objects will have all their triangles and far objects will have fewer triangles.

AreaClodMesh Parameters

AreaClodMesh acm=new AreaClodMesh("part"+i,(TriMesh) meshParent.getChild(i),null);

The first parameter is the name of the ClodMesh. All spatials must have a name. The second parameter is the TriMesh to create a ClodMesh from and the third is the ClodMesh’s records. Records tell the ClodMesh how to collapse triangles. When null is passed, the records are created for us.

// Allow 1/2 of a triangle in every pixel on the screen in
// the bounds.
acm.setTrisPerPixel(.5f);

This function tells the ClodMesh how quickly to collapse triangles. I’ve set it intentionally small which is why you can tell how much Maggie changes. In your game, you would set this value where the collapsing of the TriMesh isn’t noticeable. The ClodMesh will calculate how much area the bounding volume of acm takes on the screen and use that to figure out how to collapse the mesh’s triangles.

// Force a move of 2 units before updating the mesh geometry
acm.setDistanceTolerance(2);

This forces a collapse update every time the camera’s distance is changed by 2 units. A collapse update every frame would be extremely slow. Playing with this value allows the AutoClodMesh to update less frequently resulting in higher FPS.

// Give the clodMesh node the material state that the
// original had.
acm.setRenderState(meshParent.getChild(i).getRenderState(RenderState.RS_MATERIAL));

Because the acm has the original’s geometry doesn’t mean it has the original’s render states. Because of this, I set the acm’s material state to be the same as the original’s material state.

Moving the Camera on a Path

After creating a correct AreaClodMesh Node, I setup the key actions.

// Clear the keyboard commands that can move the camera.
input = new InputHandler();
// Insert a keyboard command that can exit the application.
input.addAction(new KeyExitAction(this), "exit", KeyInput.KEY_ESCAPE, false);

In this program I don’t want users controlling the camera like they normally would with the keyboard so I clear all mouse and keyboard input actions. I have to reinsert a KeyExitAction otherwise users would have no way of closing the application. Finally too create the path for my camera.

// The path the camera will take.
Vector3f[]cameraPoints=new Vector3f[]{
 new Vector3f(0,5,20),
 new Vector3f(0,20,90),
 new Vector3f(0,30,200),
 new Vector3f(0,100,300),
 new Vector3f(0,150,400),
 };
// Create a path for the camera.
BezierCurve bc=new BezierCurve("camera path",cameraPoints);

I define points that will be the path of my camera and create a BezierCurve out of them. A curve is actually a spatial (which is why it needs a name), but this curve won’t be rendered; instead it will be used as a path. After I have the curve setup, I create a CameraNode to move.

// Create a camera node to move along that path.
cn=new CameraNode("camera node",cam);

A CameraNode is a Spatial that can be translated and rotated just like spatials so that it can move around and with objects. The variable cam is created in SimpleGame. The above is equivalent to the following:

// Create a camera node to move along that path.
cn=new CameraNode("camera node",display.getRenderer().getCamera());

With the camera created, I can now create a controller for it.

// Create a curve controller to move the CameraNode along the path
CurveController cc=new CurveController(bc,cn);
// Cycle the animation.
cc.setRepeatType(Controller.RT_CYCLE);
// Slow down the curve controller a bit
cc.setSpeed(.25f);
// Add the controller to the node.
cn.addController(cc);
// Attach the node to rootNode
rootNode.attachChild(cn);

All of the above is pretty routine creation. A CurveController takes a spatial to move and a curve to move along. The only final part is rotating my CameraNode to face the two Maggie objects.

protected void simpleUpdate() {
    // Get the center of root's bound.
    Vector3f objectCenter = rootNode.getWorldBound().getCenter(tempVa);
    // My direction is the place I want to look minus the location
    // of the camera.
    Vector3f lookAtObject = tempVb.set(objectCenter).subtractLocal(cam.getLocation()).normalizeLocal();
    // Left vector
    tempMa.setColumn(0, up.cross(lookAtObject, tempVc).normalizeLocal());
    // Up vector
    tempMa.setColumn(1, left.cross(lookAtObject, tempVd).normalizeLocal());
    // Direction vector
    tempMa.setColumn(2, lookAtObject);
    cn.setLocalRotation(tempMa);
}

Notice I created a lot of temporary static variables for this program. They are there just to assist with some math calls I will need in simpleUpdate. The rotation matrix of the CameraNode is used to orient the camera object. The CurveController is only there for the camera’s position. The first two lines setup a vector that points from the camera towards the object. The last four are a little strange though:

// Left vector
tempMa.setColumn(0, up.cross(lookAtObject, tempVc).normalizeLocal());
// Up vector
tempMa.setColumn(1, left.cross(lookAtObject, tempVd).normalizeLocal());
// Direction vector
tempMa.setColumn(2, lookAtObject);
cn.setLocalRotation(tempMa);

The 3x3 Rotation matrix is used to rotate the camera. The first column is the left vector the camera should use, the second is the camera’s up vector, and the third is the camera’s direction it is facing. I use the cross product of the up vector and the direction to get my left vector (similarly with the up vector). The cross of two vectors is the vector perpendicular to each. For more information on cross product, hit google. After the correct rotation matrix for the camera, I simply assign it to the camera node.

Scene graph

rootNode
Cn “camera node” clodNode meshParent
Controlled by Various children that make up CLOD Maggie Various children that make up Maggie
Curve Controller
 
Except where otherwise noted, content on this wiki is licensed under the following license:CC Attribution 3.0 Unported