Motivation
I’ve always had a fascination with maps. I’ve done some mapping work with mapbox and tilemill which are two awesome products. I’ve also written a KML parser for 3dsMax which lets you exchange shapes and models between Google Earth and 3dsMax. Wouldn’t it be great to build real 3D terrains too? So one day I had a quiet moment and started creating 3D meshes based on remote sensing. Eventually this triggered me to venture out of 3dsMax and create my first ever C# library. This is part one of a three part tutorial. It’s about reading binary srtm data, using .net in maxscript and building a simple C# library from scratch. Have fun!
This article and part two and three have also appeared on the Artur Leao’s website youcandoitvfx.
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 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.
Code sample, pure maxscript
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.
(
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 read\n" 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 = @"<your folder>\S23W068.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
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.
7 Comments
Join the discussion and tell us your opinion.
[…] Part one […]
[…] Part one […]
Hey
Thanks for these 3 amazing tutorials on importing data and converting it to mesh !
I was wondering if there is also a way to get the texture data? I know that Terrain plugin for 3ds max can sort off get the textures but they are not too high res. Do you have any advice as to how could I handle textures?
Hi Dariusz, thanks for the feedback.
As a matter of fact I’m working on a tool which lets you do exactly that: import a terrain, place a texture on it (ging, stamen, openstreetmap, etc) and also work with vectormaps (shapefiles, openstreetmap). There’s still a lot to do but I’ll get there. You can look it up here under the name mapmax.
That’s incredible ! If you ever need any beta testers I would love to help you out !!!
Hello — I’m interested in self-study relating to GIS and 3D simulation. Could you possibly point me to some beginner material to better understand the theory and logic behind converting SRTM data files to something that can produce a 3D Mesh? Specifically referencing your tutorial here: https://www.klaasnienhuis.nl/wp/2014/08/building-srtm-terrains-3dsmax/
Thanks a bunch!
Hi Matt, I think this article covers the structure of SRTM files and how to translate this to a 3D mesh best.
Basically the SRTM files are just files with heights stored in them. The heights are structured in a grid in such a manner that one of these SRTM files covers an area of approximately 100*100 km. Each height is stored as a number, e.g. 23.5, which represents the height above sea level. To determine where on earth this height applies you’d have to reconstruct its lon/lat coordinates based on the filename of the srtm file and the place of the sample in the file itself.