GLESGAE:Dealing with Textures

From Pandora Wiki
Jump to: navigation, search

Contents

GLESGAE - Textures

Introduction

Textures are nice and simple to deal with.
Well... when they're in the format you want, they're nice and simple!

There are many many texture compression formats, from ETC1 and PVRTC to the usual S3TC set of DXT1, DXT3 and DXT5.
Not every platform supports the same set of compression formats either.
Our Pandoras will support ETC1 due to it being an OpenGL ES standard, and PVRTC due to the PowerVR chipset.
It'll also support uncompressed textures such as RGB and RGBA - and these are what we'll load up today in the good 'ol BMP format.

Fast Track

We're now on to SVN revision 7... svn co -r 7 http://svn3.xp-dev.com/svn/glesgae/trunk/ glesgae

Loading a BMP

There are oodles of pages and documentation on the BMP format... so we'll just get this done quickly.
Technically, this breaks our original design choice of having a pipeline feed us data as we're manually loading up data and converting it ourselves.. however, to get something up and running quickly, this'll do.
We still have a lot to get through before dealing with our data pipeline, and it's much more interesting to get something working now in a state we can upgrade later, than write screeds of code we can't test!

So yes, our quick BMP loader.

Texture.h

#ifndef _TEXTURE_H_
#define _TEXTURE_H_

#if defined(GLX)
	#include "GLee.h"
#elif defined(PANDORA)
	#if defined(GLES1)
		#include <GLES/gl.h>
	#elif defined(GLES2)
		#include <GLES2/gl2.h>
	#endif
#endif

#include <string>

namespace GLESGAE
{
	class Texture
	{
		public:
			enum TextureFormat {
				INVALID_FORMAT
			,	RGBA
			,	RGB
			};
			
			Texture() : mId(), mData(0), mWidth(), mHeight(), mType(INVALID_FORMAT) {}
			
			/// Load as BMP
			void loadBMP(const std::string& fileName);

			/// Retrieve this Texture's GL id
			GLuint getId() const { return mId; }
			
			/// Get Width
			unsigned int getWidth() const { return mWidth; }
			
			/// Get Height
			unsigned int getHeight() const { return mHeight; }
			
		protected:
			/// Create GL Id
			void createGLid();

		private:
			GLuint mId;
			unsigned char* mData;
			unsigned int mWidth;
			unsigned int mHeight;
			TextureFormat mType;
	};
}

#endif

Not much going on here.. we're storing a data pointer, the GLuint reference, the width and height, and the type of format we've loaded - be it RGB or RGBA.

To the meat!

Texture.cpp

#include "Texture.h"

#include <cstdio>

using namespace GLESGAE;

void Texture::loadBMP(const std::string& fileName)
{
	FILE *file;
	unsigned long size;                 // size of the image in bytes.
	unsigned short int planes;          // number of planes in image (must be 1) 
	unsigned short int bpp;             // number of bits per pixel (must be 24)

	// make sure the file is there.
	if ((file = fopen(fileName.c_str(), "rb"))==NULL)
		return;

	// seek through the bmp header, up to the width/height:
	fseek(file, 18, SEEK_CUR);

	if (1 != fread(&mWidth, 4, 1, file))
		return;

	// read the height 
	if (1 != fread(&mHeight, 4, 1, file))
		return;

	// read the planes
	if (1 != fread(&planes, 2, 1, file))
		return;

	if (1 != planes) // Only supporting single layer BMP just now
		return;

	// read the bpp
	if (1 != fread(&bpp, 2, 1, file))
		return;

	if (24 == bpp) {
		size = mWidth * mHeight * 3U; // RGB
		mType = RGB;
	}
	else if (32 == bpp) {
		size = mWidth * mHeight * 4U; // RGBA
		mType = RGBA;
	}

	// seek past the rest of the bitmap header.
	fseek(file, 24, SEEK_CUR);

	// read the data. 
	mData = new unsigned char[size];
	if (mData == 0)
		return;	

	if (1 != fread(mData, size, 1, file))
		return;

	if (24 == bpp) {
		for (unsigned int index(0U); index < size; index += 3U) { // reverse all of the colors. (bgr -> rgb)
			unsigned char temp(mData[index]);
			mData[index] = mData[index + 2U];
			mData[index + 2U] = temp;
		}
	}
	else if (32 == bpp) {
		for (unsigned int index(0U); index < size; index += 4U) { // reverse all of the colors. (bgra -> rgba)
			unsigned char temp(mData[index]);
			mData[index] = mData[index + 2U];
			mData[index + 2U] = temp;
		}
	}

	createGLid();

	delete [] mData;
}

void Texture::createGLid()
{
	glGenTextures(1, &mId);
	glBindTexture(GL_TEXTURE_2D, mId);
	
	// Enable some filtering
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

	// Load up our data into the texture reference
	switch (mType) {
		case RGB:
			glTexImage2D(GL_TEXTURE_2D, 0U, GL_RGB, mWidth, mHeight, 0U, GL_RGB, GL_UNSIGNED_BYTE, mData);
			break;
		case RGBA:
			glTexImage2D(GL_TEXTURE_2D, 0U, GL_RGBA, mWidth, mHeight, 0U, GL_RGBA, GL_UNSIGNED_BYTE, mData);
			break;
		case INVALID_FORMAT:
		default:
			break;
	};
}

Honestly, there's not much going on here.
We load up the file, we skip the header to the width and height info and read them out.
Then, we check how many layers there are and ensure there's only the one.
After that, we take the Bits Per Pixel value out.. we can load up RGB - or 24bit - and RGBA - or 32bit - and we check for these and adjust our size accordingly as 24bit has 3 components - R, G and B - and 32bit has 4 components - R, G, B and A.
We skip the rest of the header as we're not interested in it, then load up the actual data itself.

Now the fun bit.
BMP actually stores information in BGR/BGRA format. We need it as RGB/RGBA so we need to swizzle the texture.
We do this by running through the texture, and literally swapping the values about manually.

Once that's done, we trigger GL to actually create the Texture ID and upload the texture to it.
Check the GL guide for what's going on here and the parameters, as there's quite a few.
Once GL has our texture though, we can delete it from our memory.

Updating Material

Materials are generally the objects which define how things get drawn. How shiny they are, their colours, etc... and of course, their textures.

We therefore need to add some code to Material so we can add and fiddle with our textures!

/// Add a Texture
void addTexture(Texture* const texture) { mTextures.push_back(texture); }

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

/// Grab amount of Textures we have
unsigned int getTextureCount() const { return mTextures.size(); }

And that's all we need, really.
I had already done bits of Material's texture support already, so it contains a vector of Texture* objects, and all it really needed was addTexture and getTextureCount.
The whole file is in SVN anyway.

Mesh Fiddling

Of course, sending a texture over is nice and all, but we need to tell GL how on Earth to draw the thing - and that's where Texture Co-ordinates come in. These map the 2D image to the vertex points on your mesh.

Let's look at our little quad as it stands:

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

So we're going from top left, to top right, to bottom right, then bottom left in our Position. Let's remove the Colour values and replace it with some texture co-ordinates to match up.

float vertexData[32] = {// Position - 16 floats
			-1.0F, 1.0F, 0.0F, 1.0F,
			1.0F, 1.0F, 0.0F, 1.0F,
			1.0F, -1.0F, 0.0F, 1.0F,
			-1.0F, -1.0F, 0.0F, 1.0F,
			// Tex Coords - 8 floats
			1.0F, 1.0F, 	// top right
			0.0F, 1.0F, 	// top left
			0.0F, 0.0F, 	// bottom left
			1.0F, 0.0F};	// bottom right

Hold on, we're going the wrong way with the Texture Co-ordinates! Or are we?
The other fun thing about BMP is it stores it upside down, so I'm compensating for it here.
We also only store two floats as they're unit X/Y values from the texture... this means we need to change the data description as well:

	VertexBuffer* newVertexBuffer = new VertexBuffer(reinterpret_cast<unsigned char*>(&vertexData), vertexSize);
	newVertexBuffer->addFormatIdentifier(VertexBuffer::FORMAT_POSITION_4F, 4U);
	newVertexBuffer->addFormatIdentifier(VertexBuffer::FORMAT_TEXTURE_2F, 4U); // replacing the Colour one we used to have.

We also need to add a Texture to the Material here, so we'll modify the function to take this in, and then add it to the Material:

Mesh* makeSprite(Shader* const shader, Texture* const texture)
{
...
...
	Material* newMaterial = new Material;
	newMaterial->setShader(shader);
	newMaterial->addTexture(texture);
	Matrix4* newTransform = new Matrix4;

We dummy out the shader stuff on Fixed Function pipelines, so we can safely use it without issue.

Shaders and Textures

We'll get the harder system done and out the way first...

Our shaders are going to need updated to deal with the fact we'll be sending in Texture Co-ordinates and a Texture itself.
So let's go and do just that:

	std::string vShader =
		"attribute vec4 a_position;					\n"
		"attribute vec2 a_texCoord0;					\n"
		"varying vec2 v_texCoord0;					\n"
		"uniform mat4 u_mvp;						\n"
		"void main()							\n"
		"{								\n"
		"	gl_Position =  u_mvp * a_position;			\n"
		"	v_texCoord0 = a_texCoord0;				\n"
		"}								\n";

	std::string fShader =
		#ifdef GLES2
		"precision mediump float;					\n"
		#endif
		"varying vec2 v_texCoord0;					\n"
		"uniform sampler2D s_texture0;					\n"
		"void main()							\n"
		"{								\n"
		"	gl_FragColor = texture2D(s_texture0, v_texCoord0);	\n"
		"}								\n";

In the vertex shader, we're expecting a vec2 - a float vector of two components - with the name a_texCoord0. We then just copy it into the varying without touching it to pass it through to the fragment shader.
The fragment shader picks this up, and looks for a uniform of the special 2D Sampler type called s_texture0.
Then the interesting bit; we call a built-in function - texture2D - with our texture uniform and texture co-ordinate, and pass it to the fragment colour. That's it.. we'll get textures assuming we push in the bits we need.. so let's do that!

Remember our Uniform System?
Well, we've added a new unform here, so we need another updater:

#ifndef _TEXTURE0_UNIFORM_UPDATER_H_
#define _TEXTURE0_UNIFORM_UPDATER_H_

#include "../../Graphics/ShaderUniformUpdater.h"

class Texture0UniformUpdater : public GLESGAE::ShaderUniformUpdater
{
	public:
		Texture0UniformUpdater() : GLESGAE::ShaderUniformUpdater() {}
		
		void update(const GLint uniformId, const GLESGAE::Camera* const camera, const GLESGAE::Material* const material, const GLESGAE::Matrix4* const transform);
};

#endif

Pretty empty header file.. most of these will be like this really.

#include "Texture0UniformUpdater.h"

#if defined(GLX)
	#include "../../Graphics/GLee.h"
#elif defined(GLES2)
	#if defined(PANDORA)
		#include <GLES2/gl2.h>
	#endif
#endif

#include "../../Maths/Matrix4.h"
#include "../../Graphics/Camera.h"
#include "../../Graphics/Material.h"
#include "../../Graphics/Texture.h"

using namespace GLESGAE;

void Texture0UniformUpdater::update(const GLint uniformId, const Camera* const camera, const Material* const material, const Matrix4* const transform)
{
	Texture* const texture(material->getTexture(0U));
	glUniform1i(texture->getId(), 0U);
}

Actually, this isn't much different!
We're only doing two things - taking out the Texture object from the material at index 0 ( this is texture 0 after all... ) then we bind the id to the uniform. The 0 signifies that this is using texture co-ordinate set 0.

Pretty straight forward so far, isn't it?

However, we have no texture support in the Shader Based Context.. so we're best adding that now.

Shader Based Context Fiddling

We're going to do just a little bit of fiddling to the Shader Based Context to get it rendering our texture just now.
We will need to revisit this soon, but we need to get VBOs in first as that changes the renderer again.

So, in the header file, we'll add a new function and a new member variable:

public:
	/// Update Textures
	void updateTextures(const Material* const material);

private:
	GLenum mLastTextureUnit;

We store the last texture unit we fiddled with as an optimization. It's bad form to constantly turn texture units on and off, especially if you only just messed with the same one in the previous frame!

Now let's look at that updateTextures function:

void ShaderBasedContext::updateTextures(const Material* const material)
{
	const unsigned int textureCount(material->getTextureCount());

	for (unsigned int currentTexture(0U); currentTexture < textureCount; ++currentTexture) {
		GLenum currentTextureUnit(GL_TEXTURE0 + currentTexture);
		if (currentTextureUnit != mLastTextureUnit) {
			glActiveTexture(currentTextureUnit);
			mLastTextureUnit = currentTextureUnit;
		}
		
		Texture* const texture(material->getTexture(currentTexture));
		glBindTexture(GL_TEXTURE_2D, texture->getId());
	}
}

All we're doing here, is running through all the textures we may have on our material, and ensuring we have enough texture units available for them.
If we've already activated the texture unit, we leave it alone, but we always bind the texture as it may have changed. Nice and simple.

We call this function in the draw loop.. around here will do:

	bindShader(material->getShader());
	updateUniforms(material, transform);
	updateTextures(material);

And we can remove the variable that was storing currentTextureUnit in this function, as we don't need it any more.

Unfortunately, we didn't add in the Texture Co-ordinate bits to our big switch statement, and due to the whole attribute binding part, it's a bit trickier than we'd like.
We're only going to quickly push it in to get something showing, but we'll need to reinvestigate this area next time for VBOs anyway.
Either way, we just need to add the following to the switch statement:

 			// Texture
 			case VertexBuffer::FORMAT_TEXTURE_2F:
 				glVertexAttribPointer(a_texCoord0, 2, GL_FLOAT, GL_FALSE, 0, vertexBuffer->getData() + itr->getOffset());
 				break;

And the last little bit that we need, which is rather important, is adding this to the constructor:

glEnable(GL_TEXTURE_2D);

Which enables texturing in the first place!

Fixed Function Requirements

Amusingly, we've already done everything we need to do for the Fixed Function pipeline to render Textures.

That was easy, wasn't it?

However, we do need to update our mesh description in main.cpp, so we'll do that:

#ifndef GLES2
	FixedFunctionContext* const fixedContext(graphicsSystem->getFixedContext());
	if (0 != fixedContext) {
		fixedContext->enableFixedFunctionVertexPositions();
	}
#endif

That's us.. only had to remove the Vertex Colours description as we're not sending them over any more, and Texture Co-ordinates are done slightly differently from everything else, so we catch them dynamically when the mesh hits the context.

All source is in the SVN if you need a reminder as to what we've done.

A Concatenated Example

And now let's edit our example code to pull in our texture and display it.
Most of this should be familiar, and we've already changed it but for completeness sake:

#include <cstdio>
#include <cstdlib>

#include "../../Graphics/GraphicsSystem.h"
#include "../../Graphics/Context/FixedFunctionContext.h"
#include "../../Graphics/Context/ShaderBasedContext.h"
#include "../../Events/EventSystem.h"
#include "../../Input/InputSystem.h"
#include "../../Input/Keyboard.h"
#include "../../Input/Pad.h"

#include "../../Graphics/Camera.h"
#include "../../Graphics/Mesh.h"
#include "../../Graphics/VertexBuffer.h"
#include "../../Graphics/IndexBuffer.h"
#include "../../Graphics/Material.h"
#include "../../Graphics/Shader.h"
#include "../../Graphics/Texture.h"
#include "../../Maths/Matrix4.h"

#include "MVPUniformUpdater.h"
#include "Texture0UniformUpdater.h"

using namespace GLESGAE;

void controlCamera(Camera* const camera, Controller::KeyboardController* const keyboard);
Mesh* makeSprite(Shader* const shader, Texture* const texture);
Shader* makeSpriteShader();

int main(void)
{
	EventSystem* eventSystem(new EventSystem);
	InputSystem* inputSystem(new InputSystem(eventSystem));
	GraphicsSystem* graphicsSystem(new GraphicsSystem(GraphicsSystem::FIXED_FUNCTION_RENDERING));

	if (false == graphicsSystem->initialise("GLESGAE Texturing Test", 800, 480, 16, false)) {
		//TODO: OH NOES! WE'VE DIEDED!
		return -1;
	}

#ifndef GLES2
	FixedFunctionContext* const fixedContext(graphicsSystem->getFixedContext());
	if (0 != fixedContext) {
		fixedContext->enableFixedFunctionVertexPositions();
	}
#endif

#ifndef GLES1
	ShaderBasedContext* const shaderContext(graphicsSystem->getShaderContext());
	if (0 != shaderContext) {
		shaderContext->addUniformUpdater("u_mvp", new MVPUniformUpdater);
		shaderContext->addUniformUpdater("s_texture0", new Texture0UniformUpdater);
	}
#endif

	Texture* texture(new Texture());
	texture->loadBMP("Texture.bmp");
	Mesh* mesh(makeSprite(makeSpriteShader(), texture));
	Camera* camera(new Camera(Camera::CAMERA_3D));
	camera->getTransformMatrix().setPosition(Vector3(0.0F, 0.0F, 5.0F));

	eventSystem->bindToWindow(graphicsSystem->getWindow());

	Controller::KeyboardController* myKeyboard(inputSystem->newKeyboard());

	while(false == myKeyboard->getKey(Controller::KEY_ESCAPE)) {
		controlCamera(camera, myKeyboard);

		eventSystem->update();
		inputSystem->update();
		graphicsSystem->beginFrame();
		graphicsSystem->setCamera(camera);
		graphicsSystem->drawMesh(mesh);
		graphicsSystem->endFrame();
	}

	delete graphicsSystem;
	delete inputSystem;
	delete eventSystem;
	delete mesh;

	return 0;
}

Mesh* makeSprite(Shader* const shader, Texture* const texture)
{
	float vertexData[32] = {// Position - 16 floats
					-1.0F, 1.0F, 0.0F, 1.0F,
					1.0F, 1.0F, 0.0F, 1.0F,
					1.0F, -1.0F, 0.0F, 1.0F,
					-1.0F, -1.0F, 0.0F, 1.0F,
					// Tex Coords - 8 floats
					1.0F, 1.0F, 	// top right
					0.0F, 1.0F, 	// top left
					0.0F, 0.0F, 	// bottom left
					1.0F, 0.0F};	// bottom right

	unsigned int vertexSize = 25 * 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_4F, 4U);
	newVertexBuffer->addFormatIdentifier(VertexBuffer::FORMAT_TEXTURE_2F, 4U);
	IndexBuffer* newIndexBuffer = new IndexBuffer(reinterpret_cast<unsigned char*>(&indexData), indexSize, IndexBuffer::FORMAT_UNSIGNED_BYTE);
	Material* newMaterial = new Material;
	newMaterial->setShader(shader);
	newMaterial->addTexture(texture);
	Matrix4* newTransform = new Matrix4;

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

void controlCamera(Camera* const camera, Controller::KeyboardController* const keyboard)
{
	Vector3 newPosition;
	camera->getTransformMatrix().getPosition(&newPosition);
	if (true == keyboard->getKey(Controller::KEY_ARROW_DOWN))
		newPosition.z() -= 0.01F;
	
	if (true == keyboard->getKey(Controller::KEY_ARROW_UP))
		newPosition.z() += 0.01F;
	
	if (true == keyboard->getKey(Controller::KEY_ARROW_LEFT))
		newPosition.x() -= 0.01F;
	
	if (true == keyboard->getKey(Controller::KEY_ARROW_RIGHT))
		newPosition.x() += 0.01F;
	camera->getTransformMatrix().setPosition(newPosition);
	
	camera->update();
}

#if defined(GLX)
	#include "../../Graphics/GLee.h"
#endif

Shader* makeSpriteShader()
{
	std::string vShader =
		"attribute vec4 a_position;					\n"
		"attribute vec2 a_texCoord0;					\n"
		"varying vec2 v_texCoord0;					\n"
		"uniform mat4 u_mvp;						\n"
		"void main()							\n"
		"{								\n"
		"	gl_Position =  u_mvp * a_position;			\n"
		"	v_texCoord0 = a_texCoord0;				\n"
		"}								\n";

	std::string fShader =
		#ifdef GLES2
		"precision mediump float;					\n"
		#endif
		"varying vec2 v_texCoord0;					\n"
		"uniform sampler2D s_texture0;					\n"
		"void main()							\n"
		"{								\n"
		"	gl_FragColor = texture2D(s_texture0, v_texCoord0);	\n"
		"}								\n";

	#ifndef GLES1
		Shader* newShader(new Shader());
		newShader->createFromSource(vShader, fShader);

		return newShader;
	#else
		return 0;
	#endif
}


If we're compiling for ES1 we need to ensure we're using FIXED_FUNCTION_RENDERING, else with ES2 it'll be SHADER_BASED_RENDERING.
Other than that, they should be exactly identical!

You might wonder why we don't get alpha working properly.. we haven't set any blending modes up.. we'll get to that, but for now, we have textures and the reign of terror of the coloured quad is at an end! All hail Mr Smiley Face!

Building the Example

In the SVN there are Makefiles already setup for you.. just trigger make -f MakefileES2.pandora or whatever your chosen configuration is, and it'll happily build for you and spit out a GLESGAE.pandora binary for you to run.

Next Time

We've one more bit of core functionality to add - VBOs.. and that'll be next.

Personal tools
community