[Closed] getVert using MXS, dotnet and C#
Hi there,
A couple days ago, I was experimenting with an MXS exporter and it was really slow. So I’ve decided to do some R&D and see how faster can C# be. So I run into the C++ SDK in order to find how I can get the IMesh from an object and I found this.
There is some sample code there that I’ve tried to implement in CS but I definitely missed the delete part that I don’t fully understand so I can’t implement but I don’t feel that it affects my tests though. (if anyone can let me know what is going on there, that would be great)
Nevertheless I came out with this C# code
using Autodesk.Max;
namespace exportMesh
{
static public class Class1
{
static IGlobal global = GlobalInterface.Instance;
static private ITriObject GetTriObjectFromNode(IINode node)
{
IObject obj = node.EvalWorldState(0, true).Obj;
IClass_ID myClass = global.Class_ID.Create(9, 0);
if (obj.CanConvertToType(myClass) == 1)
{
ITriObject tri = (ITriObject)obj.ConvertToType(0, myClass);
return tri;
}
else
{
return null;
}
}
static public void exportMesh(uint handle)
{
IINode node = global.COREInterface.GetINodeByHandle(handle);
IMesh myMesh = GetTriObjectFromNode(node).Mesh_;
int vertsNum = myMesh.NumVerts;
//Get vert positions
for (int i = 1; i <= vertsNum; i++)
{
myMesh.GetVert(i);
}
}
}
}
Now, back in 3dsMax I tried to getVerts with three different ways. First of all I did what I would normally do in MXS. Secondly, I tried to implement the C# code using MXS along with the Autodesk.Max wrapper. And last but not least, I loaded the DLL and used the method I’ve developed.
(
--Load assembly
dotNet.loadAssembly @"c:\exportMesh.dll"
--Prepare scene
delete objects
myTeapot = teapot()
addModifier myTeapot (turboSmooth iterations:5)
--MXS
t = timestamp()
myMesh = myTeapot.mesh
vertsNum = meshOp.getNumVerts myMesh
for i=1 to vertsNum do ( meshOp.getVert myMesh i )
format "MXS took %s
" ((timestamp() - t)*.001)
--DotNet
t = timestamp()
g = (dotNetClass "Autodesk.Max.GlobalInterface").instance
myClass = g.Class_ID.create (dotNetClass "Autodesk.Max.BuiltInClassIDA").TRIOBJ_CLASS_ID.value__ 0
myINode = g.CoreInterface.GetINodeByHandle myTeapot.inode.handle
myOS = (myINode.EvalWorldState 0 true).obj
myTriObject = myOS.ConvertToType 0 myClass
myMesh = myTriObject.Mesh__TriObject
vertsNum = myMesh.numVerts
for i=1 to vertsNum do ( myMesh.getVert i )
format "DotNet took %s
" ((timestamp() - t)*.001)
--CS
t = timestamp()
(dotnetclass "exportMesh.Class1").exportMesh myTeapot.inode.handle
format "CS took %s
" ((timestamp() - t)*.001)
)
The results I’m getting are the following
MXS took 0.918s
DotNet took 22.889s
CS took 0.008s
OK
So first of all, am I doing the test right and if I do, are these differences normal?
I’ve used a streamwriter in CS in order to make sure that I was reading the postions and it worked so I suppose my C# code is fine.
Does it make any sense that the second version is so much slower?
Cheers,
Nick
all numbers are absolutely expected…
let’s start with DotNet…
DotNet
on every iteration of the loop the system creates two wrapper .net objects:
function(dotnetmethod) – myMesh.getVert
and
result(dotnetobject) – (myMesh.getVert i)
we can save one creation by using
dotnet_getVert = myMesh.getVert
for i=1 to vertsNum do (dotnet_getVert i)
it will make the method faster… but the second object has to be created anyway and converted after that from dotNetObject:Autodesk.Max.Wrappers.Point3 to Point3 (it also needs a time)
so … don’t use Autodesk.Max the way shown above!
MXS
all is predictable as well.
we can save a time if we put meshOp.getVert to a variable
_getVert = meshOp.getVert
for i=1 to vertsNum do ( _getVert myMesh i )
we talked many times about it… and why it makes the method faster.
so with optimization i got ~0.5 sec on my machine for the test. (where ~ 0.3 sec is the loop only).
we have to understand that the real mesh are stored in a c++ object. using mxs we have to get access to this object passing a pointer to mxs object (mesh in our case), and convert a result from api value to mxs value. it needs a time. and mxs does do it pretty fast!
CS
the system already returns you cs api objects. after that to get a vertex data from it doesn’t take any time… technically only converting from c++ mesh to cs mesh might take a time. but as we see the system does do it fast.
we can’t beat cs and of course c++ methods with mxs. that’s why all ‘heavy’ interactions with the system (export/import for example, mesh deformation, scene exploring, etc.) have to be done with SDK
but…
let’s look at our results for mxs and cs again.
we have ~0.5 and ~nothing… to save the data to binary file will take approximately the same time… ~2 secs … maybe more. to save the same amount of data to a text file ~8 secs… to XML or JSON much more.
so
are ‘much more’ + 0.5s and ‘much more’+‘nothing’ very different?
you could also test the object type first before converting with something like, save on the call to ConvertToType and some memory overheads
the c++ would be…
if (obj->IsSubClassOf(triObjectClassID))
{
Mesh& mesh = ((TriObject*)obj)->GetMesh();
// do stuff with mesh
}
else if (obj->IsSubClassOf(polyObjectClassID))
{
MNMesh& pmesh = ((PolyObject*)obj)->GetMesh();
// do stuff with mesh
}
else if (obj->IsSubClassOf(patchObjectClassID))
{
PatchMesh& pmesh pmesh = ((PatchObject*)obj)->GetMesh();
}
else if (obj->CanConvertToType(triObjectClassID)) // all others should try to convert to a triobject,
{
TriObject *triObj = (TriObject *)obj->ConvertToType(0, triObjectClassID);
Mesh& mesh = triObj->GetMesh();
}
btw theres also the ObjectWrapper class which can simplify working with mesh/mnmesh & patchmesh
The MXS and .Net times look fine to me. What surprises me is the big difference with the C# implementation.
In MXS you could use getvert instead of meshop.getvert, but based on your results, its impossible to beat your C# times with pure MXS.
(
--Prepare scene
delete objects
myTeapot = teapot()
addModifier myTeapot (turboSmooth iterations:5)
--meshOp
t = timestamp()
myMesh = myTeapot.mesh
vertsNum = meshOp.getNumVerts myMesh
for i=1 to vertsNum do ( meshOp.getVert myMesh i )
format "meshOp took %s
" ((timestamp() - t)*.001)
--getVert
t = timestamp()
myMesh = snapshotasmesh myTeapot
for i=1 to myMesh.numverts do getVert myMesh i
format "getVert took %s
" ((timestamp() - t)*.001)
free myMesh
--Loop Only
t = timestamp()
myMesh = snapshotasmesh myTeapot
for i=1 to myMesh.numverts do ()
format "Loop Only took %s
" ((timestamp() - t)*.001)
free myMesh
gc()
)
btw,
for (int i = 1; i <= vertsNum; i++)
{
myMesh.GetVert(i);
}
should be
for (int i = 0; i < vertsNum; i++)
{
myMesh.GetVert(i);
}
sdk run as 0 based arrays
Thank you all for your replies
@Klunk-1 Thanks for the sample code. I’ll try to implement it in C#. The weird thing about the arrays though, it was that I did run into an error and that printed results seemed reasonable. Thanks though. I’ll fix it!
@PolyTools3D Thanks for the sample code. Yeah, I know about the difference in speed with the getVert method. There is a thread not so long ago that we were discussing it with DenisT but unfortunately, the power of habit… grr Thanks though.
@DenisT New avatar? Nice heheh Yeah, I expecting that the C# and the MXS difference was reasonable but I couldn’t understand why the dotNet was so slow. So, thanks very much for your detailed explanation. Now I have a way better understanding on how all three attemps are working and why I get these differences! In terms of the getVert method… yes… I know…
So in your conclusion you say “are ‘much more’ + 0.5s and ‘much more’+‘nothing’ very different?”. I’m not very sure about what I’m going to say, I have to test it when I have some free time, but in my initial MXS test, I was exporting the data in Binary format and it was slow… so I commented out the saving code and I just left it reading the mesh data… but I didn’t saw any huge difference in exporting times. So I though that it was slower for MXS to get the data than save it. Anyway, I should do some more testing on this.
In my C# code, is it very wrong that I’m not working with the pointers and references like the example in the SDK documentation?
Many thanks,
Nick
The weird thing about the arrays though, it was that I did run into an error and that printed results seemed reasonable.
code like that would usually crash a pre os10 mac and usually crashes max if done in the sdk.
sorry, that was a typo. I wanted to say “I didn’t run into an error”. It printed all the positions. The last on though it was [0,0,0] but since I didn’t saw any other zeroed out values, I though that it was the bottom vertex laying onto the world zero. Now I suppose that value was because I was out of bound and I should have probably skipped the first vertex that it would possibly returned to me the same value. I must have been a coincidence. I’ll try to find some time over the weekend to do some more testing and understand why I didn’t got any errors.
Thanks,
Nick
i’ve recently designed this logo. from now on it will mark all my public tools.
Hi again,
I don’t know if I implemented correctly your C++ code Klunk but this is what I got
Revised old method
using Autodesk.Max;
using System.IO;
namespace exportMesh
{
public class Class1
{
//global interface
static IGlobal global = GlobalInterface.Instance;
//Get mesh from a given node
static private IMesh GetMeshFromNode(IINode node)
{
IObject obj = node.EvalWorldState(0, true).Obj;
IClass_ID triClass = global.Class_ID.Create(9, 0);
IClass_ID polyClass = global.Class_ID.Create(1562457754, 0);
IClass_ID patchClass = global.Class_ID.Create(4144, 0);
//Check if it is a triobject, polyobject or patchobject and get the mesh. otherwise try to convert it into triobject and get the mesh.
if (obj.IsSubClassOf(triClass))
{
return ((ITriObject)obj).Mesh; //Doesn't have a getMesh method
}
else if (obj.IsSubClassOf(polyClass))
{
//return ((IPolyObject)obj).Mesh; //How do I convert an IMNMesh to an IMesh? Cast didn't work.
ITriObject mesh = (ITriObject)((IPolyObject)obj).ConvertToType(0, triClass);
return mesh.Mesh;
}
else if (obj.IsSubClassOf(patchClass))
{
return ((IPatchObject)obj).GetMesh(0);
}
else if (obj.CanConvertToType(triClass) == 1)
{
ITriObject tri = (ITriObject)obj.ConvertToType(0, triClass);
return tri.Mesh_;
}
else
{
return null;
}
}
//Export mesh method
static public void exportMesh(uint handle, string filename)
{
IINode node = global.COREInterface.GetINodeByHandle(handle);
IMesh myMesh = GetMeshFromNode(node);
//Make sure you got a valid mesh
if (myMesh != null)
{
//Create binnary writer
BinaryWriter writer = new BinaryWriter(File.Open(filename, FileMode.Create));
//Get verts/faces
int vertsNum = myMesh.NumVerts;
int facesNum = myMesh.NumFaces;
//Write verts/faces
writer.Write((ulong)vertsNum);
writer.Write((ulong)facesNum);
//Get vert data
for (int i = 0; i < vertsNum; i++)
{
IPoint3 vertPos = myMesh.GetVert(i);
writer.Write((double)vertPos.X);
writer.Write((double)vertPos.Y);
writer.Write((double)vertPos.Z);
}
//Close file
writer.Close();
}
}
}
}
ObjectWrapper Method
using Autodesk.Max;
using System.IO;
namespace exportMesh
{
class Class2
{
//global interface
static IGlobal global = GlobalInterface.Instance;
//Export mesh method
static public void exportMesh(uint handle, string filename)
{
//Create binnary writer
BinaryWriter writer = new BinaryWriter(File.Open(filename, FileMode.Create));
//Prepare object
IINode node = global.COREInterface.GetINodeByHandle(handle);
IObjectState os = node.EvalWorldState(0, true);
IObjectWrapper ow = global.ObjectWrapper.Create();
ow.Init(0, os, false, 0, 0);
//Get verts/faces
int vertsNum = ow.NumVerts;
int facesNum = ow.NumFaces;
//Write verts/faces
writer.Write((ulong)vertsNum);
writer.Write((ulong)facesNum);
//Write vert data
for (int i = 0; i < vertsNum; i++)
{
IPoint3 vertPos = ow.GetVert(i);
writer.Write((double)vertPos.X);
writer.Write((double)vertPos.Y);
writer.Write((double)vertPos.Z);
}
//Close file
writer.Close();
}
}
}
Max script code
(
--Load assembly
dotNet.loadAssembly @"c:\exportMesh.dll"
--Prepare scene
delete objects
myTeapot = teapot()
addModifier myTeapot (turboSmooth iterations:4)
format "[onMesh]
"
convertToMesh myTeapot
--Mesh 1
t = timestamp()
(dotnetclass "exportMesh.Class1").exportMesh myTeapot.inode.handle @"C:\Mesh1.geo"
format " OldMethod: %s
" ((timestamp() - t)*.001)
--Mesh 2
t = timestamp()
(dotnetclass "exportMesh.Class2").exportMesh myTeapot.inode.handle @"C:\Mesh2.geo"
format " ObjectWrapper: %s
" ((timestamp() - t)*.001)
format "[onPoly]
"
convertToPoly myTeapot
--Poly 1
t = timestamp()
(dotnetclass "exportMesh.Class1").exportMesh myTeapot.inode.handle @"C:\Poly1.geo"
format " OldMethod: %s
" ((timestamp() - t)*.001)
--Poly 2
t = timestamp()
(dotnetclass "exportMesh.Class2").exportMesh myTeapot.inode.handle @"C:\Poly2.geo"
format " ObjectWrapper: %s
" ((timestamp() - t)*.001)
)
results
[onMesh]
OldMethod: 0.021s
ObjectWrapper: 1.572s
[onPoly]
OldMethod: 0.05s
ObjectWrapper: 0.022s
OK
The objectWrapper on Meshes seems to take a lot longer than the old method and the Old is slower on editPolys…
ObjectWrapper is only a “convenience” class to save you from having to know the ins and outs and nuances of mesh, mnmesh and patchmesh. If speed is really that much of a concern you can write code to handle each type individually which is generally the way you would do it for things like modifiers but for exporters there far more time consuming task to worry about than mesh accessing times such as Iterating the node hierarchy and material tree in large files, reconstructing the mesh into a game vertex buffer, writing the file to disc. My personal preference for exporters is IGame for the mesh tasks because it handles all the normals, binormals and tangents correctly for very little effort, though it’s a bit of a brute when it comes to memory as for speed, It’s never even cross my mind to check it.