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.
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 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.
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.
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.
5 Comments
Join the discussion and tell us your opinion.
[…] Part three […]
[…] Part three[/caption] […]
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.
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!
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 !