1
\$\begingroup\$

I have written my own Wavefront Model parser, which I have been using for quite some time to render meshes in immediate mode. However, I now wish to upgrade my system to Vertex Buffer Objects. I was doing a lot of reading online about how to create Vertex Buffer Objects, but most(really all) of the links I found online did not seem to exactly answer the question of how to upload the indices(or in my case, Faces); it's entirely plausible that my Google Kung-Fu has just been lacking though.

I'm interested in taking my current Wavefront Model implementation, tweaking it if necessary, and creating a VBO out of the data that it contains, and then rendering it. I'm certain that the only real issue is how I store the Indice data(I do so in Faces), which I think needs to be implemented as a single array. Again, I'm not very knowledgable in modern OpenGL, so I don't know how to proceed or what modifications are required going forward.

Here is my implementation:

WavefrontModel.cs

using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using GrimoireEngine.Framework.Maths;
using GrimoireEngine.Framework.Rendering.OpenGL.Texturing;
using GrimoireEngine.Framework.Rendering.OpenGL.Utilities;
using OpenTK;
using OpenTK.Graphics.OpenGL;

namespace GrimoireEngine.Framework.Rendering.OpenGL.Modeling
{
    public class WavefrontModel
    {
        /// <summary>
        /// The String Identifier of this Model
        /// </summary>
        public string Identifier { get; set; }
        /// <summary>
        /// The Vertices of this Model
        /// </summary>
        public Vector3[] Vertices;
        /// <summary>
        /// The TexCoords of this Model
        /// </summary>
        public Vector2[] TexCoords;
        /// <summary>
        /// The Normals of this Model
        /// </summary>
        public Vector3[] Normals;
        /// <summary>
        /// The Faces of this Model
        /// </summary>
        public Face[] Faces; // May be entirely deprecated.

        /// <summary>
        /// Testing some shit.
        /// </summary>
        public List<int> Indices;
        /// <summary>
        /// The Original File Source
        /// </summary>
        public string ModelSource { get; set; }

        /// <summary>
        /// The Material of this Model
        /// </summary>
        public Material Material { get; set; }


        /// <summary>
        /// The Triangle Count of this model.
        /// </summary>
        public int TriCount
        {
            get
            {
                return Faces.Length;
            }
        }

        public WavefrontModel()
        {

        }

        public WavefrontModel(string modelPath, Material material, bool load = true)
        {
            this.ModelSource = modelPath;
            this.Material = material;
            if (load)
            {
                Load(modelPath);
            }
        }

        public WavefrontModel(string modelPath, bool load = true)
        {
            this.ModelSource = modelPath;
            this.Material = null;
            if (load)
            {
                Load(modelPath);
            }
        }

        public WavefrontModel(string[] data)
        {
            this.Material = null;
            Load(data);
        }

        public WavefrontModel(string[] data, Material material)
        {
            this.Material = material;
            Load(data);
        }

        /// <summary>
        /// Loads a model from the desired Wavefront.obj source given
        /// at constructor initialization.
        /// </summary>
        public void Load()
        {
            Load(this.ModelSource);
        }

        /// <summary>
        /// Loads a model from a Wavefront.obj located on disk.
        /// </summary>
        /// <param name="file"></param>
        public void Load(string file)
        {
            Parse(File.ReadAllLines(file));
        }

        /// <summary>
        /// Loads a model from a Wavefront.obj located on disk.
        /// </summary>
        /// <param name="data"></param>
        public void Load(string[] data)
        {
            Parse(data);
        }

        /// <summary>
        /// Sets the internal Model Arrays to null.
        /// </summary>
        public void Clear()
        {
            Vertices = null;
            TexCoords = null;
            Normals = null;
            Faces = null;
        }

        /// <summary>
        /// Parses Wavefront data from desired array.
        /// </summary>
        /// <param name="data"></param>
        public void Parse(string[] data)
        {
            // Create File Manifest
            int totalVertices = 0;
            int totalNormals = 0;
            int totalTextureCoordinates = 0;
            int totalFaces = 0;
            for (int i = 0; i < data.Length; i++)
            {
                char firstChar = data[i][0];
                switch (firstChar)
                {
                    case 'v':
                        char secondChar = data[i][1];
                        switch (secondChar)
                        {
                            case ' ':
                                totalVertices++;
                                break;
                            case 't':
                                totalTextureCoordinates++;
                                break;
                            case 'n':
                                totalNormals++;
                                break;
                        }
                        break;
                    case 'f':
                        totalFaces++;
                        break;
                }
            }
            // Create the Buffers
            Vertices = new Vector3[totalVertices];
            Normals = new Vector3[totalNormals];
            TexCoords = new Vector2[totalTextureCoordinates];
            Faces = new Face[totalFaces];
            // Create File Reading Buffers
            string[] fileBuffer = new string[20];
            string[] indiceBuffer = new string[3];
            Vertex[] vertexBuffer = new Vertex[3];
            // Iterators
            int verticesIterator = 0;
            int normalsIterator = 0;
            int textureCoordinatesIterator = 0;
            int facesIterator = 0;
            for (int line = 0; line < data.Length; line++)
            {
                SplitStringFast(data[line], ' ', fileBuffer);
                char firstChar = data[line][0];
                switch (firstChar)
                {
                    case 'v':
                        char secondChar = data[line][1];
                        switch (secondChar)
                        {
                            case ' ':
                                Vector3 vertex = new Vector3
                                {
                                    X = Single.Parse(fileBuffer[1]),
                                    Y = Single.Parse(fileBuffer[2]),
                                    Z = Single.Parse(fileBuffer[3])
                                };
                                Vertices[verticesIterator] = vertex;
                                verticesIterator++;
                                break;
                            case 't':
                                Vector2 textureCoordinate = new Vector2
                                {
                                    X = Single.Parse(fileBuffer[1]), // U
                                    Y = -Single.Parse(fileBuffer[2]) // V (Inverted)
                                };
                                TexCoords[textureCoordinatesIterator] = textureCoordinate;
                                textureCoordinatesIterator++;
                                break;
                            case 'n':
                                Vector3 normal = new Vector3
                                {
                                    X = Single.Parse(fileBuffer[1]),
                                    Y = Single.Parse(fileBuffer[2]),
                                    Z = Single.Parse(fileBuffer[3])
                                };
                                Normals[normalsIterator] = normal;
                                normalsIterator++;
                                break;
                        }
                        break;
                    case 'f':
                        for (int i = 0; i < 3; i++)
                        {
                            SplitStringFast(fileBuffer[i + 1], '/', indiceBuffer);
                            Vertex indices = new Vertex
                            {
                                VertexIndice = GrimoireMathHelper.ParsePositiveInt32Fast(indiceBuffer[0]) - 1,
                                TextureIndice = GrimoireMathHelper.ParsePositiveInt32Fast(indiceBuffer[1]) - 1,
                                NormalIndice = GrimoireMathHelper.ParsePositiveInt32Fast(indiceBuffer[2]) - 1
                            };
                            vertexBuffer[i] = indices;
                        }
                        Faces[facesIterator] = new Face(vertexBuffer);
                        facesIterator++;
                        break;
                }
            }
        }

        /// <summary>
        /// A custom implementation of String.Split(). Realistically, this 
        /// function is not much faster than what .NET offers; it gains speed
        /// more from a preset buffer mechanism. There is no bounds checking done
        /// for the buffer, which is done by design to assure optimal performance.
        /// </summary>
        /// <param name="value"></param>
        /// <param name="delimiter"></param>
        /// <param name="buffer"></param>
        /// <returns></returns>
        private static void SplitStringFast(string value, char delimiter, string[] buffer)
        {
            int resultIndex = 0;
            int startIndex = 0;
            for (int i = 0; i < value.Length; i++)
            {
                if (value[i] == delimiter)
                {
                    buffer[resultIndex] = value.Substring(startIndex, i - startIndex);
                    resultIndex++;
                    startIndex = i + 1;
                }
            }
            buffer[resultIndex] = value.Substring(startIndex, value.Length - startIndex);
        }

        /// <summary>
        /// Renders the Model using deprecated immediate mode. This
        /// function exists only for testing purposes.
        /// </summary>
        public void DrawImmediate()
        {
            //GL.Enable(EnableCap.Texture2D);
            GL.Color3(Material.AmbientColor);
            Material.Bind();

            GL.Begin(PrimitiveType.Triangles);
            for (int i = 0; i < Faces.Length; i++)
            {
                /**
                 * One
                 */

                GL.Normal3(Normals[Faces[i].One.NormalIndice]);
                //GL.VertexAttrib2(1, TexCoords[Faces[i].One.TextureIndice]); // For use with shaders.
                GL.TexCoord2(TexCoords[Faces[i].One.TextureIndice]);
                GL.Vertex3(Vertices[Faces[i].One.VertexIndice]);
                /**
                 * Two
                 */
                GL.Normal3(Normals[Faces[i].Two.NormalIndice]);
                GL.TexCoord2(TexCoords[Faces[i].Two.TextureIndice]);
                //GL.VertexAttrib2(1, TexCoords[Faces[i].Two.TextureIndice]); // For use with shaders
                GL.Vertex3(Vertices[Faces[i].Two.VertexIndice]);
                /**
                 * Three
                 */
                GL.Normal3(Normals[Faces[i].Three.NormalIndice]);
                GL.TexCoord2(TexCoords[Faces[i].Three.TextureIndice]);
                //GL.VertexAttrib2(1, TexCoords[Faces[i].Three.TextureIndice]); // For use with Shaders
                GL.Vertex3(Vertices[Faces[i].Three.VertexIndice]);
            }
            GL.End();
            //GL.BindTexture(TextureTarget.Texture2D,0);
            //GL.Disable(EnableCap.Texture2D);
        }

        /// <summary>
        /// Compiles this WavefrontModel into a VertexBufferObject
        /// </summary>
        public void Compile(BufferUsageHint bufferUsageHint)
        {
            /**
             * This is where I'm starting to create my VBO's...I just don't really
             * know how to proceed.
             */
            int vboHandle;
            int size;
            // Vertices
            GL.GenBuffers(1, out vboHandle);
            GL.BindBuffer(BufferTarget.ArrayBuffer, vboHandle);
            GL.BufferData(BufferTarget.ArrayBuffer, (IntPtr)(Vertices.Length * BlittableValueType.StrideOf(Vertices)), Vertices, bufferUsageHint);
            GL.GetBufferParameter(BufferTarget.ArrayBuffer, BufferParameterName.BufferSize, out size);
            if (Vertices.Length * BlittableValueType.StrideOf(Vertices) != size)
            {
                throw new ApplicationException("Vertex data not uploaded correctly");
            }
            //
            // How do I handle indices?
        }

        /// <summary>
        /// Draw the VBO
        /// </summary>
        public void Render()
        {
            throw new NotImplementedException("Rendering VBO's not yet implemented!");
        }
    }
}

Face.cs

using System;

namespace GrimoireEngine.Framework.Rendering.OpenGL.Modeling
{
    public struct Face : IEquatable<Face>
    {
        public Vertex One;
        public Vertex Two;
        public Vertex Three;

        public static Face Empty
        {
            get
            {
                return new Face();
            }
        }

        public Face(Vertex[] vertexIndices)
        {
            this.One = vertexIndices[0];
            this.Two = vertexIndices[1];
            this.Three = vertexIndices[2];
        }

        public Face(Vertex one, Vertex two, Vertex three)
        {
            this.One = one;
            this.Two = two;
            this.Three = three;
        }

        public bool Equals(Face other)
        {
            return One == other.One && Two == other.Two && Three == other.Three;
        }

        public override bool Equals(object obj)
        {
            return obj is Face && Equals((Face)obj);
        }

        public static bool operator ==(Face f1, Face f2)
        {
            return f1.Equals(f2);
        }

        public static bool operator !=(Face f1, Face f2)
        {
            return !(f1.Equals(f2));
        }

        public Vertex[] ToArray()
        {
            return new[] { One, Two, Three };
        }

        public override int GetHashCode()
        {
            unchecked
            {
                int hashCode = One.GetHashCode();
                hashCode = (hashCode * 31) ^ Two.GetHashCode();
                hashCode = (hashCode * 31) ^ Three.GetHashCode();
                return hashCode;
            }
        }
    }
}

Vertex.cs

using System;

namespace GrimoireEngine.Framework.Rendering.OpenGL.Modeling
{
    public struct Vertex : IEquatable<Vertex>
    {

        public int VertexIndice;
        public int TextureIndice;
        public int NormalIndice;

        public static Vertex Empty
        {
            get
            {
                return new Vertex();
            }
        }

        public Vertex(int[] indices)
        {
            this.VertexIndice = indices[0];
            this.NormalIndice = indices[1];
            this.TextureIndice = indices[2];
        }

        public Vertex(int vertexIndice, int normalIndice, int textureIndice)
        {
            this.VertexIndice = vertexIndice;
            this.TextureIndice = textureIndice;
            this.NormalIndice = normalIndice;
        }

        public bool Equals(Vertex other)
        {
            return VertexIndice == other.VertexIndice &&
                   TextureIndice == other.TextureIndice &&
                   NormalIndice == other.NormalIndice;
        }

        public override bool Equals(object obj)
        {
            return obj is Vertex && Equals((Vertex)obj);
        }

        public static bool operator ==(Vertex v1, Vertex v2)
        {
            return v1.Equals(v2);
        }

        public static bool operator !=(Vertex v1, Vertex v2)
        {
            return !(v1 == v2);
        }

        /// <summary>
        /// Order = VertexIndice, TextureIndice, NormalIndice
        /// </summary>
        /// <returns></returns>
        public int[] ToArray()
        {
            return new[] { VertexIndice, TextureIndice, NormalIndice };
        }

        public override int GetHashCode()
        {
            unchecked
            {
                int hashCode = VertexIndice;
                hashCode = (hashCode * 31) ^ TextureIndice;
                hashCode = (hashCode * 31) ^ NormalIndice;
                return hashCode;
            }
        }
    }
}

My Question: How can I modify this implementation to create Vertex Buffer Objects? Or, if applicable, how can I use this implementation to create and render a Vertex Buffer Object? Thank you for your time and assistance!

Questions like this that I feel don't answer my question:

https://stackoverflow.com/questions/21845613/writing-wavefront-obj-parser-for-use-with-opengl-indexed-vertex-buffer-objects

\$\endgroup\$
2
\$\begingroup\$

The good news is that you already have most of the code that you need to do this. It's easy to take glBegin/glEnd code, modify it to write to a memory buffer instead of making direct glTexCoord/glVertex/etc calls, then send to a buffer object (using glBufferData) and finally set up your vertex attrib pointers/etc.

For example, let's say that you have the following glBegin/glEnd code. I'm going to use C/C++ style syntax here but it should be obvious what the C# equivalent is:

glBegin (GL_QUADS);

glTexCoord2f (0, 0);
glVertex2f (10, 10);

glTexCoord2f (0, 1);
glVertex2f (10, 74);

glTexCoord2f (1, 1);
glVertex2f (74, 74);

glTexCoord2f (1, 0);
glVertex2f (74, 10);

glEnd ();

How to convert that to use a buffer object instead?

Let's begin by defining a vertex structure:

struct Vertex
{
    float TexCoord[2];
    float Position[2];
};

4 vertices make a quad so let's declare 4 of them:

Vertex vertices[4];

Then we'll write out the data:

vertices[0].TexCoord[0] = 0;
vertices[0].TexCoord[1] = 0;
vertices[0].Position[0] = 10;
vertices[0].Position[1] = 10;

vertices[1].TexCoord[0] = 0;
vertices[1].TexCoord[1] = 1;
vertices[1].Position[0] = 10;
vertices[1].Position[1] = 74;

vertices[2].TexCoord[0] = 1;
vertices[2].TexCoord[1] = 1;
vertices[2].Position[0] = 74;
vertices[2].Position[1] = 74;

vertices[3].TexCoord[0] = 1;
vertices[3].TexCoord[1] = 0;
vertices[3].Position[0] = 74;
vertices[3].Position[1] = 10;

Compare each of these blocks to each glTexCoord/glVertex pair above and you should see what's happening.

Finally we can write the result out to a buffer object:

glBufferData (GL_ARRAY_BUFFER, 4 * sizeof (Vertex), vertices, GL_STATIC_DRAW);

And draw it:

glDrawArrays (GL_QUADS, 0, 4);

Just ensure that you build the buffer once-only (at loading time) rather than every frame and you'll be good.

\$\endgroup\$
  • \$\begingroup\$ I've been doing a lot of reading...seriously, I don't think I can express just how much I have been doing. It's become painfully obvious that a Wavefront Model does not inherently support VBO's out-of-the-box. According to the sources that I have seen, I will need to load in the data and modify it so that the indices line up with the Vertices array, the Normals array, and the texture array. This means that I will have to be duplicating index locations in order to obtain a congruent array of indices, which is properly aligned. Because of this I will also take the extra step of declaring my own \$\endgroup\$ – Krythic Jun 14 '17 at 7:30
  • \$\begingroup\$ model format, which will look much like a .obj, except that it will be already pre-optimized so that the indice array aligns with the other associated arrays. Baking the results into a new format will lead to less overhead during the loading and execution of my game, which is what every game developer wants at the end of the day. I'm actually kind of glad that I discovered all of this when I did, because this is a headache that needs to be dealt with sooner rather than later. I will also be choosing your answer if you can just do me a small favor and basically sum up what I have just said. \$\endgroup\$ – Krythic Jun 14 '17 at 7:35
  • \$\begingroup\$ @Krythic - .obj is actually a horrible format for this kind of use case; it's fine for tutorials and fine for interchange, but the ideal model format would be one where you could push file data directly to a VBO without any parsing or pre-processing. \$\endgroup\$ – Maximus Minimus Jun 14 '17 at 8:15

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.