ROAM: Java-JAI-J3D Image Tiling Example

Terrain rendering is of major interest to game and simulation developers. Most developers who attempt to program a terrain algorithm soon discover that dynamic caching of the terrain data becomes necessary in order to represent a data structure of any reasonably large size.

One algorithm that has gathered a lot of interest in recent years is the Real-time Optimally Adapted Mesh algorithm (Duchaineau, et al., 1997). In collaboration with Paul Byrne of Sun and Justin Couch and Alan Hudson of Yumetech, we have developed a prototype implementation of the ROAM algorithm in Java 3D with image tiling for texture mapping. Another contributor to this project is one of the authors of the original paper, Mark Duchaineau from Jet Propulsion Labs.

Overview of Terrain Rendering

Real-time terrain rendering is a particularly challenging problem in 3D graphics. If the landscape is large, it is impossible to render the entire area, so some system needs to be implemented to load the visible parts of the terrain.

Think about the memory required to render a simple 10 meter terrain. If we used a float for each height value and represented one point per millimeter, our 10 meter square patch of terrain would require 400MB of RAM just for the height information. We would still need to render the terrain, and we haven't even put any objects in the environment.

Although detail down to the millimeter is extreme, it serves to illustrate the fundamental problem of terrain rendering. You must find a way to reduce the memory load of the terrain information.

If we were to reduce the sampling density of the heights to one sample per meter (instead of one per millimeter), we dramatically reduce our memory requirements, but we end up with a poor looking rendering.

Of course, with a perfectly flat terrain, only one height is needed. Conversely, with a mountainous terrain, much more detail is needed. What is needed is a way to determine the variability of heights in the region of interest and use this information to guide our rendering. This is the key to ROAM. The algorithm is based on a variance map that can be computed for any section of terrain. Before getting into the details of the Java 3D ROAM algorithm, we introduce the two basic approaches to terrain rendering and discuss the data structure necessary for ROAM.

Two Basic Approaches

All terrain rendering techniques begin with the idea of breaking the world into smaller, more manageable chunks. These chunks are more formally called patches or tiles. The application then controls memory usage by determining which patches are visible (or, equivalently, which chunks are not visible). As the user moves around, the application must update the visibility information.

Looking deeper into the various algorithms, there appear to be two basic approaches. One, termed geo-mipmapping, is similar in concept to the texture MIPMapping that we saw in Chapter 11. The patches are refined and reduced progressively. For example, the lowest-level patch would contain four corner points covering the entire patch. At this level, the terrain is represented by two triangles. The next level of detail would contain nine points. As in image MIPMapping, the level of detail used is determined by how close the user is to the object.

In the second basic approach, termed adaptive meshing, the code manages the terrain data by creating new triangles only when needed. If the surface is flat, only a few triangles are needed. But if the surface changes frequently over space, a lot of triangles will be needed.

Each of these approaches has its proponents. Geo-mipmapping is generally more commonly found in game applications, whereas adaptive meshing is more frequently encountered in scientific visualization.

The ROAM Algorithm

The ROAM algorithm is an adaptive meshing technique that offers a number of desirable features for terrain rendering and consists of a single preprocessing step coupled with several runtime processes. One of ROAM's advantages is that the execution time is proportional to the number of triangle changes that occur in each frame. The triangle changes are typically a few percent of the size of the mesh, and, therefore, faster rendering can be accomplished.

The goal of ROAM is to make a dynamic mesh based on a triangle bintree. A bintree is highly similar to a quadtree and can be used to organize both quads and triangles. The basic idea of a quadtree comes from image analysis and compression in which a planar image is first covered with a square and then subdivided into four neighbors until the squares become essentially uniform (based on color or spatial autocorrelation). Figure 14.2 illustrates this process with triangles. The tree becomes a sort of lookup table for triangles (or quads) and becomes progressively more detailed as you move down to lower levels in the hierarchy.

Figure 14.2. Triangle bintree showing split and merge operations.


A triangle cannot undergo a split operation if its base neighbor is at a coarser level; likewise, it cannot undergo a merge operation if its neighbor is at a more detailed level. Therefore, it we want to force a split for a triangle with a coarser neighbor, we will need to recursively split the base neighbors up the tree. A merge will require a recursive merge up through the tree.

All the triangles, beginning with the base triangle, have split/merge priorities attached to them so that we can smoothly adjust the mesh. The main rule for generating the split priority is that no child can have a higher priority than its parent. Likewise, no parent can have a higher merge priority than its child. Therefore, the base triangulation has the highest priority of all possible splits, and the lowest level child has the highest priority for all possible merges.

Integrating Data Structures with the Java 3D Scenegraph

As we just stated, we need to add and remove triangles during runtime. The problem is that scene graph APIs, including Java 3D, run faster when there are fewer changes to the scene graph during runtime.

One trick is to have the coordinate system of the terrain in the coordinate system of the highest branch group of the scene graph. The reason to organize the data in this way is that we don't want to include transforms in our real-time calculations. We therefore end up with a very simple scene graph.

The Landscape BranchGroup

Under the Landscape BranchGroup exist a series of Shape3Ds that can be thought of as tiles. This is a natural structure for the JAI Tile Interface as you will see shortly. The Landscape class is a useful organization for terrain data regardless of the algorithm chosen for mesh updating.

Recall that a BranchGroup is an organizing entity for separable parts of the scene graph. Because the Landscape class represents the entire terrain, most of the changing logic occurs there. If we were working with a non-dynamic 3D terrain, we wouldn't need to organize the data in this way. We could simply make a Shape3D with an associated GeometryArray. In the case of ROAM, we need a dynamic change and we have yet to deal with visibility culling, so we really need a structure that will allow us to move data in and out of memory quickly without needing to determine whether every triangle will be visible.

Also, importantly, Java 3D's visibility culling works at the level of the Shape3D. Each Shape3D's Bounds constitute the first step in the elimination decision. (Remember, the most efficient triangle to render is the one that isn't rendered at all.) We want to make sure that our expensive computations face this test early. Java 3D's internal mechanisms can take care of some of the management for us (which is one of the reasons we have chosen to work in Java 3D instead of a lower-level API).

Each Patch in the Landscape, therefore, uses its own instance of Shape3D. Note also that because of this, we can use the GeometryUpdate interface.

The particular Geometry subclass chosen in this case is TriangleArray. A trade-off must be made; on one hand, there is the expense of pushing triangles over the graphics bus to a card that can already triangulate for us. On the other hand, though, there is the simplicity and reduced CPU dependency of matching the data structure of our geometry to the inherent use of triangle in the ROAM algorithm.

Listing 14.3 shows the Landscape package that is part of the J3D.org terrain rendering download. To get this download, go to http://code.j3d.org. It is then possible to unjar the code examples.

Listing 14.3 Landscape class
package org.j3d.terrain;

// Standard imports
import javax.media.j3d.Transform3D;

import javax.vecmath.Matrix3f;
import javax.vecmath.Tuple3f;
import javax.vecmath.Vector3f;

// Application specific imports
import org.j3d.ui.navigation.FrameUpdateListener;
import org.j3d.ui.navigation.HeightMapGeometry;


public abstract class Landscape extends javax.media.j3d.BranchGroup
    implements FrameUpdateListener, HeightMapGeometry
{
    /** The current viewing frustum that is seeing the landscape */
    protected ViewFrustum landscapeView;

    /** Raw terrain information to be rendered */
    protected TerrainData terrainData;

    /**
     * Temporary variable to hold the position information extracted from
     * the full transform class.
     */
    private Vector3f tmpPosition;

    /**
     * Temporary variable to hold the orientation information extracted from
     * the matrix class.
     */
    private Vector3f tmpOrientation;

    /**
     * Temporary variable to hold the orientation matrix extracted from
     * the full transform class.
     */
    private Matrix3f tmpMatrix;

    /**
     * Create a new Landscape with the set view and data. If either are not
     * provided, an exception is thrown.
     *
     * @param view The viewing frustum to see the data with
     * @param data The raw data to view
     * @throws IllegalArgumentException either parameter is null
     */
    public Landscape(ViewFrustum view, TerrainData data)
    {
        if(view == null)
            throw new IllegalArgumentException("ViewFrustum not supplied");

        if(data == null)
            throw new IllegalArgumentException("Terrain data not supplied");

        terrainData = data;
        landscapeView = view;

        tmpPosition = new Vector3f();
        tmpOrientation = new Vector3f();
        tmpMatrix = new Matrix3f();
    }

    //—————————————————————————————
    // Methods required by FrameUpdateListener
    //—————————————————————————————

    /**
     * The transition from one point to another is completed. Use this to
     * update the transformation.
     *
     * @param t3d The position of the final viewpoint
     */
    public void transitionEnded(Transform3D t3d)
    {
        landscapeView.viewingPlatformMoved();
        setView(t3d);
    }

    /**
     * The frame has just been updated with the latest view information.
     * Update the landscape rendered values now.
     *
     * @param t3d The position of the viewpoint now
     */
    public void viewerPositionUpdated(Transform3D t3d)
    {
        landscapeView.viewingPlatformMoved();
        setView(t3d);
    }

    //—————————————————————————————
    // Methods required by FrameUpdateListener
    //—————————————————————————————

    /**
     * Get the height at the given X,Z coordinate in the local coordinate
     * system. This implementation delegates to the underlying terrain data
     * to do the real resolution.
     *
     * @param x The x coordinate for the height sampling
     * @param z The z coordinate for the height sampling
     * @return The height at the current point or NaN
     */
    public float getHeight(float x, float z)
    {
        return terrainData.getHeight(x, z);
    }

    //—————————————————————————————
    // Local methods
    //—————————————————————————————

    /**
     * Set the current viewing direction for the user. The user is located
     * at the given point and looking in the given direction. All information
     * is assumed to be in world coordinates.
     *
     * @param position The position the user is in the virtual world
     * @param direction The orientation of the user's gaze
     */
    public abstract void setView(Tuple3f position, Vector3f direction);

    /**
     * Set the current view location information based on a transform matrix.
     * Only the position and orientation information are extracted from this
     * matrix. Any shear or scale is ignored. Effectively, this transform
     * should be the view transform (particularly if you are using navigation
     * code from this codebase in the {@link org.j3d.ui.navigation} package.
     *
     * @param t3d The transform to use as the view position
     */
    public void setView(Transform3D t3d)
    {
        t3d.get(tmpMatrix, tmpPosition);
        tmpOrientation.set(0, 0, -1);
        tmpMatrix.transform(tmpOrientation);

        setView(tmpPosition, tmpOrientation);
    }
}

The ROAM algorithm extends Landscape to make SplitMergeLandscape, as shown in Listing 14.4.

Listing 14.4 SplitMergeLandscape
public class SplitMergeLandscape extends Landscape
{
    static final int PATCH_SIZE = 64;

    /** The collection of all patches in this landscape */
    private ArrayList patches = new ArrayList();

    /** Queue manager for the patches needing splits or merges each frame */
    private TreeQueueManager queueManager = new TreeQueueManager();

    /** Number of visible triangles */
    private int triCount = 0;

    /**
     * Creates new Landscape based on the view information and the terrain
     * data.
     *
     * @param view The view frustum looking at this landscape
     * @param terrain The raw data for the terrain
     */
    public SplitMergeLandscape(ViewFrustum view, TerrainData terrain)
    {
        super(view, terrain);

        createPatches();
    }

    /**
     * Change the view of the landscape. The virtual camera is now located in
     * this position and orientation, so adjust the visible terrain to
     * accommodate the changes.
     *
     * @param position The position of the camera
     * @param direction The direction the camera is looking
     */
    public void setView(Tuple3f position, Vector3f direction)
    {
        queueManager.clear();
        landscapeView.viewingPlatformMoved();
        float accuracy = (float)Math.toRadians(0.1);
        TreeNode splitCandidate;
        TreeNode mergeCandidate;
        boolean done;
        int size = patches.size();

        for(int i = 0; i < size; i++)
        {
            Patch p = (Patch)patches.get(i);

            p.setView(position, landscapeView, queueManager);
        }

        done = false;

        while(!done)
        {
            splitCandidate = queueManager.getSplitCandidate();
            mergeCandidate = queueManager.getMergeCandidate();

            if(mergeCandidate == null && splitCandidate != null)
            {
                if (splitCandidate.variance > accuracy)
                {
                    triCount += splitCandidate.split(position,
                                                     landscapeView,
                                                     queueManager);
                }
                else
                    done = true;
            }
            else if(mergeCandidate!=null && splitCandidate == null)
            {
                if(mergeCandidate.diamondVariance < accuracy)
                {
                    triCount -= mergeCandidate.merge(queueManager);
                    //System.out.println("No split merge "+mergeCandidate+"
"+mergeCandidate.diamondVariance);
                }
                else
                    done = true;
            }
            else if(mergeCandidate != null && splitCandidate != null &&
                    (splitCandidate.variance > accuracy ||
                     splitCandidate.variance > mergeCandidate.diamondVariance))
            {
                if (splitCandidate.variance > accuracy)
                {
                    triCount += splitCandidate.split(position,
                                                     landscapeView,
                                                     queueManager);
                }
                else if (mergeCandidate.diamondVariance < accuracy)
                {
                    triCount -= mergeCandidate.merge(queueManager);
                }
            }
            else
            {
                done = true;
            }
        }


        for(int i = 0; i < size; i++)
        {
            Patch p = (Patch)patches.get(i);
            p.updateGeometry();
        }
    }

    /**
     * Create a new set of patches based on the given terrain data.
     */
    private void createPatches()
    {
        int depth = terrainData.getGridDepth() - PATCH_SIZE;
        int width = terrainData.getGridWidth() - PATCH_SIZE;

        Appearance app = new Appearance();

        app.setTexture(terrainData.getTexture());

        Material mat = new Material();
        mat.setLightingEnable(true);

        app.setMaterial(mat);

//        PolygonAttributes polyAttr = new PolygonAttributes();
//        polyAttr.setPolygonMode(PolygonAttributes.POLYGON_LINE);
//        polyAttr.setCullFace(PolygonAttributes.CULL_NONE);
//        app.setPolygonAttributes(polyAttr);

        Patch[] westPatchNeighbour = new Patch[width];
        Patch southPatchNeighbour = null;
        Patch p = null;

        for(int east = 0; east <= width; east += PATCH_SIZE)
        {
            for(int north = 0; north <= depth; north += PATCH_SIZE)
            {
                p = new Patch(terrainData,
                              PATCH_SIZE,
                              east,
                              north,
                              app,
                              landscapeView,
                              westPatchNeighbour[north/PATCH_SIZE],
                              southPatchNeighbour);

                patches.add(p);
                triCount += 2;
                this.addChild(p.getShape3D());
                southPatchNeighbour = p;
                westPatchNeighbour[north/PATCH_SIZE] = p;
            }

            southPatchNeighbour = null;
        }
    }
}
						

The split and merge queues and their usage are shown in Listing 14.4 in the setView method.

Another key component of this process is the Patch class, as shown in Listing 14.5. Note that the Patch class extends GeometryUpdater and therefore is built for updating a Shape3D. Any class that implements GeometryUpdater must override the updateData() method.

Listing 14.5 Patch.java
package org.j3d.terrain.roam;

// Standard imports
import java.util.LinkedList;

import javax.media.j3d.Appearance;
import javax.media.j3d.Geometry;
import javax.media.j3d.TriangleArray;
import javax.media.j3d.BoundingBox;
import javax.media.j3d.Shape3D;
import javax.media.j3d.GeometryUpdater;

import javax.vecmath.Point3d;
import javax.vecmath.Tuple3f;

// Application specific imports
import org.j3d.terrain.ViewFrustum;
import org.j3d.terrain.TerrainData;

/**
 * A patch represents a single piece of terrain geometry that can be
 * rendered as a standalone block.
 * <p>
 *
 * A patch represents a single block of geometry within the overall scheme
 * of the terrain data. Apart from a fixed size nothing else is fixed in this
 * patch. The patch consists of a single TriangleArray that uses a geometry
 * updater (geometry by reference is used) to update the geometry frame
 * as necessary. It will, when instructed, dynamically recalculate what
 * vertices need to be shown and set those into the geometry array.
 *
 * @author  Paul Byrne, Justin Couch
 * @version
 */
class Patch implements GeometryUpdater
{
    /** The final size in number of grid points for this patch */
    private final int PATCH_SIZE;

    /** The values of the nodes in the NW triangle of this patch */
    TreeNode NWTree;

    /** The values of the nodes in the NW triangle of this patch */
    TreeNode SETree;

    private VarianceTree NWVariance;
    private VarianceTree SEVariance;

    /** The J3D geometry for this patch */
    private Shape3D shape3D;

    private int xOrig;
    private int yOrig;

    private TerrainData terrainData;
    private Patch westPatchNeighbour;
    private Patch southPatchNeighbour;
    private VertexData vertexData;

    private TriangleArray geom;

    /** The maximum Y for this patch */
    private float maxY;

    /** The minimumY for this patch */
    private float minY;

    /**
     * Create a new patch based on the terrain and appearance information.
     *
     * @param terrainData The raw height map info to use for this terrain
     * @param patchSize The number of grid points to use in the patch on a side
     * @param xOrig The origin of the X grid coord for this patch in the
     *    global set of grid coordinates
     * @param yOrig The origin of the Y grid coord for this patch in the
     *    global set of grid coordinates
     * @param app The global appearance object to use for this patch
     * @param landscapeView The view frustum container used
     * @param westPatchNeighbour the Patch to the west of this patch
     * @param southPatchNeighbour the Patch to the south of this patch
     */
    Patch(TerrainData terrainData,
          int patchSize,
          int xOrig,
          int yOrig,
          Appearance app,
          ViewFrustum landscapeView,
          Patch westPatchNeighbour,
          Patch southPatchNeighbour)
    {
        int height = yOrig + patchSize;
        int width = xOrig + patchSize;

        this.xOrig = xOrig;
        this.yOrig = yOrig;
        this.PATCH_SIZE = patchSize;
        this.terrainData = terrainData;
        this.westPatchNeighbour = westPatchNeighbour;
        this.southPatchNeighbour = southPatchNeighbour;

        boolean has_texture = (app.getTexture() != null);

        vertexData = new VertexData(PATCH_SIZE, has_texture);

        int format = TriangleArray.COORDINATES |
                     TriangleArray.BY_REFERENCE;

        if(has_texture)
            format |= TriangleArray.TEXTURE_COORDINATE_2;
        else
            format |= TriangleArray.COLOR_3;

        geom = new TriangleArray(PATCH_SIZE * PATCH_SIZE * 2 * 3, format);

        geom.setCapability(TriangleArray.ALLOW_REF_DATA_WRITE);
        geom.setCapability(TriangleArray.ALLOW_COUNT_WRITE);
        geom.setCoordRefFloat(vertexData.getCoords());

        if(has_texture)
            geom.setTexCoordRefFloat(0, vertexData.getTextureCoords());
        else
            geom.setColorRefByte(vertexData.getColors());

        NWVariance = new VarianceTree(terrainData,
                                       PATCH_SIZE,
                                       xOrig, yOrig,
                                       width, height,
                                       xOrig, height);

        NWTree = new TreeNode(xOrig, yOrig,        // Left X, Y
                               width, height,       // Right X, Y
                               xOrig, height,       // Apex X, Y
                               1,
                               terrainData,
                               landscapeView,
                               TreeNode.UNDEFINED,
                               1,
                               NWVariance);

        SEVariance = new VarianceTree(terrainData,
                                       PATCH_SIZE,
                                       width, height,       // Left X, Y
                                       xOrig, yOrig,        // Right X, Y
                                       width, yOrig);       // Apex X, Y


        SETree = new TreeNode(width, height,       // Left X, Y
                               xOrig, yOrig,        // Right X, Y
                               width, yOrig,        // Apex X, Y
                               1,
                               terrainData,
                               landscapeView,
                               TreeNode.UNDEFINED,
                               1,
                               SEVariance);

        maxY = Math.max(NWVariance.getMaxY(), SEVariance.getMaxY());
        minY = Math.min(NWVariance.getMinY(), SEVariance.getMinY());

        NWTree.baseNeighbour = SETree;
        SETree.baseNeighbour = NWTree;

        if(westPatchNeighbour!=null)
        {
            NWTree.leftNeighbour = westPatchNeighbour.SETree;
            westPatchNeighbour.SETree.leftNeighbour = NWTree;
        }

        if(southPatchNeighbour!=null)
        {
            SETree.rightNeighbour = southPatchNeighbour.NWTree;
            southPatchNeighbour.NWTree.rightNeighbour = SETree;
        }

        Point3d min_bounds =
            new Point3d(xOrig * terrainData.getGridXStep(),
                        minY,
                        -(yOrig + height) * terrainData.getGridYStep());

        Point3d max_bounds =
            new Point3d((xOrig + width) * terrainData.getGridXStep(),
                        maxY,
                        -yOrig * terrainData.getGridYStep());

        shape3D = new Shape3D(geom, app);
        shape3D.setBoundsAutoCompute(false);
        shape3D.setBounds(new BoundingBox(min_bounds, max_bounds));

        // Just as a failsafe, always set the terrain data in the user
        // data section of the node so that terrain code will find it
        // again, even if the top user is stupid.
        shape3D.setUserData(terrainData);
    }

    //—————————————————————————————
    // Methods required by GeometryUpdater
    //—————————————————————————————

    /**
     * Update the J3D geometry array for data now.
     *
     * @param geom The geometry object to update
     */
    public void updateData(Geometry geom)
    {
        createGeometry((TriangleArray)geom);
    }

    //—————————————————————————————
    // local convenience methods
    //—————————————————————————————

    void reset(ViewFrustum landscapeView)
    {
        NWTree.reset(landscapeView);
        SETree.reset(landscapeView);

        NWTree.baseNeighbour = SETree;
        SETree.baseNeighbour = NWTree;

        if(westPatchNeighbour != null)
        {
            NWTree.leftNeighbour = westPatchNeighbour.SETree;
            westPatchNeighbour.SETree.leftNeighbour = NWTree;
        }

        if(southPatchNeighbour != null)
        {
            SETree.rightNeighbour = southPatchNeighbour.NWTree;
            southPatchNeighbour.NWTree.rightNeighbour = SETree;
        }
    }

    /**
     * Change the view to the new position and orientation. In this
     * implementation the direction information is ignored because we have
     * the view frustum to use.
     *
     * @param position The location of the user in space
     * @param landscapeView The viewing frustum information for clipping
     * @param queueManager Manager for ordering terrain chunks
     */
    void setView(Tuple3f position,
                 ViewFrustum landscapeView,
                 QueueManager queueManager)
    {
        NWTree.updateTree(position,
                          landscapeView,
                          NWVariance,
                          TreeNode.UNDEFINED,
                          queueManager);

        SETree.updateTree(position,
                          landscapeView,
                          SEVariance,
                          TreeNode.UNDEFINED,
                          queueManager);
    }

    /**
     * Request an update to the geometry. If the geometry is visible then
     * tell J3D that we would like to update the geometry. It does not directly
     * do the update because we are using GeomByRef and so need to wait for the
     * renderer to tell us when it is OK to do the updates.
     */
    void updateGeometry()
    {
        if(NWTree.visible != ViewFrustum.OUT ||
           SETree.visible != ViewFrustum.OUT ||
            vertexData.getVertexCount() != 0)
        {
            geom.updateData(this);
        }
    }

    /**
     * Fetch the number of triangles that are currently visible in this patch.
     *
     * @return The number of visible triangles
     */
    int getTriangleCount()
    {
        return vertexData.getVertexCount() / 3;
    }

    /**
     * Get the shape node that is used to represent this patch.
     *
     * @return The shape node
     */
    Shape3D getShape3D()
    {
        return shape3D;
    }

    /**
     * Create the geometry needed for this patch. Just sets how many vertices
     * are to be used based on the triangles of the two halves of the tree.
     *
     * @param geom The geometry array to work with
     */
    private void createGeometry(TriangleArray geom) {
        vertexData.reset();

        if(NWTree.visible!=ViewFrustum.OUT)
            NWTree.getTriangles(vertexData);

        if(SETree.visible != ViewFrustum.OUT)
            SETree.getTriangles(vertexData);

        geom.setValidVertexCount(vertexData.getVertexCount());
    }
}
						

The updateData() method calls the createGeometry() method, which determines if neighboring TreeNodes need to be loaded. The TreeNode class is given in Listing 14.6.

Listing 14.6 TreeNode.java
package org.j3d.terrain.roam;

// Standard imports
import java.util.LinkedList;

import javax.vecmath.Tuple3f;

// Application specific imports
import org.j3d.terrain.ViewFrustum;
import org.j3d.terrain.TerrainData;

/**
 * Represents a single node of the triangle mesh of the patch.
 *
 * @author  Paul Byrne, Justin Couch
 * @version
 */
class TreeNode
{
    /** The visibility status of this node in the tree is not known. */
    public static final int UNDEFINED = -1;

    TreeNode leftChild;
    TreeNode rightChild;

    TreeNode baseNeighbour;
    TreeNode leftNeighbour;
    TreeNode rightNeighbour;

    TreeNode parent;

    private int leftX, leftY;       // Pointers into terrainData
    private int rightX, rightY;
    private int apexX, apexY;

    private int node;

    private int depth;      // For debugging

    int visible = UNDEFINED;

    // The three corners of the triangle
    private float p1X, p1Y, p1Z;
    private float p2X, p2Y, p2Z;
    private float p3X, p3Y, p3Z;

    // Texture coordinates or colour values
    private float p1tS, p1tT, p1tR;
    private float p2tS, p2tT, p2tR;
    private float p3tS, p3tT, p3tR;

    private TerrainData terrainData;
    private VarianceTree varianceTree;

    float variance = 0f;
    float diamondVariance = 0f;

    boolean diamond = false;

    private boolean textured;

    /**
     * A cache of instances of ourselves to help avoid too much object
     * creation and deletion.
     */
    private static LinkedList nodeCache = new LinkedList();


    /**
     * Default constructor for use by TreeNodeCache.
     */
    TreeNode()
    {
    }

    /**
     * Creates new TreeNode customised with all the data set.
     */
    TreeNode(int leftX,
             int leftY,
             int rightX,
             int rightY,
             int apexX,
             int apexY,
             int node,
             TerrainData terrainData,
             ViewFrustum landscapeView,
             int parentVisible,
             int depth,
             VarianceTree varianceTree)
    {
        this.leftX = leftX;
        this.leftY = leftY;
        this.rightX = rightX;
        this.rightY = rightY;
        this.apexX = apexX;
        this.apexY = apexY;
        this.node = node;
        this.terrainData = terrainData;
        this.depth = depth;
        this.varianceTree = varianceTree;

        init(landscapeView, parentVisible);
    }

    /**
     * Used to populate a node retrieved from the TreeNodeCache
     * setting the same state as creating a new TreeNode would.
     */
    void newNode(int leftX,
                 int leftY,
                 int rightX,
                 int rightY,
                 int apexX,
                 int apexY,
                 int node,
                 TerrainData terrainData,
                 ViewFrustum landscapeView,
                 int parentVisible,
                 int depth,
                 VarianceTree varianceTree)
    {
        this.leftX = leftX;
        this.leftY = leftY;
        this.rightX = rightX;
        this.rightY = rightY;
        this.apexX = apexX;
        this.apexY = apexY;
        this.node = node;
        this.terrainData = terrainData;
        this.depth = depth;
        this.varianceTree = varianceTree;


        init(landscapeView, parentVisible);
    }

    /**
     * Reset this node by removing all it's children, set visible depending
     * on visibiling in view.
     *
     * @param landscapeView The latest view of the tree
     */
    void reset(ViewFrustum landscapeView)
    {
        if(leftChild != null)
        {
            leftChild.freeNode();
            leftChild = null;
        }

        if(rightChild != null)
        {
            rightChild.freeNode();
            rightChild = null;
        }

        baseNeighbour =null;
        leftNeighbour =null;
        rightNeighbour = null;

        visible = landscapeView.isTriangleInFrustum(p1X, p1Y, p1Z,
                                                    p2X, p2Y, p2Z,
                                                    p3X, p3Y, p3Z);
    }

    /**
     * Check to see if this treenode is a leaf or a branch. A leaf does not
     * have a left-hand child node.
     *
     * @return true if this is a leaf
     */
    boolean isLeaf()
    {
        return (leftChild == null);
    }

    /**
     * Place this node and all its children in the TreeNodeCache
     */
    void freeNode()
    {
        if(leftChild != null)
        {
            leftChild.freeNode();
            leftChild = null;
        }

        if(rightChild != null)
        {
            rightChild.freeNode();
            rightChild = null;
        }

        baseNeighbour = null;
        leftNeighbour = null;
        rightNeighbour = null;
        parent = null;
        diamond = false;

        addTreeNode(this);
    }

    /**
     * Request the recomputation of the variance of this node and place the
     * node on the queue ready for processing.
     *
     * @param position The location to compute the value from
     * @param queueManager The queue to place the node on
     */
    void computeVariance(Tuple3f position, QueueManager queueManager)
    {
        computeVariance(position);

        queueManager.addTriangle(this);
    }

    /**
     * If this triangle was half of a diamond then remove the
     * diamond from the diamondQueue
     *
     * @param queueManager The queue to remove the node from
     */
    void removeDiamond(QueueManager queueManager)
    {
        if(diamond)
        {
            queueManager.removeDiamond(this);
            diamondVariance = 0f;
            diamond = false;
        }
        else if(baseNeighbour != null && baseNeighbour.diamond)
        {
            queueManager.removeDiamond(baseNeighbour);
            baseNeighbour.diamondVariance = 0f;
            baseNeighbour.diamond = false;
        }
    }

    /**
     * Split this tree node into two smaller triangle tree nodes.
     *
     * @param position The current view location
     * @param landscapeView The view information
     * @param queueManager The queue to place newly generated items on
     * @return The number of triangles generated as a result
     */
    int split(Tuple3f position,
              ViewFrustum landscapeView,
              QueueManager queueManager)
    {
        int triCount = 0;

        //System.out.println("—————-> Splitting "+node);

        //if(mergedThisFrame)
        //    System.out.println("SPLITTING Tri that has been merged");
        //splitThisFrame = true;

        if(leftChild != null || rightChild != null)
        {
            throw new RuntimeException(" Triangle is already split "+node);
        }

        if(baseNeighbour != null)
        {
            if(baseNeighbour.baseNeighbour != this)
                triCount += baseNeighbour.split(position,
                                                landscapeView,
                                                queueManager);

            split2(position, landscapeView, queueManager);
            triCount++;
            baseNeighbour.split2(position, landscapeView, queueManager);
            //if(baseNeighbour.visible!=ViewFrustum.OUT)
            triCount++;

            leftChild.rightNeighbour = baseNeighbour.rightChild;
            rightChild.leftNeighbour = baseNeighbour.leftChild;
            baseNeighbour.leftChild.rightNeighbour = rightChild;
            baseNeighbour.rightChild.leftNeighbour = leftChild;

            diamondVariance = Math.max(variance, baseNeighbour.variance);
            diamond = true;
            queueManager.addDiamond(this);
        }
        else
        {
            split2(position, landscapeView, queueManager);
            triCount++;

            diamondVariance = variance;
            diamond = true;
            queueManager.addDiamond(this);
        }

        return triCount;
    }

    /**
     * Merge the children nodes of this node into a single triangle.
     *
     * @param queueManager The queue to put the merged node on
     * @return The number of triangles that were reduced as a result
     */
    int merge(QueueManager queueManager)
    {
        int trisRemoved = 0;

        //System.out.print("Merging ");
        //printNode(this);

        //if(splitThisFrame)
        //    System.out.println("Merging Tri that was split this frame");
        //mergedThisFrame = true;

        if(baseNeighbour != null && baseNeighbour.baseNeighbour != this)
        {
            System.out.println("***** Illegal merge ********);
            queueManager.removeDiamond(this);
            diamond = false;
            diamondVariance = 0f;
            return 0;
            //throw new RuntimeException("Illegal merge");
        }

        merge(this, queueManager);
        trisRemoved++;
        checkForNewDiamond(this.parent, queueManager);
        if(baseNeighbour!=null)
        {
            merge(baseNeighbour, queueManager);
            trisRemoved++;
            checkForNewDiamond(baseNeighbour.parent, queueManager);
        }

        queueManager.removeDiamond(this);
        diamond = false;
        diamondVariance = 0f;

        return trisRemoved;
    }

    /**
     * Add the coordinates for this triangle to the list
     */
    void getTriangles(VertexData vertexData)
    {
        if(leftChild == null)
        {
            if((visible != ViewFrustum.OUT) && (visible != UNDEFINED))
            {
                if(vertexData.textured)
                {
                    vertexData.addVertex(p1X, p1Y, p1Z,
                                         p1tS, p1tT);
                    vertexData.addVertex(p2X, p2Y, p2Z,
                                         p2tS, p2tT);
                    vertexData.addVertex(p3X, p3Y, p3Z,
                                         p3tS, p3tT);
                }
                else
                {
                    vertexData.addVertex(p1X, p1Y, p1Z,
                                         p1tS, p1tT, p1tR);
                    vertexData.addVertex(p2X, p2Y, p2Z,
                                         p2tS, p2tT, p2tR);
                    vertexData.addVertex(p3X, p3Y, p3Z,
                                         p3tS, p3tT, p3tR);
                }
            }
        }
        else
        {
            leftChild.getTriangles(vertexData);
            rightChild.getTriangles(vertexData);
        }
    }

    /**
     * Update the tree depending on the view position and variance
     */
    void updateTree(Tuple3f position,
                    ViewFrustum landscapeView,
                    VarianceTree varianceTree,
                    int parentVisible,
                    QueueManager queueManager)
    {

        //splitThisFrame = false;
        //mergedThisFrame = false;

        if(parentVisible == UNDEFINED ||
           parentVisible == ViewFrustum.CLIPPED)
        {
            visible = landscapeView.isTriangleInFrustum(p1X, p1Y, p1Z,
                                                        p2X, p2Y, p2Z,
                                                        p3X, p3Y, p3Z);
        }
        else
            visible = parentVisible;

        if(leftChild == null &&
           rightChild == null &&
           depth < varianceTree.getMaxDepth() &&
           visible != ViewFrustum.OUT)
        {
            computeVariance(position);

            queueManager.addTriangle(this);
        }
        else
        {
            if(leftChild != null)
                leftChild.updateTree(position,
                                     landscapeView,
                                     varianceTree,
                                     visible,
                                     queueManager);

            if(rightChild != null)
                rightChild.updateTree(position,
                                      landscapeView,
                                      varianceTree,
                                      visible,
                                      queueManager);

            //System.out.println(diamond+"  "+diamondVariance);
            if(diamond) {

// BUG Here, baseNeighbour may not have had its variance updated
// for the new position
                if(visible != ViewFrustum.OUT)
                {
                    computeVariance(position);

                    if(baseNeighbour != null)
                        diamondVariance = Math.max(variance,
                                                   baseNeighbour.variance);
                    else
                        diamondVariance = variance;
                }
                else
                {
                    diamondVariance = Float.MIN_VALUE;
                }

                queueManager.addDiamond(this);
            }
        }
    }

    public String toString() {
        return Integer.toString(node);
    }

    //—————————————————————————————
    // local convenience methods
    //—————————————————————————————

    /**
     * Internal common initialization for the startup of the class.
     *
     * @param landscapeView view information at start time
     * @param parentVisible Flag about the visibility state of the parent
     *    tree node
     */
    private void init(ViewFrustum landscapeView, int parentVisible)
    {
        float[] tmp = new float[3];
        float[] texTmp = new float[3];

        boolean textured = terrainData.hasTexture();

        if(textured)
            terrainData.getCoordinateWithTexture(tmp, texTmp, leftX, leftY);
        else
            terrainData.getCoordinateWithColor(tmp, texTmp, leftX, leftY);

        p1X = tmp[0];
        p1Y = tmp[1];
        p1Z = tmp[2];

        p1tS = texTmp[0];
        p1tT = texTmp[1];
        p1tR = texTmp[2];

        if(textured)
            terrainData.getCoordinateWithTexture(tmp, texTmp, rightX, rightY);
        else
            terrainData.getCoordinateWithColor(tmp, texTmp, rightX, rightY);

        p2X = tmp[0];
        p2Y = tmp[1];
        p2Z = tmp[2];

        p2tS = texTmp[0];
        p2tT = texTmp[1];
        p2tR = texTmp[2];

        if(textured)
            terrainData.getCoordinateWithTexture(tmp, texTmp, apexX, apexY);
        else
            terrainData.getCoordinateWithColor(tmp, texTmp, apexX, apexY);

        p3X = tmp[0];
        p3Y = tmp[1];
        p3Z = tmp[2];

        p3tS = texTmp[0];
        p3tT = texTmp[1];
        p3tR = texTmp[2];

        // Check the visibility of this triangle
        if(parentVisible == UNDEFINED ||
           parentVisible == ViewFrustum.CLIPPED)
        {
            visible = landscapeView.isTriangleInFrustum(p1X, p1Y, p1Z,
                                                        p2X, p2Y, p2Z,
                                                        p3X, p3Y, p3Z);
        }
        else
            visible = parentVisible;

        variance = 0;
    }

    /**
     * Compute the variance variable value.
     *
     * @param position The position for the computation
     */
    private void computeVariance(Tuple3f position)
    {
        float center_x = (p1X + p2X) * 0.5f;
        float center_z = -(p1Y + p2Y) * 0.5f;
        float pos_x = (position.x - center_x) * (position.x - center_x);
        float pos_z = (position.z - center_z) * (position.z - center_z);
        float distance = (float)Math.sqrt(pos_x + pos_z);

        float angle = varianceTree.getVariance(node) / distance;

        variance = (float)Math.abs(Math.atan(angle));
    }

    /**
     * Forceful split of this triangle and turns it into two triangles.
     */
    private void splitTriangle(Tuple3f position,
                               ViewFrustum landscapeView,
                               QueueManager queueManager)
    {
        int splitX = (leftX+rightX)/2;
        int splitY = (leftY+rightY)/2;

        if(parent != null)
            parent.removeDiamond(queueManager);

        leftChild = getTreeNode();
        rightChild = getTreeNode();

        leftChild.newNode(apexX, apexY,
                                  leftX, leftY,
                                  splitX, splitY,
                                  node << 1,
                                  terrainData,
                                  landscapeView,
                                  visible,
                                  depth + 1,
                                  varianceTree);

        rightChild.newNode(rightX, rightY,
                                   apexX, apexY,
                                   splitX, splitY,
                                   1 + (node << 1),
                                   terrainData,
                                   landscapeView,
                                   visible,
                                   depth + 1,
                                   varianceTree);

        leftChild.parent = this;
        rightChild.parent = this;

        if(depth+1 < varianceTree.getMaxDepth() && visible!=ViewFrustum.OUT)
        {
            rightChild.computeVariance(position, queueManager);
            leftChild.computeVariance(position, queueManager);
        }
    }

    private void split2(Tuple3f position,
                        ViewFrustum landscapeView,
                        QueueManager queueManager)
    {
        splitTriangle(position, landscapeView, queueManager);

        queueManager.removeTriangle(this);

        leftChild.leftNeighbour = rightChild;
        rightChild.rightNeighbour = leftChild;
        leftChild.baseNeighbour = leftNeighbour;

        if(leftNeighbour != null)
        {
            if(leftNeighbour.baseNeighbour == this)
                leftNeighbour.baseNeighbour = leftChild;
            else
            {
                if(leftNeighbour.leftNeighbour == this)
                    leftNeighbour.leftNeighbour = leftChild;
                else
                    leftNeighbour.rightNeighbour = leftChild;
            }
        }

        rightChild.baseNeighbour = rightNeighbour;

        if(rightNeighbour != null)
        {
            if(rightNeighbour.baseNeighbour == this)
                rightNeighbour.baseNeighbour = rightChild;
            else
            {
                if(rightNeighbour.rightNeighbour == this)
                    rightNeighbour.rightNeighbour = rightChild;
                else
                    rightNeighbour.leftNeighbour = rightChild;
            }
        }
    }

    private void merge(TreeNode mergeNode, QueueManager queueManager)
    {
        if(mergeNode.leftChild == null ||
           mergeNode.rightChild == null ||
           !mergeNode.leftChild.isLeaf() ||
           !mergeNode.rightChild.isLeaf())
        {
            throw new RuntimeException("Illegal merge");
        }

        if(mergeNode.leftNeighbour != null)
        {
            if(mergeNode.leftNeighbour.baseNeighbour == mergeNode.leftChild)
               mergeNode.leftNeighbour.baseNeighbour = mergeNode;
            else
            {
                if(mergeNode.leftNeighbour.leftNeighbour == mergeNode.leftChild)
                    mergeNode.leftNeighbour.leftNeighbour = mergeNode;
                else
                    mergeNode.leftNeighbour.rightNeighbour = mergeNode;
            }
        }

        if(mergeNode.rightNeighbour != null)
        {
            if(mergeNode.rightNeighbour.baseNeighbour == mergeNode.rightChild)
                mergeNode.rightNeighbour.baseNeighbour = mergeNode;
            else
            {
              if(mergeNode.rightNeighbour.rightNeighbour == mergeNode.rightChild)
                    mergeNode.rightNeighbour.rightNeighbour = mergeNode;
                else
                    mergeNode.rightNeighbour.leftNeighbour = mergeNode;
            }
        }

        if(mergeNode.leftChild.baseNeighbour != null &&
          mergeNode.leftChild.baseNeighbour.baseNeighbour
                                         == mergeNode.leftChild)
        {
            mergeNode.leftChild.baseNeighbour.baseNeighbour = mergeNode;
        }

        if(mergeNode.rightChild.baseNeighbour != null &&
         mergeNode.rightChild.baseNeighbour.baseNeighbour
                                         == mergeNode.rightChild)
        {
           mergeNode.rightChild.baseNeighbour.baseNeighbour = mergeNode;
        }

        mergeNode.leftNeighbour = mergeNode.leftChild.baseNeighbour;
        mergeNode.rightNeighbour = mergeNode.rightChild.baseNeighbour;

        if(mergeNode.visible != ViewFrustum.OUT)
            queueManager.addTriangle(mergeNode);

        queueManager.removeTriangle(mergeNode.leftChild);
        queueManager.removeTriangle(mergeNode.rightChild);

        mergeNode.leftChild.freeNode();
        mergeNode.leftChild = null;
        mergeNode.rightChild.freeNode();
        mergeNode.rightChild = null;
    }

    /**
     * Check if tn forms a diamond
     */
    private void checkForNewDiamond(TreeNode tn, QueueManager queueManager)
    {
        if(tn == null)
            return;

        if(tn.leftChild.isLeaf() && tn.rightChild.isLeaf() &&
           (tn.baseNeighbour == null ||
            tn.baseNeighbour.leftChild == null ||
            (tn.baseNeighbour.leftChild.isLeaf() &&
             tn.baseNeighbour.rightChild.isLeaf())))
        {
            tn.diamond = true;

            if(tn.visible != ViewFrustum.OUT)
            {
                if(tn.baseNeighbour != null)
                    tn.diamondVariance = Math.max(tn.variance,
                                                  tn.baseNeighbour.variance);
                else
                    tn.diamondVariance = tn.variance;
            }
            else
                tn.diamondVariance = Float.MIN_VALUE;

            queueManager.addDiamond(tn);
        }
    }

    /**
     * Either return a node from the cache or if the cache is empty, return
     * a new tree node.
     */
    private static TreeNode getTreeNode()
    {
        TreeNode ret_val;

        if(nodeCache.size() > 0)
            ret_val = (TreeNode)nodeCache.removeFirst();
        else
            ret_val = new TreeNode();

        return ret_val;
    }

    /**
     * Add the node to the free cache.
     */
    private static void addTreeNode(TreeNode node)
    {
        nodeCache.add(node);
    }
}
						

Image Tiling in JAI

We have finally reached a place where we can assign JAI Image Tiles to Patches. Recall that tiles are rectangular segments of a Raster object. Instead of working with the entire image at once (essentially one huge tile), you can work with rectangular subsegments. This is particularly useful when dealing with large images that don't easily fit into memory all at once. This is precisely the case that exists with terrain data.

The output of the image tiling ROAM example is shown in Figure 14.3, and the CachedTextureTileGenerator class is shown in Listing 14.7.

Figure 14.3. Screen shot from TiledCullingDemo, showing a simple numbered texture.


Listing 14.7 CachedTextureTileGenerator
// Standard imports
import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import java.awt.image.renderable.*;
import javax.media.j3d.*;
import javax.media.jai.JAI;
import javax.media.jai.PlanarImage;
import javax.media.jai.ImageLayout;
import java.util.HashMap;

// Application Specific imports
import org.j3d.terrain.TextureTileGenerator;

/**
 * An example TextureTileGenerator.
 * Caches textures so we don't regenerate them, but never decreases memory usage.
 *
 * @author Alan Hudson
 */
public class CachedTextureTileGenerator implements TextureTileGenerator {
    /** The source image */
    private PlanarImage source;

    /** A simple cache */
    private Texture tCache[][];

    /**
     * Construct a TileGenerator for the specified name.
     *
     * @param filename The texture to tile
     */
    public CachedTextureTileGenerator(String filename)
    {
        source = JAI.create("fileload", filename);

        ParameterBlock pb = new ParameterBlock();
        pb.addSource(source);
        pb.add(null).add(null).add(null).add(null).add(null);

        RenderableImage ren = JAI.createRenderable("renderable", pb);
        RenderedImage image = ren.createDefaultRendering();

        /* Create a texture cache of 8x8 tiles.  2K image / 256 bytes */
        tCache = new Texture[8][8];
    }

    /**
     * Get the size of each texture tile.
     *
     * @return The dimensions of the tile
     */
    public Dimension getTextureSize()
    {
        return new Dimension(256,256);
    }

    /**
     * Get the texture tile for bounded region.
     *
     * @param bounds The region
     */
    public Texture getTextureTile(Rectangle bounds)
    {
        int x = bounds.x / 256;
        int y = bounds.y / 256;

        if (tCache[x][y] != null) {
            return (tCache[x][y]);
        }

        Rectangle rect = new Rectangle(bounds.x, bounds.y, bounds.width,
             bounds.height);

        BufferedImage bi = source.getAsBufferedImage(rect, null);

        int format = ImageComponent2D.FORMAT_RGB;

        ImageComponent2D img =
            new ImageComponent2D(format, bi, true, false);

        Texture texture = new Texture2D(Texture.BASE_LEVEL,
                                        Texture.RGB,
                                        img.getWidth(),
                                        img.getHeight());
        texture.setImage(0, img);

        tCache[x][y] = texture;

        return texture;
    }
}
							

Listing 14.8 is a shell program from running the ROAM code.

Listing 14.8 TiledCullingDemo
// Standard imports
import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import java.awt.image.renderable.*;
import javax.swing.*;
import javax.media.j3d.*;
import javax.vecmath.*;
import javax.media.jai.JAI;
import javax.media.jai.PlanarImage;
import javax.media.jai.ImageLayout;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.Hashtable;

// Application Specific imports
import org.j3d.geom.Box;

import org.j3d.loaders.HeightMapTerrainData;
import org.j3d.loaders.SimpleTiledTerrainData;
import org.j3d.loaders.vterrain.BTHeader;
import org.j3d.loaders.vterrain.BTParser;

import org.j3d.terrain.AbstractStaticTerrainData;
import org.j3d.terrain.AbstractTiledTerrainData;
import org.j3d.terrain.AppearanceGenerator;
import org.j3d.terrain.Landscape;
import org.j3d.terrain.TerrainData;
import org.j3d.terrain.TextureTileGenerator;
import org.j3d.terrain.roam.SplitMergeLandscape;

import org.j3d.texture.TextureCache;
import org.j3d.texture.TextureCacheFactory;

import org.j3d.ui.navigation.MouseViewHandler;
import org.j3d.ui.navigation.NavigationStateManager;
import org.j3d.ui.navigation.NavigationState;

import org.j3d.util.interpolator.ColorInterpolator;
import org.j3d.util.frustum.ViewFrustum;

/**
 * Demonstration of the ROAM code using tiled textures.
 *
 *
 * @author Alan Hudson
 * @version $Revision: 1.1 $
 */
public class TiledCullingDemo extends DemoFrame
    implements ItemListener, AppearanceGenerator
{
    private static final double BACK_CLIP_DISTANCE = 3000.0;
    private static final double FRONT_CLIP_DISTANCE = 1;

    /** The main canvas that we are navigating on */
    private Canvas3D navCanvas;

    /** The canvas that provides a birds-eye view of the scene */
    private Canvas3D topDownCanvas;

    /** Global material instance to use */
    private Material material;

    /** Global polygon attributes to use */
    private PolygonAttributes polyAttr;

    private MouseViewHandler groundNav;
    private MouseViewHandler topDownNav;

    /** The view frustum for the ground canvas */
    private ViewFrustum viewFrustum;

    /** The landscape we are navigating around */
    private Landscape landscape;

    /** The branchgroup to add our terrain to */
    private BranchGroup terrainGroup;

    /** TG that holds the user view position. Used when new terrain set */
    private TransformGroup gndViewTransform;

    /** TG that holds the top-down user view position. Used when new terrain set */
    private TransformGroup topViewTransform;

    private HashMap terrainFilesMap;
    private HashMap textureFilesMap;

    /** Mapping of the button to the polygon mode value */
    private HashMap polyModeMap;

    /** The color interpolator for doing height interpolations with */
    private ColorInterpolator heightRamp;

    /**
     * Construct a new demo with no geometry currently showing, but the
     * default type is set to quads.
     */
    public TiledCullingDemo()
    {
        super("Tiled Culling Demo");

        topDownCanvas = createCanvas();
        navCanvas = createCanvas();

        Cursor curse = Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR);
        navCanvas.setCursor(curse);

        terrainFilesMap = new HashMap();
        textureFilesMap = new HashMap();

        Panel p0 = new Panel(new GridLayout(1, 2));
        p0.add(navCanvas);
        p0.add(topDownCanvas);

        add(p0, BorderLayout.CENTER);

        JPanel p1 = new JPanel(new FlowLayout());

        ButtonGroup grp = new ButtonGroup();
        JRadioButton button = new JRadioButton("Crater Lake");
        button.addItemListener(this);
        grp.add(button);
        p1.add(button);

        terrainFilesMap.put(button, "crater_0513.bt");
        textureFilesMap.put(button, null);

        add(p1, BorderLayout.SOUTH);

        // Panel for the polygon mode style
        polyModeMap = new HashMap();

        JPanel p2 = new JPanel(new GridLayout(4, 1));

        p2.add(new JLabel("Render As..."));

        grp = new ButtonGroup();
        button = new JRadioButton("Polygons", true);
        button.addItemListener(this);
        grp.add(button);
        p2.add(button);
        polyModeMap.put(button, new Integer(PolygonAttributes.POLYGON_FILL));


        button = new JRadioButton("Lines");
        button.addItemListener(this);
        grp.add(button);
        p2.add(button);
        polyModeMap.put(button, new Integer(PolygonAttributes.POLYGON_LINE));


        button = new JRadioButton("Points");
        button.addItemListener(this);
        grp.add(button);
        p2.add(button);
        polyModeMap.put(button, new Integer(PolygonAttributes.POLYGON_POINT));


        JPanel p3 = new JPanel(new BorderLayout());
        p3.add(p2, BorderLayout.NORTH);

        add(p3, BorderLayout.EAST);

        groundNav = new MouseViewHandler();
        groundNav.setCanvas(navCanvas);
        groundNav.setButtonNavigation(MouseEvent.BUTTON1_MASK,
                                      NavigationState.FLY_STATE);
        groundNav.setButtonNavigation(MouseEvent.BUTTON2_MASK,
                                      NavigationState.TILT_STATE);
        groundNav.setButtonNavigation(MouseEvent.BUTTON3_MASK,
                                      NavigationState.PAN_STATE);


        NavigationStateManager gnd_nav_mgr =
            new NavigationStateManager(navCanvas);
        gnd_nav_mgr.setMouseHandler(groundNav);

        topDownNav = new MouseViewHandler();
        topDownNav.setCanvas(topDownCanvas);
        topDownNav.setButtonNavigation(MouseEvent.BUTTON1_MASK,
                                       NavigationState.PAN_STATE);

        NavigationStateManager top_nav_mgr =
            new NavigationStateManager(topDownCanvas);
        top_nav_mgr.setMouseHandler(topDownNav);

        buildScene();

        viewFrustum = new ViewFrustum(navCanvas);

        // Now set up the material and appearance handling for the generator
        material = new Material();
        material.setLightingEnable(true);

        polyAttr = new PolygonAttributes();
        polyAttr.setCapability(PolygonAttributes.ALLOW_MODE_WRITE);
        polyAttr.setCullFace(PolygonAttributes.CULL_NONE);
        polyAttr.setBackFaceNormalFlip(true);

        heightRamp = new ColorInterpolator(ColorInterpolator.HSV_SPACE);
        heightRamp.addRGBKeyFrame(-20,  0,    0,    1,     0);
        heightRamp.addRGBKeyFrame(0,    0,    0.7f, 0.95f, 0);
        heightRamp.addRGBKeyFrame(5,    1,    1,    0,     0);
        heightRamp.addRGBKeyFrame(10,   0,    0.6f, 0,     0);
        heightRamp.addRGBKeyFrame(100,  0,    1,    0,     0);
        heightRamp.addRGBKeyFrame(1000, 0.6f, 0.7f, 0,     0);
        heightRamp.addRGBKeyFrame(1500, 0.5f, 0.5f, 0.3f,  0);
        heightRamp.addRGBKeyFrame(2500, 1,    1,    1,     0);
    }

    //—————————————————————————————
    // Methods required by ItemListener
    //—————————————————————————————

    /**
     * Process the change of state request from the colour selector panel.
     *
     * @param evt The event that caused this method to be called
     */
    public void itemStateChanged(ItemEvent evt)
    {
        if(evt.getStateChange() != ItemEvent.SELECTED)
            return;

        Object src = evt.getSource();

        if(textureFilesMap.containsKey(src))
        {
            // map change request
            String terrain = (String)terrainFilesMap.get(src);
            String texture = (String)textureFilesMap.get(src);

            loadTerrain(terrain, texture);
        }
        else
        {
            Integer mode_int = (Integer)polyModeMap.get(src);
            polyAttr.setPolygonMode(mode_int.intValue());
        }
    }

    //—————————————————————————————
    // Methods required by AppearanceGenerator
    //—————————————————————————————

    /**
     * Create a new appearance instance. We set them all up with different
     * appearance instances, but share the material information.
     *
     * @return The new appearance instance to use
     */
    public Appearance createAppearance()
    {
        Appearance app = new Appearance();
        app.setMaterial(material);
        app.setPolygonAttributes(polyAttr);

        return app;
    }

    //—————————————————————————————
    // Internal convenience methods
    //—————————————————————————————

    /**
     * Build the scenegraph for the canvas
     */
    private void buildScene()
    {
        Color3f ambientBlue = new Color3f(0.0f, 0.02f, 0.5f);
        Color3f white = new Color3f(1, 1, 1);
        Color3f black = new Color3f(0.0f, 0.0f, 0.0f);
        Color3f blue = new Color3f(0.00f, 0.20f, 0.80f);
        Color3f specular = new Color3f(0.7f, 0.7f, 0.7f);

        VirtualUniverse universe = new VirtualUniverse();
        Locale locale = new Locale(universe);

        BranchGroup view_group = new BranchGroup();
        BranchGroup world_object_group = new BranchGroup();

        PhysicalBody body = new PhysicalBody();
        PhysicalEnvironment env = new PhysicalEnvironment();

        Point3d origin = new Point3d(0, 0, 0);
        BoundingSphere light_bounds =
            new BoundingSphere(origin, BACK_CLIP_DISTANCE);
        DirectionalLight headlight = new DirectionalLight();
        headlight.setColor(white);
        headlight.setInfluencingBounds(light_bounds);
        headlight.setEnable(true);

        //
        // View group for the ground navigation system that the
        // roam code will apply to.
        //

        ViewPlatform gnd_camera = new ViewPlatform();

        Transform3D angle = new Transform3D();

        gndViewTransform = new TransformGroup();
        gndViewTransform.setCapability(TransformGroup.ALLOW_TRANSFORM_READ);
        gndViewTransform.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE);

        gndViewTransform.addChild(gnd_camera);
        gndViewTransform.addChild(headlight);
//        gndViewTransform.addChild(new Box(10, 10, 10));

        View gnd_view = new View();
        gnd_view.setBackClipDistance(BACK_CLIP_DISTANCE);
        gnd_view.setFrontClipDistance(FRONT_CLIP_DISTANCE);
        gnd_view.setPhysicalBody(body);
        gnd_view.setPhysicalEnvironment(env);
        gnd_view.addCanvas3D(navCanvas);
        gnd_view.attachViewPlatform(gnd_camera);

        groundNav.setViewInfo(gnd_view, gndViewTransform);
        groundNav.setNavigationSpeed(50.0f);

        view_group.addChild(gndViewTransform);
        view_group.addChild(groundNav.getTimerBehavior());

        //
        // View transform group for the system that looks in a top-down view
        // of the entire scene graph.
        //

        ViewPlatform god_camera = new ViewPlatform();
        god_camera.setCapability(TransformGroup.ALLOW_LOCAL_TO_VWORLD_READ);

        angle = new Transform3D();
        angle.setTranslation(new Vector3d(0, 0, 50));

        topViewTransform = new TransformGroup(angle);
        topViewTransform.setCapability(TransformGroup.ALLOW_TRANSFORM_READ);
        topViewTransform.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE);
        topViewTransform.setCapability(TransformGroup.ALLOW_LOCAL_TO_ VWORLD_READ);

        topViewTransform.addChild(god_camera);
//        topViewTransform.addChild(headlight.cloneNode(false));

        angle = new Transform3D();
        angle.rotX(-Math.PI / 2);

        TransformGroup god_view_tg = new TransformGroup(angle);
        god_view_tg.setCapability(TransformGroup.ALLOW_LOCAL_TO_VWORLD_READ);
        god_view_tg.setCapability(TransformGroup.ALLOW_TRANSFORM_READ);
        god_view_tg.addChild(topViewTransform);

        View god_view = new View();
        god_view.setBackClipDistance(3*BACK_CLIP_DISTANCE);
        god_view.setFrontClipDistance(FRONT_CLIP_DISTANCE);
        god_view.setPhysicalBody(body);
        god_view.setPhysicalEnvironment(env);
        god_view.addCanvas3D(topDownCanvas);
        god_view.attachViewPlatform(god_camera);

        topDownNav.setViewInfo(god_view, topViewTransform);
        topDownNav.setNavigationSpeed(500);

        view_group.addChild(god_view_tg);
        view_group.addChild(topDownNav.getTimerBehavior());

        // Just an axis for reference
//        world_object_group.addChild(new Axis());

        // Create a new branchgroup that is for the geometry. Initially starts
        // with a null child at position zero so that we only need to write the
        // child and not extend. One less capability to set is good.
        terrainGroup = new BranchGroup();
        terrainGroup.setCapability(Group.ALLOW_CHILDREN_WRITE);
        terrainGroup.setCapability(Group.ALLOW_CHILDREN_EXTEND);
//        terrainGroup.addChild(null);

        world_object_group.addChild(terrainGroup);

        Material mat = new Material(ambientBlue, ambientBlue, blue, specular, 0);
        Appearance app = new Appearance();
        app.setMaterial(mat);
        Box box = new Box(50, 50, 1000, app);


        angle.set(new Vector3f(0, 0, -500));
        TransformGroup tg = new TransformGroup(angle);
        tg.addChild(box);

        gndViewTransform.addChild(tg);

        // Add everything to the locale
        locale.addBranchGraph(view_group);
        locale.addBranchGraph(world_object_group);
    }

    /**
     * Load the terrain and get it read to roll. If the texture file is not
     * specified then no texture will be loaded and colour information is
     * used instead.
     *
     * @param filename The name of the terrain file
     * @param textureName The name of the texture file, or null
     */
    private void loadTerrain(String filename, String textureName)
    {
        BTParser ldr = new BTParser();
        File bt_file = new File(filename);

        View v = navCanvas.getView();
        v.stopView();

        try
        {
            if(landscape != null)
            {
                landscape.setAppearanceGenerator(null);
                landscape.detach();
                landscape = null;
            }

            System.gc();

            System.out.println("Loading terrain file. Please wait");

            ldr.reset(new FileInputStream(bt_file));
            ldr.parse();

            TerrainData terrain = null;

            BTHeader header = ldr.getHeader();

            SimpleTiledTerrainData t = new SimpleTiledTerrainData(ldr);
            terrain = t;

            System.out.println("Terrain loading complete");

            // Use a tiled texture
            TextureTileGenerator myGen = new CachedTextureTileGenerator  ("numgrid.jpg");
            ((AbstractTiledTerrainData)terrain).setTextureTileGenerator(myGen);

                System.out.println("Finished texture");
            System.out.println("Building landscape");

            landscape = new SplitMergeLandscape(viewFrustum, terrain);
            landscape.setCapability(BranchGroup.ALLOW_DETACH);
            landscape.setAppearanceGenerator(this);

            float[] origin = new float[3];
            terrain.getCoordinate(origin, 1, 1);

            Transform3D angle = new Transform3D();

            // setup the top view by just raising it some amount and we want
            Vector3f pos = new Vector3f();
            pos.z += 15000;
            pos.x = origin[0];
            pos.y = origin[2];
            angle.setTranslation(pos);

            topViewTransform.setTransform(angle);

            // the initial view to be some way off the ground too and rotate at
            // 45 deg to look into the "middle" of the terrain.
            terrain.getCoordinate(origin, 0, 0);
            pos.set(origin);
            pos.y += 100;
            pos.x -= 100;
            pos.z -= 100;
            angle.rotY(Math.PI * -0.25); // 45 deg looking into the terrain
            angle.setTranslation(pos);

            gndViewTransform.setTransform(angle);


            // Force a single render so that the view transform is updated
            // and the projection matrix is correct for the view frustum.
            v.renderOnce();

            viewFrustum.viewingPlatformMoved();

            Matrix3f mtx = new Matrix3f();
            Vector3f orient = new Vector3f(0, 0, -1);

            angle.get(mtx, pos);
            mtx.transform(orient);

            landscape.initialize(pos, orient);

            groundNav.setFrameUpdateListener(landscape);

            terrainGroup.removeAllChildren();
            terrainGroup.addChild(landscape);

            // Set the nav speed to be one grid square per second
            groundNav.setNavigationSpeed((float)terrain.getGridXStep());


            v.startView();

            System.out.println("Ready for rendering");
        }
        catch(IOException ioe)
        {
            System.out.println("I/O Error " + ioe);
            ioe.printStackTrace();
        }
    }

    public static void main(String[] argv)
    {
        TiledCullingDemo demo = new TiledCullingDemo();
        demo.setSize(600, 400);
        demo.setVisible(true);
    }
}
							

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset