移动和旋转
我们上一个程序有些简单,我们不是要学3D编程吗?上一个程序看起来完全是2D啊,现在就让我们做一些有意思的事情——让图形旋转起来。
为了做到这一点,我们要理解OpenGL里面的移动和旋转。我们想象有一只鸟在场景里面飞行。它从原点开始,沿着z轴的负方向飞行,它可以移动、旋转、长大和缩小。当我们用glVertex将点传输给OpenGL的时候,OpenGL就用它来将鸟进行关联起来。所以,如果我们将这只鸟扩大两倍,并向右移动两个单元,从它的角度看,点(0,4,0)对它来说就是(1,2,0)。如果我们将这只鸟以x轴旋转90度,向上移动2个单元,点(0,0,-1)相对于它来说就是(0,-1,-2)。下面的图片就是上面的描述的情况,鸟是我通过橡皮泥捏成的。
此时你会想,“这个太愚蠢了,我为什么不直接画出已经移动好的点呢?”,不要着急,这节课后你将会明白为什么这么做。
我们将从上一节课的代码继续将其,有些注释将会删除。首先,我们将我们的“鸟”移动5个单位长度,用0作为z坐标的值,而不是原来用-5作为z坐标的值。我们调用glTranslatef来移动图形。
glLoadIdentity(); //Reset the drawing perspective
glTranslatef(0.0f, 0.0f, -5.0f); //Move forward 5 units
glBegin(GL_QUADS);
//Trapezoid
glVertex3f(-0.7f, -1.5f, 0.0f);
glVertex3f(0.7f, -1.5f, 0.0f);
glVertex3f(0.4f, -0.5f, 0.0f);
glVertex3f(-0.4f, -0.5f, 0.0f);
glEnd();
glBegin(GL_TRIANGLES);
//Pentagon
glVertex3f(0.5f, 0.5f, 0.0f);
glVertex3f(1.5f, 0.5f, 0.0f);
glVertex3f(0.5f, 1.0f, 0.0f);
glVertex3f(0.5f, 1.0f, 0.0f);
glVertex3f(1.5f, 0.5f, 0.0f);
glVertex3f(1.5f, 1.0f, 0.0f);
glVertex3f(0.5f, 1.0f, 0.0f);
glVertex3f(1.5f, 1.0f, 0.0f);
glVertex3f(1.0f, 1.5f, 0.0f);
//Triangle
glVertex3f(-0.5f, 0.5f, 0.0f);
glVertex3f(-1.0f, 1.5f, 0.0f);
glVertex3f(-1.5f, 0.5f, 0.0f);
glEnd();
如果我们编译运行这段代码,结果与上一节课的结果是一样的,这也是我们所希望的结果。
让我们再来看看上节课遗留下来的glLoadIdentity函数,它的作用就是重置我们的“鸟”,让我们的“鸟”回到原点,朝向z的负方向。
现在,让我们利用一下移动这个功能,使得我们要对图形进行处理的时候都是以图形的中心为参考点的。
glLoadIdentity(); //Reset the drawing perspective
glTranslatef(0.0f, 0.0f, -5.0f); //Move forward 5 units
glPushMatrix(); //Save the transformations performed thus far
glTranslatef(0.0f, -1.0f, 0.0f); //Move to the center of the trapezoid
glBegin(GL_QUADS);
//Trapezoid
glVertex3f(-0.7f, -0.5f, 0.0f);
glVertex3f(0.7f, -0.5f, 0.0f);
glVertex3f(0.4f, 0.5f, 0.0f);
glVertex3f(-0.4f, 0.5f, 0.0f);
glEnd();
glPopMatrix(); //Undo the move to the center of the trapezoid
glPushMatrix(); //Save the current state of transformations
glTranslatef(1.0f, 1.0f, 0.0f); //Move to the center of the pentagon
glBegin(GL_TRIANGLES);
//Pentagon
glVertex3f(-0.5f, -0.5f, 0.0f);
glVertex3f(0.5f, -0.5f, 0.0f);
glVertex3f(-0.5f, 0.0f, 0.0f);
glVertex3f(-0.5f, 0.0f, 0.0f);
glVertex3f(0.5f, -0.5f, 0.0f);
glVertex3f(0.5f, 0.0f, 0.0f);
glVertex3f(-0.5f, 0.0f, 0.0f);
glVertex3f(0.5f, 0.0f, 0.0f);
glVertex3f(0.0f, 0.5f, 0.0f);
glEnd();
glPopMatrix(); //Undo the move to the center of the pentagon
glPushMatrix(); //Save the current state of transformations
glTranslatef(-1.0f, 1.0f, 0.0f); //Move to the center of the triangle
glBegin(GL_TRIANGLES);
//Triangle
glVertex3f(0.5f, -0.5f, 0.0f);
glVertex3f(0.0f, 0.5f, 0.0f);
glVertex3f(-0.5f, -0.5f, 0.0f);
glEnd();
glPopMatrix(); //Undo the move to the center of the triangle
如果我们再次编译运行这段代码,结果还是一样的。
在这段代码里面,我们用到了两个新的并且重要的函数:glPushMatrix和glPopMatrix,我们利用这两个函数保存和恢复我们“鸟”的状态。glPushMatrix保存它的状态,glPopMatrix恢复它的状态。值得注意的是,就像每一个glBegin后都有一个glEnd一样,每一个glPushMatrix后都要有一个glPopMatrix。我们利用glPushMatrix来保存“鸟”的状态,以至于不用重新将坐标移动到-5值。
其实我们可以保存多个“鸟”的状态,因为我们有一个状态栈。每一次我们调用glPushMatrix就是将状态放入栈顶,每次调用过来PopMatrix就是将状态弹出栈。OpenGL的状态栈最大可以存储32个状态。
glPopMatrix和glPushMatrix之所以这样命名是因为OpenGL通过矩阵来表示“鸟”的状态。现在,你无需担心矩阵是怎样工作的。
现在让我们改变一下我们的程序。让每一个图形都选择30度,五角星图形缩小到原来的70%。
float _angle = 30.0f;
//Draws the 3D scene
void drawScene() {
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glMatrixMode(GL_MODELVIEW); //Switch to the drawing perspective
glLoadIdentity(); //Reset the drawing perspective
glTranslatef(0.0f, 0.0f, -5.0f); //Move forward 5 units
glPushMatrix(); //Save the transformations performed thus far
glTranslatef(0.0f, -1.0f, 0.0f); //Move to the center of the trapezoid
glRotatef(_angle, 0.0f, 0.0f, 1.0f); //Rotate about the z-axis
glBegin(GL_QUADS);
//Trapezoid
glVertex3f(-0.7f, -0.5f, 0.0f);
glVertex3f(0.7f, -0.5f, 0.0f);
glVertex3f(0.4f, 0.5f, 0.0f);
glVertex3f(-0.4f, 0.5f, 0.0f);
glEnd();
glPopMatrix(); //Undo the move to the center of the trapezoid
glPushMatrix(); //Save the current state of transformations
glTranslatef(1.0f, 1.0f, 0.0f); //Move to the center of the pentagon
glRotatef(_angle, 0.0f, 1.0f, 0.0f); //Rotate about the y-axis
glScalef(0.7f, 0.7f, 0.7f); //Scale by 0.7 in the x, y, and z directions
glBegin(GL_TRIANGLES);
//Pentagon
glVertex3f(-0.5f, -0.5f, 0.0f);
glVertex3f(0.5f, -0.5f, 0.0f);
glVertex3f(-0.5f, 0.0f, 0.0f);
glVertex3f(-0.5f, 0.0f, 0.0f);
glVertex3f(0.5f, -0.5f, 0.0f);
glVertex3f(0.5f, 0.0f, 0.0f);
glVertex3f(-0.5f, 0.0f, 0.0f);
glVertex3f(0.5f, 0.0f, 0.0f);
glVertex3f(0.0f, 0.5f, 0.0f);
glEnd();
glPopMatrix(); //Undo the move to the center of the pentagon
glPushMatrix(); //Save the current state of transformations
glTranslatef(-1.0f, 1.0f, 0.0f); //Move to the center of the triangle
glRotatef(_angle, 1.0f, 2.0f, 3.0f); //Rotate about the the vector (1, 2, 3)
glBegin(GL_TRIANGLES);
//Triangle
glVertex3f(0.5f, -0.5f, 0.0f);
glVertex3f(0.0f, 0.5f, 0.0f);
glVertex3f(-0.5f, -0.5f, 0.0f);
glEnd();
glPopMatrix(); //Undo the move to the center of the triangle
现在我们程序的运行结果是这样的:
我们引入了一个新的变量,_angle,它用来存储我们想要图形的旋转度数。我们这里用到了两个新的函数。我们调用glRotatef来旋转我们的“鸟”。我们调用glRotatef(_angle,0.0f,0.0f,1.0f)来将我们的“鸟”绕着z轴旋转_angle大小的角度,我们调用glRotatef(_angle,1.0f,2.0f,3.0f)来将我们的“鸟”绕着向量(1,2,3)旋转_angle大小的角度。我们调用glScalef(0.7f,0.7f,0.7f)将我们的“鸟”的x、y、z坐标缩小70%。如果我们调用glScalef(2.0f,1.0f,1.0f),它会将我们的“鸟”的x坐标扩大2倍。
非常重要的一点是:glTranslatef、glRotatef、glScalef不能在glBegin-glEnd块中调用。
现在让我们改变镜头的角度——向左偏转10度。
float _cameraAngle = 10.0f;
//Draws the 3D scene
void drawScene() {
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glMatrixMode(GL_MODELVIEW); //Switch to the drawing perspective
glLoadIdentity(); //Reset the drawing perspective
glRotatef(-_cameraAngle, 0.0f, 1.0f, 0.0f); //Rotate the camera
glTranslatef(0.0f, 0.0f, -5.0f); //Move forward 5 units
我们程序的运行结果将会是:
我们用了一个小技巧来改变镜头的角度——将镜头反方向旋转10度。这是一个很好用的技术,我们在3D编程中常常用到。
在我们进入下一小节之前,我们先来介绍一下glMatrixMode函数。我们调用glMatrixMode(GL_MODEL_VIEW),我们设置点的转化用于场景的转化。如果我们调用glMatrixMode(GL_PORJECTION),就像handleResize函数中实现的那样,我们设置点的转化用于除正常转化外的其他转化。让我们看看handleResize函数,我们转换成投影矩阵模式,调用glLoadIdentity()函数重置它的矩阵,然后调用gluPerspective函数。gluPerspective函数执行了一个怪异的转化——给我们的点一个“视图”。不要担心他们是怎么工作,你只需要知道我们用GL_PROJECTION来设置我们的视图,用GL_MODEL_VIEW来说做其他的事情。
计时器
void update(int value) {
_angle += 2.0f;
if (_angle > 360) {
_angle -= 360;
}
glutPostRedisplay(); //Tell GLUT that the scene has changed
//Tell GLUT to call update again in 25 milliseconds
glutTimerFunc(25, update, 0);
}
这就是我们的更新函数。首先我们对角度增加2度。如果角度达到了360度,我们就减去360度。其实我们没有必要去这样做,但我们让角度尽量小,因为它涉及到浮点型的精度问题。这里我们不详细去介绍浮点精度。我们调用glutPostRedisplay函数去通知OpenGL我们的场景已经改变,让GLUT重新绘制。最后我们调用glutTimierFunc(25,update,0)让程序每25毫秒调用一次。
glutTimerFunc(25, update, 0); //Add a timer
在我们的主函数中增加另一个函数glutTimerFunc,以至于当程序运行时每25毫秒调用一次update。