SRTM, C# and maxscript

posted in: Blog | 0

This is a guest post written by Klaas Nienhuis.

Part 3

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!
 

Check out the other parts in this tutorial

Part 1
Part 2

 

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
Make the new project a class library
Delete the Class1.cs file and add a new class
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 chunked mesh.

C# code

using System;

using System.IO;



namespace srtmReader

{

public 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
The new project is a console app
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

 

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 = @"N:GitHubKML for 3dsMaxTestDataN34W119.hgtN34W119.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
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 = @"N:GitHubKML for 3dsMaxsrtmReadersrtmReaderbinDebugsrtmReader.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:TempsrtmReader.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 = @"N:GitHubKML for 3dsMaxTestDataS23W068.hgtS23W068.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 for instance.

 

Check out the other parts in this tutorial

Part 1
Part 2

 

This is a guest post written by Klaas Nienhuis.
Original article: http://www.klaasnienhuis.nl/2014/08/srtm-c-3ds-max/

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.