GLESGAE:Making a Mesh

From Pandora Wiki
Jump to: navigation, search

Contents

GLESGAE - Making a Mesh

Introduction

Originally, I was just going to dive right in to Shader Based Rendering.
This was a stupid idea... what was I going to render?
So, I've split up the original behemoth of a Chapter and decided to split out the Vertex Array part as it was fairly self-contained and could prove handy as another alternative to the existing tutorial on the Wiki.

Vertex Array Overview

In the olden days, OpenGL had Immediate Mode.
This allowed you to do glBegin() .. .. .. glEnd() and effectively whatever points and things you specified would be drawn in that order, immediately, on the screen. Nice and simple.
Obviously this was a bit iffy, so Display Lists were developed to take a copy of your glBegin/glEnd logic's final outcome, and instance it all over the place instead of stopping to plot your points again. Magic!
However, this was still slow in that you still had to do the initial glBegin/glEnd logic, which although is rather easy, it's also rather wordy and has a hit on what the Graphics Card is actually doing. This is what Vertex Arrays are designed to fix.

Vertex Arrays describe Meshes.
How they describe them is effectively up to you, but in general they're going to have Vertices, Normals, Texture Co-ordinates, Colours and potentially Indices as well.
These can all be managed in one of two ways - separate Vertex Arrays for each Attribute, or one giant Interleaved Array with relevant offsets and stride values.

In GLESGAE, we use interleaved arrays.

Interleave my What-now?

Interleaving is just a fancy way of squishing all the data in one array. This actually works out faster as we know for a fact that all our Vertex information is going to be in one long memory chunk, so we just set a pointer somewhere and run through it all, rather than having to jump about all the time to find bits and pieces.

Here's some example vertex data - in particular, the vertex data we will be drawing later on:

	float vertexData[24] = {// Position - 12 floats
					-1.0F, 1.0F, 0.0F,
					1.0F, 1.0F, 0.0F,
					1.0F, -1.0F, 0.0F,
					-1.0F, -1.0F, 0.0F,
					// Colour - 12 floats
					0.0F, 1.0F, 0.0F,
					1.0F, 0.0F, 0.0F,
					0.0F, 0.0F, 1.0F,
					1.0F, 1.0F, 1.0F};

As you can see, I'm using three floats to define a positional point, and three floats to define a colour.
With four points, we can draw a quad. No, really! As GLESGAE also deals with index buffers. Here's the one for the quad:

unsigned char indexData[6] = { 0, 1, 2, 2, 3, 0 }

This is stating that we're using point 0, 1 and 2 as Triangle 1, and 2, 3 and 0 as Triangle 2 - making up our quad on the screen.

  • Point 0 corresponds to -1.0F, 1.0F, 0.0F
  • Point 1 corresponds to 1.0F, 1.0F, 0.0F
  • Point 2 corresponds to 1.0F, -1.0F, 0.0F

and so on...

Defining a Format

So let's go create a Vertex Buffer class to tie everything up, and allow us to tell the engine how to draw our vertex data.

VertexBuffer.h

#ifndef _VERTEX_BUFFER_H_
#define _VERTEX_BUFFER_H_

#include <vector>

namespace GLESGAE
{
	class VertexBuffer
	{
		public:
			enum FormatType
			{
				// Float
				FORMAT_CUSTOM_4F
			,	FORMAT_CUSTOM_3F
			,	FORMAT_CUSTOM_2F
			,	FORMAT_POSITION_2F
			,	FORMAT_POSITION_3F
			,	FORMAT_POSITION_4F
			,	FORMAT_NORMAL_3F
			,	FORMAT_COLOUR_3F	// Not available in GLES1
			,	FORMAT_COLOUR_4F
			,	FORMAT_TEXTURE_2F
			,	FORMAT_TEXTURE_3F
			,	FORMAT_TEXTURE_4F
				// Unsigned/Byte
			,	FORMAT_CUSTOM_2B
			,	FORMAT_CUSTOM_3B
			,	FORMAT_CUSTOM_4B
			,	FORMAT_POSITION_2B
			,	FORMAT_POSITION_3B
			,	FORMAT_POSITION_4B
			,	FORMAT_NORMAL_3B
			,	FORMAT_COLOUR_3UB	// Not available in GLES1
			,	FORMAT_COLOUR_4UB
			,	FORMAT_TEXTURE_2B
			,	FORMAT_TEXTURE_3B
			,	FORMAT_TEXTURE_4B
				// Short
			,	FORMAT_CUSTOM_2S
			,	FORMAT_CUSTOM_3S
			,	FORMAT_CUSTOM_4S
			,	FORMAT_POSITION_2S
			,	FORMAT_POSITION_3S
			,	FORMAT_POSITION_4S
			,	FORMAT_NORMAL_3S
			,	FORMAT_COLOUR_3S	// Not available in GLES1
			,	FORMAT_COLOUR_4S	// Not available in GLES1
			,	FORMAT_TEXTURE_2S
			,	FORMAT_TEXTURE_3S
			,	FORMAT_TEXTURE_4S
			};

			class Format
			{
				public:
					Format(const FormatType type, unsigned int offset)
					: mType(type)
					, mOffset(offset)
					{
						switch (mType)
						{
							case FORMAT_CUSTOM_2F:
							case FORMAT_POSITION_2F:
							case FORMAT_TEXTURE_2F:
								mSize = sizeof(float) * 2;
								break;
							case FORMAT_CUSTOM_3F:
							case FORMAT_POSITION_3F:
							case FORMAT_NORMAL_3F:
							case FORMAT_COLOUR_3F:
							case FORMAT_TEXTURE_3F:
								mSize = sizeof(float) * 3;
								break;
							case FORMAT_CUSTOM_4F:
							case FORMAT_POSITION_4F:
							case FORMAT_COLOUR_4F:
							case FORMAT_TEXTURE_4F:
								mSize = sizeof(float) * 4;
								break;
							case FORMAT_CUSTOM_2B:
							case FORMAT_POSITION_2B:
							case FORMAT_TEXTURE_2B:
								mSize = sizeof(char) * 2;
								break;
							case FORMAT_CUSTOM_3B:
							case FORMAT_POSITION_3B:
							case FORMAT_NORMAL_3B:
							case FORMAT_COLOUR_3UB:
							case FORMAT_TEXTURE_3B:
								mSize = sizeof(char) * 3;
								break;
							case FORMAT_CUSTOM_4B:
							case FORMAT_POSITION_4B:
							case FORMAT_COLOUR_4UB:
							case FORMAT_TEXTURE_4B:
								mSize = sizeof(char) * 4;
							case FORMAT_CUSTOM_2S:
							case FORMAT_POSITION_2S:
							case FORMAT_TEXTURE_2S:
								mSize = sizeof(short) * 2;
								break;
							case FORMAT_CUSTOM_3S:
							case FORMAT_POSITION_3S:
							case FORMAT_NORMAL_3S:
							case FORMAT_COLOUR_3S:
							case FORMAT_TEXTURE_3S:
								mSize = sizeof(short) * 3;
								break;
							case FORMAT_CUSTOM_4S:
							case FORMAT_POSITION_4S:
							case FORMAT_COLOUR_4S:
							case FORMAT_TEXTURE_4S:
								mSize = sizeof(short) * 4;
								break;
							default:
								break;
						};
					}

					/// Retrieve the type of this Format Identifier
					const FormatType getType() const { return mType; }

					/// Retrieve the offset of this Format Identifier
					const unsigned int getOffset() const { return mOffset; }

					/// Retrieve the size of this Format Identifier
					const unsigned int getSize() const { return mSize; }

				private:
					FormatType mType;
					unsigned int mSize;
					unsigned int mOffset;
			};

			VertexBuffer(unsigned char* const data, const unsigned int size, const std::vector<Format>& format);
			VertexBuffer(unsigned char* const data, const unsigned int size);
			VertexBuffer(const VertexBuffer& vertexBuffer);

			/// Retrieve format details
			const std::vector<Format>& getFormat() const { return mFormat; }

			/// Retrieve data
			const unsigned char* getData() const { return mData; }

			/// Retrieve size
			const unsigned int getSize() const { return mSize; }

			/// Retrieve stride
			const unsigned int getStride() const { return mStride; }

			/// Add a Format Identifier
			void addFormatIdentifier(const FormatType formatType, const unsigned int amount);

		protected:
			/// Protected equals operator - use the copy constructor instead.
			VertexBuffer operator=(const VertexBuffer&) { return *this; }

		private:
			unsigned char* mData;
			unsigned int mSize;
			unsigned int mStride;
			std::vector<Format> mFormat;
	};
}

#endif

Firstly, I'm being my usual pedantic self and ensuring I can support every thing I can get away with. There are a few things which ES1 doesn't support however; which I have highlighted.
To render anything effectively, and if you can at all get away with it, your meshes should really use the smallest data unit you can. For example, using unsigned bytes for Colour values, Shorts for Vertices ( Bytes are probably a bit too small for large meshes, ) etc.. also be careful in that ES 1 expects 4 colour values rather than 3 - it requires that extra alpha bit!

Ignore the Custom declarations for now, as they're for Shader based rendering, where your Vertex Attributes can be whatever you like and you're not limited to the standard four types.

Of course, the irony is that as a fair chunk of the code is defined in the header, the meat of the object file is actually pretty small:

VertexBuffer.cpp

#include "VertexBuffer.h"

#include <cstring> // for memcpy

using namespace GLESGAE;

VertexBuffer::VertexBuffer(unsigned char* const data, const unsigned int size, const std::vector<Format>& format)
: mData(new unsigned char[size])
, mSize(size)
, mFormat(format)
, mStride(0U)
{
	std::memcpy(mData, data, size);

	for (std::vector<Format>::const_iterator itr(mFormat.begin()); itr < mFormat.end(); ++itr)
		mStride += itr->getSize();
}

VertexBuffer::VertexBuffer(unsigned char* const data, const unsigned int size)
: mData(new unsigned char[size])
, mSize(size)
, mFormat()
, mStride(0U)
{
	std::memcpy(mData, data, size);
}

VertexBuffer::VertexBuffer(const VertexBuffer& vertexBuffer)
: mData(vertexBuffer.mData)
, mSize(vertexBuffer.mSize)
, mFormat(vertexBuffer.mFormat)
, mStride(vertexBuffer.mStride)
{
}

void VertexBuffer::addFormatIdentifier(const FormatType formatType, const unsigned int amount)
{
	Format newFormat(formatType, mStride);
	mStride += newFormat.getSize() * amount;
	mFormat.push_back(newFormat);
}

All we're really doing here, is making sure we copy the mesh structure properly when we have to, and that we deal with the Format parameters properly and update our current stride amount when adding a new format identifier. Simples!

IndexBuffer.h

Considering all our Index Buffer has to do is store the order of the vertices to render, there's not much to it.

#ifndef _INDEX_BUFFER_H_
#define _INDEX_BUFFER_H_

namespace GLESGAE
{
	class IndexBuffer
	{
		public:
			enum FormatType {
				FORMAT_FLOAT			// unsupported by ES 1
			,	FORMAT_UNSIGNED_BYTE
			,	FORMAT_UNSIGNED_SHORT
			};

			IndexBuffer(unsigned char* const data, const unsigned int size, const FormatType format);
			IndexBuffer(const IndexBuffer& indexBuffer);

			/// Retrieve format details
			const FormatType getFormat() const { return mFormat; }

			/// Retrieve data
			const unsigned char* getData() const { return mData; }

			/// Retrieve size
			const unsigned int getSize() const { return mSize; }

		protected:
			/// Protected equals operator - use the copy constructor instead.
			IndexBuffer operator=(const IndexBuffer&) { return *this; }

		private:
			unsigned char* mData;
			unsigned int mSize;
			FormatType mFormat;
	};
}

#endif

Again, as with the Vertex Buffer, if you can at all get away with it, use the smallest data unit you can for your indices.
Be advised that ES 1 does not support Float indices so you're best using unsigned short. Actually, I'd recommend unsigned short overall... if you're dealing with meshes large enough to require Floats, you're probably doing it wrong or have pretty specific needs which this set of tutorials probably isn't going to help you with.

IndexBuffer.cpp

#include "VertexBuffer.h"

#include <cstring> // for memcpy

using namespace GLESGAE;

VertexBuffer::VertexBuffer(unsigned char* const data, const unsigned int size, const std::vector<Format>& format)
: mData(new unsigned char[size])
, mSize(size)
, mFormat(format)
, mStride(0U)
{
	std::memcpy(mData, data, size);

	for (std::vector<Format>::const_iterator itr(mFormat.begin()); itr < mFormat.end(); ++itr)
		mStride += itr->getSize();
}

VertexBuffer::VertexBuffer(unsigned char* const data, const unsigned int size)
: mData(new unsigned char[size])
, mSize(size)
, mFormat()
, mStride(0U)
{
	std::memcpy(mData, data, size);
}

VertexBuffer::VertexBuffer(const VertexBuffer& vertexBuffer)
: mData(vertexBuffer.mData)
, mSize(vertexBuffer.mSize)
, mFormat(vertexBuffer.mFormat)
, mStride(vertexBuffer.mStride)
{
}

void VertexBuffer::addFormatIdentifier(const FormatType formatType, const unsigned int amount)
{
	Format newFormat(formatType, mStride);
	mStride += newFormat.getSize() * amount;
	mFormat.push_back(newFormat);
}

The object file is just as weedy as the last one.

The Mesh Object

Now, to actually draw this, we're still in need of a few more bits and pieces.. but as we haven't really gotten to them yet, we'll just do some place holders.

Place holder Objects

Generally, Meshes will have a Material definition. This will tell the Graphics System how shiny the object is, for example, and how the lighting affects it. However, how Materials are actually used are a bit dependent upon the Rendering System's context, so are a bit out of scope for now.

We can therefore get away with the following:

Material.h

#ifndef _MATERIAL_H_
#define _MATERIAL_H_

#include <vector>

namespace GLESGAE
{
	class Shader;
	class Texture;
	class Material
	{
		public:
			Material();

			/// Grab the Shader that's linked to this Material
			Shader* const getShader() const { return mShader; }

			/// Set a new Shader on this Material
			void setShader(Shader* const shader) { mShader = shader; }

			/// Grab a Texture
			Texture* const getTexture(unsigned int index) const { return mTextures[index]; }

		private:
			Shader* mShader;
			std::vector<Texture*> mTextures;
	};
}

#endif

You might have noticed the Shader and Texture pointers there.
Luckily, as we've forward declared them up top, we don't really need to define them much more than this, so can ignore them for now!

Transforms

This one's a bit more complex as generally a Transform Matrix is a 4x4 Matrix, and requires a bit of Math knowledge to fiddle with.
Again, place holder time:

Matrix4.h
#ifndef _MATRIX4_H_
#define _MATRIX4_H_

namespace GLESGAE
{
	class Matrix4
	{
		public:
			Matrix4();

			/// Retrieve a pointer to the matrix
			float* const getData() { return mMatrix; }

			/// Set a new matrix
			void setMatrix(const float matrix[16]);

			/// Set to Identity
			void setToIdentity();

			/// Set to Zero
			void setToZero();

		private:
			float mMatrix[16];
	};

}

#endif
Matrix4.cpp
#include "Matrix4.h"

#include <cstring> // for memcpy

using namespace GLESGAE;

Matrix4::Matrix4()
: mMatrix()
{
	setToIdentity();
}

void Matrix4::setMatrix(const float matrix[16])
{
	std::memcpy(mMatrix, matrix, sizeof(float) * 16);
}

void Matrix4::setToIdentity()
{
	//	0	1	2	3
	//	4	5	6	7
	//	8	9	10	11
	//	12	13	14	15
	mMatrix[0] = mMatrix[5] = mMatrix[10] = mMatrix[15] = 1.0F;
	mMatrix[1] = mMatrix[2] = mMatrix[3] = 0.0F;
	mMatrix[4] = mMatrix[6] = mMatrix[7] = 0.0F;
	mMatrix[8] = mMatrix[9] = mMatrix[11] = 0.0F;
	mMatrix[12] = mMatrix[13] = mMatrix[14] = 0.0F;
}

void Matrix4::setToZero()
{
	mMatrix[0] = mMatrix[1] = mMatrix[2] = mMatrix[3] = 0.0F;
	mMatrix[4] = mMatrix[5] = mMatrix[6] = mMatrix[7] = 0.0F;
	mMatrix[8] = mMatrix[9] = mMatrix[10] = mMatrix[11] = 0.0F;
	mMatrix[12] = mMatrix[13] = mMatrix[14] = mMatrix[15] = 0.0F;
}

Fairly standard stuff.. an Identity matrix just has 1's down the diagonal and 0's elsewhere as shown, and is pretty handy for drawing stuff dead centre. We'll be using this to draw our quad on the screen starting from the centre so it fills the screen with coloured joy. A zero matrix pretty much explains itself.

Mesh.h

Binding all this up into one Mesh object is then fairly straight-forward:

#ifndef _MESH_H_
#define _MESH_H_

namespace GLESGAE
{
	class VertexBuffer;
	class IndexBuffer;
	class Material;
	class Matrix4;
	class Mesh
	{
		public:
			Mesh(VertexBuffer* const vertexBuffer
			,	IndexBuffer* const indexBuffer
			,	Material* const material
			,	Matrix4* const matrix);
			~Mesh();
			
			/// Grab the current Vertex Buffer - read-only
			const VertexBuffer* const getVertexBuffer() const { return mVertexBuffer; }
			
			/// Grab the current Index Buffer - read-only
			const IndexBuffer* const getIndexBuffer() const { return mIndexBuffer; }
			
			/// Grab the current Material - read-only
			const Material* const getMaterial() const { return mMaterial; }
			
			/// Grab the current Transform Matrix - read-only
			const Matrix4* const getMatrix() const { return mMatrix; }
			
			/// Grab an Editable Vertex Buffer
			VertexBuffer* const editVertexBuffer() { return mVertexBuffer; }
			
			/// Grab an Editable Index Buffer
			IndexBuffer* const editIndexBuffer() { return mIndexBuffer; }
			
			/// Grab an Editable Material
			Material* const editMaterial() { return mMaterial; }
			
			/// Grab an Editable Transform Matrix
			Matrix4* const editMatrix() { return mMatrix; }
			
		private:
			VertexBuffer* mVertexBuffer;
			IndexBuffer* mIndexBuffer;
			Material* mMaterial;
			Matrix4* mMatrix;
	};
}

#endif

Mesh.cpp

#include "Mesh.h"

using namespace GLESGAE;

Mesh::Mesh(VertexBuffer* const vertexBuffer, IndexBuffer* const indexBuffer, Material* const  material, Matrix4* const matrix)
: mVertexBuffer(vertexBuffer)
, mIndexBuffer(indexBuffer)
, mMaterial(material)
, mMatrix(matrix)
{
	
}

Mesh::~Mesh()
{
	//TODO: Resource Management
	delete mVertexBuffer;
	delete mIndexBuffer;
	delete mMaterial;
	delete mMatrix;
}

What could be simpler than that? a bunch of accessors, and a constructor which takes in a pointer to each part that makes up the Mesh.

Do note that we delete everything in the constructor - we're assuming that as soon as you give the Mesh the stuff it needs, that it takes full ownership of it.

Making a Mesh

Going back to our earlier example of showing you some vertex data to define a quad, here it is again in a handy function using what we've just done:

Mesh* makeSprite()
{
	float vertexData[24] = {// Position - 12 floats
					-1.0F, 1.0F, 0.0F,
					1.0F, 1.0F, 0.0F,
					1.0F, -1.0F, 0.0F,
					-1.0F, -1.0F, 0.0F,
					// Colour - 12 floats
					0.0F, 1.0F, 0.0F,
					1.0F, 0.0F, 0.0F,
					0.0F, 0.0F, 1.0F,
					1.0F, 1.0F, 1.0F};

	unsigned int vertexSize = 24 * sizeof(float);

	unsigned char indexData[6] = { 0, 1, 2, 2, 3, 0 };
	unsigned int indexSize = 6 * sizeof(unsigned char);

	VertexBuffer* newVertexBuffer = new VertexBuffer(reinterpret_cast<unsigned char*>(&vertexData), vertexSize);
	newVertexBuffer->addFormatIdentifier(VertexBuffer::FORMAT_POSITION_3F, 4U);
	newVertexBuffer->addFormatIdentifier(VertexBuffer::FORMAT_COLOUR_3F, 4U);
	IndexBuffer* newIndexBuffer = new IndexBuffer(reinterpret_cast<unsigned char*>(&indexData), indexSize, IndexBuffer::FORMAT_UNSIGNED_BYTE);
	Material* newMaterial = new Material;
	Matrix4* newTransform = new Matrix4;

	return new Mesh(newVertexBuffer, newIndexBuffer, newMaterial, newTransform);
}

Nice and simple, huh? While I'm not using the smallest format for everything; I am doing a reasonably even mix.

Checking out the SVN

There isn't actually any useful code for this Chapter that would make it standalone.
You'll just need to bear with me and wait till you get to the Rendering Contexts.

Building the Example

There is no example this time, as there'd be nothing to show!
Instead, jump to either GLESGAE:Fixed Function Rendering Contexts or GLESGAE:Shader Based Contexts for displaying your Mesh in the chosen manner.

Next Time

We'll be looking at rendering this mess... err, mesh.

Personal tools
community