Notifications
Clear all

[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

20 Replies
1 Reply
(@denist)
Joined: 11 months ago

Posts: 0

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, it’s 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

the main problem for the MXS method will be a memory use.

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.

1 Reply
(@sinokgr)
Joined: 11 months ago

Posts: 0

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.

Page 1 / 2