Posted on Category

Height Maps from Elevation Maps

In this article we'll convert a IMG file (a digital elevation model file) into a PNG height map and load it into Unreal Engine 4. We'll retain as much precision as possible. Note that using real-world elevation data does create hyper-realistic results, but modeling a sixteen square mile area isn't reasonable unless you generate it. It might also be necessary to subdivide the terrain.

I had a copy of Unreal Engine and decided to use the landscape tool to create a level. I'm not much of a designer so I tried using geological data. Unfortunately geological data comes in obscure formats and I spent several hours attempting to convert a file of Crater Lake into a gray-scale PNG. The following texts depicts how I finally managed.

Obtaining Elevation Data

Elevation data of the US can be found through the USGS national map viewer. To obtain the elevation data for Crater Lake zoom in on Oregon, find the crater, select elevation availability, and press the download button. Multitudes of files will pop up, but chances are only one will be the file of intention. I will be using the following file:

ned19_n43x00_w122x25_or_craterlake_2010.img

Existing Software

Instead of spending time trying to decode craterlake.img, it might be possible to extract the raw data using existing software. Thanks to the Virtual Terrain Project we can convert the IMG file into a text file containing the elevation points in feet. Note that the VTP uses an ASC extension instead of a TXT extension. We can then write a program around this textual data so that we can create a file that our program supports. In the following example the file starts with meta data separated by newline characters. The file starts with a several attributes such as "ncols" and "nrows," each followed by a space and a value. The rest of the file consists of space separated numbers.

Programming

The following source code is an example that can be build to convert our text file into a 16 bit PNG image from the elevation data.

main.java — HeightMap.jar8
/**
 * Converts a text file consisting of space separated numbers into a 16-bit PNG
 * image.
 */
public class HeightMap {
	private static int width = 0;
	private static int height = 0;
	private static String fileName;

	public static void main(String[] args) {
		if (args.length > 0) {
			if (args.length > 1) {
				fileName = args[1];
			} else {
				fileName = "map.png";
			}
			int[] image = load(Paths.get(args[0]));
			if (image != null) {
				export(image);
			}
		} else {
			System.err.println("Error: No file specified.");
		}
	}

	private static int[] load(Path path) {
		Charset charset = Charset.forName("US-ASCII");
		try (BufferedReader reader = Files.newBufferedReader(path, charset)) {
			String line;
			char chr;
			while ((line = reader.readLine()) != null) {
				String values[] = line.replaceFirst("^ ", "").split(" ");
				if (Character.isDigit(values[0].charAt(0))) {

					/* Presumably an attribute can't be a number. If the new
					 * line starts with a number, then assume it's elevation
					 * data. */
					if (width <= 0 || height <= 0) {
						System.err.println(
							"Error: Dimensions have to be greater than zero.");
						return null;
					}
					break;
				} else {
					if (values[0].contains("ncols")) {
						width = Integer.parseInt(values[1]);
					} else if (values[0].contains("nrows")) {
						height = Integer.parseInt(values[1]);
					}
				}
			}

			int length = width * height, index = 0;
			float max = 0, min = 0;
			float depth[] = new float[length];

			while ((line = reader.readLine()) != null) {
				String values[] = line.replaceFirst("^ ", "").split(" ");
				if (values.length + index > length) {
					break;
				}

				for (String str : values) {
					depth[index] = Float.parseFloat(str);
					if (depth[index] > max) {
						max = depth[index];
					}

					/* Zero is omitted because it tends to mean N/A in this
					 * case. */
					else if (depth[index] != 0 &&
							(depth[index] < min || min == 0)) {

						min = depth[index];
					}
					index++;
				}
			}

			int image[] = new int[length];

			/* Translate and scale data between 2^16 - 1 and 0. */
			float dif = max - min;
			if (dif == 0) {
				dif = 1;
			}
			float mult = 65535 / dif;
			for (int i = 0; i < length; i++) {
				depth[i] -= min;
				image[i] = (int) (depth[i] * mult);
			}
			return image;
		} catch (IOException io) {
			System.err.format("IOException: %s%n", io);
		}
		return null;
	}

	private static void export(int[] image) {
		BufferedImage dImage = new BufferedImage(width, height,
		BufferedImage.TYPE_USHORT_GRAY);
		WritableRaster raster = dImage.getRaster();
		raster.setPixels(0, 0, width, height, image);
		File output = new File(fileName);
		try {
			ImageIO.write(dImage, "png", output);
		} catch (IOException io) {
			System.err.format("IOException: %s%n", io);
		}
	}
}

Now that's over with, in a terminal or command prompt write something like:

java -jar HeightMap.jar Desktop/map.asc Desktop/map.png

That should produce "map.png" from the text file "map.asc." If we load this image into Unreal Engine 4, the result should look like the following image:

craterlake

Elevation data: US Geological Survey & Game engine: Unreal Engine 4

And yes this script doesn't export to RAW. Of course we could easily alter this program so that it can export to multiple file types. For quick reference ImageIO supports:

  • JPEG
  • PNG
  • BMP
  • WBMP
  • GIF

And that's how to create a PNG height-map from a DEM file.