Monday, 31 August 2009

Fast interoperability of 2D shapes between 3D applications and your software

Preface

Wikipedia says that interoperability is “a property referring to the ability of diverse systems and organizations to work together (inter-operate)”. Regarding software, it says that interoperability is “the capability of different programs to exchange data via a common set of exchange formats”. This has always been a problem, specially when talking about software that manages 3D or 2D information, due to the volume and diversity of the information to be exchanged.

Why is that a problem?

In 2D images, for instance, it has always been more or less clear what kind of information applications should share: 2D tables of colors, expressed in one format or another, but color or amount of light, after all. There are some ‘de-facto’ standards like TGA (coming from IBM) and others developed by institutions or committees, like JPEG (name coming from Joint Photographic Experts Group). Some formats try to include other information, like opacity (alpha channels), or to express the same information in other forms, like the so-called HDR (High Dynamic Range) images, which need to store lighting information in floating point.
3D is a completely different story. Mostly because while 2D imaging is a mature field (and a relatively simple one too), 3D visualization is still changing a lot form month to month, with new techniques and algorithms, that need new and complex information stored in files. In addition to that, there are several kind of 3D applications, with different needs too: 3D modeling packages, real-time applications, videogames, etc. All this stuff makes interoperability a hard issue, and sharing a scene from two different applications can be a nightmare, with un-recognized parameters, missing information, etc.
However, and despite the written above, there have been many tries to become certain file formats a standard. It is the case of the old 3DS (from AutoDesk), the X File Format (from Microsoft –DirectX-), or the newer FBX, which is the latest try from AutoDesk to build a standard 3D file format, and is also supported by XNA.

The 2D shapes case

It may seem something obvious, but 3D formats are very focused in 3D, and 2D shapes are frequently left apart. For instance, neither 3DS or the default FBX file formats support 2D shapes. And that’s a problem, as 2D shapes like splines are very useful for many things in our software: paths or trajectories for animation, A.I., etc.

What’s the solution?

In fact, there are many solutions:
  1. Use a different file format that supports 2D shapes
  2. Use the FBX SDK to make your own exporters/importers
  3. Export 2D shapes as 3D objects (this is what 3DSMax does when using the 3DS file format, for instance).
  4. etc.
I guess the best option would be the second one: use the FBX format, which seems to be the next standard, and until support for 2D shapes is included on it, make your own exporters/importers. However, that would take quite a bunch of ours, learning to use the SDK, and making exporters and importers for the different 3D modeling packages round there. You should go this way, but if you do not have time enough, I´ll show you here an easier solution, using an existing file format.

Choosing the proper file format

There are several file formats which support 2D shapes, but not all of them are Open Formats. Among the usually supported by 3D modeling applications, we find two “mostly open” formats: the ASE file format (ASCII) and the OBJ file format, developed by WaveFront. Both of them would make it, but we will choose the second (OBJ) because the first one is a bit more complex, and because 3DSMax does not include an ASE importer by default (it comes with an exporter only).

The OBJ parser (2D shapes only)

Please note: I won’t write here a full parser of the OBJ format. We will just include a few lines of code to read OBJ files which hold information of 2D splines.
The following class will read each 2D shape present in the OBJ file, and store the list of vertices in a Dictionary (keyed by the name of the shape). Please note that I have just tested this code with OBJ files exported from 3DS Max, storing 2D splines only. You can make your own tests and change the code as necessary. A call to “LoadFile” will fill the “mFoundSplines” collection with the information of the shapes found in the file.
Note: Some 3D applications, like 3DSMax, use axis coordinates with the “Z” pointing “up”, while others prefer coordinates with the “Y” pointing up. In my case, this second option is the default, so if you want Z to point up (to read or save files from/to 3DSMax), you should set the parameter “pSwapYZ” to true in the call to LoadFile.
  public class OBJFileParser
  {
        public static Dictionary<string, DX.Vector3[]> mFoundSplines;
        private static System.IO.StreamReader mStrmReader;
 
        /// <summary>
        ///
        /// </summary>
        public static void LoadFile(string pFullFileName, bool pSwapYZ)
        {
            try
            {
                mFoundSplines = new Dictionary<string, Microsoft.DirectX.Vector3[]>();
                mStrmReader = new System.IO.StreamReader(pFullFileName);
                while (!mStrmReader.EndOfStream)
                {
                    string line = mStrmReader.ReadLine();
                    if (line.StartsWith("# shape"))
                    {
                        string[] parts = line.Split(' ');
                        if (parts.Length != 3)
                            continue;
                        ReadShape(parts[2], pSwapYZ);
                    }
                }
            }
            finally { mStrmReader.Close(); }
        }
        /// <summary>
        ///
        /// </summary>
        private static void ReadShape(string pShapeName, bool pSwapYZ)
        {
            List<DX.Vector3> vertices = new List<Microsoft.DirectX.Vector3>();
 
            while (!mStrmReader.EndOfStream)
            {
                string line = mStrmReader.ReadLine();
 
                // Skip lines starting with #
                if (line.StartsWith("#"))
                    continue;
 
                // Read Vertex
                if (line.StartsWith("v"))
                {
                    // Important !: There are two spaces (‘ ‘) between the “v” and the
                    // first component of the vector. Split will return 5 strings
                    string[] parts = line.Split(' ');
                    if (parts.Length != 5)
                        continue;
 
                    if(pSwapYZ)
                        vertices.Add(new Microsoft.DirectX.Vector3(float.Parse(parts[2]), float.Parse(parts[4]), float.Parse(parts[3])));
                    else vertices.Add(new Microsoft.DirectX.Vector3(float.Parse(parts[2]), float.Parse(parts[3]), float.Parse(parts[4])));
                }
 
                // Shapes have a line starting with "g" (plus the name of the shape), and 
                // another one with what seems to be indices to vertices. In this case,
                // we won´t read that info, so we will use the "g"-starting line to detect
                // the end of a shape
                if (line.StartsWith("g"))
                    break;
            }
 
            // Add Spline
            if (vertices.Count > 0)
                    mFoundSplines.Add(pShapeName, vertices.ToArray());           
        }
    }

The OBJ saver (2D shapes only)

We will save now our Splines following the OBJ format, and making sure that 3DS Max is able to read it. To use it you need to call each method like:
  • OBJFileSaver.StartNewFile(): To reset internal collections
  • OBJFileSaver.AddShape(): To add each shape you want to save
  • OBJFileSaver.SaveFile(): To finally save everything to disk.
Note: Some 3D applications, like 3DSMax, use axis coordinates with the “Z” pointing “up”, while others prefer coordinates with the “Y” pointing up. In my case, this second option is the default, so if you want Z to point up (to read or save files from/to 3DSMax), you should set the parameter “pSwapYZ” to true in the call to SaveFile.
    public class OBJFileSaver
    {
        private static System.IO.StreamWriter mStrmWriter = null;
        private static Dictionary<string, DX.Vector3[]> mShapes;
 
        /// <summary>
        ///
        /// </summary>
        /// <param name="pFullFileName"></param>
        public static void StartNewFile()
        {
            mShapes = new Dictionary<string, Microsoft.DirectX.Vector3[]>();
        }
        /// <summary>
        ///
        /// </summary>
        /// <param name="?"></param>
        /// <param name="pShape"></param>
        public static void AddShape(string pShapeName, DX.Vector3[] pShape)
        {
            mShapes.Add(pShapeName, pShape);
        }
        /// <summary>
        ///
        /// </summary>
        public static void SaveFile(string pFullFileName, bool pSwapYZ)
        {
            try
            {
                mStrmWriter = new System.IO.StreamWriter(pFullFileName);
                int vertexIdx = 1;
                foreach (string name in mShapes.Keys)
                {
                    WriteShape(name, vertexIdx, pSwapYZ);
                    vertexIdx += mShapes[name].Length;
                }
            }
            finally
            {
                mStrmWriter.Close();
            }
        }
        /// <summary>
        ///
        /// </summary>
        /// <param name="pName"></param>
        private static void WriteShape(string pShapeName, int pVertexStartIdx, bool pSwapYZ)
        {
            // Write header
            mStrmWriter.WriteLine("");
            mStrmWriter.WriteLine("#");
            mStrmWriter.WriteLine("# shape " + pShapeName);
            mStrmWriter.WriteLine("#");
            mStrmWriter.WriteLine("");
 
            // Write vertices. Important !!: Set two (2) spaces between "v" and X component of vertex.
            foreach (DX.Vector3 vec in mShapes[pShapeName])
            {
                if(pSwapYZ)
                    mStrmWriter.WriteLine(string.Format("v  {0} {1} {2}", vec.X, vec.Z, vec.Y));
                else mStrmWriter.WriteLine(string.Format("v  {0} {1} {2}", vec.X, vec.Y, vec.Z));
            }
 
            // Write number of vertices
            mStrmWriter.WriteLine("# " + mShapes[pShapeName].Length + " vertices");
            mStrmWriter.WriteLine("");
 
            // Write the "g" line, with the name of the shape again
            mStrmWriter.WriteLine("g " + pShapeName);
 
            // Write what seems to be a trivial list of indices to vertices.
            // It doesn´t re-start for each shape
            string indices = "l ";
            int vertexIdx = pVertexStartIdx;
            for (int i = 0; i < mShapes[pShapeName].Length; i++)
            {
                indices += vertexIdx + " ";
                vertexIdx++;
            }
            mStrmWriter.WriteLine(indices);
 
            mStrmWriter.WriteLine("");
            mStrmWriter.Flush();
        }
 
    }

 

Limitations of this class

Keep in mind that this class will only export the output geometry of your Splines (only vertex positions) and the name of the shape. Other parameters like Spline tension, continuity, tangents, etc is not included. Any other information regarding 3DSMax or any other modeling application (like interpolation properties) will not be saved neither.

Conclusion

Until more standard formats like FBX support 2D shapes (something that should happen soon), this is a very easy way to add support for 2D shapes to your applications.
Hope you find it useful !