3
\$\begingroup\$

I have been working a simple 2D Game using C# language with OpenTK (OpenGL wrapper/binding for .NET). It uses a lot of sprites.

Since it uses lot of sprites, I think it's important to make everything implemented properly in order to prevent performance loss and gain more performance boost. Also, I take care of the compatibility with old hardware too.

Can someone confirm whether this class is 'acceptable'/'good' implementation of VertexBufferObject?

Notes: This class called RenderImplementation that contains 2 Rendering Implementation, VertexBufferObject and Immediate Mode, Immediate Mode is used when its preferred or.. VertexBufferObject is not supported by the system. Feel free to suggest me the better name for this class.

/// <summary>
/// Define Rendering Implementation Processing Logic.
/// Implementation will use Vertex Bufer Object if supported and preferred, otherwise Immediate Mode will be used.
/// </summary>
public class RenderImplementation : IDisposable
{
    private static bool _isCompatibilityChecked = false;
    private static bool _isSupported = true;

    /// <summary>
    /// Gets whether the system support Vertex Buffer Object rendering.
    /// <para>Return true if supported, otherwise false.</para>
    /// </summary>
    public static bool IsVertexBufferObjectSupported
    {
        get
        {
            // I think calling this too much will lead performance issue.
            // Let's check it once and return the checked value.
            if (!_isCompatibilityChecked)
            {
                _isSupported = new Version(GL.GetString(StringName.Version).Substring(0, 3)) >= new Version(1, 5) ? true : false;
                _isCompatibilityChecked = true;
            }

            return _isSupported;
        }
    }

    /// <summary>
    /// Gets or Sets whether the Implementation should use Immediate Mode instead Vertex Buffer Object.
    /// </summary>
    public static bool UseImmediateMode
    {
        get; set;
    }

    // Handles
    // I dont think it necessary to make it public
    // Everything is processed under this class.
    private int _vertexBufferId = -1;
    private int _indexBufferId = -1;

    // Vertices and Indices
    private VertexArray _vertices = new VertexArray(PrimitiveType.Quads, 4);
    private ushort[] _indices = new ushort[0];

    // Implementation State.
    private bool _isUploaded = false;

    /// <summary>
    /// Get or Set Vertices that used by this <see cref="RenderImplementation"/>.
    /// </summary>
    public VertexArray Vertices
    {
        get { return _vertices; }
        set {
            if (_vertices == value)
                return;

            _isUploaded = false; _vertices = value;
        }
    }

    /// <summary>
    /// Get or Set Element Indices.
    /// <para>This will only used when VertexBufferObject is supported and preferred.</para>
    /// </summary>
    public ushort[] Indices
    {
        get { return _indices; }
        set {
            if (_indices == value)
                return;

            _isUploaded = false;  _indices = value;
        }
    }

    /// <summary>
    /// Gets a value indicating whether the implementation buffer handles has been disposed.
    /// </summary>
    public bool IsDisposed { get; private set; }

    /// <summary>
    /// Construct a new <see cref="RenderImplementation"/>.
    /// </summary>
    /// <param name="vertices"><see cref="VertexArray"/> to Upload into Buffer.</param>
    public RenderImplementation(VertexArray vertices)
        : this (vertices, new ushort[] { 0, 1, 2, 3 })
    {
    }

    /// <summary>
    /// Construct a new <see cref="RenderImplementation"/>.
    /// </summary>
    /// <param name="vertices"><see cref="VertexArray"/> to Upload into Buffer.</param>
    /// <param name="indices">Indices of Elements.</param>
    public RenderImplementation(VertexArray vertices, ushort[] indices)
    {
        Vertices = vertices;
        Indices = indices;

        // Check whether the system support Vertex Buffer Object implementation.
        if (!IsVertexBufferObjectSupported || UseImmediateMode)
            // Use immediate mode if it's not supported.
            UseImmediateMode = true;
        else
            // The system support it, let's generate the handles.
            GenerateHandle();
    }

    /// <summary>
    /// Generate Implementation Handle.
    /// <para>This function will do nothing if VertexBufferObject is NOT supported or not preferred.</para>
    /// </summary>
    /// <param name="overrideHandle">Specify whether existing handle should be deleted and recreated or not, true to delete and recreate it, 
    /// otherwise it may throw exception if handle is already exist.
    /// </param>
    public void GenerateHandle(bool overrideHandle = false)
    {
        // PS: I'm not sure whether this function should be public or private.
        // Since those handles are private and only could be processed under this class.

        // Check whether the implementation has been disposed.
        if (IsDisposed)
            throw new ObjectDisposedException(ToString());

        // Check whether the system support VBO and if its preferred.
        if (!IsVertexBufferObjectSupported || UseImmediateMode)
            return;

        // Check whether the handle is exist, and delete it if asked to do so.
        if (_vertexBufferId > 0 && overrideHandle)
            GL.DeleteBuffer(_vertexBufferId);
        // .. and throw the exception if its not asked to delete existing buffer.
        else if (_vertexBufferId > 0 && !overrideHandle)
            throw new InvalidOperationException("Vertex Buffer Handle is already exist.");

        // Generate the handle
        _vertexBufferId = GL.GenBuffer();

        // Do the same thing for index buffer handle
        if (_indexBufferId > 0 && overrideHandle)
            GL.DeleteBuffer(_indexBufferId);
        else if (_indexBufferId > 0 && !overrideHandle)
            throw new InvalidOperationException("Index Buffer Handle is already exist.");

        _indexBufferId = GL.GenBuffer();
    }

    /// <summary>
    /// Upload the Vertices into Buffer Data.
    /// </summary>
    public void Upload()
    {
        // Check whether the implementation has been disposed.
        if (IsDisposed)
            throw new ObjectDisposedException(ToString());

        // We dont need to upload if its not supported / Immediate mode is prefered.
        if (!IsVertexBufferObjectSupported || UseImmediateMode)
            return;

        // Get the array of vertices
        Vertex[] vertices = Vertices.ToArray();

        // Bind vertex buffer handle and upload vertices to buffer data.
        GL.BindBuffer(BufferTarget.ArrayBuffer, _vertexBufferId);
        GL.BufferData(BufferTarget.ArrayBuffer, (IntPtr)(vertices.Length * Vertex.Stride),
            vertices, BufferUsageHint.StaticDraw);

        // Bind index buffer handle and upload indices to buffer data.
        GL.BindBuffer(BufferTarget.ElementArrayBuffer, _indexBufferId);
        GL.BufferData(BufferTarget.ElementArrayBuffer, (IntPtr)(_indices.Length * sizeof(ushort)),
            _indices, BufferUsageHint.StaticDraw);

        // Everything is uploaded, and its ready to render.
        _isUploaded = true;
    }

    /// <summary>
    /// Render the Implementation.
    /// </summary>
    public void Render()
    {
        // Check whether the implementation has been disposed.
        if (IsDisposed)
            throw new ObjectDisposedException(ToString());

        // The system support vbo and prefer to not use immediate mode.
        // Let's use Vertex Buffer Object implementation.
        if (IsVertexBufferObjectSupported && !UseImmediateMode)
        {
            // Check whether the current vertices and indices is already uploaded.
            if (!_isUploaded)
                // No? let's upload it.
                Upload();

            // Enable required client state
            GL.EnableClientState(ArrayCap.VertexArray);
            GL.EnableClientState(ArrayCap.TextureCoordArray);
            GL.EnableClientState(ArrayCap.ColorArray);
            GL.EnableClientState(ArrayCap.IndexArray);

            // Bind the Vertex Buffer handle.
            GL.BindBuffer(BufferTarget.ArrayBuffer, _vertexBufferId);

            // Update the vertex pointers.
            GL.VertexPointer(2, VertexPointerType.Float, Vertex.Stride, 0);
            GL.TexCoordPointer(2, TexCoordPointerType.Float, Vertex.Stride, Vector2.SizeInBytes);
            GL.ColorPointer(4, ColorPointerType.Float, Vertex.Stride, Vector2.SizeInBytes * 2);

            // Bind the index buffer and draw the elements.
            GL.BindBuffer(BufferTarget.ElementArrayBuffer, _indexBufferId);
            GL.DrawElements(Vertices.Type, Vertices.GetVertexCount(), DrawElementsType.UnsignedShort, IntPtr.Zero);

            // Unbind the buffer handle.
            GL.BindBuffer(BufferTarget.ArrayBuffer, 0);
            GL.BindBuffer(BufferTarget.ElementArrayBuffer, 0);

            // Disable the client state.
            GL.DisableClientState(ArrayCap.VertexArray);
            GL.DisableClientState(ArrayCap.TextureCoordArray);
            GL.DisableClientState(ArrayCap.ColorArray);
            GL.DisableClientState(ArrayCap.IndexArray);
        }
        // The user prefer immediate mode..
        else
        {
            // Notes:
            // GL.GetError() cannot be called upon render the immediate mode implementation.
            // There is no way to check automatically whether our data is valid or not.
            // Just make sure the specified colors, texcoords and positions are valid and correct.

            // Specify primitive mode.
            GL.Begin(Vertices.Type);

            // Set the Color, Texture Coordinate and Positions.
            // (PS: Not sure whether i can use 'Indices' here)
            for (int i = 0; i < Vertices.GetVertexCount(); i++)
            {
                // Color.
                GL.Color4(Vertices[i].color);

                // TexCoord.
                GL.TexCoord2(Vertices[i].texCoord);

                // Position.
                GL.Vertex2(Vertices[i].position);
            }

            // Finished.
            GL.End();
        }
    }

    /// <summary>
    /// Dispose the buffer handles that used by this <see cref="RenderImplementation"/>.
    /// </summary>
    public void Dispose()
    {
        if (_vertexBufferId > 0)
            GL.DeleteBuffer(_vertexBufferId);
        if (_indexBufferId > 0)
            GL.DeleteBuffer(_indexBufferId);
        _vertices = null;

        IsDisposed = true;
    }
}

Vertex struct:

/// <summary>
/// Define a position, texture coordinate and color.
/// </summary>
public struct Vertex
{
    /// <summary>
    /// Get the Size of <see cref="Vertex"/>, in bytes.
    /// </summary>
    public static int Stride { get { return System.Runtime.InteropServices.Marshal.SizeOf(default(Vertex)); } }

    public Vector2 position;
    public Vector2 texCoord;
    public Vector4 color;

    /// <summary>
    /// Construct a new <see cref="Vertex"/>.
    /// </summary>
    public Vertex(Vector2 position, Vector2 texCoord, Color color)
    {
        this.position = position;
        this.texCoord = texCoord
        this.color = new Vector4(color.R / 255f, color.G / 255f, color.B / 255f, color.A / 255f);
    }
}

VertexArray class:

/// <summary>
/// Define a set of one or more 2D primitives. 
/// </summary>
public class VertexArray : IEnumerable<Vertex>
{
    private Vertex[] _vertices;

    /// <summary>
    /// Get or Set Primitive Type of Vertices.
    /// </summary>
    public PrimitiveType Type
    {
        get;
        set;
    }

    /// <summary>
    /// Return Vertices that interates for Enumeration.
    /// </summary>
    /// <returns><see cref="Vertex"/></returns>
    public IEnumerator<Vertex> GetEnumerator()
    {
        for (int i = 0; i < _vertices.Length - 1; i++)
            yield return _vertices[i];
    }

    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        return this.GetEnumerator();
    }


    /// <summary>
    /// Create a new <see cref="VertexArray"/>.
    /// </summary>
    /// <param name="type">Type of primitive.</param>
    /// <param name="count">Initial number of vertex in this array.</param>
    public VertexArray(PrimitiveType type, int count)
    {
        _vertices = new Vertex[count];
        _vertices.Initialize();

        Type = type;
    }

    /// <summary>
    /// Create a new <see cref="VertexArray"/>.
    /// </summary>
    /// <param name="type">Type of primitive.</param>
    /// <param name="vertices">Existing vertices.</param>
    public VertexArray(PrimitiveType type, Vertex[] vertices)
    {
        _vertices = vertices;
        Type = type;
    }

    /// <summary>
    /// Get the <see cref="Vertex"/> based of index on Array.
    /// </summary>
    /// <param name="index">Index of <see cref="Vertex"/> in Array.</param>
    /// <returns>Selected <see cref="Vertex"/> based specified index.</returns>
    public Vertex this[int index]
    {
        get
        {
            if (index >= _vertices.Length || index < 0 || _vertices.Length == 0)
                return new Vertex();

            return _vertices[index];
        }
        set
        {
            if (index >= _vertices.Length || index < 0 || _vertices.Length == 0)
                return;

            _vertices[index] = value;
        }
    }

    /// <summary>
    /// Resize the <see cref="VertexArray"/>.
    /// </summary>
    /// <param name="size">New Size of <see cref="VertexArray"/>.</param>
    public void Resize(int size)
    {
        Array.Resize(ref _vertices, size);
    }

    /// <summary>
    /// Add <see cref="Vertex"/> into <see cref="VertexArray"/>.
    /// </summary>
    /// <param name="vertex">The <see cref="Vertex"/> that to be added into <see cref="VertexArray"/></param>
    public void Append(Vertex vertex)
    {
        Resize(_vertices.Length + 1);
        _vertices[_vertices.Length - 1] = vertex;
    }

    /// <summary>
    /// Get Vertex Count
    /// </summary>
    /// <returns>Number of Vertex in Vertex Array</returns>
    public int GetVertexCount()
    {
        return _vertices.Length;
    }

    /// <summary>
    /// Clear the Vertex Array
    /// </summary>
    public void Clear()
    {
        Array.Clear(_vertices, 0, _vertices.Length);
    }

    /// <summary>
    /// Return Vertices as an Array of <see cref="Vertex"/>.
    /// </summary>
    /// <returns>An Array of <see cref="Vertex"/>.</returns>
    public Vertex[] ToArray()
    {
        return _vertices;
    }
}

And this is how I initialize my sprite:

        Texture texture;
        RenderImplementation impl;


        // Setup the Texture.
        // I don't think its necessary to post the texture class here.
        texture = new Texture();
        texture.LoadFromFile("Path/To/The/Texture.png");

        // Setup the Vertex Array.
        VertexArray v = new VertexArray(Primitives.Quads, 4);
        v[0] = new Vertex(new Vector2(0f, 0f), new Vector2(0f, 0f), Color.White);
        v[1] = new Vertex(new Vector2(800f, 0f), new Vector2(1f, 0f), Color.White);
        v[2] = new Vertex(new Vector2(800f, 600f), new Vector2(1f, 1f), Color.White);
        v[3] = new Vertex(new Vector2(0f, 600f), new Vector2(0f, 1f), Color.White);

        // Setup the Indices.
        ushort[] indices = new ushort[]
        {
            0, 1, 2, 3
        };

        // Setup the RenderImplementation.
        impl = new RenderImplementation(v, indices);

        // We don't have to upload it manually since the implementation will upload it for us automatically
        // But I think it will be better to upload it when initialization phase.
        impl.Upload();

And draw it like this:

        Texture.Bind(texture):
        impl.Render();

While wondering about the Implementation is 'acceptable' / 'good', there are also a few questions that I want to ask:

  1. The sprite positions may changed often (e.g: the player sprite move as the player input / the sprites uses texture atlas so it may change texcoord frequently), while the sprite positions (or another one of Vertex property) has changed, it will upload the updated Vertex, not onlyVertex.position, but whole Vertex (which mean as well as Vertex.texCoord and Vertex.color). Will this lead performance issue? If so, do I need to create separate buffer handle for position, texcoord and color? Or is there a better way?
  2. As you may see in sprite initialization and drawing code, each sprite instance has an instance of RenderImplementation in it, is it good idea? or should I use one instance of RenderImplementation for every sprite that I have in my game? If so, isn't that mean I should upload the sprite vertex before I draw each of them (in case VertexBufferObject is preferred)?
\$\endgroup\$
3
\$\begingroup\$

Your implementation suffers of over-engineering and is pretty rigid in terms of extensibility.

Legacy

  • are there really that many platforms still under OGL < 1.5 ?
  • if you have such case, does it even support C# ?
  • IMO you should have a LegacyRenderer (immediate) and a ModernRenderer, don't mix them

Implementation

  • your system only supports one type of vertex declaration : position, color, UV
  • nowhere you are letting the user specify its custom declaration
  • nowhere you are letting the user specify a shader
  • the binding of vertex attributes have fixed positions

I would suggest to take a look at the following library I wrote a while ago, it does mimic what XNA does in the sense that you get VBOs, VAOs, Effects classes; and these are untied to any specific vertex declaration as you did.

Repository / usage example:

https://github.com/aybe/GLA https://github.com/aybe/GLA/blob/master/GLADemo/GameDemoGLA.cs#L125

(you will see 3 examples: lines, triangles, textures)

Dynamic assignment of vertex attributes according user declaration:

https://github.com/aybe/GLA/blob/master/GLA/VertexArray.cs https://github.com/aybe/GLA/blob/master/GLA/VertexDeclaration.cs https://github.com/aybe/GLA/blob/master/GLA/VertexElement.cs https://github.com/aybe/GLA/blob/master/GLA/VertexElementFormat.cs https://github.com/aybe/GLA/blob/master/GLA/VertexElementUsage.cs

I designed that library by reverse-engineering XNA using a decompiler such as DotPeek.

Also, another great source of inspiration to you could be the MonoGame project:

https://github.com/mono/MonoGame/tree/develop/MonoGame.Framework/Graphics/Vertices

Last thing, spending a few days in using XNA or MonoGame would certainly help you get a better picture of what's a good API and how you should design yours consequently :D

EDIT:

For your sprites at different positions, either render them by each time updating the transform matrix in your shader or use instanciation.

\$\endgroup\$
  • \$\begingroup\$ Thanks for the review, currently I cannot touch the my source now but here my responses (and questions) Legacy 1 and 2: I know there is only few machines that probably still run under OpenGL < 1.5, but i have "unexplainable" reasons why i care about compatibility (especially to support Immediate Mode and without Shader) and i'm pretty sure it support .NET Application (I'm using .NET 2.0) 3. I will keep in mind, I think i will create like an Interface of Renderer then derive it to each implementation. I will view the rest and edit the comments later \$\endgroup\$ – SirusDoma Aug 2 '15 at 19:54
  • \$\begingroup\$ Implementation: 1. It's intended to use one type of vertex declaration since im dealing with simple 2D, but I will probably add it later. 2 & 3. What is the custom declaration? is it shader related? well, like I stated before, I will not use shader in my program. 4. fixed positions, you mean 800x600? I'm really sorry but it was meant to be an example, at the end it uses Sprite.Width and Sprite.Height. I will take a look the links tomorrow, i think it will be useful example for me :D,(fyi, i'm trying to make the API look similar like SFML later.) \$\endgroup\$ – SirusDoma Aug 2 '15 at 20:22
  • 1
    \$\begingroup\$ I think for legacy you should support an old OGL version < 1.5, for a modern one just go version 3.0+ (the new approach). A declaration is your Vertex type, it can be anything you'd like to pass along your rendering pipeline, such as normals when doing 3D. The order I'm talking is in the declaration, by doing like in XNA, when you build a new declaration you will refer to names Position, UV, etc ... not to integer indices + classes I've shown will do the mapping automatically. \$\endgroup\$ – Aybe Aug 3 '15 at 14:08
  • \$\begingroup\$ If you don't have more requirements than what you've laid out, that's fine then, you can probably go on. However, spending a bit more time in looking at how others did it will help you get a better picture and certainly save you some time in the future. There are small architectural decisions that might be huge time savers in the future. BTW for OGL 3+ you'll use shaders, that's not a big deal, in one afternoon you should know how to use them (see my examples). \$\endgroup\$ – Aybe Aug 3 '15 at 14:13

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.