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

Listing 15-1: agl15.cpp
#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!