Previous: Hello Collision, Next: Hello Audio
One way to create a 3D landscape is to sculpt a huge terrain model. This will give you a lot of artistic freedom – but rendering such a huge model can be quite slow. jME supports heightmaps to solve this common performance issue of terrains.
This tutorial explains how to create terrains from heightmaps and how to use splat textures to make the terrain look good.
package jme3test.helloworld;
import com.jme3.app.SimpleApplication;
import com.jme3.material.Material;
import com.jme3.renderer.Camera;
import com.jme3.terrain.geomipmap.TerrainLodControl;
import com.jme3.terrain.heightmap.AbstractHeightMap;
import com.jme3.terrain.geomipmap.TerrainQuad;
import com.jme3.terrain.heightmap.HillHeightMap; // is used in example 2
import com.jme3.terrain.heightmap.ImageBasedHeightMap;
import com.jme3.texture.Texture;
import com.jme3.texture.Texture.WrapMode;
import java.util.ArrayList;
import java.util.List;
import jme3tools.converters.ImageToAwt;
public class HelloTerrain extends SimpleApplication {
private TerrainQuad terrain;
Material mat_terrain;
public static void main(String[] args) {
HelloTerrain app = new HelloTerrain();
app.start();
}
@Override
public void simpleInitApp() {
flyCam.setMoveSpeed(50);
/** 1. Create terrain material and load four textures into it. */
mat_terrain = new Material(assetManager, "Common/MatDefs/Terrain/Terrain.j3md");
/** 1.1) Add ALPHA map (for red-blue-green coded splat textures) */
mat_terrain.setTexture("m_Alpha",
assetManager.loadTexture("Textures/Terrain/splat/alphamap.png"));
/** 1.2) Add GRASS texture into the red layer (m_Tex1). */
Texture grass = assetManager.loadTexture("Textures/Terrain/splat/grass.jpg");
grass.setWrap(WrapMode.Repeat);
mat_terrain.setTexture("m_Tex1", grass);
mat_terrain.setFloat("m_Tex1Scale", 64f);
/** 1.3) Add DIRT texture into the green layer (m_Tex2) */
Texture dirt = assetManager.loadTexture("Textures/Terrain/splat/dirt.jpg");
dirt.setWrap(WrapMode.Repeat);
mat_terrain.setTexture("m_Tex2", dirt);
mat_terrain.setFloat("m_Tex2Scale", 32f);
/** 1.4) Add ROAD texture into the blue layer (m_Tex3) */
Texture rock = assetManager.loadTexture("Textures/Terrain/splat/road.jpg");
rock.setWrap(WrapMode.Repeat);
mat_terrain.setTexture("m_Tex3", rock);
mat_terrain.setFloat("m_Tex3Scale", 128f);
/** 2. Create the height map */
final Texture heightMapImage =
assetManager.loadTexture("Textures/Terrain/splat/mountains512.png");
final AbstractHeightMap heightmap =
new ImageBasedHeightMap(
ImageToAwt.convert(
heightMapImage.getImage(), false, true, 0));
heightmap.load();
/** 3. We have prepared material and heightmap. Now we create the actual terrain:
* 3.1) We create a TerrainQuad and name it "my terrain".
* 3.2) A good value for terrain tiles is 64x64 -- so we supply 64+1=65.
* 3.3) We prepared a heightmap of size 512x512 -- so we supply 512+1=513.
* 3.4) As LOD step scale we supply Vector3f(1,1,1).
* 3.5) At last, we supply the prepared heightmap itself.
*/
terrain = new TerrainQuad("my terrain", 65, 513, heightmap.getHeightMap());
/** 4. We give the terrain its material, position & scale it, and attach it. */
terrain.setMaterial(mat_terrain);
terrain.setLocalTranslation(0, -100, 0);
terrain.setLocalScale(2f, 1f, 2f);
rootNode.attachChild(terrain);
/** 5. The LOD (level of detail) depends on were the camera is: */
List<Camera> cameras = new ArrayList<Camera>();
cameras.add(getCamera());
TerrainLodControl control = new TerrainLodControl(terrain, cameras);
terrain.addControl(control);
}
}
When you run this sample you should see a landscape with dirt mountains, grass plains, plus some winding roads in between.
Heightmaps are an efficient way of representing the shape of a hilly landscape. Picture a heightmap as a float array containing height values between 0f and 255f. Here is a very simple example of a heightmap with 25 height values.
Important things to note:
Now when looking at Java data types to hold an array of floats between 0 and 255, the Image class comes to mind. Storing a terrain's height values as a grayscale image has one big advantage: The outcome is a very userfriendly, almost topographical, representation of a landscape:
Look at the next screenshot: In the top left you see the 128x128 grayscale image (heightmap) that was used as a base to generate the depicted terrain. To make the hilly shape better visible, the mountain tops are colored white, valleys brown, and the areas inbetween green:
In a real game, you will want to use more complex and smoother terrains than the simple heightmaps shown here. Heightmaps typically have square sizes of 512x512 or 1024x1024, and contain hundred thousands to 1 million height values. No matter which size, the concept is the same as described here.
The first step is always to create the heightmap. You can create it yourself in any standard graphic application. Make sure it has the following properties:
The file mountains512.png
that you see here is a typical example of a heightmap.
Here is how you create the heightmap object in your jME code:
ImageToAwt.convert()
ed image filefinal Texture heightMapImage = assetManager.loadTexture("Textures/Terrain/splat/mountains512.png"); final AbstractHeightMap heightmap = new ImageBasedHeightMap( ImageToAwt.convert( heightMapImage.getImage(), false, true, 0)); heightmap.load();
Texture splatting allows you create a custom textured material and "paint" on it. This is very useful for terrains: As you see in the example here, you can paint a grass texture into the valleys, a dirt texture onto the mountains, and free-form roads inbetween.
How is it done? We have three texture layers to paint on, m_Tex1
, m_Tex2
and m_Tex3
(these names are found by opening the Terrain.j3md file, under the Material Parameters section; they may be changed) . before we start we have to make a few decisions:
m_Tex1
m_Tex2
m_Tex3
Now we start painting the texture:
mountains512.png
, so you know the shape of the landscape.alphamap.png
.alphamap.png
in a graphic editor and switch the image mode to color image.Note: In the future, the jMonkeyPlatform will take over some of these steps so you don't have to worry about the details.
As usual, we create a Material object. We base it on the Material Definition Terrain.j3md
that is included in the jME3 framework.
Material mat_terrain = new Material(assetManager, "Common/MatDefs/Terrain/Terrain.j3md");
Now we load four textures into this material. The first one, m_Alpha
, is the alphamap that we just created.
mat_terrain.setTexture("m_Alpha", assetManager.loadTexture("Textures/Terrain/splat/alphamap.png"));
Three other textures are the layers that we have previously decided to paint: grass, dirt, and road. We create texture objects and load the three textures as usual. Note how we assign them to their respective texture layers (m_Tex1, m_Tex2, and m_Tex3) inside the Material!
/** 1.2) Add GRASS texture into the red layer (m_Tex1). */ Texture grass = assetManager.loadTexture("Textures/Terrain/splat/grass.jpg"); grass.setWrap(WrapMode.Repeat); mat_terrain.setTexture("m_Tex1", grass); mat_terrain.setFloat("m_Tex1Scale", 64f); /** 1.3) Add DIRT texture into the green layer (m_Tex2) */ Texture dirt = assetManager.loadTexture("Textures/Terrain/splat/dirt.jpg"); dirt.setWrap(WrapMode.Repeat); mat_terrain.setTexture("m_Tex2", dirt); mat_terrain.setFloat("m_Tex2Scale", 32f); /** 1.4) Add ROAD texture into the blue layer (m_Tex3) */ Texture rock = assetManager.loadTexture("Textures/Terrain/splat/road.jpg"); rock.setWrap(WrapMode.Repeat); mat_terrain.setTexture("m_Tex3", rock); mat_terrain.setFloat("m_Tex3Scale", 128f);
The individual texture scales (e.g. mat_terrain.setFloat("m_Tex3Scale", 128f);
) depend on the size of the textures you use. You can tell you picked a too small scale if, for example, your road tiles appear like tiny grains of sand, or to big if the blades of grass look like twigs.
We use setWrap(WrapMode.Repeat)
to make the small texture fill the wide area. If the repetition is too visible, try adjusting the Tex*Scale value.
Later, after we have created the actual terrain object, we must not forgot to set the material on it:
terrain.setMaterial(mat_terrain);
Internally, the generated terrain mesh is broken down into tiles and blocks. This is an optimization for culling. You do not need to worry about tiles and blocks too much, just use recommended values for now.
Let's assume we want to generate a small 512x512 terrain. We already have created the heightmap object. Here are the steps that we perform everytime we create a new terrain.
We create a TerrainQuad with the following arguments:
my terrain
.Here's the code:
terrain = new TerrainQuad( "my terrain", // name 65, // tile size 513, // block size heightmap.getHeightMap()); // heightmap
Don't forget to attach the terrain to the rootNode. You can scale and translate the terrain just like any other Spatial.
jME3 includes an optimization that adjusts the level of detail of the rendered terrain depending on how close or far the camera is.
List<Camera> cameras = new ArrayList<Camera>(); cameras.add(getCamera()); TerrainLodControl control = new TerrainLodControl(terrain, cameras); terrain.addControl(control);
Close parts of the terrain are rendered in full detail. Terrain parts that are further away are not clearly visible anyway, and jME improves performance by rendering them less detailed. This way you can afford to load huge terrains with no penalty caused by invisible details.
What happens if you swap two layers, for example m_Tex1
and m_Tex2
?
... mat_terrain.setTexture("m_Tex2", grass); ... mat_terrain.setTexture("m_Tex1", dirt);
It's easier to swap layers in the code than to change the color in the alphamap.
These two lines generate the hightmap from a user defined image:
Texture heightMapImage = assetManager.loadTexture("Textures/Terrain/splat/mountains512.png"); heightmap = new ImageBasedHeightMap( ImageToAwt.convert(heightMapImage.getImage(), false, true, 0), 1f);
You can also let jME create a random landscape.
HillHeightMap heightmap = null; try { heightmap = new HillHeightMap(513, 1000, 50, 100, (byte) 3); } catch (Exception ex) { ex.printStackTrace(); }
Tip: You can keep using the splatted texture from the sample code above. Just don't be surprised that the textures do not automatically adjust to the randomized landscape.
You have learned how to create terrains that are more efficient as loading a giant model. You are now also able to texture a terrain.
In the next chapter, you will learn how to add sounds to your game.
See also: Terrain Collision