OpenGL与旋转、缩放、平移

2018-05-09

   旋转看起来挺费劲的,其实非常简单。我们只需要给shader传入MVP矩阵即可。旋转分为两类:camera旋转、物体旋转。当指定MVP矩阵时,Model矩阵是每个物体携带的数据,Projection矩阵是由camera 的fov、aspect、near/far距离决定的,对于camera旋转,我们只需要更新View矩阵即可。View矩阵由三部分组成:eye pos,lookat center,up。这就是一个camera参数。camera一般围绕center旋转,up不变,那么eye 新的位置就可以通过旋转角度求出。对于物体旋转,我们只需要更新M矩阵即可。
   我们在屏幕像素空间拖动鼠标,这个操作只影响三个空间维度中两个维度。我们很容易就能求解出两个方向的旋转角度。假设屏幕横纵向的距离代表的角度为90°,横纵方向移动的像素距离w,w/width() * 90°就是横向旋转的角度了,纵向同理。开始移动的时候,我们需要确定旋转所围绕的向量。
   旋转物体时,需要以物体中心构造arcball来旋转。有两种处理方式:改变物体的坐标、改变物体的Model矩阵。对于显示来讲,无甚区别。一般,我们拖动物体或者旋转物体的时候,都会以物体的中心构造arcball,就是轨迹球。物体旋转示例代码如下:

glm::vec3 get_arcball_vector(double x, double y)
{
    glm::vec3 coord = glm::vec3(1.0*x / width() * 2 - 1.0,
        1.0*y / height() * 2 - 1.0, 0);
    coord.y = -coord.y;
    float length_squared = coord.x * coord.x + coord.y * coord.y;
    if (length_squared <= 1.0)
        coord.z = sqrt(1.0 - length_squared);
    else
        coord = glm::normalize(coord);
    return coord;
}
glm::vec3 prevPos = get_arcball_vector(startPos.x(), startPos.y());
glm::vec3  currPos = get_arcball_vector(e->pos().x(), e->pos().y());
float angle = acos(std::min(1.0f, glm::dot(prevPos, currPos)));
glm::vec3 camAxis = glm::cross(prevPos, currPos);
glm::mat4 viewRotation = glm::rotate(glm::degrees(angle)*0.01f, camAxis);
cloudModel = (viewRotation)* cloudModel;
startPos = e->pos();

camera旋转示例代码:

			float yaw_beta = (float)delta.x() / width() * M_PI_4;		// yaw radian
			float pitch_theta = (float)delta.y() / height() * M_PI_4;		// pitch radian
			glm::vec3 roll = glm::normalize(eye - center);
			glm::vec3 pitch = glm::normalize(glm::cross(up, roll));		// pitch 
			glm::vec3 yaw = glm::normalize(glm::cross(roll, pitch));		// yaw axis
			glm::mat4 yawMat = glm::rotate(yaw_beta, yaw);
			glm::mat4 pitchMat = glm::rotate(pitch_theta, pitch);

			eye = pitchMat * yawMat * glm::vec4(eye - center, 0.0) + glm::vec4(center, 0.0);
			up = yaw;


   在camera旋转中,旋转中心(center)的选择,这也是影响操作的主要因素。如果没有选中物体,眼睛-鼠标点射线方向上任一个点都可以作为center,假设选择了很远的一个点,当我们稍微拖动鼠标,旋转角度很小,但是物体已经处在camera左侧或者右侧非常远的地方了。故关键问题便在于如何在涉嫌上选取合理的一个点。如果我们使用角度来计算,那就会遇到一个非常著名的问题:万向节死锁。这里的讲解非常到位。
  对于平移,很重要的一个问题是,对于眼睛在不同的位置,拖动相同的像素距离,camera的真实平移距离是多少?对于旋转,我们只考虑相对角度,平移所对应的绝对距离就是关键问题。首先要确定平移的向量transVec,也可求得在近平面上的移动距离ed ,这样子就可以求解lookat center 的 平移向量。最后更新eye位置,这里最大的影响因素就是nearPlane,如果这个值很小,那么意味着我们认为transVec近似为eye的平移向量。当此值逐渐变大时,那么lookat center的平移向量就偏小,就意味着此时的平移就带有绕lookat center旋转的成分,可以看到远处的东西比我们预想移动的要慢。示例代码如下:

			float d = glm::length(center - eye);	// eye center distance
			glm::vec3 newEye = screen2World(e->pos());	//新的
			glm::vec3 startCoord = screen2World(startPos);
			glm::vec3 transVec =  startCoord - newEye;
			float ed = glm::length(transVec);	// 近平面移动距离
			float cd = ed/nearPlane *d + ed;	// lookat center 移动距离
			center = center + glm::normalize(transVec) * cd;
			eye = eye + transVec;

   缩放是最容易实现的功能。因为只需要更改camera的信息即可。至于是等距离移动,还是按比例距离移动,则取决于需求。另外一个问题是:如何选择缩放的方向。方式一,无论鼠标点在哪里,滚动滑轮时eye只在视线方向移动,其示例代码如下:

	float numStep = (e->angleDelta().y() / 8) / 15;
	float step = 1.0f/10;
	if (1 == numStep) {
	}
	else {
		step = -1.0/9;
	}
	glm::vec3 delta = (center - eye);	// (screen2World(event->pos())
	eye += step * delta ;

   方式二,eye与鼠标点在近平面上的投影点构成的直线上移动。这就要求更新eye pos,lookat center,up。在透视投影模式下,可以改变fov。相较于改变camera位置,这看起来更像时缩放[1]。 在平行投影模式下,我们可以仅仅调整glOrtho() 所需参数即可。
   我们最常用到两个坐标转换的函数 screen2World() world2Screen(),可使用glm来实现。需要注意的是,Qt的屏幕坐标系以左上角为原点。我们希望使用左下角为像素坐标原点,需要自行转换。
   关于投影模式,以前我们需要通过gluPerspective 设置远近平面,用来剪裁可视空间,等同于现在我们可以直接指定projection矩阵,其实就只影响MVP矩阵中的P。因为现在所有的mvp矩阵都是我们自己写代码计算的,就不需要glMatrixMode()与gluPerspective() 这俩函数产生一个mvp矩阵并传入到着色器。对于最新的OpenGL,我们尽可能少的使用gl开头的接口函数。老式的gl接口真是反人类,当时学习的过程真是令人难受。示例代码如下:

	if(projectView)	// GL_PROJECTION
		proj = glm::perspective((fov), width() / (float)height(), nearPlane, farPlane);
	else 
		proj = glm::ortho(0.0f, (float)width(), 0.0f, (float)height(), nearPlane, farPlane);	// GL_MODELVIEW

   上面的示例代码都默认时在透视投影模式下的。如果想要实现平行投影,稍微更改一下计算即可。

 

  1. https://en.wikibooks.org/wiki/OpenGL_Programming/Modern_OpenGL_Tutorial_Arcball
  2. https://www.opengl.org/discussion_boards/showthread.php/199416-Rotating-object-after-transformation-in-modern-OpenGL
  3. https://stackoverflow.com/questions/12138721/rotating-a-open-gl-camera-correctly-using-glm
  4. https://www.zhihu.com/question/37710539
  5. https://blog.csdn.net/yuzhongchun/article/details/22749521
  6. https://en.wikipedia.org/wiki/Aircraft_principal_axes
  7. how-can-i-orbit-a-camera-about-its-target-point 这篇文章这的是太重要了,我花费了一天时间才把这个bug fix 掉。
  8. http://www.opengl-tutorial.org/cn/intermediate-tutorials/tutorial-17-quaternions/