« Previous: Starter Tutorial 8 - Hello Intersection
Next: Starter Tutorial 10 - Hello Animation (1) »
(Tip: Up-to-date source files for the tutorials are always in the repository)
This program introduces jME’s basic terrain utility class. You will learn how to use TerrainBlock, ImageBasedHeightMap, MidPointHeightMap, and ProceduralTextureGenerator. All of this will allow you to create nice looking terrain with little effort.
import java.net.URL; import javax.swing.ImageIcon; import com.jme.app.SimpleGame; import com.jme.bounding.BoundingBox; import com.jme.image.Texture; import com.jme.math.Vector3f; import com.jme.scene.state.TextureState; import com.jme.util.TextureManager; import com.jmex.terrain.TerrainBlock; import com.jmex.terrain.util.ImageBasedHeightMap; import com.jmex.terrain.util.MidPointHeightMap; import com.jmex.terrain.util.ProceduralTextureGenerator; /** * Started Date: Aug 19, 2004<br><br> * * This program introduces jME's terrain utility classes and how they are used. It * goes over ProceduralTextureGenerator, ImageBasedHeightMap, MidPointHeightMap, and * TerrainBlock. * * @author Jack Lindamood */ public class HelloTerrain extends SimpleGame { public static void main(String[] args) { HelloTerrain app = new HelloTerrain(); app.setConfigShowMode(ConfigShowMode.AlwaysShow); app.start(); } protected void simpleInitGame() { // First a hand made terrain homeGrownHeightMap(); // Next an automatically generated terrain with a texture generatedHeightMap(); // Finally a terrain loaded from a greyscale image with fancy textures on it. complexTerrain(); } private void homeGrownHeightMap() { // The map for our terrain. Each value is a height on the terrain float[] map=new float[]{ 1,2,3,4, 2,1,2,3, 3,2,1,2, 4,3,2,1 }; // Create a terrain block. Our integer height values will scale on the map 2x larger x, // and 2x larger z. Our map's origin will be the regular origin. TerrainBlock tb=new TerrainBlock("block",4, new Vector3f(2,1,2), map, new Vector3f(0,0,0)); // Give the terrain a bounding box. tb.setModelBound(new BoundingBox()); tb.updateModelBound(); // Attach the terrain TriMesh to our rootNode rootNode.attachChild(tb); } private void generatedHeightMap() { // This will be the texture for the terrain. URL grass=HelloTerrain.class.getClassLoader().getResource( "jmetest/data/texture/grassb.png"); // Use the helper class to create a terrain for us. The terrain will be 64x64 MidPointHeightMap mph=new MidPointHeightMap(64,1.5f); // Create a terrain block from the created terrain map. TerrainBlock tb=new TerrainBlock("midpoint block",mph.getSize(), new Vector3f(1,.11f,1), mph.getHeightMap(), new Vector3f(0,-25,0)); // Add the texture TextureState ts = display.getRenderer().createTextureState(); ts.setTexture(TextureManager.loadTexture(grass, Texture.MinificationFilter.BilinearNearestMipMap, Texture.MagnificationFilter.Bilinear)); tb.setRenderState(ts); // Give the terrain a bounding box. tb.setModelBound(new BoundingBox()); tb.updateModelBound(); // Attach the terrain TriMesh to rootNode rootNode.attachChild(tb); } private void complexTerrain() { // This grayscale image will be our terrain URL grayScale=HelloTerrain.class.getClassLoader().getResource("jmetest/data/texture/bubble.jpg"); // These will be the textures of our terrain. URL waterImage=HelloTerrain.class.getClassLoader().getResource("jmetest/data/texture/water.png"); URL dirtImage=HelloTerrain.class.getClassLoader().getResource("jmetest/data/texture/dirt.jpg"); URL highest=HelloTerrain.class.getClassLoader().getResource("jmetest/data/texture/highest.jpg"); // Create an image height map based on the gray scale of our image. ImageBasedHeightMap ib=new ImageBasedHeightMap( new ImageIcon(grayScale).getImage() ); // Create a terrain block from the image's grey scale TerrainBlock tb=new TerrainBlock("image icon",ib.getSize(), new Vector3f(.5f,.05f,.5f),ib.getHeightMap(), new Vector3f(0,0,0)); // Create an object to generate textured terrain from the image based height map. ProceduralTextureGenerator pg=new ProceduralTextureGenerator(ib); // Look like water from height 0-60 with the strongest "water look" at 30 pg.addTexture(new ImageIcon(waterImage),0,30,60); // Look like dirt from height 40-120 with the strongest "dirt look" at 80 pg.addTexture(new ImageIcon(dirtImage),40,80,120); // Look like highest (pure white) from height 110-256 with the strongest "white look" at 130 pg.addTexture(new ImageIcon(highest),110,130,256); // Tell pg to create a texture from the ImageIcon's it has recieved. pg.createTexture(256); TextureState ts=display.getRenderer().createTextureState(); // Load the texture and assign it. ts.setTexture( TextureManager.loadTexture( pg.getImageIcon().getImage(), Texture.MinificationFilter.Trilinear, Texture.MagnificationFilter.Bilinear, true ) ); tb.setRenderState(ts); // Give the terrain a bounding box tb.setModelBound(new BoundingBox()); tb.updateModelBound(); // Move the terrain in front of the camera tb.setLocalTranslation(new Vector3f(0,0,-50)); // Attach the terrain to our rootNode. rootNode.attachChild(tb); } }
This program shows how jME’s terrain package works. All Terrain comes from the base class TerrainBlock, which is a TriMesh. The TerrainBlock takes an array of integer values and creates a terrain from it. In this program I create 3 different types of terrain. The first is done by hand, the second uses a utility class to generate a terrain for us, and the third creates a terrain from a file. We’ll start with the first.
// The map for our terrain. Each value is a height on the terrain int[] map = { 1, 2, 3, 4, 2, 1, 2, 3, 3, 2, 1, 2, 4, 3, 2, 1 }; // Create a terrain block. Our integer height values will // scale on the map 2x larger x, // and 2x larger z. Our map's origin will be the regular // origin, and it won't create an // AreaClodMesh from it. TerrainBlock tb=new TerrainBlock("block",4, new Vector3f(2,1,2), map, new Vector3f(0,0,0));
Look at the map array. It is just an array of integer values. Each integer represents a height. This map will be low down the diagonal, and slope up just like the integer values do. This is exactly what it looks like when the program is run.
The terrain is a square. If you took the map [] and created a height for each number, it would look like this. This is how terrain is generated with TerrainBlock. The first parameter of TerrainBlock is the block’s name (because all spatial objects must have a name). The next is the size of the block (4x4). The 3rd is a scale value for the terrain. We pass in integer terrain values, but what if we want float heights. The scale value for the terrain creates the terrain stretched twice as large along the x and z axis. Using this, you can manipulate the appearance and size of your terrain. The last parameter is the origin for the terrain (0, 0, 0 for this example).
Next, look at a generated terrain.
// Use the helper class to create a terrain for us. The // terrain will be 64x64 MidPointHeightMap mph = new MidPointHeightMap(64, 1.5f); // Create a terrain block from the created terrain map. TerrainBlock tb = new TerrainBlock("midpoint block", mph.getSize(), new Vector3f(1, .11f, 1), mph.getHeightMap(), new Vector3f(0, -25, 0), false);
Here we use MidPointHeightMap to generate a terrain for us. The terrain will be 64 by 64 and we give it a roughness value of 1.5. The roughness value changes how smooth the terrain becomes. You can play with it for the desired result. The only new thing about using TerrainBlock is we get our size and height map array from the midpoint height map.
Finally we create a terrain from a gray scale image. First, take a look at the gray scale image we use, and then the terrain we create. Both views are from above.
You’ll notice the parts of my terrain that are the farthest up are the parts of the gray scale image that are the whitest. The class ImageBasedHeightMap creates a height map from an image where the tallest parts are white and the smallest parts are black.
// Create an image height map based on the gray scale of our image. ImageBasedHeightMap ib=new ImageBasedHeightMap( new ImageIcon(grayScale).getImage());
We have to pass ImageBasedHeightMap an Image object of the grayscale so it can work. To do that, we create an ImageIcon from the image’s URL and get an Image from that.
After I have my height map and create a TerrainBlock, the next new thing is to create that "strange" texture I use. First, take a look at the three textures I’m mashing together.
The first is water.png, the second is dirt.jpg and the last is highest.jpg. Notice that highest is simply white. Now take another look at the complex terrain from the side.
Notice it’s a blending that has the water image at the bottom, the dirt image in the middle, and the white image at the top. The next question is “How do I do that?” Well, to solve that we use ProceduralTextureGenerator. It takes a height map and some ImageIcons and produces a blend at the heights you specify for each texture.
// Create an object to generate textured terrain from the // image based height map. ProceduralTextureGenerator pg=new ProceduralTextureGenerator(ib); // Look like water from height 0-60 with the strongest // "water look" at 30 pg.addTexture(new ImageIcon(waterImage),0,30,60); // Look like dirt from height 40-120 with the strongest // "dirt look" at 80 pg.addTexture(new ImageIcon(dirtImage),40,80,120); // Look like highest (pure white) from height 110-256 // with the strongest "white look" at 130 pg.addTexture(new ImageIcon(highest),110,130,256);
When I create pg, I pass it the height map (The actual height map object not an int []). It uses this height map to create the texture. Next I pass three image icons for the 3 parts. Let’s look at one of those. The other two work in similar ways.
// Tell pg to create a texture from the ImageIcon's it has recieved. pg.createTexture(256);
You’ll notice when I create the Texture object to use with the TextureState, I get the texture from pg.
TextureState ts = display.getRenderer().createTextureState(); // Load the texture and assign it. ts.setTexture( TextureManager.loadTexture( pg.getImageIcon().getImage(), Texture.MinificationFilter.Trilinear, Texture.MagnificationFilter.Bilinear, true ) );
That’s all there is too it. A Terrain class which is pretty important that I didn’t touch on is TerrainPage. It creates a terrain and splits it into different BoundingBoxes so that the part of the Terrain you’re not viewing can be culled.
Here’s the obligatory scene graph:
| rootNode | ||
| “block” tb | “midpoint block” tb | “image icon” tb |
To learn more details about Terrains (and especially TerrainPages), continue with the topic Terrains, Heightmaps, Texturing in the advanced tutorial section.