GLESGAE:Fixed Function Transformations
Contents |
GLESGAE - Fixed Function Transformations
Introduction
The previous part is essential reading before you get here: GLESGAE:The Transform Stack
I won't be going over what's already been covered, I'll instead jump into the technical implementation.
While we'll be doing a lot of the same things as the Shader-Based transformation setup, I've split it up as ES 1 does have specific Matrix Stacks internally that you need to push onto. ES 2 does not. So the implementations do end up a bit different.
Fast Track
We're on SVN revision 5 now. This includes everything in this article, plus the extra bits of maths we need. svn co -r 5 http://svn3.xp-dev.com/svn/glesgae/trunk/ glesgae
Meet the Stacks
Deja Vu, perhaps?
OpenGL ES 1 ( and indeed, OpenGL 1.4 ) has a set of Matrix Modes, which are set via glMatrixMode, so that any matrix manipulations you do affect that set of matrices alone.
For our purposes, we want to mess with the following: GL_PROJECTION, GL_MODELVIEW, and GL_TEXTURE.
Gotcha: Hold the phone, we didn't talk about the Texture Stack in the last article! That's because it _only_ has any relevance in a Fixed Function pipeline as shaders are free to do as they please. Don't worry, we shall go through it in this article. Additionally, we've got a combined Model and View Matrix to deal with here. Life is hard.
We'll deal with the actual screen view and projection bits first, as they go directly into the Graphics System itself.
A View To The World
We shall be creating the Camera Object today.
This shall be a separate object, which shall deal with all it's own matrices so we can have multiple cameras all independent of one another. Particularly handy for doing effects such as Render To Texture and other Portal effects ( reflections in mirrors, for example. )
So, we take the three functions we devised in the previous article, and slap them into our Camera object.
We also need it to store it's own Transform matrix, as well as the View and Projection matrices unique to it, and various other little bits and pieces required to calculate these matrices. We therefore end up with an interface looking somewhat like this:
#ifndef _CAMERA_H_
#define _CAMERA_H_
#include "../Maths/Vector3.h"
#include "../Maths/Matrix4.h"
namespace GLESGAE
{
class Camera
{
public:
enum CameraType
{
CAMERA_2D
, CAMERA_3D
};
Camera(const CameraType& type);
/// Get the Type of Camera this is
const CameraType getType() { return mType; }
/// Update the camera's View and Projection matrices while looking ahead.
void update();
/// Update the camera's View and Projection matrices while looking at something.
void update(const Vector3& target);
/// Set Near Clip
void setNearClip(const float nearClip) { mNearClip = nearClip; }
/// Set Far Clip
void setFarClip(const float farClip) { mFarClip = farClip; }
/// Set 2d Parameters
void set2dParams(const float left, const float bottom, const float right, const float top) { m2dLeft = left; m2dRight = right; m2dTop = top; m2dBottom = bottom; }
/// Set 3d Parameters
void set3dParams(const float fov, const float aspectRatio) { mFov = fov; mAspectRatio = aspectRatio; }
/// Set the Transform Matrix
void setTransformMatrix(const Matrix4& transformMatrix) { mTransformMatrix = transformMatrix; }
/// Set the View Matrix
void setViewMatrix(const Matrix4& viewMatrix) { mViewMatrix = viewMatrix; }
/// Set the Projection Matrix
void setProjectionMatrix(const Matrix4& projectionMatrix) { mProjectionMatrix = projectionMatrix; }
/// Get the Transform Matrix
Matrix4& getTransformMatrix() { return mTransformMatrix; }
/// Get the View Matrix
Matrix4& getViewMatrix() { return mViewMatrix; }
/// Get the Projection Matrix
Matrix4& getProjectionMatrix() { return mProjectionMatrix; }
/// Get Near Clip
float getNearClip() const { return mNearClip; }
/// Get Far Clip
float getFarClip() const { return mFarClip; }
/// Get Field of View
float getFov() const { return mFov; }
/// Create a viewMatrix
static Matrix4 createViewMatrix(const Vector3& eye, const Vector3& centre, const Vector3& up);
/// Create a 2d projection matrix
static Matrix4 create2dProjectionMatrix(const float left, const float bottom, const float right, const float top, const float nearClip, const float farClip);
/// Create a 3d projection matrix
static Matrix4 create3dProjectionMatrix(const float nearClip, const float farClip, const float fov, const float aspectRatio);
private:
CameraType mType;
float mNearClip;
float mFarClip;
float m2dTop;
float m2dBottom;
float m2dLeft;
float m2dRight;
float mFov;
float mAspectRatio;
Matrix4 mTransformMatrix;
Matrix4 mViewMatrix;
Matrix4 mProjectionMatrix;
};
}
#endif
Nice and straight forward so far. We're just storing some matrices and values, providing access to them, and throwing our view and projection matrix creation functions on it.
We do have two update functions though - one which takes a target, and another which just updates itself. This is so we can have this camera follow a spline but look at something else, for example. Though this of course only makes much sense in a 3d world.. trying to use it on a 2d camera makes for some interesting results!
We specify what type of Camera this is in the constructor. This is so that when we trigger the update function, it regenerates the correct projection matrix for us. I've also put default values in the constructor so that in effect, you can just do Camera* camera(new Camera(Camera::CAMERA_2D)); and be done with it; call camera->update(); if you're moving it about, then just pass it in to the Graphics System.
Of course, full code is in the repository.. and as there's a lot of it, that's where it'll stay rather than being plastered here.
However, the update function is interesting, so let's go through it as you've already seen the view and projection matrix functions and they're no different ( bar some minor changes as I updated my Vector and Matrix classes. )
void Camera::update()
{
Matrix3 rotation;
Vector3 eye;
mTransformMatrix.decompose(&rotation, &eye);
const Vector3 target(eye + rotation.getFrontVector());
const Vector3 up(rotation.getUpVector());
mViewMatrix = createViewMatrix(eye, target, up);
switch (mType) {
case CAMERA_2D:
mProjectionMatrix = create2dProjectionMatrix(m2dLeft, m2dBottom, m2dRight, m2dTop, mNearClip, mFarClip);
break;
case CAMERA_3D:
mProjectionMatrix = create3dProjectionMatrix(mNearClip, mFarClip, mFov, mAspectRatio);
break;
default:
break;
};
}
Really nothing to it, is there?
The other update function, which takes a target vector, is exactly the same; minus the fact that target is already calculated.
All we do, is decompose our transform into position and rotation, then grab a few vectors out of the rotation matrix.
As our matrices are column-major, our right, up and front vectors correspond to the first, second and third columns of the rotation matrix. We then just pass in all our variables to generate our view and projection matrices for the correct mode and we're sorted.
Models and Projections and Views, oh my!
With our Camera object created, we need to pass it through to our Fixed Function Rendering Context. As we've abstracted everything out, this involves passing it to the Graphics System, then through to the Platform Context ( GLES1RenderContext or GLXRenderContext for instance ) and finally to the FixedFunctionRenderContext itself. This is just trivial interface work, and it's in the repository if you're curious.
What we're really interested in, is what goes into the Fixed Function context itself...
A Camera is a funny thing, in that generally it'll always be moving. Therefore, there is absolutely no point in optimising the calls to check if the camera has moved. With this in mind, we can write our setCamera function as follows:
void FixedFunctionContext::setFixedFunctionCamera(Camera* const camera)
{
glMatrixMode(GL_PROJECTION);
glLoadMatrixf(camera->getProjectionMatrix().getData());
glMatrixMode(GL_MODELVIEW);
glLoadMatrixf(camera->getViewMatrix().getData());
mCamera = camera;
}
Nice and simple.
Now, as you notice, I've not stored any of this in a stack. I'm just manipulating the top matrices in the Projection and ModelView stack. While I did go on about having a stack in the last article, the truth of the matter is that relying on OpenGL to do your matrices properly can get you into bother. The GL specs state that you are only guaranteed a minimum of two matrices in the Projection and Texture stacks, and sixteen in the ModelView stack. This might be enough for you, or it might not. Either way, dealing with them ourselves is much more preferable.
Our Camera object is also rather simple, and there's nothing stopping us from creating multiple Cameras dotted about the place, and switching the views around as and when we like. We also have the ability to go in and fiddle with the matrices once the Cameras have updated themselves, anyway... so stack manipulation isn't really a big deal for us here. We can safely ignore it.
Anyway, now we have set our Projection and View matrices, we need to concatenate the Model's matrix over the top of the View matrix so that our meshes render where we think they will.
This is much easier than it sounds, don't worry!
In our drawMeshFixedFunction function, we just add the following little bits.
We pull out the transform matrix from the Mesh when we pull out the Index and Vertex Buffers, and Material.
Then, when we're about to draw the object itself, we change the whole bottom segment to this:
glPushMatrix();
glMultMatrixf(transform->getTranspose().getData());
disableFixedFunctionTexturing(currentTextureUnit); // Disable any excess texture units
switch (indexBuffer->getFormat()) {
case IndexBuffer::FORMAT_FLOAT:
glDrawElements(GL_TRIANGLES, indexBuffer->getSize(), GL_FLOAT, indexBuffer->getData());
break;
case IndexBuffer::FORMAT_UNSIGNED_BYTE:
glDrawElements(GL_TRIANGLES, indexBuffer->getSize(), GL_UNSIGNED_BYTE, indexBuffer->getData());
break;
case IndexBuffer::FORMAT_UNSIGNED_SHORT:
glDrawElements(GL_TRIANGLES, indexBuffer->getSize(), GL_UNSIGNED_SHORT, indexBuffer->getData());
break;
default:
break;
};
mFixedFunctionLastTexUnit = currentTextureUnit;
glPopMatrix();
All we've done is add three lines.. very important lines, however!
We tell OpenGL to give us a new matrix, pushing the current one down a level.This is our View matrix. We then multiply this with our current Transform matrix - or more specifically, the Transpose of our current Transform matrix. Now we draw as normal. Finally, we pop our modified Transform matrix off the stack, so we're back to the View matrix sitting on top again.
So, why the transpose of the Transform matrix? It's to do with matrix multiplication. If we multiply two matrices, A and B to get C, the value C(3, 4) is going to be the row 3 of A multiplied by column 4 of B. So this means that potentially we're fiddling about in the matrices in the wrong areas. And you can test this by removing the getTranspose() call and then trying to translate the mesh. It sortof does this weird warping thing instead! Not ideal. See this wiki page for more info, along with a nice illustration of the matrix multiplication I've described - http://en.wikipedia.org/wiki/Matrix_multiplication#Illustration
Anyway, yes.. we transpose it, which effectively flips the values about so that A(2, 3) becomes A(3, 2) and then when we multiply it, the rows and columns match up to the correct values - so we multiply the position with the position, rather than part of the rotation.
Right, what about the Texture matrices?
That's even simpler than dealing with the camera matrices.
void FixedFunctionContext::setFixedFunctionTextureMatrix(Matrix4* const matrix)
{
glMatrixMode(GL_TEXTURE);
glLoadMatrixf(matrix->getData());
glMatrixMode(GL_MODELVIEW);
}
Now, this only has any effect on a Fixed Function pipeline, really. We also can't really use it for now as we have no Texture support.. but literally, all we're doing is setting the Matrix to the Texture Matrix, replacing it with our specified matrix, and returning to the ModelView.
We'll have a play with the texture matrix when we get to loading up textures and displaying them.
Surprisingly, that's us done.
We just need to fiddle with the example code a bit so it does something a bit more useful, now.
Cameras are Fun
I'm going to paste the full example code here, then we'll go through it.. and I'll highlight the interesting little gotchas involved.
#include <cstdio>
#include <cstdlib>
#include "../../Graphics/GraphicsSystem.h"
#include "../../Graphics/Context/FixedFunctionContext.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 "../../Maths/Matrix4.h"
using namespace GLESGAE;
void controlCamera(Camera* const camera, Controller::KeyboardController* const keyboard);
Mesh* makeSprite();
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 Fixed Function Test", 640, 480, 16, false)) {
//TODO: OH NOES! WE'VE DIEDED!
return -1;
}
Mesh* mesh(makeSprite());
Camera* camera(new Camera(Camera::CAMERA_3D));
camera->getTransformMatrix().setPosition(Vector3(0.0F, 0.0F, -5.0F));
#ifndef GLES2
FixedFunctionContext* const fixedContext(graphicsSystem->getFixedContext());
if (0 != fixedContext) {
fixedContext->enableFixedFunctionVertexPositions();
fixedContext->enableFixedFunctionVertexColours();
}
#endif
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()
{
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};
unsigned int vertexSize = 32 * 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_COLOUR_4F, 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);
}
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();
}
We haven't really changed much from the stock example still.
We've added some camera controls, so that when we press the arrow keys, our camera translates in the x and z axes.
We also start our camera -5 units from the centre of the world... and this is one of the gotchas. Why is this a gotcha? Make your right hand into a fist, point your index finger straight in front of you, your thumb directly up and your middle finger to the left. This is what your camera is like in OpenGL. Now, move your whole hand to the left. Imagine you were looking down it, you would see your objects moving right. Fair enough. But our camera seems to go the opposite direction! This is because OpenGL's right vector points in a different direction from where ours does.. this is the Right Handed and Left Handed systems biting us in the bum. You can either get used to it, and handle it within your code, or you can go back to the Camera's view matrix creation, and negate the right vector when we set it's values to the matrix.
Oh and a quick note... DirectX and OpenGL disagree on which way the right vector is ( and which way to point down the Z axis! ).. so conversion between the two handed systems is always a good skill to have when dealing with multiple APIs.
Now for the second gotcha.
Change the camera to a 2D camera and run the example again.
Why is the quad larger than the full screen?
Look back at the mesh description we have:
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};
Our mesh points are between -1.0F and 1.0F.. which is a full screen quad when we're rendering in 3d mode, and our origin is the centre of the screen. ( Ok, well technically even that is wrong, it would be -0.5 and 0.5... but in our pre view and projection matrix phase, it did match up. )
In 2d mode, the origin is at the bottom left corner of the screen.. so -1.0 ( or -0.5 for the nit picky ) is way beyond the left of the screen where you can't see it. So, if we change this to between 0.0 and 1.0, we should have it working ( or 0.0 and -1.0 in our case due to the right vector nonsense. )
You might be wondering why I haven't "fixed" the right hand vector to actually point in the correct direction.. this is because I haven't decided on a loadable mesh format yet... some mesh formats have Z as up, instead of Y for example. Some use positive Z and others use negative Z. Granted, we should define what the engine uses, and then convert whatever formats we load up to our format, but as I haven't really decided what our format actually is yet, I'm leaving things open! At any rate, you know exactly what's going on and why, and you know where to fix it if it annoys you.
I also suggest reading through the Shader Based Transforms article.. as we explain a bit more of what the camera is up to there: GLESGAE:Shader Based Transformations
Building the Example
In the SVN there are Makefiles already setup for you.. just trigger make -f MakefileES1.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're going to do much the same for the Shader Based pipeline.. this requires a bit more work to our shader system, but we can reuse our Camera object as-is.