Tuesday, April 14, 2015

OpenTK Tutorial 7: Simple Objects from OBJ files

This tutorial is going to be rather short. What we'll do is add a new class that can open very simple OBJ files and add some sample models to the project.



First, we need to make a small change to the Volume class. Up until now, the VertCount, IndiceCount, and ColorDataCount were fields. We're going to need to make them into properties, so that we can override them in later classes, without changing any functionality in our current classes. Replace the current definitions of these fields with the following:

        public virtual int VertCount { get; set; }
        public virtual int IndiceCount { get; set; }
        public virtual int ColorDataCount { get; set; }

In our existing classes, this change won't have any noticeable effects. The new properties will seamlessly replace the fields, and changing/reading their values won't appear to be any different.

Now, create a new class called ObjVolume. This class will add some new functionality to our Volume class to load an OBJ file.

To begin, we'll need to make the class a subclass of the Volume class that makes it easy for us to set the model data:

    class ObjVolume : Volume
    {
        Vector3[] vertices;
        Vector3[] colors;
        Vector2[] texturecoords;

        List<Tuple<int, int, int>> faces = new List<Tuple<int, int, int>>();

        public override int VertCount { get { return vertices.Length; } }
        public override int IndiceCount { get { return faces.Count * 3; } }
        public override int ColorDataCount { get { return colors.Length; } }

        /// <summary>
        /// Get vertices for this object
        /// </summary>
        /// <returns></returns>
        public override Vector3[] GetVerts()
        {
            return vertices;
        }

        /// <summary>
        /// Get indices to draw this object
        /// </summary>
        /// <param name="offset">Number of vertices buffered before this object</param>
        /// <returns>Array of indices with offset applied</returns>
        public override int[] GetIndices(int offset = 0)
        {
            List<int> temp = new List<int>();

            foreach (var face in faces)
            {
                temp.Add(face.Item1 + offset);
                temp.Add(face.Item2 + offset);
                temp.Add(face.Item3 + offset);
            }

            return temp.ToArray();
        }

        /// <summary>
        /// Get color data.
        /// </summary>
        /// <returns></returns>
        public override Vector3[] GetColorData()
        {
            return colors;
        }

        /// <summary>
        /// Get texture coordinates
        /// </summary>
        /// <returns></returns>
        public override Vector2[] GetTextureCoords()
        {
            return texturecoords;
        }


        /// <summary>
        /// Calculates the model matrix from transforms
        /// </summary>
        public override void CalculateModelMatrix()
        {
            ModelMatrix = Matrix4.Scale(Scale) * Matrix4.CreateRotationX(Rotation.X) * Matrix4.CreateRotationY(Rotation.Y) * Matrix4.CreateRotationZ(Rotation.Z) * Matrix4.CreateTranslation(Position);
        }

    }



Now we just need to write some functions to make this class load its data from an OBJ file. An OBJ file is just a text file that contains information about the vertices in a model and the faces made up by them. For this tutorial, we will only write code to handle the vertices and basic triangular faces. A later example will handle more types of shape (OBJs can include any kind of shape, including points, lines and polygons of any number of sides), other values we can get from the file (normals and texture coordinates) and combining it with MTL files to load the information about colors and textures from a file as well.

Add the following static functions to the ObjVolume class:

        public static ObjVolume LoadFromFile(string filename)
        {
            ObjVolume obj = new ObjVolume();
            try
            {
                using (StreamReader reader = new StreamReader(new FileStream(filename, FileMode.Open, FileAccess.Read)))
                {
                    obj = LoadFromString(reader.ReadToEnd());
                }
            }
            catch (FileNotFoundException e)
            {
                Console.WriteLine("File not found: {0}", filename);
            }
            catch (Exception e)
            {
                Console.WriteLine("Error loading file: {0}", filename);
            }

            return obj;
        }

        public static ObjVolume LoadFromString(string obj)
        {
            // Seperate lines from the file
            List<String> lines = new List<string>(obj.Split('\n'));

            // Lists to hold model data
            List<Vector3> verts = new List<Vector3>();
            List<Vector3> colors = new List<Vector3>();
            List<Vector2> texs = new List<Vector2>();
            List<Tuple<int, int, int>> faces = new List<Tuple<int, int, int>>();

            // Read file line by line
            foreach (String line in lines)
            {
                if (line.StartsWith("v ")) // Vertex definition
                {
                    // Cut off beginning of line
                    String temp = line.Substring(2);

                    Vector3 vec = new Vector3();

                    if (temp.Count((char c) => c == ' ') == 2) // Check if there's enough elements for a vertex
                    {
                        String[] vertparts = temp.Split(' ');

                        // Attempt to parse each part of the vertice
                        bool success = float.TryParse(vertparts[0], out vec.X);
                        success |= float.TryParse(vertparts[1], out vec.Y);
                        success |= float.TryParse(vertparts[2], out vec.Z);

                        // Dummy color/texture coordinates for now
                        colors.Add(new Vector3((float) Math.Sin(vec.Z), (float) Math.Sin(vec.Z), (float) Math.Sin(vec.Z)));
                        texs.Add(new Vector2((float) Math.Sin(vec.Z), (float) Math.Sin(vec.Z)));

                        // If any of the parses failed, report the error
                        if (!success)
                        {
                            Console.WriteLine("Error parsing vertex: {0}", line);
                        }
                    }

                    verts.Add(vec);
                }
                else if (line.StartsWith("f ")) // Face definition
                {
                    // Cut off beginning of line
                    String temp = line.Substring(2);

                    Tuple<int, int, int> face = new Tuple<int, int, int>(0, 0, 0);

                    if (temp.Count((char c) => c == ' ') == 2) // Check if there's enough elements for a face
                    {
                        String[] faceparts = temp.Split(' ');

                        int i1, i2, i3;

                        // Attempt to parse each part of the face
                        bool success = int.TryParse(faceparts[0], out i1);
                        success |= int.TryParse(faceparts[1], out i2);
                        success |= int.TryParse(faceparts[2], out i3);

                        // If any of the parses failed, report the error
                        if (!success)
                        {
                            Console.WriteLine("Error parsing face: {0}", line);
                        }
                        else
                        {
                            // Decrement to get zero-based vertex numbers
                            face = new Tuple<int, int, int>(i1 - 1, i2 - 1, i3 - 1);
                            faces.Add(face);
                        }
                    }
                }
            }

            // Create the ObjVolume
            ObjVolume vol = new ObjVolume();
            vol.vertices = verts.ToArray();
            vol.faces = new List<Tuple<int, int, int>>(faces);
            vol.colors = colors.ToArray();
            vol.texturecoords = texs.ToArray();

            return vol;
        }


Now our work in the ObjVolume class is done, and we're ready to load in some models! First, we need to find some very simple models to display. I found some available on a website from an MIT class. For this example, I'm using the cow model (saved as "cow.obj") and the teapot model (as "teapot.obj"), but any of the files will be good. The only rules are that the files only use triangles and don't include texture coordinate or normal data in the face definitions (so the face lines look like "f # # #", not "f #/#/# #/#/# #/#/#").

Add the model file to your Visual Studio project, and make sure "Copy to Output" is set to "Copy always" in the Properties window.

Now we need to add some code in our Game class to load the models and add them to the objects List. Near the end of initProgram (after the TexturedCubes are already being added), add the following (switch out the model names if you used different ones):
            ObjVolume obj1 = ObjVolume.LoadFromFile("cow.obj");
            objects.Add(obj1);

            ObjVolume obj2 = ObjVolume.LoadFromFile("teapot.obj");
            obj2.Position += new Vector3(0, 2f, 0);
            objects.Add(obj2); 
 


After this, you should be able to run the code and see both models displayed:

(I moved the camera away and to the other side of the models to show them and the existing cubes better)

The models are black because they don't have a texture assigned to them. If you want to see how the model would look with a texture applied, just set the TextureID field on the objects to one of the textures already loaded:
In a future tutorial, we'll get texture coordinates and load material information for our models, giving us a proper display.

20 comments:

  1. Very nice tutorial as always. You have been a great help to me and I thank you for this. Unfortunately, I am unable to load in most obj's as they contain the texture coordinates and normal data which the object loader doesn't take in. Is there any quick edit that can allow these objects to loaded? I would really appreciate the help, thanks.

    ReplyDelete
    Replies
    1. Hi,

      You'd need to do is add some more branches to the if statements to parse lines starting with "vn " or "vt ". Then you'd need to have it break apart the face definitions where there's slashes, to use the right data for each vertex of each face (which means you'll have to change how it's all stored in the end).

      Sorry it's not a quick fix, but that's why I plan on posting another tutorial on just that.

      Delete
    2. Hello,

      I figured as much but I had a (some what quick) fix which was to simply remove the slashes and numbers after the slashes in the file. Was quite tedious but worked in the end. Obviously texturing is a bit weird especially with things that have many faces but it will to until your next tutorial.

      Thank you again and keep up the great work! Looking forward to your next tutorial :D

      Delete
  2. This comment has been removed by the author.

    ReplyDelete
  3. Thank you for those OpenTK tutorials, they are very helpful to me. I especially like that your code is made very carefully and clearly. Many OpenGL/OpenTK tutorials I found are made in one class and that does not make it easy to learn from.

    You mentioned you're going to make tutorial about lighting. Can I ask when you're planing to post it? I have problem with lighting and I can't make it work. Do you have links to complate lighting example in OpenTK or something like that so I could check what I am doing wrong?

    ReplyDelete
    Replies
    1. Hi monilip,

      Lighting/normals will be the next tutorial (followed by an extension of the code in this tutorial to load normals and texture information from a file). I've just been really busy with work and school for the past few weeks.

      If you'd like, you could always post questions here in the meantime. I try to respond to comments as soon as I can.

      Delete
    2. Thanks for answer. My problem was simple: I used object data with normals (so I don't have to count them by myself) but they were wrong. As soon as I decided to count them myself, my ligh started to work :)

      Good luck with work and school

      Delete
  4. +1 for lighting/shaders

    ReplyDelete
  5. Can smb link me a project? I cant run yours(

    ReplyDelete
  6. https://twitter.com/vytek75/status/606449325430964224

    ReplyDelete
  7. Ah, finally an obj loader too!!!

    ReplyDelete
  8. Hi, Thanks for your post.
    Can I ask you something relative with .obj file?

    Now, I treat point cloud taken from kinect v2 and want to make this data to .obj file.
    The problem is there are no information how could make the .obj file in openTK.
    I search that but there just are information about loading .obj file.
    So, Can you answer how could I make .obj file?
    (I have point cloud data, its corresponding color index(maybe uv coordinate) and color image taken from Kinect v2.)

    ReplyDelete
    Replies
    1. You can store points in OBJ files (with "p" at the beginning of a line, followed by the vertices for each point), but OBJ might be a poor match for that type of data. You also might have difficulty finding software that would render that data, since the spec says they normally shouldn't be rendered at all.

      There's another format called PTS/PTX (PTX adds some extra information), meant for storing point clouds with color data, which might be more useful.

      Delete
    2. obj files basically store co-ordinates for each vertices and faces with the vertex number of each vertex that constitutes them. And also normals and texture co-ordinates. I dont know about point cloud type data, but i believe you might be able to convert the point data into vertex data and face data. Once you have that, you have all that is necessary for your OBJ.

      Delete
  9. These are the best openTK tutorials.
    Thanks.

    ReplyDelete
  10. I had one problem with TryParse (don't know if .NET version reletad or what...)

    I fixed it by definyng NumberStyle and CultureInfo and passing it to function.

    NumberStyles style = NumberStyles.Float;
    CultureInfo culture = CultureInfo.CreateSpecificCulture("en-US");

    and modified all TryParse parts like float.TryPars(input, style, culture, output);

    If anybody stumbles upon same problem :)

    ReplyDelete
    Replies
    1. Thanks! this is a very important help for me

      Delete
    2. Thanks man, had the same problem.

      Delete
  11. Thanks for a wonderful tutorial. I just have a slight problem with the code. When I load both the cow and teapot the teapot doesn't draw properly and there are two cows offset from one another. However when I draw them individually (by commenting out either object) they draw perfectly. This is the code I use:

    ObjVolume obj1 = ObjVolume.LoadFromFile("ObjectFiles/cow.obj");
    //obj1.TextureID = tc.TextureID;
    objects.Add(obj1);

    ObjVolume obj2 = ObjVolume.LoadFromFile("ObjectFiles/teapot.obj");
    //obj2.TextureID = tc2.TextureID;
    obj2.Position += new Vector3(0, 2f, 0);
    objects.Add(obj2);

    Any ideas why this could be?

    ReplyDelete
    Replies
    1. Would you be able to post your entire Game.cs file?

      Delete