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
andImmediate 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:
- 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 ofVertex
property) has changed, it will upload the updatedVertex
, not onlyVertex.position
, but wholeVertex
(which mean as well asVertex.texCoord
andVertex.color
). Will this lead performance issue? If so, do I need to create separate buffer handle for position,texcoord
andcolor
? Or is there a better way? - 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 ofRenderImplementation
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 caseVertexBufferObject
is preferred)?