OpenGL 编程/现代 OpenGL 教程 虚拟轨迹球
虚拟轨迹球是一个用鼠标自然地旋转物体的工具。
想象一个虚拟球,它就在屏幕后面。用鼠标点击它,你就会把它捏住,然后移动鼠标,你就会让球绕着它的中心旋转。相同的旋转也会应用到 OpenGL 场景中的一个物体上!
右边的图显示了虚拟球的俯视图。底部的黑线是屏幕,x1 和 x2 是拖动过程中两个连续的鼠标位置。这是在 2D 中显示的,但相同的原理也适用于 3D。
目标是计算 α 角和旋转轴。这是你理解数学一旦你开始认真对待 OpenGL 就非常必要的点。特别是,我们将需要勾股定理、向量点积和叉积。
我们将
- 将屏幕坐标(以像素为单位)转换为相机坐标(在 [-1, 1] 中)
- 计算向量 OP1 和 OP2,即与我们的鼠标点击匹配的球体表面的点
- x 和 y 坐标直接取自相机坐标中的点击
- z 坐标使用经典勾股定理计算
- 如果 P1 或 P2 距离球体太远 (),我们将对其进行归一化以获得球体表面上的最近点
- 我们有 ,球体的大小为 1 (),所以我们使用 获取角度。
- 在 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;
}
}
我们添加一个新函数来计算轨迹球表面点。
/**
* 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(这里命名为 va
和 vb
),我们就可以使用 acos(dot(va,vb))
计算角度。
由于我们使用的是 float
变量,因此可能会出现精度问题:dot
可能会返回一个略大于 1 的值,acos
将返回 nan
,这意味着一个无效的浮点数。结果是我们的旋转矩阵将全部乱套,通常我们的物体将从屏幕上消失!为了解决这个问题,我们用最大值 1.0
对该值进行限制。
另一个技巧是将旋转轴从相机坐标转换为物体坐标。当相机和物体放置不同时,这很有用。例如,如果你将物体绕 Y 轴旋转 90°(“转向”右边),然后用鼠标进行垂直移动,你会在相机 X 轴上进行旋转,但对于物体来说应该变成绕 Z 轴旋转(平面桶滚)。通过将轴转换为物体坐标,旋转将尊重用户在相机坐标系中工作(所见即所得)。为了从相机坐标系转换为物体坐标系,我们取 MV 矩阵(来自 MVP 矩阵三元组)的逆。
最后,我们可以像往常一样使用 glm::rotate
应用变换 :)
- 旋转角度是否与鼠标移动成正比?尝试在虚拟球体的边界附近移动鼠标。
- 当鼠标距离太远时,虚拟球体将停止滚动。其他鼠标控制也是可能的。例如,研究如何在 Blender 3D 建模器中使用中间按钮拖动。
- 尝试不同的滚动速度,方法是将旋转角度相乘。