Building SRTM terrains in 3ds Max

posted in: Blog | 0

This is a guest post written by Klaas Nienhuis.

Part 1

Mars Mojave crater. Source: NASA

 

Terrain data

Digital terrain data comes in many flavors. There are also many sources of digital terrain data: local, national and global, commercial and free. Usually the larger the database the coarser the data. Depending on the goal you’re aiming for, you need to select your data source. I went with the Shuttle Radar Topography Mission (SRTM) data for this tutorial. It’s free, has an almost global coverage and last but not least has a very simple binary file-structure. Read more about SRTM here. Many other data sources use the GeoTiff format which is a bit harder to parse. But after this project might be actually within reach.

In this series I’m using this particular file, this one, and this one, but you can pick any other srtm file you wish. Just unzip it once downloaded.

 

SRTM data coverage is almost global

 

Check out the other parts in this tutorial

Part 2
Part 3

 

SRTM data format

The SRTM files contain a grid of altitude samples. These samples are spaced about 30 meters (SRTM1: for the US) or 90 meters (SRTM3: for the rest of us) apart. I’ll focus on the SRTM3 data in the rest of this tutorial. The SRTM data is chopped into square tiles of 1201 by 1201 samples. This means an SRTM3 tile covers about 108 by 108 kilometers of earth. Each tile overlaps one row or column with its neighbour. The location on earth of each tile is determined by its filename. Filenames refer to the latitude and longitude of the lower left corner of the tile, e.g. N37W105 has its lower left corner at 37 degrees north

latitude and 105 degrees west longitude. You can get the SRTM data files here.

Here’s documentation on the SRTM data format.

Building meshes

The general idea is to read the samples of the binary data files and apply these to a mesh of 1201 by 1201 vertices. Each sample in the file determines the height of a vertex. The result is a digital terrain. Maxscript can read binary data, so it should be pretty easy. I’ll provide the code here and discuss.

A rendered piece of 3D terrain

 

There are three methods: one reads and interprets the data from the file, one builds a basic mesh and one applies the heights to the mesh.

 Code sample pure maxscript

(

   function fn_readBytes strFilePath &outputmessage =

   (

   /*<FUNCTION>

   Description

   reads all bytes in a binary file, creates integers while swapping the endian type

   Arguments

   <string> strFilePath: the file we're reading

   <value by reference> outputmessage: a message we're reporting to

   Return

   <return_type> Function returns (anything?).

   <FUNCTION>*/

  

   local theStream = fopen strFilePath "rb" --open a binary filestream

   local fileSize = getFileSize strFilePath

  

   --read two bytes, swap their places and make an integer

   local theInt = [0,0]

   local arrInt = for i = 1 to fileSize by 2 collect

   (

   theInt.x = ReadByte theStream #unsigned

   theInt.y = ReadByte theStream #unsigned

   bit.or (bit.shift theInt.x 8) theInt.y --shifting a byte converts the data from big endian to little endian

   )

   FClose theStream

   format "File has % bytes, % integers readn" fileSize arrInt.count to:outputmessage

   arrInt

   )



   function fn_buildMesh segments:1200 segmentSize:90 =

   (

   /*<FUNCTION>

   Description

   builds a mesh. the mesh will be arranged in such a way that the first vertex is at the top left and the last vertex is the bottom right

   Arguments

   <integer> segments: the amount of width and length segments

   <integer> segmentSize: the size of a single segment

   Return

   <mesh object> the newly created mesh object

   <FUNCTION>*/

  

   --build a planar mesh

   local theMesh = Editable_mesh wirecolor:(color 80 70 0)

   setMesh theMesh

   width:(segments*segmentSize)

   length:-(segments*segmentSize) --a negative length puts the first vertex at the top left. This matches nicely with the data

   widthsegs:segments

   lengthsegs:segments

  

   --flip the normals because we set the length to a negative value

   addModifier theMesh (Normalmodifier flip:true)

   convertToMesh theMesh

  

   update theMesh

   theMesh

   )



   function fn_applyHeights theMesh arrHeight =

   (

   /*<FUNCTION>

   Description

   applies the heights to the vertices in the mesh

   Arguments

   <mesh object> theMesh: the mesh we're editing

   <array> arrHeight: an array of integers we'll use as heights

   Return

   <FUNCTION>*/

  

   local meshvert = undefined

   local arrVert = for i = 1 to arrHeight.count collect

   (

   meshvert = getVert theMesh i

   meshvert.z = arrHeight[i]

   meshvert

   )

   setMesh theMesh vertices:arrvert

   update theMesh

   )

   

   gc()

   local st = timeStamp()

   local mem = heapFree

   local msg = "" as stringstream

   local strFile = @"N:GitHubKML for 3dsMaxTestDataS23W068.hgtS23W068.hgt"

   

   local theMesh = fn_buildMesh segments:1200 segmentSize:90

   local arrInt = fn_readBytes strFile &msg

   fn_applyHeights theMesh arrInt

   

   format "Time: % ms, memory: %n" (timestamp()-st) (mem-heapfree)

   format "%" (msg as string)

)

 

Data juggling

While parsing the data I’ve done a few things to make it work. First, each sample consists of two bytes in the file. Two bytes combined make up a 16 bit integer. According to the file specs it’s a signed integer which means the values run from -32768 to +32767 meters. Another thing: according to the spec the data is provided as “Big endian”. Effectively this means we need to swap the order of each pair of bytes before converting it to the integer. More about that here on wikipedia. The method fn_readBytes deals with this.

The grid

The data file is a long series of bytes, 1201*1201*2=2884802 bytes to be precise. They’re stored in row major order, first the bytes of row 1, then row 2 etcetera. The first value in the file is at the north-west corner of the grid, the second value is its neighbour to the east, and so on.

We need to match it to a grid of 1201*1201=1442401 vertices in the mesh. The vertices are arranged in a grid. When building the mesh, 3dsMax arranges the vertices also in row major order. But in the mesh, the first vertex is located at the south-west corner. This means the first row in the file maps to the last row in the mesh. We need to put the first vert of the mesh at the top left corner to save ourselves a headache later on. This is done by creating a mesh object with a negative length. this puts the first vert at the right position. It also flips the normals of the mesh, but this is easily solved.

The heights

Finally we’re editing the mesh object with the heights we got from the file. The setMesh method is really great. You can just feed it the things you want to edit and the rest stays the same. We only want to change the vertices, so that’s what I’m passing into the setMesh method. It’s a really simple procedure since we’ve made sure before the vertex order in the mesh lines up with the data order in the file.

Evaluation

Another render of the same terrain. Note the data spike, which shouldn’t be there.

 

The first run is pure maxscript. It needs about 7 seconds and a whole lot of memory to build a 1.5 mln vert mesh. For a one time operation this is workable, but if you need to create terrains like these multiple times it’s too slow. Besides that, the mesh is one big chunk which slows max down. It would be a good idea to slice the mesh into multiple chunks to improve viewport performance which is sluggish to say the least.

There’s also an issue with some spikes. According to the spec, unknown values should be -32768. In our case, the spikes are pointing upward.

 

Check out the other parts in this tutorial

Part 2
Part 3

 
This is a guest post written by Klaas Nienhuis.
Original article: http://www.klaasnienhuis.nl/2014/08/building-srtm-terrains-3dsmax/

 

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.