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.
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.
///
///
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.