跳转到内容

OpenGL 编程/现代 OpenGL 教程 虚拟轨迹球

来自维基教科书,开放世界开放书籍

虚拟轨迹球是一个用鼠标自然地旋转物体的工具。

旋转期间的虚拟轨迹球角度

想象一个虚拟球,它就在屏幕后面。用鼠标点击它,你就会把它捏住,然后移动鼠标,你就会让球绕着它的中心旋转。相同的旋转也会应用到 OpenGL 场景中的一个物体上!

右边的图显示了虚拟球的俯视图。底部的黑线是屏幕,x1 和 x2 是拖动过程中两个连续的鼠标位置。这是在 2D 中显示的,但相同的原理也适用于 3D。

目标是计算 α 角和旋转轴。这是你理解数学一旦你开始认真对待 OpenGL 就非常必要的点。特别是,我们将需要勾股定理、向量点积和叉积。

我们将

  1. 将屏幕坐标(以像素为单位)转换为相机坐标(在 [-1, 1] 中)
  2. 计算向量 OP1 和 OP2,即与我们的鼠标点击匹配的球体表面的点
    • x 和 y 坐标直接取自相机坐标中的点击
    • z 坐标使用经典勾股定理计算
    • 如果 P1 或 P2 距离球体太远 (),我们将对其进行归一化以获得球体表面上的最近点
  3. 我们有 ,球体的大小为 1 (),所以我们使用 获取角度。
  4. 在 3D 中获取旋转轴,我们计算 ,这将提供一个垂直单位向量

捕获鼠标事件

[编辑 | 编辑源代码]

GLUT 提供了一种获取鼠标点击和拖动事件的方法

glutMouseFunc(onMouse) 将为每次鼠标点击调用 onMouse(int button, int state, int x, int y),其中

  • button 是 GLUT_LEFT_BUTTON、GLUT_MIDDLE_BUTTON 或 GLUT_RIGHT_BUTTON
  • state 是 GLUT_DOWN 或 GLUT_UP
  • x 和 y 是屏幕坐标,从左上角开始(y 与 OpenGL 坐标相反!)

glutMotionFunc(onMotion) 将为每次按下任何按钮的鼠标移动调用 onMotion(int x, int y),其中 x 和 y 是屏幕坐标。

您还有 glutPassiveMotionFunc(...),它在没有按下任何按钮的情况下以类似的方式处理鼠标移动。

因此,我们添加了两个函数来跟踪按下左键时的鼠标移动

/* Global */
int last_mx = 0, last_my = 0, cur_mx = 0, cur_my = 0;
int trackball_on = false;
/* main() */
    glutMouseFunc(onMouse);
    glutMotionFunc(onMotion);
void onMouse(int button, int state, int x, int y) {
  if (button == GLUT_LEFT_BUTTON && state == GLUT_DOWN) {
    trackball_on = true;
    last_mx = cur_mx = x;
    last_my = cur_my = y;
  } else {
    trackball_on = false;
  }
}

void onMotion(int x, int y) {
  if (trackball_on) {  // if left button is pressed
    cur_mx = x;
    cur_my = y;
  }
}

计算 OP1 和 OP2

[编辑 | 编辑源代码]

我们添加一个新函数来计算轨迹球表面点。

/**
 * Get a normalized vector from the center of the virtual ball O to a
 * point P on the virtual ball surface, such that P is aligned on
 * screen's (X,Y) coordinates.  If (X,Y) is too far away from the
 * sphere, return the nearest point on the virtual ball surface.
 */
glm::vec3 get_trackball_vector(int x, int y) {
  glm::vec3 P = glm::vec3(1.0*x/screen_width*2 - 1.0,
			  1.0*y/screen_height*2 - 1.0,
			  0);
  P.y = -P.y;
  float OP_squared = P.x * P.x + P.y * P.y;
  if (OP_squared <= 1*1)
    P.z = sqrt(1*1 - OP_squared);  // Pythagoras
  else
    P = glm::normalize(P);  // nearest point
  return P;
}

我们首先将 x,y 屏幕坐标转换为 [-1,1] 坐标(并反转 y 坐标)。然后我们使用勾股定理来检查 OP 向量的长度并计算 z 坐标,如上所述。

计算角度和轴

[编辑 | 编辑源代码]
  /* onIdle() */
  if (cur_mx != last_mx || cur_my != last_my) {
    glm::vec3 va = get_trackball_vector(last_mx, last_my);
    glm::vec3 vb = get_trackball_vector( cur_mx,  cur_my);
    float angle = acos(min(1.0f, glm::dot(va, vb)));
    glm::vec3 axis_in_camera_coord = glm::cross(va, vb);
    glm::mat3 camera2object = glm::inverse(glm::mat3(transforms[MODE_CAMERA]) * glm::mat3(mesh.object2world));
    glm::vec3 axis_in_object_coord = camera2object * axis_in_camera_coord;
    mesh.object2world = glm::rotate(mesh.object2world, glm::degrees(angle), axis_in_object_coord);
    last_mx = cur_mx;
    last_my = cur_my;
  }

一旦我们有了 OP1 和 OP2(这里命名为 vavb),我们就可以使用 acos(dot(va,vb)) 计算角度。

由于我们使用的是 float 变量,因此可能会出现精度问题:dot 可能会返回一个略大于 1 的值,acos 将返回 nan,这意味着一个无效的浮点数。结果是我们的旋转矩阵将全部乱套,通常我们的物体将从屏幕上消失!为了解决这个问题,我们用最大值 1.0 对该值进行限制。

另一个技巧是将旋转轴从相机坐标转换为物体坐标。当相机和物体放置不同时,这很有用。例如,如果你将物体绕 Y 轴旋转 90°(“转向”右边),然后用鼠标进行垂直移动,你会在相机 X 轴上进行旋转,但对于物体来说应该变成绕 Z 轴旋转(平面桶滚)。通过将轴转换为物体坐标,旋转将尊重用户在相机坐标系中工作(所见即所得)。为了从相机坐标系转换为物体坐标系,我们取 MV 矩阵(来自 MVP 矩阵三元组)的逆。

最后,我们可以像往常一样使用 glm::rotate 应用变换 :)

  • 旋转角度是否与鼠标移动成正比?尝试在虚拟球体的边界附近移动鼠标。
  • 当鼠标距离太远时,虚拟球体将停止滚动。其他鼠标控制也是可能的。例如,研究如何在 Blender 3D 建模器中使用中间按钮拖动。
  • 尝试不同的滚动速度,方法是将旋转角度相乘。

< OpenGL 编程

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