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
RenderImplementationthat contains 2 Rendering Implementation,VertexBufferObjectandImmediate Mode,Immediate Modeis used when its preferred or..VertexBufferObjectis 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:
- 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
texcoordfrequently), while the sprite positions (or another one ofVertexproperty) has changed, it will upload the updatedVertex, not onlyVertex.position, but wholeVertex(which mean as well asVertex.texCoordandVertex.color). Will this lead performance issue? If so, do I need to create separate buffer handle for position,texcoordandcolor? Or is there a better way? - As you may see in sprite initialization and drawing code, each sprite instance has an instance of
RenderImplementationin it, is it good idea? or should I use one instance ofRenderImplementationfor 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 caseVertexBufferObjectis preferred)?