Terrains, Heightmaps, and Terrain Textures

A Terrain (from the com.jmex.terrain package) is a trimesh used as a natural looking floor in outdoor scenes of 3-D games. The trivial solution is to create a static landscape in a 3-D mesh editor and load it just like any other model. Compared to the trivial solution, a Terrain renders faster, but it also has a few limitations.

In this article you learn how to create and load advanced Terrains using the com.jmex.terrain.TerrainPage package. We also cover various types of heightmaps, and advanced ways of how to texture terrains.

These are the general steps you must perform:

  1. create a heightmap (from bitmap or using a randomizer)
  2. create a terrain (from heightmap, size, blocksize)
  3. create texture(s) (from texture images)
  4. set texture of terrain
  5. attach terrain to root node

Representing Terrains With Heightmaps

A 3-dimensional Terrain is generated from a 2-dimensional heightmap. A heightmap is a list containing y values (heights) for coordinates on the x/z plane. A heightmap is always square.

You do not need to specify every pixel's height in the heightmap, a few thousand points giving the rough outline are enough. JME interpolates the rest of the height values when the terrain is rendered. This is an efficient way of describing a continous hilly landscape.

The only limitation of heightmaps is that there can be no gaps (holes or caves) in the terrain: No holes because the whole surface is filled in by the interpolation; and no caves because a heightmap only specifies one height value per floor coordinate; a cave would require additional y values for its floor and ceiling.

No caves: one limitation of terrains from 2D heightmaps

HeightMap: From Array or Bitmap

There are two ways to represent heightmap data:

  1. a float array,
  2. a grayscale bitmap graphic.

HeightMap From Float Array

In jME, a heightmap is a float array containing height values between 0f and 255f. This simple heightmap uses 25 height values to roughly describe a terrain shape. Low values (e.g. 0 or 50) are valeys, high values (e.g. 200, 255) are hills.

float[] map = new float[]
  {
    150, 255, 200, 255, 200,
    150, 200, 250, 255, 200,
    100, 100, 150, 200, 100,
    100, 100, 000, 050, 150,
    000, 050, 000, 000, 000
  };
Vector3f scale = new Vector3f(10f, 0.1f, 10f); 
TerrainBlock tb = new TerrainBlock("Simple terrain", 5, scale, map, new Vector3f(0, 0, 0));

The following screenshot shows what such a simple terrain looks like.

Creating a Terrain from a float array-based heightmap

More complex and smoother terrains (as you would use in a game) typically have sizes such as 256 or 512 and contain ten thousands of height values. But the concept is the same.

HeightMap: From Grayscale Bitmap

A grayscale bitmap is another way of representing heightmap data. A grayscale image has lighter or darker pixels at each coordinate, while a heightmap has hills or valeys at each coordinate. Since both are just lists of floats between 0 and 255, we can use grayscale values to describe a terrain's shape. The advantage is that grayscales images are visually more intuitive than raw numbers for the designer who is laying out the landscape.

  1. In your jme application, create a source folder named media.
  2. Open a graphic editor (Paint, Gimp, GraphicConverter, PhotoShop, …). Create a new grayscale bitmap image with a size of 129×129.
  3. Choose a small blurry brush and paint lighter grays for mountains, and darker grays for valleys. See the example (framed red) in the picture below.
  4. Save the file as media/terrain-129.jpg. It's a good practice to sort textures in media folders by type and size, or (if you use only few textures) at least choose explicit file names.

From such a bitmap with a size of n^2+1 (e.g. 33, 65, 129, 257, or 513) pixels, JME can create a TerrainPage of size n^2 (e.g. 32, 64, 128, 256, or 512) world units. The following screenshot shows what a terrain based on such a heightmap will look like after it has been rendered with textures. Compare the snow-covered hills with the white areas of the heightmap; compare the brownish valleys with the dark areas of the heightmap.

Creating a terrain from an image-based heightmap

This is the kind of terrain you'd use in a game. Let's have a closer look at the code that we use to get this result in the next section.

Generating HeightMaps in jME

You already learned that you can create a heightmap object from a float array or from a grayscale bitmap. JME supports either. You can either paint the bitmap by hand in a graphic editor, or jME can generate a bitmap using a randomizer.

The following jME classes create a heightmap from a grayscale file, either a normal bitmap (jpg, png, etc) or a raw file:

/* com.jmex.terrain.util.ImageBasedHeightMap -- good for custom landscapes */
String heightmapPath = "media/terrain-129.jpg"; // supply path to grayscale bitmap
URL heightmapImg = TerrainTest.class.getClassLoader().getResource(heightmapPath);
AbstractHeightMap map = new ImageBasedHeightMap(new ImageIcon(heightmapImg).getImage());

or

/* com.jmex.terrain.util.RawHeightMap -- good for custom landscapes */
String path = "media/heights.raw"; // supply path to .raw file
URL heightmapImg = TerrainTest.class.getClassLoader().getResource(heightmapPath);
AbstractHeightMap heightMap = new RawHeightMap(heightmapImg, 129, RawHeightMap.FORMAT_16BITLE, false);

Alternatively, you can use one the following jME classes to generate a random heightmap. In these examples, I'm creating heightmaps of size 129/128:

/* com.jmex.terrain.util.MidPointHeightMap -- good for islands.  */
AbstractHeightMap map = new MidPointHeightMap(128, 1.5f); // must be 2^n, TerrainBlock only!

or

/* com.jmex.terrain.util.FaultFractalHeightMap -- good for planes. */
AbstractHeightMap map = new FaultFractalHeightMap(129, 10, 5, 20, 0.4f); 

or

/* com.jmex.terrain.util.HillHeightMap -- good for hilly landscapes. */
AbstractHeightMap map = new HillHeightMap(129, 10, 5f, 20f, (byte)10); 

Calling map.getHeightMap() on any of these heightmap objects returns a float[] array that can be input into a terrain constructor. We will use this function in the next section to create a terrain.

Generating Terrains in jME

The com.jmex.terrain package contains the TerrainBlock and TerrainPage classes. You can use a TerrainBlock for a quick and simple “brute force” terrain, but a TerrainPage is the more performant, recommended option.

A TerrainPage is a quad tree of terrain blocks. Each TerrainPage has four child nodes, either four TerrainPages or four TerrainBlocks. JME's organization of the terrain into a quad tree allows for very fast culling of the terrain.

To create a TerrainPage of size n^2 you must provide:

  1. map – A heightmap of size 2^n+1 (!). (We have just created terrain-129.jpg above.)
  2. size – The width of the heightmap in pixels. (In our example, this is 129.)
  3. stretch and scale – These factors makes the terrain appear more natural, they are explained further down.
  4. The blocksize of subpages – Optimal values for the block size depend on your application: For now, let's use 33.
static final float scale = 4f; // a constant scale factor 
int      size    = map.getSize(); // in our example it is 129.
Vector3f stretch = new Vector3f( 256f*scale / size-1, 1f, 256f*scale / size-1 ); 
// TerrainPage constructor: name, block size, heightmap size, scale factor, heightmap.
TerrainPage terrain = new TerrainPage( "My TerrainPage", 33, size, stretch, map.getHeightMap());
terrain.setModelBound(new BoundingBox());
terrain.updateModelBound();

You see that the Terrain constructor requires a (float[] array) heightmap, here you use the afore-mentioned map.getHeightMap() method. For the required size value, you use the map.getSize() method.

Common Questions and Answers

What values to use in the constructor for what effect? Quality vs Detail?

TODO/work in progress…

MidPointHeightMap(128, 1.5f);FaultFractalHeightMap(128, 10, 5, 20, 0.4f);HillHeightMap(128, 10, 5f, 20f, (byte)10);

How wide will an unscaled terrain appear in the rendered world?

The width of a terrain depends directly on the heightmap size. For instance, a heightmap of size 256×256 pixels results in a terrain that is 256×256 world units wide. Note that the TerrainPage constructor requires a heightmap of the size 2^n+1 (129,257,513, etc) and results in a terrain of width n (128, 256, 512, etc).

How tall will an unscaled terrain appear in the rendered world?

The heightmap's values are between 0.0f (black) and 255.0f (white). These height values correspond directly to world units. An unscaled terrain will be rendered between 0 (lows) and 255 (highs) world units high. This means, a gray heightmap pixel with a value of 128f will always end up at a height of 128 world units, etc.

How to Vertically center a Terrain?

To vertically align a terrain around the y=0 plane, you must know the highest and lowest values (max, min) in its heightmap. terrain.setLocalTranslation( t.x , t.y-(0.5*min-0.5*max), t.z ); TODO: impact on texturing?

How to Horizontally center a terrain?

A TerrainPage is automatically horizontally centered around the y-axis. A TerrainBlock is not centered, but its left back corner is at (0,0,0). To horizontally center a TerrainBlock around the y axis, you must know the size of the heightmap: s = map.getSize(); terrain.setLocalTranslation( t.x-0.5*s, t.y, t.z-0.5*s );

Why do I need to Scale my Terrains?

The highest spot of the terrain (white) has always a height of 255 world units, and the lowest (black) is at 0 world units. Assuming you use a heightmap of, for example, 128 pixels, you are working with a 128*128*256 block. Such an unscaled terrain has extremely steep slopes, which is usually not what you want for a game: A natural landscape is usually wider than it is tall.

Theoretically, you could base the terrain on a heightmap that is 1024 or even 1536 pixels wide – but that much detail might unnecessarily slow down performance. (ToDO benchmarks) Instead, just stretch the terrain until the slopes look natural. For a terrain with size n, use a scale factor such as new Vector3f(256f*scale / n-1, 1f, 256f*scale / n-1). As a rough guideline I suggest 6.0f > scale > 4.0f.

TODO: Another way to achieve a more natural scale is to compress the heightmap's height: heightMap.setHeightScale(0.001f) – effect?

How do I set a TerrainPage's “blocksize”?

You don't see it, but internally, a TerrainPage is split into subpages (and eventually into TerrainBlocks) of a certain blocksize in world units. Bigger blocks means less subpages are generated, smaller blocks means more subpages are generated. JME's culling of subpages can bring performance improvements during the game, but on the other hand, generating too many subpages increases the game's startup time. You need to find the optimal value for your game by trial and error.

Tip: Start with blocksize 33, and after your game is pretty solid, try other numbers and see whether performance improves or not. The blocksize can be any integer number, but what makes sense is something between 20-100.

What is “com.jme.system.JmeException, Terrain page sizes may only be (2^N + 1)” ?

A Terrainpage's heightmap's size must be 2^n+1 (e.g. 513×513)! Whereas a TerrainBlock's heightmap must be 2^n (512×512)! If you get this error, you must change the heightmap size (if it's an image based heightmap, open it in a graphic editor).

How do I get around the “no caves, no holes” limitation?

A 2-D heightmap-based terrain may be efficient, but it just cannot describe 3-D features such as caves, ledges, or holes. To render a landscape with caves etc, you have to create a static custom trimesh and load it like any other model. Be aware that big landscape models can be quite slow, so prefer heightmap based TerrainPages where ever possible. For simple, small caves one solution is to place an appropriately textured cave model into a terrain valley, and just make it look as if the landscape was one piece.

A) Static model: Completely custom landscape (including caves and holes) but slower. – B) TerrainBlock: Okay for small simple landscapes without caves or holes. – C) TerrainPage: Optimized for large landscapes without caves or holes.

Where is a Terrain Placed Vertically By Default?

Where is a terrain vertically placed if it does not contain any significant low points – in mid air, or always at the “bottom” (touching the x/z plane)? If you consider the x/z plane the floor, then it will be “in mid air”. A grayscale of value y will always be y world units above the x/z plane (where 0<y<256).

If you use a y scale value other than 1.0f on the terrain, the terrain can be higher than 255 or lower than 0, respectively. TODO how does this affect texturing?

Part 2: Texturing Terrains

TODO/work in progress…!

In the simplest case, you assign one texture to one node all over. But there are also smarter types of textures: They allow you to combine more than one texture on one object, and they can be made look good from near as well as from afar. A texture generated from several image files is called a procedural texture. Terrains are a good example where you would use procedural textures.

For the following examples, let's use a heightmap and terrain like the one we created above:

// Create heightmap
String heightmapPath = "media/terrain-257.jpg"; 
URL heightmapImg = TerrainTest.class.getClassLoader().getResource(heightmapPath);
ImageBasedHeightMap map = new ImageBasedHeightMap(new ImageIcon(heightmapImg).getImage()); 

// From the heightmap, we create the terrain.
Vector3f stretch = new Vector3f(256f*scale / size-1, 1f, 256f*scale / size-1); // terrain scaling factor
TerrainPage terrain = new TerrainPage( "landscape", 33, map.getSize(), stretch, map.getHeightMap());

Procedural Heightmap Textures

You can have jME generate a procedural texture, by combining different textures for different terrain heights, respectively. This way, a landscape can have several horizontal “climate zones” which looks more natural than “all grass” or “all rock”. Here a few examples:

  • Green grass in the valleys, snow-covered mountain tops, and rocks between the two. Add trees to the lower region, and you have a nice mountainous wilderness. Or:
  • Rocks in the valleys, grassy hill tops, and sand between the two. Add a WaterQuad, and you can use the “rocky valleys” as underwater regions, and the “sand” as beach.

We are using the map and terrain that we created above.

// From the heightmap, we create a procedural texture generator
ProceduralTextureGenerator pg = new ProceduralTextureGenerator(map);

// Load various landscape textures
String lowestPath = "media/grassy.jpg"; 
URL lowestImg = TerrainTest.class.getClassLoader().getResource(lowestPath);
String middlePath = "media/rocky.jpg"; 
URL middleImg = TerrainTest.class.getClassLoader().getResource(middlePath);
String highestPath = "media/snowy.jpg"; 
URL highestImg = TerrainTest.class.getClassLoader().getResource(highestPath);

// Add landscape textures into different horizontal zones
pg.addTexture(new ImageIcon(lowestImg), -128,   0, 128); // lowest zone
pg.addTexture(new ImageIcon(middleImg),    0, 128, 255); // middle zone
pg.addTexture(new ImageIcon(highestImg), 128, 255, 384); // highest zone

// Create the final texture which has now grassy, rocky, and snowy zones.
pg.createTexture(256);

Every zone is defined by three values. The first and last value specify the lowest and highest point this texture will reach. Around the middle value, this texture will be 100%. It looks best if the highest value of one zone and the lowest value of the next overlap slightly. The values proposed here are a good starting point to see the effect, but you will have to adjust them for your usecase.

Remember that a terrain is by default between 0 and 255 world units high; the three values used here correspond directly to that (0 is the lowest, 128 the middle, and 255 the highest point). Everything outside the interval will be textured black, which is usually not a desired effect; to push the black areas out of the visible part of the terrain, specify the lowest low below 0 (e.g. here -128), and the highest high above 255 (e.g. here 256+128=384).

Procedural Splat Textures

Texture splatting means laying several textures on top of each other. The top textures are partially transparent to gain the desired overlapping effect. Here are a few common usecases for splat textures on already textured terrains:

  • criss-crossing paths/trails/streets…
  • bare or burned spots in grassy areas..
  • flowers or grassy spots in bare deserts and on concrete…
  • cracks, bullet holes, craters…
  • simple shadows around bushes/trees, or simple lit areas under light sources…

TODO: can this be changed live during the game or will it remain static?

We are still using the same map and terrain that we created above. Here's the code:

// From the heightmap, we create a procedural texture generator for Splatting
ProceduralSplatTextureGenerator pg = new ProceduralSplatTextureGenerator(map);

// Load transparency and texture for trails
String trailsAlphaPath = "media/trails-alpha.png"; // must have transparancy!
URL    trailsAlpha  = TerrainTest.class.getClassLoader().getResource(trailsAlphaPath);
String trailsPath = "media/trails.png"; // normal texture
URL    trailsImg  = TerrainTest.class.getClassLoader().getResource(trailsPath);
pg.addSplatTexture(new ImageIcon(trailsAlpha), new ImageIcon(trailsImg));

// Load transparency and texture for craters
String cratersAlphaPath = "media/craters-alpha.png"; // must have transparancy!
URL    cratersAlpha  = TerrainTest.class.getClassLoader().getResource(cratersAlphaPath);
String cratersPath = "media/craters.png"; // normal texture
URL    cratersImg  = TerrainTest.class.getClassLoader().getResource(cratersPath);
pg.addSplatTexture(new ImageIcon(cratersAlpha), new ImageIcon(cratersImg));

// Create the final texture which is now splatted with trails and craters:
pg.createTexture(256);

You can combine the splatting effect with the height-based procedural texture described above! The ProceduralSplatTextureGenerator is a subclass of ProceduralTextureGenerator, so it also supports the setTexture() method. Just replace the line ProceduralTextureGenerator pg = new ProceduralTextureGenerator(map); with ProceduralSplatTextureGenerator pg = new ProceduralSplatTextureGenerator(map);, and you can have both.

Detail, MIP map, texture units

In this code sample we use the texture in the TextureGenerator pg, and the terrain, that we have just created.

// Load the generated texture from the the texture generator pg:
Texture t1 = TextureManager.loadTexture( pg.getImageIcon().getImage(), 
               Texture.MinificationFilter.Trilinear,
               Texture.MagnificationFilter.Bilinear, 
               true ); // this also flips the image as needed

// Load an additional detail texture from a file:
String detailPath = "media/detail.jpg";
URL    detailImg  = TerrainTest.class.getClassLoader().getResource(detailPath);
Texture t2 = TextureManager.loadTexture(detailImg, 
               Texture.MinificationFilter.Trilinear, 
               Texture.MagnificationFilter.Bilinear
);

// Combine the detailed pattern with the generated one
t1.setApply(Texture.ApplyMode.Combine);
t1.setCombineFuncRGB(Texture.CombinerFunctionRGB.Modulate);
t1.setCombineSrc0RGB(Texture.CombinerSource.CurrentTexture);
t1.setCombineOp0RGB(Texture.CombinerOperandRGB.SourceColor);
t1.setCombineSrc1RGB(Texture.CombinerSource.PrimaryColor);
t1.setCombineOp1RGB(Texture.CombinerOperandRGB.SourceColor);
t2.setApply(Texture.ApplyMode.Combine);
t2.setCombineFuncRGB(Texture.CombinerFunctionRGB.AddSigned);
t2.setCombineSrc0RGB(Texture.CombinerSource.CurrentTexture);
t2.setCombineOp0RGB(Texture.CombinerOperandRGB.SourceColor);
t2.setCombineSrc1RGB(Texture.CombinerSource.Previous);
t2.setCombineOp1RGB(Texture.CombinerOperandRGB.SourceColor);

// Create texture state and set textures in it.
TextureState ts=display.getRenderer().createTextureState();
ts.setEnabled(true);
ts.setTexture( t2, 1);     // describe variants...
ts.setTexture( t1, 0 );    // describe variants...

// Finally we assign the texture state to the terrain.
terrain.setRenderState(ts);
terrain.setDetailTexture(1,16);  // argument?
rootNode.attachChild(terrain);

./jmetest/terrain/TestIsland.java


/var/www/wiki/data/pages/terrains_heightmaps_texturing.txt · Last modified: 2010/01/31 11:34 by zathras  
Recent changes · Show pagesource · Login

Recent changes RSS feed Creative Commons License Donate Powered by PHP Valid XHTML 1.0 Valid CSS Driven by DokuWiki

subscribe to jME latest jme headlines


site design by bleedcrimson designs © 2008