OpenGL Development Cookbook
上QQ阅读APP看书,第一时间看更新

Implementing a vector-based camera with FPS style input support

We will begin this chapter by designing a simple class to handle the camera. In a typical OpenGL application, the viewing operations are carried out to place a virtual object on screen. We leave the details of the transformations required in between to a typical graduate text on computer graphics like the one given in the See also section of this recipe. This recipe will focus on designing a simple and efficient camera class. We create a simple inheritance from a base class called CAbstractCamera. We will inherit two classes from this parent class, CFreeCamera and CTargetCamera, as shown in the following figure:

Getting ready

The code for this recipe is in the Chapter2/src directory. The CAbstractCamera class is defined in the AbstractCamera.[h/cpp] files.

class CAbstractCamera
{
public:
  CAbstractCamera(void);
  ~CAbstractCamera(void);
  void SetupProjection(const float fovy, const float aspectRatio, const float near=0.1f, const float far=1000.0f);
  virtual void Update() = 0;
  virtual void Rotate(const float yaw, const float pitch, const float roll);
  const glm::mat4 GetViewMatrix() const;
  const glm::mat4 GetProjectionMatrix() const;
  void SetPosition(const glm::vec3& v);
  const glm::vec3 GetPosition() const;
  void SetFOV(const float fov);
  const float GetFOV() const;
  const float GetAspectRatio() const; 
  void CalcFrustumPlanes();
  bool IsPointInFrustum(const glm::vec3& point);
  bool IsSphereInFrustum(const glm::vec3& center, const float radius);
  bool IsBoxInFrustum(const glm::vec3& min, const glm::vec3& max);
  void GetFrustumPlanes(glm::vec4 planes[6]);
  glm::vec3 farPts[4];
  glm::vec3 nearPts[4];
protected:
  float yaw, pitch, roll, fov, aspect_ratio, Znear, Zfar;
  static glm::vec3 UP;
  glm::vec3 look;
  glm::vec3 up;
  glm::vec3 right; 
  glm::vec3 position;
  glm::mat4 V;       //view matrix
  glm::mat4 P;       //projection matrix
  CPlane planes[6];  //Frustum planes
};

We first declare the constructor/destructor pair. Next, the function for setting the projection for the camera is specified. Then some functions for updating the camera matrices based on rotation values are declared. Following these, the accessors and mutators are defined.

The class declaration is concluded with the view frustum culling-specific functions. Finally, the member fields are declared. The inheriting class needs to provide the implementation of one pure virtual function—Update (to recalculate the matrices and orientation vectors). The movement of the camera is based on three orientation vectors, namely, look, up, and right.

How to do it…

In a typical application, we will not use the CAbstractCamera class. Instead, we will use either the CFreeCamera class or the CTargetCamera class, as detailed in the following recipes. In this recipe, we will see how to handle input using the mouse and keyboard.

In order to handle the keyboard events, we perform the following processing in the idle callback function:

  1. Check for the keyboard key press event.
  2. If the W or S key is pressed, move the camera in the look vector direction:
    if( GetAsyncKeyState(VK_W) & 0x8000)
      cam.Walk(dt);
    if( GetAsyncKeyState(VK_S) & 0x8000)
      cam.Walk(-dt);
  3. If the A or D key is pressed, move the camera in the right vector direction:
    if( GetAsyncKeyState(VK_A) & 0x8000)
      cam.Strafe(-dt); 
    if( GetAsyncKeyState(VK_D) & 0x8000)
      cam.Strafe(dt);
  4. If the Q or Z key is pressed, move the camera in the up vector direction:
    if( GetAsyncKeyState(VK_Q) & 0x8000)
      cam.Lift(dt); 
    if( GetAsyncKeyState(VK_Z) & 0x8000)
      cam.Lift(-dt);

For handling mouse events, we attach two callbacks. One for mouse movement and the other for the mouse click event handling:

  1. Define the mouse down and mouse move event handlers.
  2. Determine the mouse input choice (the zoom or rotate state) in the mouse down event handler based on the mouse button clicked:
    if(button == GLUT_MIDDLE_BUTTON)
      state = 0;
    else
      state = 1;
  3. If zoom state is chosen, calculate the fov value based on the drag amount and then set up the camera projection matrix:
    if (state == 0) {
      fov += (y - oldY)/5.0f;
      cam.SetupProjection(fov, cam.GetAspectRatio());
    }
  4. If the rotate state is chosen, calculate the rotation amount (pitch and yaw). If mouse filtering is enabled, use the filtered mouse input, otherwise use the raw rotation amount:
    else {
      rY += (y - oldY)/5.0f; 
      rX += (oldX-x)/5.0f; 
      if(useFiltering) 
        filterMouseMoves(rX, rY);
      else {
        mouseX = rX;
        mouseY = rY;
      } 
      cam.Rotate(mouseX,mouseY, 0);
    }

There's more…

It is always better to use filtered mouse input, which gives smoother movement. In the recipes, we use a simple average filter of the last 10 inputs weighted based on their temporal distance. So the previous input is given more weight and the 5th latest input is given less weight. The filtered result is used as shown in the following code snippet:

void filterMouseMoves(float dx, float dy) {
  for (int i = MOUSE_HISTORY_BUFFER_SIZE - 1; i > 0; --i) {
    mouseHistory[i] = mouseHistory[i - 1];
  }
  mouseHistory[0] = glm::vec2(dx, dy);
  float averageX = 0.0f,  averageY = 0.0f, averageTotal = 0.0f, currentWeight = 1.0f;
 
  for (int i = 0; i < MOUSE_HISTORY_BUFFER_SIZE; ++i) {
    glm::vec2 tmp=mouseHistory[i];
    averageX += tmp.x * currentWeight;
    averageY += tmp.y * currentWeight;
    averageTotal += 1.0f * currentWeight;
    currentWeight *= MOUSE_FILTER_WEIGHT;
  }
  mouseX = averageX / averageTotal;
  mouseY = averageY / averageTotal;
}
Note

When using filtered mouse input, make sure that the history buffer is filled with the appropriate initial value; otherwise you will see a sudden jerk in the first few frames.

See also