Unlimited Terrain Engine Using Heightmaps

Hello all. I have been reinventing Fyrestone's terrain loader, and this is a fairly successful attempt.



How it works:
This system works by creating tiles and then reusing them. There will always be 9 tiles in the game, and they'll be destroyed / recycled as you move throughout the world. Tile-Coordinates are coordinates like 0,2 which don't represent the player's position, but instead represent where that tile needs to be.



The "Tile" Class: The tile class extends node and should be directly attached to your root  or terrain node. Tiles are created by simply passing the tile coordinates.



The "TileManager" Class:
A simple Singleton class which allows you to edit settings like your heightmap size, your map scale, your height scale, a default map, and the directories where you store your maps. In addition, it has a method which return a vector3f for setting local translation when passed a tile-coordinate, and an array list of tile-coordinates that should be loaded when you're standing on a specific tile.



The "TileCoordinate" Class:
Simply an x,y grouped into a class for ease-of-use.



The file-system: set the location of your maps in the TileManager class, then name your maps by coordinate names (i.e. "0,0.raw" and "1,0.raw"). When you're moving around, the Tiles will load the correct heightmap (or the default heightmap if you're moving into uncharted areas that don't have specific heightmaps) and move them to the correct location.



Known Bugs:


  • The way the Tile-Manager works will not detect when you walk into the first set of negative numbers. You have to walk > 1 tile into negative numbers before the terrain system will realize you've changed tiles.

  • No texture splatting has been implemented yet.



If you have any questions feel free to ask, any modifications/improvements are definitely welcome. Hope somebody finds this useful.

Tile.java:

/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package fyrestoneterrain;

import com.gibbon.mfkarpg.terrain.splatting.TerrainPassNode;
import com.jme.image.Texture;
import com.jme.scene.Node;
import com.jme.scene.PassNode;
import com.jme.scene.state.TextureState;
import com.jme.util.TextureManager;
import com.jmex.terrain.TerrainBlock;
import com.jmex.terrain.util.RawHeightMap;
import java.util.ArrayList;

/**
 *
 * @author Tyler Trussell
 */
public class Tile extends Node
{
    private int x, y;
    private RawHeightMap myMap;
    private TerrainBlock myBlock;
    private PassNode splat;
   
    public int getX()
    {
        return x;
    }
   
    public int getY()
    {
        return y;
    }
   
    public Tile(int x, int y)
    {
        this.x = x;
        this.y = y;
       
        if(TestFyrestoneTerrain.class.getClassLoader().getResource(TileManager.mapDirectory + x + "," + y + TileManager.mapExtension) == null)
        {
            myMap = new RawHeightMap(TestFyrestoneTerrain.class.getClassLoader().getResource(TileManager.mapDirectory + TileManager.defaultMap + TileManager.mapExtension), TileManager.mapSize + 1, TileManager.mapFormat, true);
        }
        else this.myMap = new RawHeightMap(TestFyrestoneTerrain.class.getClassLoader().getResource(TileManager.mapDirectory + x + "," + y + TileManager.mapExtension), TileManager.mapSize + 1, TileManager.mapFormat, true);
       
        myBlock = new TerrainBlock(x + "," + y, myMap.getSize(), TileManager.scale, myMap.getHeightMap(), TileManager.getOriginFor(x, y), false);
       
        attachChild(myBlock);
    }
   
    public void changeMap(int x, int y)
    {
        System.out.println("Tile changed from " + this.x + "," + this.y + " to " + x + "," + y);
        this.x = x;
        this.y = y;
       
        if(TestFyrestoneTerrain.class.getClassLoader().getResource(TileManager.mapDirectory + x + "," + y + TileManager.mapExtension) == null)
        {
            myMap = new RawHeightMap(TestFyrestoneTerrain.class.getClassLoader().getResource(TileManager.mapDirectory + TileManager.defaultMap + TileManager.mapExtension), TileManager.mapSize + 1, TileManager.mapFormat, true);
        }
        else myMap = new RawHeightMap(TestFyrestoneTerrain.class.getClassLoader().getResource(TileManager.mapDirectory + x + "," + y + TileManager.mapExtension), TileManager.mapSize + 1, TileManager.mapFormat, true);

        myMap.load();
        myBlock.setHeightMap(myMap.getHeightMap());
        myBlock.updateFromHeightMap();
        myBlock.setLocalTranslation(TileManager.getOriginFor(x, y));
       
        myBlock.updateRenderState();
    }
}



TileManager.java

/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package fyrestoneterrain;

import com.jme.math.Vector3f;
import com.jme.scene.state.AlphaState;
import com.jme.system.DisplaySystem;
import com.jmex.terrain.util.RawHeightMap;
import java.util.ArrayList;

/**
 *
 * @author Tyler Trussell
 */
public class TileManager
{
    public static final String mapDirectory = "maps/tiles/";
    public static final String defaultMap = "0";
   
    public static final int mapFormat = RawHeightMap.FORMAT_16BITLE;
    public static final String mapExtension = ".raw";
   
    public static final int mapSize = 64;
    public static final int mapScale = 10;
    public static final float mapHeightScale = .0007f;
    public static final int tileSize = mapSize * mapScale;
   
    /* NOT YET IMPLEMENTED
    public static final int layers = 4;
    public static final String alphaDirectory = "maps/alphas/";
    public static final String textureDirectory = "maps/textures/";
    public static final String[] layerNames = {"layer1", "layer2", "layer3", "layer4" };
    public static final int textureScale = 1;
    */
   
    public static final Vector3f scale = new Vector3f(mapScale, mapHeightScale, mapScale);
   
    public static final AlphaState forTextures = DisplaySystem.getDisplaySystem().getRenderer().createAlphaState();
    public static final AlphaState forLights = DisplaySystem.getDisplaySystem().getRenderer().createAlphaState();
   
    public static Vector3f getOriginFor(int x, int y)
    {
        int x_, y_, z_;
        x_ = x * tileSize;
        y_ = 0;
        z_ = y * tileSize;
       
        return new Vector3f(x_, y_, z_);
    }
   
    public static ArrayList<TileCoordinate> getRequiredTiles(int x, int y)
    {
        ArrayList<TileCoordinate> toReturn = new ArrayList();
        toReturn.add(new TileCoordinate(x - 1, y - 1));
        toReturn.add(new TileCoordinate(x - 1, y));
        toReturn.add(new TileCoordinate(x - 1, y + 1));
        toReturn.add(new TileCoordinate(x, y - 1));
        toReturn.add(new TileCoordinate(x, y));
        toReturn.add(new TileCoordinate(x, y + 1));
        toReturn.add(new TileCoordinate(x + 1, y - 1));
        toReturn.add(new TileCoordinate(x + 1, y));
        toReturn.add(new TileCoordinate(x + 1, y + 1));
        return toReturn;
    }
   
    public static AlphaState textureAlpha()
    {
        forTextures.setBlendEnabled(true);
        forTextures.setSrcFunction(AlphaState.SB_SRC_ALPHA);
        forTextures.setDstFunction(AlphaState.DB_ONE_MINUS_SRC_ALPHA);
        forTextures.setTestEnabled(true);
        forTextures.setTestFunction(AlphaState.TF_GREATER);
        forTextures.setEnabled(true);
       
        return forTextures;
    }
   
    public static AlphaState lightAlpha()
    {
        forTextures.setBlendEnabled(true);
        forTextures.setSrcFunction(AlphaState.SB_DST_COLOR);
        forTextures.setDstFunction(AlphaState.DB_SRC_COLOR);
        forTextures.setTestEnabled(true);
        forTextures.setTestFunction(AlphaState.TF_GREATER);
        forTextures.setEnabled(true);
       
        return forTextures;
    }
}



TileCoordinate.java:

/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package fyrestoneterrain;

/**
 *
 * @author Tyler Trussell
 */
public class TileCoordinate
{
    private int x;
    private int y;
   
    public TileCoordinate(int x, int y)
    {
        this.x = x;
        this.y = y;
    }
   
    public int getX()
    {
        return x;
    }
   
    public int getY()
    {
        return y;
    }
   
    @Override
    public String toString()
    {
        return x + "," + y;
    }
}



TestFyrestoneTerrain.java:

/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package fyrestoneterrain;

import com.jme.app.SimpleGame;
import com.jme.light.DirectionalLight;
import com.jme.math.Vector3f;
import com.jme.renderer.ColorRGBA;
import java.util.ArrayList;

/**
 *
 * @author Tyler
 */
public class TestFyrestoneTerrain extends SimpleGame
{
    private int x, y, tempx, tempy;
    ArrayList<TileCoordinate> mustLoad;
    ArrayList<Tile> notNeeded;
    private Tile[] world;
   
    public static void main(String[] args)
    {
        TestFyrestoneTerrain app = new TestFyrestoneTerrain();
        app.setDialogBehaviour(ALWAYS_SHOW_PROPS_DIALOG);
        app.start();
    }

    @Override
    protected void simpleInitGame()
    {
        lightState.detachAll();
        DirectionalLight dl = new DirectionalLight();
        dl.setDirection(new Vector3f(.5f, -.5f, 0));
        dl.setAmbient(ColorRGBA.white);
        dl.setDiffuse(ColorRGBA.white);
        dl.setEnabled(true);
        lightState.attach(dl);
        x = (int) cam.getLocation().x / TileManager.tileSize;
        y = (int) cam.getLocation().z / TileManager.tileSize;
        loadFirstTiles();
        updateTiles();
    }
   
    @Override
    protected void simpleUpdate()
    {
        updateTiles();
    }
   
    private void loadFirstTiles()
    {
        world = new Tile[9];
        int index = 0;
       
        for(int i=0; i<3; i++)
        {
            for(int n=0; n<3; n++)
            {
                System.out.println("Loaded tile in slot " + index);
                world[index] = new Tile(i, n);
                rootNode.attachChild(world[index]);
                index += 1;
            }
        }
    }
   
    private void updateTiles()
    {
        tempx = (int) (cam.getLocation().x / TileManager.tileSize);
        tempy = (int) (cam.getLocation().z / TileManager.tileSize);
       
        if(tempx != x || tempy != y)
        {
            x = tempx;
            y = tempy;
           
            System.out.println("Tile change detected.");
            System.out.println("New tile: " + x + "," + y);
           
            //Make a list of all tiles that need to be rendered.
            mustLoad = TileManager.getRequiredTiles(x, y);
            notNeeded = new ArrayList();
           
            //Add tiles that we don't need to a list.
            for(int i=0; i<world.length; i++)
            {
                boolean kill = true;
                for(int n=0; n<mustLoad.size(); n++)
                {
                    if(world[i].getX() == mustLoad.get(n).getX() && world[i].getY() == mustLoad.get(n).getY())
                        kill = false;
                }
                if(kill)
                    notNeeded.add(world[i]);
            }
           
            //Remove already loaded tiles from the list.
            for(int i=0; i<mustLoad.size(); i++)
            {
                boolean kill = false;
                for(int n=0; n<world.length; n++)
                {
                    if(mustLoad.get(i).getX() == world[n].getX() && mustLoad.get(i).getY() == world[n].getY())
                    {
                        kill = true;
                    }
                }
                if(kill)
                {
                    mustLoad.remove(i);
                    i-=1;
                }
            }
           
            for(int i=0; i<mustLoad.size(); i++)
            {
                notNeeded.get(i).changeMap(mustLoad.get(i).getX(), mustLoad.get(i).getY());
            }
        }
    }
}

New: Texture Splatting



Here's the new one with texture splatting and all. This works quite well except in negative numbers.



How to implement it:



The same as before with these differences:


  • mapDirectory represents the locations of the heightmaps

  • mapFormat allows you to change the format of the maps

  • mapExtension allows you to change the extension of the maps

  • alphaDirectory and textureDirectory are directories where you'll store your alpha maps and textures, respectively.



In your texture directory you should store your different layers as resembled in the layerNames object in the TileManager class. Your alpha maps need to be stored by layer, so you'll have a folder for each layer and then an alpha map for each tile (named like an x,y coordinate). If a tile doesn't use a particular layer, don't put an alpha in for it.

Hope you find this useful. Still need to implement LOD.

Tile.java

/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package fyrestoneterrain;

import com.gibbon.mfkarpg.terrain.splatting.TerrainPass;
import com.gibbon.mfkarpg.terrain.splatting.TerrainPassNode;
import com.jme.bounding.BoundingBox;
import com.jme.image.Texture;
import com.jme.scene.Node;
import com.jme.scene.state.CullState;
import com.jme.scene.state.RenderState;
import com.jme.system.DisplaySystem;
import com.jme.util.TextureManager;
import com.jmex.terrain.TerrainBlock;
import com.jmex.terrain.util.RawHeightMap;
import java.util.ArrayList;

/**
 *
 * @author Tyler Trussell
 */
public class Tile extends Node
{
    private int x, y;
    private RawHeightMap myMap;
    private TerrainBlock myBlock;
    private TerrainPassNode tpass;
    private Texture base, lightmap;
    private CullState cs;
    private ArrayList<Texture> details, alphas;
    
    public int getX()
    {
        return x;
    }
    
    public int getY()
    {
        return y;
    }
    
    public Tile(int x, int y)
    {
        this.x = x;
        this.y = y;
        
        details = new ArrayList();
        alphas = new ArrayList();
        
        if(TestFyrestoneTerrain.class.getClassLoader().getResource(TileManager.mapDirectory + x + "," + y + TileManager.mapExtension) == null)
        {
            myMap = new RawHeightMap(TestFyrestoneTerrain.class.getClassLoader().getResource(TileManager.mapDirectory + TileManager.defaultMap + TileManager.mapExtension), TileManager.mapSize + 1, TileManager.mapFormat, true);
        }
        else this.myMap = new RawHeightMap(TestFyrestoneTerrain.class.getClassLoader().getResource(TileManager.mapDirectory + x + "," + y + TileManager.mapExtension), TileManager.mapSize + 1, TileManager.mapFormat, true);
        
        myBlock = new TerrainBlock(x + "," + y, myMap.getSize(), TileManager.scale, myMap.getHeightMap(), TileManager.getOriginFor(x, y), false);
        cs = DisplaySystem.getDisplaySystem().getRenderer().createCullState();
        cs.setCullMode(CullState.CS_BACK);
        cs.setEnabled(true);
        myBlock.setRenderState(cs);
        tpass = new TerrainPassNode();
        
        tpass.setRenderMode(TerrainPass.MODE_DEFAULT);
        tpass.setTileScale(150);
        tpass.attachChild(myBlock);
        
        base = TextureManager.loadTexture(TestFyrestoneTerrain.class.getClassLoader().getResource(TileManager.textureDirectory + "base.jpg"));
        
        tpass.addDetail(base, null);
        
        for(int i=0; i<TileManager.layers; i++)
        {
            if(TestFyrestoneTerrain.class.getClassLoader().getResource(TileManager.alphaDirectory + "layer" + (i+1) + "/" + x + "," + y + ".png") != null)
            {
                alphas.add(TextureManager.loadTexture(TestFyrestoneTerrain.class.getClassLoader().getResource(TileManager.alphaDirectory + "layer" + (i+1) + "/" + x + "," + y + ".png")));
                details.add(TextureManager.loadTexture(TestFyrestoneTerrain.class.getClassLoader().getResource(TileManager.textureDirectory + "layer" + (i+1) + ".jpg")));
            }
        }
        
        for(int i=0; i<details.size(); i++)
        {
            tpass.addDetail(details.get(i), alphas.get(i));
        }
        
        myBlock.updateRenderState();        
        attachChild(tpass);
    }
    
    public void changeMap(int x, int y)
    {
        this.x = x;
        this.y = y;
        
        if(TestFyrestoneTerrain.class.getClassLoader().getResource(TileManager.mapDirectory + x + "," + y + TileManager.mapExtension) == null)
        {
            myMap = new RawHeightMap(TestFyrestoneTerrain.class.getClassLoader().getResource(TileManager.mapDirectory + TileManager.defaultMap + TileManager.mapExtension), TileManager.mapSize + 1, TileManager.mapFormat, true);
        }
        else myMap = new RawHeightMap(TestFyrestoneTerrain.class.getClassLoader().getResource(TileManager.mapDirectory + x + "," + y + TileManager.mapExtension), TileManager.mapSize + 1, TileManager.mapFormat, true);
        
        tpass.clearDetails();
        alphas.clear();
        details.clear();
        
        myBlock.setHeightMap(myMap.getHeightMap());
        myBlock.setLocalTranslation(TileManager.getOriginFor(x, y));
        myBlock.updateFromHeightMap();
        
        base = TextureManager.loadTexture(TestFyrestoneTerrain.class.getClassLoader().getResource(TileManager.textureDirectory + "base.jpg"));
        
        tpass.addDetail(base, null);
        
        for(int i=0; i<TileManager.layers; i++)
        {
            if(TestFyrestoneTerrain.class.getClassLoader().getResource(TileManager.alphaDirectory + "layer" + (i+1) + "/" + x + "," + y + ".png") != null)
            {
                alphas.add(TextureManager.loadTexture(TestFyrestoneTerrain.class.getClassLoader().getResource(TileManager.alphaDirectory + "layer" + (i+1) + "/" + x + "," + y + ".png")));
                details.add(TextureManager.loadTexture(TestFyrestoneTerrain.class.getClassLoader().getResource(TileManager.textureDirectory + "layer" + (i+1) + ".jpg")));
            }
        }
        
        for(int i=0; i<details.size(); i++)
        {
            tpass.addDetail(details.get(i), alphas.get(i));
        }
        
        
        myBlock.updateRenderState();
        tpass.updateRenderState();
    }
}



TileManager.java


/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package fyrestoneterrain;

import com.jme.math.Vector3f;
import com.jme.scene.Node;
import com.jme.scene.state.AlphaState;
import com.jme.system.DisplaySystem;
import com.jmex.terrain.util.RawHeightMap;
import java.util.ArrayList;

/**
 *
 * @author Tyler Trussell
 */
public class TileManager
{
    public static final String mapDirectory = "maps/tiles/";
    public static final String defaultMap = "0";
    
    public static final int mapFormat = RawHeightMap.FORMAT_16BITLE;
    public static final String mapExtension = ".raw";
    
    public static final int mapSize = 64;
    public static final int mapScale = 10;
    public static final float mapHeightScale = .0007f;
    public static final int tileSize = mapSize * mapScale;
    
    public static final int layers = 4;
    public static final String alphaDirectory = "maps/layers/";
    public static final String textureDirectory = "maps/textures/";
    public static final String lightmapDirectory = "maps/layers/lightmaps/";
    public static final String[] layerNames = {"layer1", "layer2", "layer3", "layer4" };
    public static final int textureScale = 1;
    
    public static final Vector3f scale = new Vector3f(mapScale, mapHeightScale, mapScale);
    
    public static final AlphaState forTextures = DisplaySystem.getDisplaySystem().getRenderer().createAlphaState();
    public static final AlphaState forLights = DisplaySystem.getDisplaySystem().getRenderer().createAlphaState();
    
    private static ArrayList<TileCoordinate> mustLoad;
    private static ArrayList<Tile> notNeeded;
    private static Tile[] world;
    
    /*
     * <code>getOriginFor</code> Returns a Vector3f object representing the translation of a tile that owuld be located at a specific tile-coordinate.
     */
    public static Vector3f getOriginFor(int x, int y)
    {
        int x_, y_, z_;
        x_ = x * tileSize;
        y_ = 0;
        z_ = y * tileSize;
        
        return new Vector3f(x_, y_, z_);
    }
    
    /*
     * <code>loadFirstTiles</code>loads the tiles 0,0 through 3,3 to begin.
     */
    public static void loadFirstTiles(Node rootNode)
    {
        world = new Tile[9];
        int index = 0;
        
        for(int i=0; i<3; i++)
        {
            for(int n=0; n<3; n++)
            {
                System.out.println("Loaded tile in slot " + index);
                world[index] = new Tile(i, n);
                rootNode.attachChild(world[index]);
                index += 1;
            }
        }
    }
    
    /*
     *
     * <code>updateTiles</code>Moves tiles around to make sure you never fall off the earth.
     *
     * @param tpf tpf of your game
     * @param rootNode the node which your terrain is attached to
     * @param x the player's x position in tile-coordinates
     * @param y the player's y position in tile-coordinates
     *
     */
    public static void updateTiles(float tpf, Node rootNode, int x, int y)
    {
        //Make a list of all tiles that need to be rendered.
            mustLoad = getRequiredTiles(x, y);
            notNeeded = new ArrayList();
            
            //Add tiles that we don't need to a list.
            for(int i=0; i<world.length; i++)
            {
                boolean kill = true;
                for(int n=0; n<mustLoad.size(); n++)
                {
                    if(world[i].getX() == mustLoad.get(n).getX() && world[i].getY() == mustLoad.get(n).getY())
                        kill = false;
                }
                if(kill)
                    notNeeded.add(world[i]);
            }
            
            //Remove already loaded tiles from the list.
            for(int i=0; i<mustLoad.size(); i++)
            {
                boolean kill = false;
                for(int n=0; n<world.length; n++)
                {
                    if(mustLoad.get(i).getX() == world[n].getX() && mustLoad.get(i).getY() == world[n].getY())
                    {
                        kill = true;
                    }
                }
                if(kill)
                {
                    mustLoad.remove(i);
                    i-=1;
                }
            }
            
            for(int i=0; i<mustLoad.size(); i++)
            {
                notNeeded.get(i).changeMap(mustLoad.get(i).getX(), mustLoad.get(i).getY());
            }
            
            rootNode.updateGeometricState(tpf, true);
            rootNode.updateRenderState();
    }
    
    /*
     * <code>getRequiredTiles</code>Returns an ArrayList of Tiles that need to be loaded if a character is located at a specific tile-coordinate.
     */
    public static ArrayList<TileCoordinate> getRequiredTiles(int x, int y)
    {
        ArrayList<TileCoordinate> toReturn = new ArrayList();
        toReturn.add(new TileCoordinate(x - 1, y - 1));
        toReturn.add(new TileCoordinate(x - 1, y));
        toReturn.add(new TileCoordinate(x - 1, y + 1));
        toReturn.add(new TileCoordinate(x, y - 1));
        toReturn.add(new TileCoordinate(x, y));
        toReturn.add(new TileCoordinate(x, y + 1));
        toReturn.add(new TileCoordinate(x + 1, y - 1));
        toReturn.add(new TileCoordinate(x + 1, y));
        toReturn.add(new TileCoordinate(x + 1, y + 1));
        return toReturn;
    }
    
    public static AlphaState textureAlpha()
    {
        forTextures.setBlendEnabled(true);
        forTextures.setSrcFunction(AlphaState.SB_SRC_ALPHA);
        forTextures.setDstFunction(AlphaState.DB_ONE_MINUS_SRC_ALPHA);
        forTextures.setTestEnabled(true);
        forTextures.setTestFunction(AlphaState.TF_GREATER);
        forTextures.setEnabled(true);
        
        return forTextures;
    }
    
    public static AlphaState lightAlpha()
    {
        forTextures.setBlendEnabled(true);
        forTextures.setSrcFunction(AlphaState.SB_DST_COLOR);
        forTextures.setDstFunction(AlphaState.DB_SRC_COLOR);
        forTextures.setTestEnabled(true);
        forTextures.setTestFunction(AlphaState.TF_GREATER);
        forTextures.setEnabled(true);
        
        return forTextures;
    }
}



TestFyrestoneTerrain.java


/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package fyrestoneterrain;

import com.jme.app.SimpleGame;
import com.jme.light.DirectionalLight;
import com.jme.math.Vector3f;
import com.jme.renderer.ColorRGBA;
import java.util.ArrayList;

/**
 *
 * @author Tyler
 */
public class TestFyrestoneTerrain extends SimpleGame
{
    private int x, y, tempx, tempy;
    
    public static void main(String[] args)
    {
        TestFyrestoneTerrain app = new TestFyrestoneTerrain();
        app.setDialogBehaviour(ALWAYS_SHOW_PROPS_DIALOG);
        app.start();
    }

    @Override
    protected void simpleInitGame()
    {
        cam.setLocation(new Vector3f(10, 10, 10));
        DirectionalLight dl = new DirectionalLight();
        dl.setDirection(new Vector3f(.5f, -.5f, 0));
        dl.setAmbient(ColorRGBA.white);
        dl.setDiffuse(ColorRGBA.white);
        dl.setEnabled(true);
        lightState.attach(dl);
        x = (int) cam.getLocation().x / TileManager.tileSize;
        y = (int) cam.getLocation().z / TileManager.tileSize;
        
        TileManager.loadFirstTiles(rootNode);
        TileManager.updateTiles(tpf, rootNode, x, y);
    }
    
    @Override
    protected void simpleUpdate()
    {
        tempx = (int) cam.getLocation().x / TileManager.tileSize;
        tempy = (int) cam.getLocation().z / TileManager.tileSize;
        
        if(x != tempx || y != tempy)
        {
            x = tempx;
            y = tempy;
            TileManager.updateTiles(tpf, rootNode, x, y);
        }
    }
}



And here's the modified version of TerrainPassNode you'll need


/*
 * Copyright (c) 2007, MFKARPG 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 the Gibbon Entertainment
 * 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 'Gibbon Entertainment' "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 'Gibbon Entertainment' 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 com.gibbon.mfkarpg.terrain.splatting;

import java.net.URL;
import java.util.ArrayList;
import java.util.List;

import com.jme.image.Texture;
import com.jme.renderer.Renderer;
import com.jme.scene.Geometry;
import com.jme.scene.Node;
import com.jme.scene.PassNode;
import com.jme.scene.PassNodeState;
import com.jme.scene.Spatial;
import com.jme.scene.state.AlphaState;
import com.jme.scene.state.FogState;
import com.jme.scene.state.FragmentProgramState;
import com.jme.scene.state.GLSLShaderObjectsState;
import com.jme.scene.state.TextureState;
import com.jme.util.TextureManager;

/**
 *
 * @author blackbluegl
 * @created 25.04.2008
 *
 */
public class TerrainPassNode extends PassNode {

    /**
     * serialVersionUID
     */
    private static final long serialVersionUID = 1L;

    public static final int MODE_BEST = -1, MODE_DEFAULT = -1, MODE_FIXED_FUNC = 0, MODE_FRAG_PROGRAM = 1,
            MODE_GLSL = 2;

    protected SplatEnv env = new SplatEnv();

    protected BaseLayer base = null;

    protected List<AlphaDetailLayer> detail = new ArrayList<AlphaDetailLayer>();

    protected LightLayer light = null;

    protected boolean lightmap = false;

    protected FogLayer fog = null;

    protected FogState fogstate = null;

    protected int renderMethod = -1;

    protected int fallback = MODE_DEFAULT;

    protected boolean generated = false;

    /**
     * Sets the rendering mode <br>
     * Possible values:<br>
     * MODE_BEST - Uses the best supported rendering mode (GLSL, then frag program, then fixed func).<br>
     * MODE_DEFAULT - Uses the default rendering mode. This is the same as MODE_BEST MODE_FIXED_FUNC - Uses fixed
     * function rendering mode. Requires more passes than other modes, but is the most compatible.<br>
     * MODE_FRAG_PROGRAM - Uses ARB fragment program.<br>
     * MODE_GLSL - Uses GLSL shader.<br>
     */
    public void setRenderMode(final int mode) {
        if (mode < MODE_BEST || mode > MODE_GLSL) {
            throw new IllegalArgumentException("Invalid mode specified");
        }

        fallback = mode;
    }

    /**
     * Specify how many detailmaps to tile over the whole terrain
     */
    public void setTileScale(final int scale) {
        env.setTileScale(scale);
    }
   
    public void clearDetails()
    {
        detail.clear();
        env = new SplatEnv();
        generated = false;
    }

    /**
     * Adds a detailmap, using the specified alphamap for blending.
     */
    public void addDetail(final Texture detailmap, final Texture alphamap, final int tileScale) {
        if (alphamap == null) {
            base = new BaseLayer(detailmap);
        }
        else {
            AlphaDetailLayer adl = new AlphaDetailLayer(detailmap, alphamap);
            if (tileScale != -1) {
                adl.setScaleOverride(tileScale);
            }

            detail.add(adl);
        }
    }

    public void addDetail(final Texture detailmap, final Texture alphamap) {
        addDetail(detailmap, alphamap, -1);
    }

    /**
     * Same as addTile(Texture,Texture) but uses URLs instead
     */
    public void addDetail(final URL detailmap, final URL alphamap) {
        addDetail(TextureManager.loadTexture(detailmap, Texture.MM_LINEAR_LINEAR, Texture.FM_LINEAR, 0f, false),
                alphamap == null ? null : TextureManager.loadTexture(alphamap, Texture.MM_LINEAR_LINEAR,
                        Texture.FM_LINEAR, 0f, false), -1);
    }

    public void setLightmap(final Texture lightmap, final float modulateScale) {
        light = new LightLayer(lightmap, modulateScale);
        this.lightmap = true;
    }

    public void setLightmap(final URL lightmap, final float modulateScale) {
        setLightmap(TextureManager.loadTexture(lightmap, Texture.MM_LINEAR_LINEAR, Texture.FM_LINEAR, 0f, false),
                modulateScale);
    }

    public void setDynamicLighting(final float modulateScale) {
        light = new LightLayer(modulateScale);
        lightmap = false;
    }

    public void setFog(final FogState fs) {
        fog = new FogLayer();
        fogstate = fs;
    }

    protected void copySpatialCoords(final Spatial spat, final int batch, final int srcUnit, final int targetUnit,
            final int scale) {
        if (spat instanceof Geometry) {
            ((Geometry) spat).copyTextureCoords(batch, srcUnit, targetUnit, scale);
        }
        else if (spat instanceof Node) {
            for (Spatial s : ((Node) spat).getChildren()) {
                copySpatialCoords(s, batch, srcUnit, targetUnit, scale);
            }
        }
    }

    protected void copyPassCoords(final int batch, final int srcUnit, final int targetUnit, final int scale) {
        for (Spatial spat : getChildren()) {
            copySpatialCoords(spat, batch, srcUnit, targetUnit, scale);
        }
    }

    protected void generate(final Renderer r) {
        GLSLShaderObjectsState glsl = r.createGLSLShaderObjectsState();
        FragmentProgramState fp = r.createFragmentProgramState();
        PassNodeState splatState = new PassNodeState();

        if (glsl.isSupported() && (fallback == MODE_BEST || fallback == MODE_GLSL)) {
            renderMethod = 2;
        }
        else if (fp.isSupported() && (fallback == MODE_BEST || fallback == MODE_FRAG_PROGRAM)) {
            renderMethod = 1;
        }
        else if (fallback == MODE_BEST || fallback == MODE_FIXED_FUNC) {
            renderMethod = 0;
        }

        boolean lightAdded = false;
        if (!lightmap && light != null && renderMethod == 0) {
            env.addLayer(light);
            lightAdded = true;
        }

        env.addLayer(base);
        for (int i = 0; i < detail.size(); i++) {
            env.addLayer(detail.get(i));
        }
        if (!lightAdded && light != null) {
            env.addLayer(light);
        }
        if (fog != null) {
            env.addLayer(fog);
            if (fogstate != null) {
                splatState.setPassState(fogstate);
            }
        }

        if (renderMethod == MODE_GLSL) {
            glsl = env.createGLSLShader(r);
            TextureState[] ts = env.createShaderPasses(r);

            copyPassCoords(0, 0, 1, env.getTileScale());

            splatState.setPassState(ts[0]);
            splatState.setPassState(glsl);

            this.addPass(splatState);
        }
        else if (renderMethod == MODE_FRAG_PROGRAM) {
            copyPassCoords(0, 0, 1, env.getTileScale());

            TextureState[] ts = env.createShaderPasses(r);
            fp = env.createARBShader(r);

            splatState.setPassState(ts[0]);
            splatState.setPassState(fp);

            this.addPass(splatState);
        }
        else {
            for (int i = 0; i < TextureState.getNumberOfFixedUnits(); i++) {
                copyPassCoords(0, 0, i, 1);
            }

            TextureState[] ts = env.createFixedFuncPasses(r);
            AlphaState[] as = env.getFixedFuncAlphaStates();

            for (int i = 0; i < ts.length; i++) {
                if (fogstate != null) {
                    splatState.setPassState(fogstate);
                }
                splatState.setPassState(ts[i]);

                if (as[i] != null) {
                    splatState.setPassState(as[i]);
                }

                this.addPass(splatState);

                splatState = new PassNodeState();

            }
        }

    }

    @Override
    public void draw(final Renderer r) {
        if (!generated) {
            generate(r);
            generated = true;
        }

        super.draw(r);
    }
}

Can you post a zip of the NetBeans project?  :slight_smile:

I'm terribly sorry–I usually always do that! I'll be sure to do that first thing tomorrow!

???

jaguar said:

???

I assume you do that because you haven't seen the NetBeans project... My development computer went corrupt so I have been moving everything over to a backup hard-drive, but work/school have been getting in the way... I need to reinstall Windows onto the development computer, but it won't detect my  SATA drive so I have to make a slipstreamed disc to install it. I'll get this up in just a few days.

Has anybody sorted the negative numbers bug on this? I've had a look at it but am still trying to work out how it all works, and why the tiles aren't updating when entering zero y row…

No I haven't ever bothered to solve it. All it would take is an if-statement regarding negative numbers.

@Override
    protected void simpleUpdate()
    {
if(cam.getLocation().x > 0)
        tempx = (int) cam.getLocation().x / TileManager.tileSize;
else
        tempx =  ((int) cam.getLocation().x - 1) / TileManager.tileSize;
       
if(cam.getLocation().z > 0)
        tempy = (int) cam.getLocation().z / TileManager.tileSize;
else
        tempy = ((int) cam.getLocation().z - 1) / TileManager.tileSize;
       
        if(x != tempx || y != tempy)
        {
            x = tempx;
            y = tempy;
            TileManager.updateTiles(tpf, rootNode, x, y);
        }
    }



Try that...

I see your reasoning, but unfortunately that code doesn't work for me, also it really slows down the render update…



I'll have a play around with it all to see if I can come up with a solution.

You're doing something else that's slowing down the render method. The changes I made would not affect that.

@Override
        protected void simpleUpdate()
        {
            if(cam.getLocation().x <= 0)
                tempx =  ((int) cam.getLocation().x  / TileManager.tileSize)- 1;
            else
                tempx = (int) cam.getLocation().x / TileManager.tileSize;

            if(cam.getLocation().z <= 0)
                tempy = ((int) cam.getLocation().z / TileManager.tileSize)- 1;
            else
                tempy = (int) cam.getLocation().z / TileManager.tileSize;

            if(x != tempx || y != tempy)
            {
                x = tempx;
                y = tempy;
                TileManager.updateTiles(tpf, rootNode, x, y);
        }



Just the subtractions were in the wrong place, amended code above :)

Now it's truly unlimited terrain, cheers!

And sorry about the render method comment, one of my memory sticks was in the middle of dying when testing that code, all is good now, except the expense of a new memory stick :frowning:

Oh whoops. I meant to subtract the position by the TileSize. That would've worked as well. Thanks for the fix, though!

Phenominal code trussel .Can i ask a quick question do i need multiple heightmaps to properly use it… or can i simply load one i was planning on creating all of my maps in blender and save the heitmaps of them and load in jME i was originally going to use terrain page for fps sake…I was also planning on using a single heitmap over multiple times and simply re-texturing them and use picking to place and destroy objects…would it be better to use terrain page or unlimited terrain(ask because i'm not exactly sure if it fits what i'm looking for better fps…)

This system is designed to hold a bunch of little heightmaps… If you cut your big heightmap up into a bunch of smaller ones this would be what you're looking for. Loading one huge heightmap usually causes memory problems and sloooowwwwws down your game.

Trussell said:

This system is designed to hold a bunch of little heightmaps.. If you cut your big heightmap up into a bunch of smaller ones this would be what you're looking for. Loading one huge heightmap usually causes memory problems and sloooowwwwws down your game.


Okay so even if i was to use terrain page instead thier would still be an issue.... Okay thx for the quick reply final Q I can use method getY() from TileCoordinate, to determine my height in a given area of a map that i move my character to correct or is it better I use the


 float characterMinHeight = tb.getHeight(player
                .getLocalTranslation())+((BoundingBox)player.getWorldBound()).yExtent;
        if (!Float.isInfinite(characterMinHeight) && !Float.isNaN(characterMinHeight)) {
            player.getLocalTranslation().y = characterMinHeight;
        }

The getY() method is used to find your coordinates in terms of Tile objects, not in terms of world coordinate system. You'll need to use the second blurb you posted.

should have seen this post earlier, would have saved me from coding the same stuff :slight_smile:

you can also change the code to load tiles on demand simulating a mmo world (just a bit of changes to tilemanager and naming of tiles along their coordinates)