• Blog
  • Work
  • Resources
    • Rigs
  • Resume
  • Contact
I do technical things...

Saving and Loading Mesh Data in 3ds Max .Net SDK

7/19/2014

0 Comments

 
I've been making some progress with 3ds Max and its .Net api. As a learning exerscise I thought I'd try and write some code that saves and loads mesh data back from XML using serialization.

Here's the 'Populate' method (part of the save process) that grabs the data needed from a given mesh (actually I have the ability to save multiple meshes per file). Later in the code the class this code lives in is serialized to XML.
   ///  
/// Function that populates necassary mesh data to this class, from scene data. Called before serialization. ///
  ///  
 The IINode, containing the mesh, to be serialized public void Populate(IINode node) {         // make sure the node is valid         if (node == null)                 return;          // get the object ref as a tri object         ITriObject triObj = MxsSharp.MeshOps.GetITriObject(node);         // bail if we failed to retrieve         if (triObj == null)                 return;                  // get the mesh/IMesh from the tri object         IMesh mesh = triObj.Mesh;         // bail if we failed to retrieve         if (mesh == null)                 return;          // initialize the base data variables          this.Verts = new List();         this.Faces = new List();         this.Normals = new List();         this.Name = node.Name;          // serialize the transform         this.Transform = new Matrix();         this.Transform.Populate(node);          // create serialized data for mesh vertex         int count = 0;         foreach (IPoint3 vPos in mesh.Verts)         {                             // Create custom Vertex class that can be serialized                 Vertex vrt = new Vertex();                 // Populate the vertex class with data - cannot do this on itilialize due to serialization                 vrt.Populate(vPos);                 // Add to this class' Verts list                 this.Verts.Add(vrt);                  count++;         }          // create serialized data for mesh faces         foreach (IFace iface in mesh.Faces)         {                 // Create custom Face class that can be serialized                 Face face = new Face();                 // Populate the Face class with data - cannot do this on itilialize due to serialization                 face.Populate(iface);                 // Add to this class' Faces list                 this.Faces.Add(face);         }          // Serialize any map channels the mesh may have.. uvs and such         if (mesh.NumMaps > 0)         {                 this.Maps = new List();                 for (int mi = 0; mi < mesh.NumMaps; mi++)                 {                         // Create custom MeshMap class that can be serialized                         MeshMap map = new MeshMap();                         // Populate the MeshMap class with data - cannot do this on itilialize due to serialization                         map.Populate(mesh, mi);                         // Add to this class' Maps list                         this.Maps.Add(map);                 }         }          // build / check normals before we serialize them. otherwise we may not have any normals.         mesh.BuildNormals();         mesh.BuildRenderNormals();                  // create serialized data for mesh normals         for (int i = 0; i < mesh.NormalCount; i++)         {                     // Create custom Normal class that can be serialized                             IPoint3 inml = mesh.GetNormal(i);                 Normal normal = new Normal();                 // Populate the Normal class with data - cannot do this on itilialize due to serialization                 normal.Populate(inml);                 // Add to this class' Normals list                 this.Normals.Add(normal);         }          mesh.DisplayNormals(1, 3.0f);   }   
Below is the code that is called after my class is de-serialized and I want the loaded data re-created in the scene. It accesses data outside of this method, that lives in its containing class, but it should be pretty self explanatory.
   ///  
/// This method is used to build a mesh with the data contained in the class. /// It will ususally will be called when the object is rebuilt during deserialization ///
   ///  private ITriObject BuildMesh(bool build_normals = true, bool build_maps = true) {         // got guidance from here : http://eliang.blogspot.com/2011/10/creating-triangle-mesh-with-3ds-max-sdk.html          // Make a new TriObject class         ITriObject triobj = Kernel.Global.TriObject.Create();         // bail if null         if (triobj == null)                 return null;          // grab the IMesh instance in the TriObject         IMesh mesh = triobj.Mesh;         // bail if null         if (triobj == null)                 return null;          int numVertices =   this.Verts.Count;         int numTriangles =  this.Faces.Count;         int numNormals =    this.Normals.Count;         int numMaps =       this.Maps.Count;          // set vertex array size in the mesh         mesh.SetNumVerts(numVertices, false, true);         // set triangle array size in the mesh         mesh.SetNumFaces(numTriangles, false, true);          // Loop and set vertex positions         for (int i = 0; i < numVertices; i++)                 mesh.SetVert(i, this.Verts[i].Position.ToIPoint3());          // Loop and set the basic face data         for (int i = 0, j = 0; i < numTriangles; i++, j += 3)         {                 int v0 = (int)this.Faces[i].Verts[0];                 int v1 = (int)this.Faces[i].Verts[1];                 int v2 = (int)this.Faces[i].Verts[2];                  // vertex positions                 IFace face = mesh.Faces[i];                 face.MatID = 1;                 face.SetEdgeVisFlags(this.Faces[i].EdgeVis[0], this.Faces[i].EdgeVis[1], this.Faces[i].EdgeVis[2]);                 face.SetVerts(v0, v1, v2);                  // set the fac smoothing group if there was one set.                 if (this.Faces[i].SmoothingGroup != -1)                 {                         face.SmGroup = (uint)this.Faces[i].SmoothingGroup;                 }          }          // build uv information using the maps of the mesh         if (build_maps == true)         {                 // initialize the map array to the needed size                 mesh.SetNumMaps(numMaps, false);                  // start at 1 as zero is Vert color.. i think.                 for (int m = 1; m < numMaps; m++)                 {                         // enable map support                         mesh.SetMapSupport(m, true);  // enable map channel                          // get the current IMeshMap iteration                         IMeshMap map = mesh.Maps[m];                          // set num of verts to the vert count of the map List<>                         int numMapVerts = this.Maps[m].TVerts.Count;                         mesh.SetNumMapVerts(m, numMapVerts, false);                          for (int i = 0; i < numMapVerts; i++)                         {                                 mesh.SetMapVert(m, i, (this.Maps[m].TVerts[i]).ToIPoint3());                         }                           // set num of verts to the face count of the map List<>                         int numMapFaces = this.Maps[m].TFaces.Count;                         mesh.SetNumMapFaces(m, numMapFaces, false, 0);                          // grab the now intitialzed list of faces                         IList mapFaces = mesh.MapFaces(m);                          // loop and associate vertecies by the stored indecies.                         for (int i = 0; i < numMapFaces; i++)                         {                                 ITVFace tFace = mapFaces[i];                                 int v0 = (int)this.Maps[m].TFaces[i].Verts[0];                                 int v1 = (int)this.Maps[m].TFaces[i].Verts[1];                                 int v2 = (int)this.Maps[m].TFaces[i].Verts[2];                                  tFace.SetTVerts(v0, v1, v2);                         }                  }         }          // build normal information for each vert and face association         if (build_normals == true)         {                 // call this to initialize the normal arrays                 mesh.BuildNormals();                  // tell the mesh we will be specifying normals                 mesh.SpecifyNormals();                  // class to aid normal updating                 IMeshNormalSpec normalSpec = mesh.SpecifiedNormals;                                  // clear any pre calculated normals                 normalSpec.ClearNormals();                  // set the normal vertex array size                 normalSpec.SetNumNormals(numVertices);                  // loop and set vertex normals                 for (int i = 0; i < numVertices; i++)                 {                         IPoint3 ip3 = (this.Normals[i].Direction.ToIPoint3()).Normalize;                         normalSpec.NormalArray.Set(ip3.X, ip3.Y, ip3.Z);                         normalSpec.SetNormalExplicit(i, false);                 }                  // set normal face array size                 normalSpec.SetNumFaces(numTriangles);                 for (int i = 0, j = 0; i < numTriangles; i++, j += 3)                 {                         int v0 = (int)this.Faces[i].Verts[0];                         int v1 = (int)this.Faces[i].Verts[1];                         int v2 = (int)this.Faces[i].Verts[2];                          // vertex positions                         IFace face = mesh.Faces[i];                          // vertex normals                         IMeshNormalFace normalFace = normalSpec.Face(i);                          normalSpec.SetNormal(i, v0, (this.Normals[v0].Direction.ToIPoint3()).Normalize);                         normalSpec.SetNormal(i, v1, (this.Normals[v1].Direction.ToIPoint3()).Normalize);                         normalSpec.SetNormal(i, v2, (this.Normals[v2].Direction.ToIPoint3()).Normalize);                          normalFace.SpecifyAll(true);                         //normalFace.SetNormalID(0, v0);                         //normalFace.SetNormalID(1, v1);                         //normalFace.SetNormalID(2, v2);                         normalFace.SetSpecified(0, true);                         normalFace.SetSpecified(1, true);                         normalFace.SetSpecified(2, true);                  }         }          // tell the mesh to rebuild its internal cache's... like a recompute.         mesh.InvalidateGeomCache();         mesh.InvalidateTopologyCache();                  return triobj; }  ///  
/// Is called after the class is deserialized from xml. Takes the data that was deserialized and /// creates/sets max scene data as needed. /// Called from the MeshFile.DoDeserialize() ///
   public void DoDeserialize() {         if (this.Verts == null)                 return;         if (this.Faces == null)                 return;          ITriObject triObj = this.BuildMesh();          // if the tri obj creation clang'd out         if (triObj == null)         {                 Kernel.Print("Tri Object creation failed!");                 return;         }         string tmp_name = this.Name;         Kernel.Interface.MakeNameUnique(ref tmp_name);         IINode node = Kernel.Interface.CreateObjectNode(triObj, tmp_name);          node.SetNodeTM(0, this.Transform.ToIMatrix3()); }   
The full code currently saves and re-builds mesh vertices, faces, normals and uv maps to and from xml, and is nice and fast too. I was pretty suprised ho quickly I could cobble this together also, all in all it took around a weekend. The code needs some cleanup and could use some more error checking and handling, but the guts of the it is there.

When starting out, I scoured the net for some help on this, and found a few nuggets on cgtalk and other sites. However this is the post that helped me the most. So many thanks to Chang-Hung Liang for posting that! As a result I thought I'd post my findings also... to help the next person.
0 Comments

Volume Range Node: Max Edition

2/6/2014

0 Comments

 
I previously posted about my custom maya node that does some 'pose' based calculations, for use in driving deformations and what not.

I recently found myself wishing I had it to use in 3ds Max. So, I wrote one.
Below I'm just driving three point and orient constraint weights via the output of the node.
This is generally a bit simpler than my Maya version. It just defines two radius' and remaps the angle, between two matrix axis vectors, from 0-1 (or reverse). The Maya node allowed a definition of an ellipse shape for both min and max 'areas', so I could elongate the shape over one axis more than another. However I'm getting good results from just the circular shapes, so I'm happy for now.

To achieve this I wrote a Scripted Manipulator Plugin to draw the shapes, and to store the needed parameters for node associations, to control the shape and store the output. Then, inside the Scripted plugin, I'm adding a Scripted Controller, on creation, to the Output parameter.  The scripted controller is actually pretty simple:
   /* Float Scripted Controller code that handles 'pose space' calculations. It is assigned to the output of our custom node  The Float Script controller has two node variables assigned to it: 	srcObj : 	this is essentially 'this' object... the object this controller is assigned to 	targObj : 	The Target node that we are comparing axese to */  -- the return value... init with invalid value local theOutput = -1.0;  -- make sure the source node is valid if srcObj != undefined then ( 	 	-- make sure the target node is valid 	if targObj != undefined then ( 	 		-- this is the angle variable that will be converted to a valid 0-1 output 		local theAngle = 0.0; 		 		-- Grab this nodes z axis... we always use the z axis for the source (this) node 		local srcAxis = srcObj.transform.row3  		-- Here we are getting some 'settings' from properties on the target node... 		-- ..here we grab the target axis of the target node based on the drop down ui/property user specification 		local targAxis = [0,0,0] 		if srcObj.TargetAxis == 1 then targAxis = targObj.transform.row1; 		if srcObj.TargetAxis == 2 then targAxis = targObj.transform.row2; 		if srcObj.TargetAxis == 3 then targAxis = targObj.transform.row3;  		-- calculate the angle between the two axes..  		theAngle = (acos (dot targAxis srcAxis ));  		-- again grab more settings from the source node. Here we grab the min and max 		-- angle settings 		local minimum = srcObj.innerRadius; 		local maximum = srcObj.outerRadius; 		 		-- Here we are calculating the output based on another node property setting. 		if srcObj.ReverseOutput == false then(	 			-- default calculates output as a 0-1 value.. 'inside' the specified cone 			if theAngle < minimum then theOutput  = 1.0; 			else if theAngle > maximum then theOutput = 0.0; 			else theOutput = 1 + -((theAngle - minimum) / (maximum - minimum)) ;				 		)else( 			-- Reverse output is a 0-1... outside the cone..  			if theAngle < minimum then theOutput = 0.0; 			else if theAngle > maximum then theOutput = 1.0; 			else theOutput = ((theAngle - minimum) / (maximum - minimum)) ;				 			 		) 	) ) -- return / set the controller value theOutput ;   
It all seems pretty fast, so that's a plus. I was dubious of the speed needs for something like this using Maxscript and Scripted Controllers, but it seems decent.
0 Comments

A 3DS Max Gui you say?

1/24/2014

0 Comments

 
Wow. Almost Ten months of inactivity and two posts within a few hours!

So, recently I finished up a first pass on a GUI idea I've had for a while (though it's mainly a 'picker' at the moment).

I initially did a few prototypes using maxscript, with the dot net wrappers, and also plain c# winforms. However, I could not, easily, achieve some key features I wanted. Or, lets say, they were messier to manage when implementing these features. 
Mainly I wanted  the ability to pan and zoom around. Scale and pan functionality would allow me to really fill a gui 'page' with lots of stuff. From there the user can zoom and focus in to the area they are interested in, fingers for example. Out of the 3, WPF definitely handled 'transforming' ui elements the best.

I also wanted to be able to create and edit gui layouts inside the tool window, being able to lay out new button and edit existing ones as needed, even by an animator.

So here's what I ended up with:
In the video I'm using a default Cat Ape rig as an example, then creating and associating nodes with the gui form. 

To achieve this I used WPF for the window and buttons layout code. In fact it is all done in c#, using the .Net api to handle the Max node selections and some callbacks.

WPF uses lovely vector graphics to draw its ui elements so the implementation of my zoom functionality works pretty well, other than  the background image looking pixelated when zoomed right in!

For the 'Editing Mode' I had to implement a custom property on a class inherited from a Canvas. I had originally tried to manipulate an InkCanvas to my will, as it already has an 'Editing Mode' and the ability to move and manipulate child UI Elements. However, there were some drawing and selection issues with that that didn't fit what I wanted. 
I was dubious about the complexity on managing a 'state' like this across the drawing elements manually, as this was my first forray into WPF. It worked out pretty well in the end. Implementing it this way allowed me to control the mode changes with all the granularity I needed, and it wasn't that much work.

Again, I used the .Net api for the selection, callbacks and all max scene needs. I did, originally, plan on using maxscript for the max side stuff and exposing events through c#, then hooking them up on the other end, as outlined in this document. However, that code looked a bit messy, management wise, and didn't look like it would scale well. I really wanted to keep it all together and clean. 

After some searching, digging and trial and error I managed to get what I needed from max's .Net dll's. 
Now, it should be noted I am not doing anything fancy with the .Net API here. Mostly, I'm re-cursing the scene tree, grabbed from the Autodesk.Max.IInterface13.COREInterface13 instance, and collecting the IINodes I need. Once I have the IINodes selecting and deselecting becomes very easy.

It was a huge win to have everything in one project and codebase!
All in one VS Project!
All in one VS Project!
I inherited the ui elements I needed from basic WPF classes; buttons, canvas, tabs, etc, in to custom classes that simply add some properties and methods that store IINodes and manage the lists of IINodes.
I also implemented selection and file open callbacks to refresh and validate the nodes in the classes as needed. 

I created two 'types' of buttons the user can add to the gui, seen in the video. The first is a 'Selector', that is associated with a single node via name, and the second is an 'Executor' button, that can select multiple nodes and also run basic maxscript commands via string.  This allows me to be able to create the selection buttons and have 'selection set' and tools buttons.

I use WPF Adorners in Editing Mode to manipulate the with and height of the buttons, relative to their parent canvas. Adorners were a little tricky to get my mushy brain around, as it's basically an invisible layer on top of the ui element, and you have use the 'thumbs' in the Adorner system to drive what you want. However, once you get used to it (or as in my case got a really good example from the net and re-work it to my needs) it makes sense and really didnt need too much maintenance. 

Any 'gui layout' can be saved to loaded from xml files, that simply describe the buttons, tab and properties.

Again this is Pretty first pass stuff. There are lots of things I'd like to add to this in the future, sliders to manipulate properties and auto generate rig buttons to name a couple. For now the foundation is in and working really well. 

Also, it was really nice to use c# for a while. Damn nice language.
0 Comments

Shadows Of The Past!

1/24/2014

0 Comments

 
So recently, on the webs of the net, dropped the first gameplay video of Middle Earth : Shadow of Mordor.

Middle-earth: Shadow of Mordor

I had the great privilege of working on this title while at WB in Seattle. I was a Rigger/Technical Artist on this for around a year, mostly in pre-production... Those orcs look very familiar :D.

It's really, really great to see it, and looking damned sweet. The guys there have really taken it on leaps and bounds over the last year and I'm really excited to see what my old comrades have done, when I get to play it.  It really is such a talented team. Fond memories.
0 Comments

Writing Out a Max Node's Animation

4/4/2013

0 Comments

 
I'm in the midst of working on a Max animation clip saving tool, so I thought I'd share some progress.  This is one of the first substantial tools I've worked on since moving to 3Ds Max, from Maya, and it's proving a good learning experience for understanding some of Max's underpinnings. Max is quite different, to Maya, in how it encapsulates an derives an end matrix. At first the 'controller' tracks, used for node transformations, can seem a bit confusing. It can be very powerful, but also can tend make things overly complicated.

..read full post
0 Comments

Guardians Of Middle Earth!

8/9/2012

0 Comments

 
Just thought I'd share a video of an upcomming title I worked on. Not only was it a blast to work on, it's a blast to play!


0 Comments

Volume Range Node

3/25/2012

2 Comments

 
I've been playing about with Maya api, with python, a bit more. I took my experiences from this post on a bit further. The previous node i created, in the mentioned post, had a few downfalls as to useability. The main one was I could only define a radial offset from the comparison axis. I wanted to be able to define a more arbitrary shape for the angle comparison, essentially I wanted to elongate the shape, This would help me define more useable shapes to define as my target pose. If that makes any sense! Anyway here’s the node in action.

The node outputs a 0-1 blended value representing if the axis of the node is within the range set. I've just piped that output into a sphere for visual guide.

This node should aid my rigging in terms of deformations. My main reason for wanting to create this, was a solution to some armor rigging that is needed in work. Here's a super rough, quick test where I'm driving an aim constraint and its up vector node via three volume range nodes.
And here’s the obligatory bicep deformation shot. Again a very rough test, but should help give you the idea... like you needed it!
2 Comments

Getting COM (win32com) working in Maya 2012

2/29/2012

0 Comments

 
I'm starting to look into Communicating between applications from Maya. So If you’re looking to install the COM python module for Maya, here's how I got it working.

Get the appropriate installer here
    For Maya 2012 64 bit get the pywin32-217.win-amd64-py2.6.exe
    For Maya 2012 32 bit get the pywin32-217.win32-py2.6.exe
Run the downloaded executable, it should install to your C:\Python26\Lib\site-packages.
Copy the contents of that folder to your C:\Program Files\Autodesk\Maya2012\Python\lib\site-packages.

That should do it!

Of course be aware of what other packages you had installed previously in both site package folders, if you're concerned about overwriting something.
0 Comments

Sometimes I feel stupid.

2/21/2012

2 Comments

 
..Maybe because I am.
    ls -type "joint" "|*" ;
Sometimes we just need to get something working, and we write functions to get simple pieces of data or whatever. Well I was at my desk recently and the above piece of code just dawned on me. The amount of times I've written code to get all joints at the root of the scene, looping parent queries.... Sometimes I feel stupid.

2 Comments

Custom Maya halt window.

2/21/2012

0 Comments

 
If you want to display a custom ui and halt all other maya ui interaction whilst it's open, here’s a script.

This uses Maya’s new-ish layoutDialog ui element, and it basically lists things and returns the user's specification. Nothing special, but it is nice that it basically acts like a confirmDialog.. The code is a bit hacked together as I originally thought I could get a textScrollList, in python, to simply accept data with some *args or **kargs callback functions, but no joy there. So I had to wrap this up in a class and use some class attributes to store and access the needed data on events.

Anyway here's the script. This may not be the cleanest way to do this, but I had to get it working. Hope someone finds it useful.

selectitemdialogui.py
File Size: 3 kb
File Type: py
Download File

0 Comments
<<Previous

    Andy T.

    I do Tech things for Artist.

    Archives

    July 2014
    February 2014
    January 2014
    April 2013
    August 2012
    March 2012
    February 2012
    December 2011
    July 2010

    Categories

    All
    3ds Max C#
    3ds Max .net Api
    Animation
    Api
    Face Rig
    Maxscript
    Maya
    Mel
    Python
    Python Api
    Rigging
    Scripting
    Ui
    Work

    Links To Cool Folks

    Mic Marvin
    Alex Utting
    Rick Vicens
    Gary Talbot
    Richie Prado
    Felipe Nogueira

    RSS Feed