OpenGL Programming/Glescraft 4

A voxel world.

Introduction edit

There are several ways to manipulate the view of a 3D scene. For Computer Aided Design (CAD) software, you usually have a fixed camera point, and use a (3D) mouse or trackball to rotate or move the object of interest. In many games, especially the First Person Shooters, the world is static, but you move the camera around the world. The graphics card makes no distinction between those two ways, it just applies the model-view-projection matrix you provide. The major distinction is in the way we manipulate the MVP matrix with the mouse and/or keyboard. For our first-person camera, we derive the MVP matrix from two vectors; the camera position and the view angles.

glm::vec3 position;
glm::vec2 angles;

Capturing mouse movement in GLUT edit

GLUT has two callbacks to capture mouse movement. One is called when the mouse is moved while at least one mouse button is pressed, the other is called when no mouse buttons are pressed. In our case, we don't want to distinguish between the two cases, so we can register the same function for both callbacks:

glutMotionFunc(motion);
glutPassiveMotionFunc(motion);

When your application or game is in first person mode, you usually don't want to see the mouse cursor. We can disable it in this way:

glutSetCursor(GLUT_CURSOR_NONE);

If are not in first person mode anymore, for example when you are displaying menus, or if the game is paused, you can show the default mouse cursor again with glutSetCursor(GLUT_CURSOR_INHERIT).

The motion() callback function will get the current mouse coordinates relative to the top left of the window (or relative to the top left of the screen if the window is in full screen mode). However, we don't want to know the current coordinates, we are only interested in knowing how much the mouse was moved. Of course, we could subtract the coordinates from the previous call to motion() from the current ones, but that doesn't work anymore when the mouse cursor is at the edge of the window or screen! The solution in GLUT is, whenever the mouse is moved, to move the mouse cursor back to the center of the window with the glutWarpPointer() function. However, using this function causes GLUT to call the motion callbacks again, so we have to ignore every second call to motion():

void motion(int x, int y) {
  static bool wrap = false;

  if(!wrap) {
    int ww = glutGet(GLUT_WINDOW_WIDTH);
    int wh = glutGet(GLUT_WINDOW_HEIGHT);

    int dx = x - ww / 2;
    int dy = y - wh / 2;

    // Do something with dx and dy here

    // move mouse pointer back to the center of the window
    wrap = true;
    glutWarpPointer(ww / 2, wh / 2);
  } else {
    wrap = false;
  }
}

In the above function, dx and dy variables hold the distince in pixels that the mouse cursor moved.

View direction edit

As human beings living on a planet, we make a big distinction between looking around us in the horizontal plane, and between looking up and down. Partly because the most interesting things happen at the same level above ground as we are on ourself, but also because it is easy to turn around our (vertical) axis as much as we want, but we can only turn our heads up and down so much. In FPS games, one can rotate around the vertical axis as much as you want, but up and down is restricted to +/- 90 degrees. Apart from this restriction, the change in viewing angles is equal to the amount of mouse movement times a scaling factor:

    const float mousespeed = 0.001;

    angles.x += dx * mousespeed;
    angles.y += dy * mousespeed;

    if(angles.x < -M_PI)
      angles.x += M_PI * 2;
    else if(angles.x > M_PI)
      angles.x -= M_PI * 2;

    if(angles.y < -M_PI / 2)
      angles.y = -M_PI / 2;
    if(angles.y > M_PI / 2)
      angles.y = M_PI / 2;

From the angles vector, we can calculate the direction we are looking at using simple trigonometry:

glm::vec3 lookat;
lookat.x = sinf(angles.x) * cosf(angles.y);
lookat.y = sinf(angles.y);
lookat.z = cosf(angles.x) * cosf(angles.y);

Given a position vector and the lookat vector, we can create a view matrix as follows:

glm::vec3 position;
glm::mat4 view = glm::lookAt(position, position + lookat, glm::vec3(0, 1, 0));

Exercises:

  • Many games restrict looking up and down to slightly less than 90 degrees. Can you think of a reason for this?
  • What would happen if you go beyond 90 degrees for the up/down angle? Would the scene look upside-down?
  • Instead of calculating the lookat vector and using the glm::lookAt() function, try constructing the view matrix using glm::rotate(). Does this give the same results? Also if you go beyond 90 degrees for the up/down angle?

Camera movement edit

Since we have already used the mouse to determine the viewing angle, we are left with the keyboard for movement of the camera position. While the computer can tell how far you moved the mouse, keys are either pressed or not pressed. Although we can easily register a keyboard callback that moves the camera position by a fixed amount each time a key is pressed, this results in very jerky movement of the camera. Instead of controlling the position directly, we want to change our speed using the keyboard. If no keys are pressed, our speed is zero. If the up arrow key is pressed, we have a certain speed in the forward direction. If the down arrow key is pressed, we have a negative speed in the forward direction. If the right arrow is pressed, we have a certain speed in the sideways direction, and so on. We can register a callback with glutSpecialFunc() that is called each time a "special" key (such as a cursor key) is pressed. However, we also need to know when that key is released again. For that, we can register a callback with glutSpecialUpFunc():

glutSpecialFunc(special);
glutSpecialUpFunc(specialup);

The two callback functions look like this:

const int left = 1; 
const int right = 2;
const int forward = 4; 
const int backward = 8; 
const int up = 16; 
const int down = 32;

int move = 0;

void special(int key, int x, int y) {
  if(key == KEY_LEFT)
    move |= left;     
  if(key == KEY_RIGHT)
    move |= right;
  if(key == KEY_UP)
    move |= forward;     
  if(key == KEY_DOWN)
    move |= backward;     
  if(key == KEY_PAGEUP)
    move |= up;     
  if(key == KEY_PAGEDOWN)
    move |= down;
}

void specialup(int key, int x, int y) {
  if(key == KEY_LEFT)
    move &= ~left;     
  if(key == KEY_RIGHT)
    move &= ~right;
  if(key == KEY_UP)
    move &= ~forward;     
  if(key == KEY_DOWN)
    move &= ~backward;     
  if(key == KEY_PAGEUP)
    move &= ~up;     
  if(key == KEY_PAGEDOWN)
    move &= ~down;
}

The move variable will then contain a bitmask of the movement keys that are currently pressed. The camera position should be updated regularly, preferably once per frame. We can register a function in GLUT that is called whenever it has nothing to do:

glutIdle(idle);

We can then update the camera position in that function and tell GLUT to redraw the scene. In order to update the camera position vector, we need to know which direction "forward", "right" and so on is. In most games, "forward" is the direction we are looking at, but only in the horizontal plane. Usually, this is because you are standing on the ground, and even if you walk around while looking a little bit up or down, you stay on the floor. From the "forward" vector, we can also derive a "right" vector, and the "up" vector simply points in the positive y direction. The result looks like this:

void idle() {
  static int pt = 0;
  const float movespeed = 10;

  // Calculate time since last call to idle()
  int t = glutGet(GLUT_ELAPSED_TIME);
  float dt = (t - pt) * 1.0e-3;
  pt = t;
  
  // Calculate movement vectors
  glm::vec3 forward_dir = vec3(sinf(angles.x), 0, cosf(angles.x));
  glm::vec3 right_dir = vec3(-forward_dir.z, 0, forward_dir.x);

  // Update camera position
  if(move & left)
    position -= right_dir * movespeed * dt;
  if(move & right)
    position += right_dir * movespeed * dt;
  if(move & forward)
    position += forward_dir * movespeed * dt;
  if(move & backward)
    position -= forward_dir * movespeed * dt;
  if(move & up)
    position.y += movespeed * dt;
  if(move & down)
    position.y -= movespeed * dt;

  // Redraw the scene
  glutPostRedisplay();
}

Exercises:

  • Instead of directly using trigonometry, try to derive forward_dir and right_dir from lookat and the "up" vector using vector algebra.

< OpenGL Programming

Browse & download complete code