SRTM, C# and maxscript

More and faster loading terrains with C#
More and faster loading terrains with C#

After I’ve used this on-the-fly assembly I wanted to know how to build a regular assembly in C#. I just had to! I figured since I already had most of the code, putting it in a normal assembly shouldn’t be that hard.

It did take some time to figure it out though. I also used a book called Head First C# to get around the basic C# concepts. I can really recommend it!

This article and part one and two have also appeared on the Artur Leao’s website youcandoitvfx.

Check out the other parts in this tutorial

Mars Mojave crater. Source: NASA
Part one

This terrain is split up into 9 chunks
Part two

Visual Studio Express

I’ve used visual studio express to build the assembly. In VS you can build all sorts of projects: a commandline program, one with a gui or an assembly (dll). We’re making an assembly we can call from 3dsMax. Since you can’t just execute a dll in VS, you need an additional project to test the dll you’re working on. This secondary project can be a console app for instance. It’s used just for testing and won’t end up in 3dsMax.

Setting up the solution

Let’s create a new project and make it a Class Library. Delete the class which comes with the project (Class1.cs). Rightclick on it in the solution explorer on the right and choose “Delete”. Add a new class with the name “srtmReader”. Most of these actions can be accessed through the rightclick menu in the solution explorer. In this new class, edit the top lines. We don’t need to use all the libraries and we need to add System.IO. Finally add the code.

Start a new project
Start a new project
Make the new project a class library
Make the new project a class library
Delete the Class1.cs file and add a new class
Delete the Class1.cs file and add a new class
Make a new class and name it srtmFile.cs
Make a new class and name it srtmFile.cs

Finally, write the code. I’ve already done this, so you can paste this in and replace the code VS put in there by itself. Make sure the name of the class (srtmFile in my case) matches the name of the class you just added. Also keep in mind that unlike maxscript, C# is case sensitive. This code is a single method called ReadChunk which looks a lot like the one from the previous part of this tutorial. I’ve added a few things. The method now doesn’t read the entire srtm file in one go, but only gets a chunk of the data which corresponds to the sliced mesh.

using System;
using System.IO;

namespace srtmReader
{
    public static class srtmFile
    {
            /// <summary>
            /// accesses a srtm files and reads a specific grid of data from it
            /// </summary>
            /// <param name="file">the path to the srtm file</param>
            /// <param name="chunksamples">the width and height of the chunk we want to read</param>
            /// <param name="gridsamples">the width and height of the total datagrid</param>
        /// <param name="posx">the x position of the chunk</param>
            /// <param name="posy">the y position of the chunk</param>
            /// <returns></returns>
        public static Int16[] ReadChunk(string file, int chunksamples, int gridsamples, int posx, int posy)
        {
            // setup the output to hold an array of integers
                    Int16[] result = new Int16[(chunksamples * chunksamples)];
                    using (FileStream fs = File.OpenRead(file))
                    {
                            //we're going to access one row of bytes at a time and will pick the int16 values from that
                            byte[] rowBuffer = new byte[gridsamples * 2];
                //skip the rows we don't need
                            fs.Seek((posy * gridsamples * 2), SeekOrigin.Begin);

                            int theIndex = 0;
                            for (int y = 0; y < chunksamples; y++)
                            {
                                    //read one row of data into the buffer
                                    fs.Read(rowBuffer, 0, rowBuffer.Length);
                                    //set up a loop to get the appropriate bytes from the row of data
                                    for (int x = (posx * 2); x < (((posx + chunksamples) * 2)); x += 2, theIndex++)
                                    {
                                        //we're reading the data in reversed byte order.
                                        //The data is big-endian, but we need to have little-endian.
                                        result[theIndex] = (Int16)(rowBuffer[x] << 8 | rowBuffer[x + 1]);
                                    }
                            }
                    }
                    return result;
            }
    }
}

Add a Console Application

Like I said before, we can’t run this in Visual Studio directly. We need to add another project to this solution which does that for us. This new project is only for debugging while coding in VS. Add a new project to the solution (rightclick the solution) and add a new Console Application. Make this console application the startup project. This means that when debugging the solution, this application is run first. Finally add a reference from the srtmReader project to the console app. This makes sure we can actually see the things we write in the srtmReader class.

Add a new project
Add a new project
The new project is a console app
The new project is a console app
Set the project as the startup project and add a reference
Set the project as the startup project and add a reference
Add the reference. This makes sure the test project can actually work with the assembly it’s testing
Add the reference. This makes sure the test project can actually work with the assembly it’s testing

In the Main method of the console app we’ll write a test. This test will get a chunk of the data and report back to the console. If everything goes well, we can load up our assembly in 3dsMax.

C# code for the test

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace testconsole
{
    class Program
    {
        static void Main(string[] args)
            {
                    //set up some test data. Make sure you actually have the dataflie!
                    string hgtPath = @"<your folder>\N34W119.hgt";
                    int chunksize = 601;
                    int gridsize = 1201;
                    int posx = 0;
                    int posy = 0;
                    Console.WriteLine("Getting a chunk from {3}\nat x:{0},y:{1} with size {2}", posx, posy, chunksize, hgtPath);

                    //run the test
                    Int16[] output = srtmReader.srtmFile.ReadChunk(hgtPath, chunksize, gridsize, posx, posy);
                    Console.WriteLine("Read a chunk with {0} samples.\nFirst sample is {1}", output.Length, output[0]);

                    // Keep the console window open in debug mode.
                    Console.WriteLine("Press any key to exit.");
                    Console.ReadKey();
            }
    }
}

Creating the assembly

In VS switch from debug mode to release mode and press “Start” or F5. This should create the srtmReader.dll file in the Bin/Release folder of the project.

Switch to “Release” to build the assembly
Switch to “Release” to build the assembly
This is the dll we’ve done all this work for. Let’s see if it does what it should.
This is the dll we’ve done all this work for. Let’s see if it does what it should.

The assembly in 3dsMax

When still developing in 3dsMax and VS it’s practical not to load the assembly from disk, but from memory. This is similar to the on-the-fly assembly. The big advantage in loading from memory is that you don’t lock the assembly and don’t have to restart 3dsMax each time to update the assembly. When everything’s working as it should and you release your script you can use the normal dotnet.loadassembly method to load the assembly from disk. Keep in mind you can’t load assemblies from network locations by default.

I’ve added the loadassembly code in this sample, but it’s commented out.

Maxscript code

(
 
    struct str_chunk
    (
        pos = [0,0], --the position in samples
        segments = [100,100], --the amount of width and length segments for this chunk
        segmentSize = 90, --the size of a single segment. For srtm3 this is 3 arc seconds which is about 90 meters
        data = #() --the dataarray for this chunk
    )

    function fn_initDataGrid &outputmessage slices:5 gridSamples:1201  =
    (
        /*<FUNCTION>
        Description
            builds a grid of data structs based on a datagrid of a particular size
            each datastruct has a size and a position. they're created in such a way that they cover the input gridSamples
            the chunks tile across the datagrid north > south, west > east
        Arguments
            <value by reference> outputmessage: a message we're reporting to
            <integer> slices: the amount of width and length slices we want to slice the input grid into
            <integer> gridSamples: the amount of width and length samples the input grid has
        Return
            <array> an array of structs
        <FUNCTION>*/
            
        --calculate the sizes of the chunks based on the amount of slices you want to split the input grid into
        --adjacent chunks will share vertices
        local chunkSegments = [gridSamples as integer/slices as integer,gridSamples as integer/slices as integer]
        --if we're splitting into slices, add an extra row and column to allow for the overlap
        if slices > 1 do chunkSegments += [1,1]
        --make sure all samples are being used. add them to the chunks at the end of the row and column
        local lastChunkSegments = [chunkSegments.x + (mod (gridSamples-1) slices),chunkSegments.y + (mod (gridSamples-1) slices)]
        format "chunkSegments: %\nLastChunkSize: %\nAmount of chunks: %\n" chunkSegments lastChunkSegments (slices^2) to:outputmessage

        --create and collect the datastructs
        local arrChunkData = #()
        for x = 1 to slices do for y = 1 to slices do
        (
            --build a chunk struct and determine its size and position in the datagrid
            local theData = str_chunk()
            theData.segments.x = if x == slices then lastChunkSegments.x else chunkSegments.x
            theData.segments.y = if y == slices then lastChunkSegments.y else chunkSegments.y
            theData.pos.x = (x-1)*(chunkSegments.x-1)
            theData.pos.y = (y-1)*(chunkSegments.y-1)
            format "\tChunk %, position [%,%]\n" ((x-1)*slices + y) theData.pos.x theData.pos.y to:outputmessage
            
            append arrChunkData theData
        )
        arrChunkData
    )

    function fn_buildMesh theChunk &outputmessage =
    (
        /*<FUNCTION>
        Description
            builds a mesh object from rows and columns of heights. Intended to use with hgt files. this is data also known as srtm.
            uses a datastruct to determine what's being built
        Arguments
            <chunk struct> theChunk: a datastruct, containing the info needed to create and translate the mesh
            <value by reference> outputmessage: a message we're reporting to
        Return
            <mesh> the created mesh
        <FUNCTION>*/
        
        --build a planar mesh
        local theMesh = Editable_mesh wirecolor:(random (color 30 20 0) (color 30 30 10))
        setMesh theMesh\
            width:((theChunk.segments.x-1)*theChunk.segmentSize)\
            length:-((theChunk.segments.y-1)*theChunk.segmentSize)\ --a negative length puts the first vertex at the top left. This matches nicely with the data
            widthsegs:(theChunk.segments.x-1)\
            lengthsegs:(theChunk.segments.y-1)
        
        --flip the normals because we set the length to a negative value
        addModifier theMesh (Normalmodifier flip:true)
        convertToMesh theMesh
        
        --place the mesh in the right position of the grid
        theMesh.position = [theChunk.pos.x*theChunk.segmentSize,-theChunk.pos.y*theChunk.segmentSize,0]
        
        update theMesh
        forceCompleteRedraw()
        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 pos = theMesh.pos
        theMesh.pos = [0,0,0]
        
        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
        theMesh.pos = pos
        
    )

    local strAssemblyPath = @"<your assembly folder>\srtmReader.dll"
    local srtmReaderAssembly = (dotnetClass "System.Reflection.assembly").Load ((dotnetClass "System.IO.File").ReadAllBytes strAssemblyPath)
    local dotNetType = srtmReaderAssembly.GetType("srtmReader.srtmFile") --get Type of className as a dot Net value
    local srtmFileClass = (dotNetClass "System.Activator").CreateInstance dotNetType

--     strAssemblyPath = @"C:\Temp\srtmReader.dll"
--     dotNet.loadAssembly strAssemblyPath --only when assembly is on C drive. Networkdrive doesn't work.

    gc()
    local st = timeStamp()
    local mem = heapFree
    local msg = "" as stringstream
    local strFile = @"<your folder>\S23W068.hgt"
        
    local arrChunkData = fn_initDataGrid &msg slices:6
    for chunk in arrChunkData do
    (
--         chunk.data = (dotnetClass "srtmReader.srtmFile").ReadChunk strFile chunk.segments.x 1201 chunk.pos.x chunk.pos.y
        chunk.data = srtmFileClass.ReadChunk strFile chunk.segments.x 1201 chunk.pos.x chunk.pos.y
        local theMesh = fn_buildMesh chunk &msg
        fn_applyHeights theMesh chunk.data
    )

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

Evaluation

The code creates the meshes in about 5 seconds which is faster than the on-the-fly solution. One advantage over the previous example is we can create a specific chunk individually. Only the needed data is read. Though the biggest bottleneck now is creating the meshes, not reading the data.

What’s next?

The meshes look nice, but they’re actually distorted. They completely ignore the curvature of the earth. We need map projection for that which is actually pretty complicated. A next project could be to correctly project the srtm data using the WGS84 datum for instance. This would make it possible to match these terrains with data from Google Earth, Bing or openstreetmap amongst others.

Check out the other parts in this tutorial

Mars Mojave crater. Source: NASA
Part one

This terrain is split up into 9 chunks
Part two

5 Comments

Join the discussion and tell us your opinion.

  • 2014-08-06 at 18:19

    […] Part three […]

  • 2014-08-09 at 16:09

    […] Part three[/caption] […]

  • 2015-09-08 at 22:27

    Hello. Thanks for all this. I am a student and this is my final project. I searched in internet but no one made an explanation like you. I did all parts this project with c# but when you come to the 3dsmax I stuck. Please help me. How can I do this things in 3dsmax? I dont know even where will I paste this codes because I didnt use before. Can you help me? I will very happy if you can answer and help me. Thanks for your interest.

  • 2015-09-09 at 06:48

    Hi Huseyin, glad I could be of help.
    If you open a maxscript editor window (main menu > maxscript > maxscript editor) in 3ds Max and paste the maxscript code in I’ve posted here you can just execute it by pressing Ctrl+e. Make sure you fill in the paths to your assembly and srtm data.
    If you need more help with maxscript basics I can recommend these sources: https://vimeo.com/album/1514565 and https://davewortley.wordpress.com/. Have fun!

  • 2016-02-08 at 18:26

    Hi Klaas ,
    I am writing a farm tool to dispatch jobs on the farm using vb through shell commands …I am at a point where I need to read data( like camera ) from max files without 3ds max.
    I read that I can access the The method ‘getMAXFileAssetMetadata’ with OpenMCDF on VB or C# , but it doesn’t hold the CAMERA informations ( Name / Position ect ) !! Any clue on how to achieve this ?
    Many thanks !

Leave a reply

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