Lesson 15: Camera Movement
Games with a fixed perspective are all well and good, but to get some level of realism for most types of games you need to let the character move around - and let the camera follow. In this tutorial you will learn how to use translation and rotation to create the effect of a moving camera.
Theory
Camera movement is really not very complicated. All you have to do is store the X, Y, and Z position of the camera, as well as it's yaw, pitch and roll. Yaw means rotation on the Y axis, pitch means rotation on the X axis, and roll means rotation on the Z axis - these are commonly used terms for rotations, so I thought I would use them here.
Anyhow, once you've got this information stored about the position and orientation of the camera, all you have to do is translate the entire world the opposite way. Why the opposite way? Think about what the world looks like as you move around in it. If you move forwards, it looks like everything else is moving backwards. If you strafe to the left, everything else moves to the right. If you jump up, the world moves down, then comes back up to meet you as you fall back down. If you rotate your head to the left, the world rotates around you to the right. Quite simple, isn't it? It's all about relativity.
That's pretty much all you have to know. Oh, and one last thing - for camera movement to look right, you cannot use the normal "object" transformation order of rotation then translation. You have to translate first, then rotate. And since in OpenGL, matrix operations are effectuated in the opposite order in which you apply them, you have to glRotate() before you glTranslate() for camera movement.
Implementation
#include <allegro.h>
#include <alleggl.h>
#include <cmath>
using namespace std;
// Constant for changing degrees into radians
const double Deg2Rad = 0.0174532925199432957692369076848861;
// The texture handle
GLuint Tex;
// A guy with a camera : CameraMan
class CameraMan
{
private:
float x, y, z; // Position in world
float yaw, pitch, roll; // Rotations
static const float StandingHeight = 3.0;
static const float CrouchingHeight = 1.5;
public:
CameraMan()
: x(0.0), y(StandingHeight), z(0.0),
yaw(0.0), pitch(0.0), roll(0.0) {}
~CameraMan() {}
// Walk forwards or backwards by d
void Walk(float d)
{
float nyaw = (-yaw - 90.0) * Deg2Rad;
x += cos(nyaw) * d;
z += sin(nyaw) * d;
}
// Sidestep left or right by d
void Sidestep(float d)
{
float nyaw = -yaw * Deg2Rad;
x += cos(nyaw) * d;
z += sin(nyaw) * d;
}
// Crouch at the speed of d
void Crouch(float d)
{
if(y > CrouchingHeight) y -= d;
if(y < CrouchingHeight) y = CrouchingHeight;
}
// Adjust rotations at a certain speed
void Yaw(float d) { yaw += d; }
void Pitch(float d) { pitch += d; }
void Roll(float d) { roll += d; }
// Update the CameraMan for one logic frame
void Update()
{
// Move roll, pitch, and height towards default values
roll *= 0.98;
pitch *= 0.98;
if(y < StandingHeight) y += 0.05;
}
// Apply CameraMan's transformations to the 3D world
void ApplyTransformations()
{
glRotatef(-pitch, 1.0, 0.0, 0.0);
glRotatef(-roll, 0.0, 0.0, 1.0);
glRotatef(-yaw, 0.0, 1.0, 0.0);
glTranslatef(-x, -y, -z);
}
};
// Timer interrupt system
volatile int Time = 0;
void TimerFunc()
{
Time++;
}
END_OF_FUNCTION(TimerFunc);
void Init()
{
// Start up Allegro and AllegroGL systems
allegro_init();
install_allegro_gl();
install_timer();
install_keyboard();
// Set up the timer interrupts
LOCK_VARIABLE(Time);
LOCK_FUNCTION(TimerFunc);
install_int_ex(TimerFunc, BPS_TO_TIMER(50));
// Set some AllegroGL options
allegro_gl_set(AGL_DOUBLEBUFFER, true);
allegro_gl_set(AGL_COLOR_DEPTH, 16);
allegro_gl_set(AGL_RENDERMETHOD, true);
allegro_gl_set(AGL_SUGGEST, AGL_DOUBLEBUFFER | AGL_COLOR_DEPTH | AGL_RENDERMETHOD);
// Set up a suitable viewing window
set_color_depth(16);
set_gfx_mode(GFX_OPENGL_WINDOWED, 640, 480, 0, 0);
// Enable texturing
glEnable(GL_TEXTURE_2D);
// Set up culling
glCullFace(GL_BACK);
glEnable(GL_CULL_FACE);
// We are using 24 bit textures in this case.
allegro_gl_set_texture_format(GL_RGB8);
// Load the texture
BITMAP* temp_bmp = load_bitmap("layer0.bmp", 0);
Tex = allegro_gl_make_texture(temp_bmp);
glTexEnvi(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexEnvi(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
destroy_bitmap(temp_bmp);
// Set up a perspective projection
glMatrixMode(GL_PROJECTION);
glFrustum(-1.0, 1.0, -1.0, 1.0, 1.0, 1000.0);
glMatrixMode(GL_MODELVIEW);
// Set the background color to black
glClearColor(0.0, 0.0, 0.0, 0.0);
}
int main()
{
Init();
float speed = 0.1, rspeed = 1.0;
CameraMan cm;
while(!key[KEY_ESC])
{
while(Time > 0)
{
// Move CameraMan according to user input
if(key[KEY_LEFT]) cm.Yaw(rspeed);
if(key[KEY_RIGHT]) cm.Yaw(-rspeed);
if(key[KEY_UP]) cm.Pitch(rspeed);
if(key[KEY_DOWN]) cm.Pitch(-rspeed);
if(key[KEY_Z]) cm.Roll(rspeed);
if(key[KEY_X]) cm.Roll(-rspeed);
if(key[KEY_W]) cm.Walk(speed);
if(key[KEY_S]) cm.Walk(-speed);
if(key[KEY_D]) cm.Sidestep(speed);
if(key[KEY_A]) cm.Sidestep(-speed);
if(key[KEY_LCONTROL]) cm.Crouch(speed);
// Update the CameraMan
cm.Update();
Time--;
}
// Clear the screen
glClear(GL_COLOR_BUFFER_BIT);
// Load the identity matrix, then apply the CameraMan's transformations
glLoadIdentity();
cm.ApplyTransformations();
// Draw the "floor"
glBindTexture(GL_TEXTURE_2D, Tex);
glBegin(GL_QUADS);
glColor4f(1.0, 1.0, 1.0, 1.0);
glTexCoord2i(0, 1);
glVertex3f(-20.0, 0.0, -20.0);
glTexCoord2i(0, 0);
glVertex3f(-20.0, 0.0, 20.0);
glTexCoord2i(1, 0);
glVertex3f(20.0, 0.0, 20.0);
glTexCoord2i(1, 1);
glVertex3f(20.0, 0.0, -20.0);
glEnd();
// Flush drawing commands and flip the backbuffer
glFlush();
allegro_gl_flip();
}
return 0;
}
END_OF_MAIN();
This program is a sort of camera man simulation :) The controls are as follows: W, A, S, and D to move forwards, backwards, and to strafe; the arrow keys to turn and look up and down(yaw and pitch); Z and X to roll; and left control to crouch.
Picking Apart agl15.cpp
After including the C math header and defining a constant for mapping degrees to radians, we declare a texture handle for the floor texture(which will be the checkerboard pattern used in the last lesson). Then, the CameraMan class is defined. The class contains the position and orientation of the camera, as well as a few constants for the height of the camera man when crouching and when standing.
There ae class functions for walking forwards and sideways, for crouching, and for adjusting the yaw, pitch, and roll of the camera man. The functions for walking forwards and sideways use cos() and sin() on the camera man's yaw in order to find out which direction forwards and sideways are for the camera man.
CameraMan::Update() adjusts the roll, pitch, and height of the camera man in order to bring them back to default positions. CameraMan::ApplyTransformations() applies the camera transofrmations to the world.
In the Init() function, we enable backface culling(just in case) and load the floor texture, among other things which you've seen a dozen times by now ;)
Finally, in the main loop, we control the camera man through user input and draw the checkerboard floor. If you like, you can add some extra rendering code here to draw some cubes or some squares in order to see the camera transformations in more detail, but I figured that 186 lines was enough ;)
And that's all there is to it. I told you it was easy. Oh, wait... no, I didn't. But it is, isn't it?
In the Next Lesson...
Next up, you will learn how to use some of OpenGL's more advanced graphics primitives!
|