Implementation
To implement a heightmap in M3G:
- Load a heightmap.
- Create a new array that is scaled proportionally to the grid size.
- Read pixels from the heightmap and store in the new array.
- Use the array to generate quads with varying heights.
It's a straightforward procedure that begins by inspecting the private members of the HeightMap class; see Listing Four. The heightMap array is the scaled array that holds the heights. It is not holding the pixels from the heightmap image. The Mesh table is holding all the generated Quads you render. Finally, the water Mesh is a blue plane that represents the water (rivers, lakes, and so on) in the terrain. Listing Five creates a HeightMap by first checking for invalid resolution values. Invalid values are values beyond 1.0f (a quad has four corners, so the smallest grid sector is a 2×2) and below 0.0001f (a very low resolution that more or less creates the entire terrain with one quad).
// Actual heightmap containing the Y-coords of our triangles private short[] heightMap; private int[] data; private int imgw, imgh; // Map dimensions private int mapWidth; private int mapHeight; // Actual quads private Mesh[][] map; // Water private Mesh water; // Local transform used for internal calculations private Transform localTransform = new Transform();
public HeightMap(String imageName, float resolution, int waterLevel) throws IOException { // Check for invalid resolution values if(resolution <= 0.0001f || resolution > 1.0f) throw new IllegalArgumentException("Resolution too small or too large"); // Load image and allocate the internal array loadImage(imageName, resolution); // Create quads createQuads(); // Create the water createWater(waterLevel); }
Next, Listing Six loads the image supplied as a constructor parameter and extracts its pixel values. Using the resolution parameter supplied from the constructor, you then create a size grid and fill it with pixel values. Last, you perform manual garbage collection to get rid of unnecessary data because the loadImage method is a memory-intensive method and you want to ensure garbage data isn't taking up vital memory for the next few tasks.
// Load actual image Image img = Image.createImage(path); // Allocate temporary memory to store pixels data = new int[img.getWidth() * img.getHeight()]; // Get its rgb values img.getRGB(data, 0, img.getWidth(), 0, 0, img.getWidth(), img.getHeight()); imgw = img.getWidth(); imgh = img.getHeight(); // Clear image img = null; System.gc(); // Calculate new width and height mapWidth = (int)(res * imgw); mapHeight = (int)(res * imgh); // Allocate heightmap heightMap = new short[mapWidth * mapHeight]; // Calculate height and width offset into image int xoff = imgw / mapWidth; int yoff = imgh / mapHeight; // Set height values for(int y = 0; y < mapHeight; y++) { for(int x = 0; x < mapWidth; x++) { heightMap[x + y * mapWidth] = (short)((data[x * xoff + y* yoff * imgw] & 0x000000ff) * 10); } } // Clear data data = null; img = null; System.gc();
The createQuads method in the constructor body is a straightforward method that takes the generated heightMap array and creates quads from it; see Listing Seven. Here, you iterate over the heightMap table and extract four values, which are used as the height values in the MeshFactory.createQuad method. The MeshFactory.createPlane method then creates a large plane textured with a watery mesh.
private void createQuads() { map = new Mesh[mapWidth][mapHeight]; short[] heights = new short[4]; for(int x = 0; x < (mapWidth - 1); x++) { for(int y = 0; y < (mapHeight - 1); y++) { // Set heights setQuadHeights(heights, x, y, mapWidth); // Create mesh map[x][y] = MeshFactory.createQuad(heights,PolygonMode.CULL_NONE); } } }