跳转到内容

OpenGL 编程/Glescraft 4

来自维基教科书,开放世界中的开放书籍
一个体素世界。

有多种方法可以操纵 3D 场景的视角。对于计算机辅助设计 (CAD) 软件,您通常使用固定相机点,并使用 (3D) 鼠标或轨迹球来旋转或移动感兴趣的对象。在许多游戏中,尤其是第一人称射击游戏中,世界是静止的,但您可以移动相机在世界中移动。显卡没有区分这两种方法,它只是应用您提供的模型-视图-投影矩阵。主要区别在于我们使用鼠标和/或键盘操纵 MVP 矩阵的方式。对于我们的第一人称相机,我们从两个向量派生 MVP 矩阵;相机位置和视角。

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

在 GLUT 中捕获鼠标移动

[编辑 | 编辑源代码]

GLUT 有两个回调函数来捕获鼠标移动。一个是在至少按下了一个鼠标按钮时移动鼠标时调用,另一个是在没有按下任何鼠标按钮时调用。在我们的例子中,我们不想区分这两种情况,因此我们可以为这两个回调函数注册相同的函数

glutMotionFunc(motion);
glutPassiveMotionFunc(motion);

当您的应用程序或游戏处于第一人称模式时,您通常不想看到鼠标光标。我们可以通过这种方式禁用它

glutSetCursor(GLUT_CURSOR_NONE);

如果您不再处于第一人称模式,例如当您显示菜单或游戏暂停时,您可以使用以下方法再次显示默认鼠标光标glutSetCursor(GLUT_CURSOR_INHERIT).

这个motion()回调函数将获取相对于窗口左上角的当前鼠标坐标(如果窗口处于全屏模式,则相对于屏幕左上角)。但是,我们不想知道当前坐标,我们只想知道鼠标移动了多少。当然,我们可以从之前的调用中减去坐标motion()从当前坐标中减去,但这在鼠标光标位于窗口或屏幕边缘时不再起作用!GLUT 中的解决方案是,每当鼠标移动时,使用以下方法将鼠标光标移回窗口中心glutWarpPointer()函数。但是,使用此函数会导致 GLUT 再次调用运动回调函数,因此我们必须忽略每次对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;
  }
}

在上面的函数中,dxdy变量保存鼠标光标移动的像素距离。

观察方向

[编辑 | 编辑源代码]

作为生活在行星上的地球人,我们在水平面上的环顾四周与上下看之间有着很大的区别。部分原因是我们自己所处的与地面相同的水平面上发生了最有趣的事情,但也因为我们可以随意围绕我们的(垂直)轴旋转,但我们只能将头向上和向下旋转一定程度。在 FPS 游戏中,您可以随意围绕垂直轴旋转,但向上和向下的旋转限制在 +/- 90 度。除了这种限制之外,视角的变化等于鼠标移动量乘以一个缩放系数

    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;

angles向量,我们可以使用简单的三角函数计算我们正在观察的方向

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

给定一个位置向量和lookat向量,我们可以创建如下视图矩阵

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

练习

  • 许多游戏将向上和向下的观看限制在略小于 90 度。你能想到这样做的原因吗?
  • 如果将向上/向下角度超过 90 度会发生什么?场景会上下颠倒吗?
  • 而不是计算lookat向量并使用glm::lookAt()函数,尝试构建view使用矩阵glm::rotate()。这会产生相同的结果吗?如果将向上/向下角度超过 90 度也是如此吗?

相机移动

[编辑 | 编辑源代码]

由于我们已经使用鼠标来确定视角,因此只剩下键盘来移动相机位置。虽然计算机可以告诉您移动了鼠标多远,但按键要么被按下,要么没有被按下。虽然我们可以轻松地注册一个键盘回调函数,每次按下按键时都会将相机位置移动固定量,但这会导致相机移动非常卡顿。我们不想直接控制位置,而是想使用键盘改变速度。如果没有按下任何按键,我们的速度为零。如果按下向上箭头键,我们将在向前方向上具有一定的速度。如果按下向下箭头键,我们将在向前方向上具有负速度。如果按下向右箭头,我们在侧向方向上具有一定的速度,等等。我们可以使用以下方法注册一个回调函数glutSpecialFunc()每次按下“特殊”键(如光标键)时都会调用该函数。但是,我们还需要知道该键何时再次释放。为此,我们可以使用以下方法注册一个回调函数glutSpecialUpFunc():

glutSpecialFunc(special);
glutSpecialUpFunc(specialup);

这两个回调函数如下所示

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;
}

这个move变量将包含当前按下的移动键的位掩码。应定期更新相机位置,最好每帧更新一次。我们可以在 GLUT 中注册一个函数,该函数在它什么都不用做时被调用

glutIdle(idle);

然后,我们可以在该函数中更新相机位置向量,并告诉 GLUT 重新绘制场景。为了更新相机位置向量,我们需要知道“向前”、“向右”等方向。在大多数游戏中,“向前”是我们正在观察的方向,但只在水平面上。通常,这是因为您站在地面上,即使您在向上或向下看时走动,您也会停留在地板上。从“向前”向量,我们还可以推导出“向右”向量,“向上”向量 simply 指向正 y 方向。结果如下

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();
}

练习

  • 不要直接使用三角函数,尝试推导出forward_dirright_dir来自lookat和“向上”向量使用向量代数。

< OpenGL 编程

浏览并下载 完整代码
华夏公益教科书