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:

?

123public virtual int VertCount { getset; }public virtual int IndiceCount { getset; }public virtual int ColorDataCount { getset; }

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:

?

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768class ObjVolume : Volume{    Vector3[] vertices;    Vector3[] colors;    Vector2[] texturecoords;    List<Tuple<intintint>> faces = new List<Tuple<intintint>>();    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:

?

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107public 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<intintint>> faces = new List<Tuple<intintint>>();    // 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<intintint> face = new Tuple<intintint>(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<intintint>(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<intintint>>(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):

?

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

?

1 

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.

https://neokabuto.blogspot.com/p/tutorials.html

Leave a comment

Trending