This is a guest post written by Klaas Nienhuis.
Part 3

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!
![]() |
![]() |
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.




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.




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.


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.
![]() |
![]() |
This is a guest post written by Klaas Nienhuis.
Original article: http://www.klaasnienhuis.nl/2014/08/srtm-c-3ds-max/
Leave a Reply