跳转至

3.2 3D 坐标和变换

第二章中,我们比较详细地讨论了二维计算机图形中的坐标系和变换。在本节和下一节中,我们将把这个讨论扩展到三维空间。在三维空间中,事情变得更加复杂,但许多基本概念仍然相同。

In Chapter 2, we looked fairly closely at coordinate systems and transforms in two-dimensional computer graphics. In this section and the next, we will move that discussion into 3D. Things are more complicated in three dimensions, but a lot of the basic concepts remain the same.

3.2.1 3D坐标

3D Coordinates

坐标系是一种将数字分配给点的方式。在二维空间中,您需要一对数字来指定一个点。坐标通常被称为x和y,尽管名称是任意的。更重要的是,将一对数字分配给点本身在很大程度上是任意的。点和物体是真实存在的东西,但坐标只是我们分配给它们的数字,以便我们可以轻松地引用它们并进行数学操作。当我们讨论变换时,我们已经看到了这种力量,变换在数学上是用坐标来定义的,但它们具有真实而有用的物理含义。

在三维空间中,您需要三个数字来指定一个点。(这基本上就是三维的意思。)第三个坐标通常称为z。z轴垂直于x轴和y轴。

这个演示展示了一个三维坐标系。x、y和z轴的正方向显示为大箭头。x轴为绿色,y轴为蓝色,z轴为红色。您可以在轴上拖动以旋转图像。

这个示例是一个二维图像,但它看起来具有三维外观。(如果您旋转图像,这种幻觉会更强烈。)有几个因素共同促成了这种效果。首先,远离观察者的三维物体在二维图像中看起来更小。这是由于将三维场景“投影”到二维的方式。我们将在下一节讨论投影。另一个因素是物体的“阴影”。物体被阴影着,以模拟物体与照亮它们的光的相互作用。我们将延迟讨论光照直到第四章。在本节中,我们将集中讨论如何在三维中构建场景——我们所说的建模。

OpenGL程序员通常使用一种坐标系思维方式,其中x轴和y轴位于屏幕的平面上,而z轴垂直于屏幕,z轴的正方向指向屏幕的外侧,朝向观察者。现在,在OpenGL中,默认的坐标系统(如果不应用任何变换)类似,但z轴的正方向指向屏幕的内侧。这并不矛盾:实际使用的坐标系统是任意的。它是通过一种变换设置的。OpenGL中的约定是使用一个坐标系统,其中正z方向指向观察者,负z方向指向远离观察者。进入默认坐标的变换会反转z轴的方向。

这种轴的常规排列产生了一个右手坐标系。这意味着如果你用右手的拇指指向正z轴的方向,那么当你弯曲手的手指时,它们会沿着从正x轴到正y轴的方向弯曲。如果你看着你的拇指尖,弯曲将是逆时针方向的。另一种思考方式是,如果你用右手的手指从正x轴到正y轴方向弯曲,那么你的拇指将指向正z轴的方向。默认的OpenGL坐标系统(再次强调,几乎不会被使用)是一个左手坐标系。你应该花一些时间来尝试可视化右手和左手坐标系。动动你的手!

所有这些都描述了从观察者的角度来看的自然坐标系统,即所谓的“眼睛”或“观察”坐标系统。然而,这些眼坐标系不一定是世界上的自然坐标。世界上的坐标系统——即场景被组装的坐标系统——被称为世界坐标

回想一下,对象通常不直接在世界坐标中指定。相反,对象在它们自己的坐标系中指定,称为对象坐标,然后应用建模变换将对象放置到世界中,或者放置到更复杂的对象中。在OpenGL中,对象坐标是在glVertex*函数中用来指定对象顶点的数字。然而,在对象出现在屏幕上之前,它们通常会经过一系列的变换,从建模变换开始。

A coordinate system is a way of assigning numbers to points. In two dimensions, you need a pair of numbers to specify a point. The coordinates are often referred to as x and y, although of course, the names are arbitrary. More than that, the assignment of pairs of numbers to points is itself arbitrary to a large extent. Points and objects are real things, but coordinates are just numbers that we assign to them so that we can refer to them easily and work with them mathematically. We have seen the power of this when we discussed transforms, which are defined mathematically in terms of coordinates but which have real, useful physical meanings.

In three dimensions, you need three numbers to specify a point. (That's essentially what it means to be three dimensional.) The third coordinate is often called z. The z-axis is perpendicular to both the x-axis and the y-axis.

This demo illustrates a 3D coordinate system. The positive directions of the x, y, and z axes are shown as big arrows. The x-axis is green, the y-axis is blue, and the z-axis is red. You can drag on the axes to rotate the image.

This example is a 2D image, but it has a 3D look. (The illusion is much stronger if you rotate the image.) Several things contribute to the effect. For one thing, objects that are farther away from the viewer in 3D look smaller in the 2D image. This is due to the way that the 3D scene is "projected" onto 2D. We will discuss projection in the next section. Another factor is the "shading" of the objects. The objects are shaded in a way that imitates the interaction of objects with the light that illuminates them. We will put off a discussion of lighting until Chapter 4. In this section, we will concentrate on how to construct a scene in 3D—what we have referred to as modeling.

OpenGL programmers usually think in terms of a coordinate system in which the x- and y-axes lie in the plane of the screen, and the z-axis is perpendicular to the screen with the positive direction of the z-axis pointing out of the screen towards the viewer. Now, the default coordinate system in OpenGL, the one that you are using if you apply no transformations at all, is similar but has the positive direction of the z-axis pointing into the screen. This is not a contradiction: The coordinate system that is actually used is arbitrary. It is set up by a transformation. The convention in OpenGL is to work with a coordinate system in which the positive z-direction points toward the viewer and the negative z-direction points away from the viewer. The transformation into default coordinates reverses the direction of the z-axis.

This conventional arrangement of the axes produces a right-handed coordinate system. This means that if you point the thumb of your right hand in the direction of the positive z-axis, then when you curl the fingers of that hand, they will curl in the direction from the positive x-axis towards the positive y-axis. If you are looking at the tip of your thumb, the curl will be in the counterclockwise direction. Another way to think about it is that if you curl the figures of your right hand from the positive x to the positive y-axis, then your thumb will point in the direction of the positive z-axis. The default OpenGL coordinate system (which, again, is hardly ever used) is a left-handed system. You should spend some time trying to visualize right- and left-handed coordinates systems. Use your hands!

All of that describes the natural coordinate system from the viewer's point of view, the so-called "eye" or "viewing" coordinate system. However, these eye coordinates are not necessarily the natural coordinates on the world. The coordinate system on the world—the coordinate system in which the scene is assembled—is referred to as world coordinates.

Recall that objects are not usually specified directly in world coordinates. Instead, objects are specified in their own coordinate system, known as object coordinates, and then modeling transforms are applied to place the objects into the world, or into more complex objects. In OpenGL, object coordinates are the numbers that are used in the glVertex* function to specify the vertices of the object. However, before the objects appear on the screen, they are usually subject to a sequence of transformations, starting with a modeling transform.

3.2.2 基本 3D 变换

Basic 3D Transforms

在3D中的基本变换是你已经熟悉的来自2D的基本变换的扩展:旋转、缩放和平移。我们将看一下3D中的等效变换,并了解它们在作为建模变换时如何影响对象。我们还将讨论如何在OpenGL中使用这些变换。

平移是最简单的。在2D中,平移会将一些数字添加到每个坐标上。在3D中也是如此;我们只需要三个数字,来指定沿着每个坐标轴的运动量。通过(dx, dy, dz)进行的平移将点(x, y, z)变换为点(x+dx, y+dy, z+dz)。在OpenGL中,这个平移将由以下命令指定:

glTranslatef( dx, dy, dz );

或者由以下命令指定:

glTranslated( dx, dy, dz );

这个平移会影响在命令之后进行的任何绘制。请注意,这个命令有两个版本。名称以"f"结尾的第一个版本,将三个float值作为参数。名称以"d"结尾的第二个版本,将double类型的参数。例如,

glTranslatef( 0, 0, 1 );

会将对象沿z方向平移一个单位。

缩放的工作方式类似:你需要三个缩放因子,而不是一个。缩放的OpenGL命令是glScale*,其中"*"可以是"f"或"d"。命令

glScalef( sx, sy, sz );

将点(x, y, z)变换为(xsx, ysy, z*sz)。也就是说,它在x方向缩放sx倍,在y方向缩放sy倍,在z方向缩放sz倍。缩放是关于原点的;也就是说,它将点移动到原点(0,0,0)更远或更近的地方。对于均匀缩放,三个因子将是相同的。你可以使用缩放因子为负一来应用反射。例如,

glScalef( 1, 1, -1 );

通过反转z坐标的符号,将对象反射通过xy平面。请注意,反射会将右手坐标系转换为左手坐标系,反之亦然。请记住,左手/右手的区别不是世界的属性,而是你选择在世界上布置坐标的方式。

在3D中,旋转更加复杂。在2D中,旋转是围绕一个点旋转,通常被认为是原点。在3D中,旋转是围绕一条线旋转,这条线被称为旋转轴。想象地球围绕其轴旋转。旋转轴是通过北极和南极的线。随着地球围绕它旋转,轴保持不变,不在轴上的点围绕轴运动成圆。任何线都可以是旋转轴,但我们通常使用通过原点的轴。旋转轴的最常见选择是坐标轴,即x轴、y轴或z轴。然而,有时候使用不同的线作为轴会更加方便。

有一种简单的方法可以指定一个通过原点的线:只需指定另一个在该线上的点,除了原点。这就是OpenGL中的做法:一个旋转轴由三个数字(ax,ay,az)指定,这三个数字不都为零。该轴是通过(0,0,0)和(ax,ay,az)确定的线。要在3D中指定旋转变换,你必须指定一个轴和围绕该轴的旋转角度。

我们仍然必须考虑正角度和负角度之间的差异。我们不能简单地说顺时针或逆时针。如果你从北极上方向下看旋转的地球,你会看到逆时针旋转;如果你从南极上方向下看,你会看到顺时针旋转。因此,这两者之间的差异并不明确。为了定义3D中的旋转方向,我们使用右手定则,它说:用你的右手拇指指向轴的方向——从点(0,0,0)指向确定轴的点(ax,ay,az)。然后旋转的方向对于正角度是由你的手指弯曲的方向决定的。我应该强调,右手定则只有在你在右手坐标系中工作时才有效。如果你已经切换到左手坐标系,那么你需要使用左手定则来确定旋转的正方向。

你可以使用下面的演示来帮助你理解三维空间中的围绕轴的旋转。使用标有“+X”,“-X”等按钮使立方体围绕坐标轴旋转,或输入任何(x,y,z)点并点击“Set”。在图像上拖动鼠标来旋转场景。

<iframe src="../../../en/demos/c3/rotation-axis.html" width="600" height="375"></iframe>

OpenGL中的旋转函数是glRotatef(r,ax,ay,az)。你也可以使用glRotated。第一个参数指定旋转角度,以度为单位。其他三个参数指定旋转轴,它是从(0,0,0)到(ax,ay,az)的线。

以下是在OpenGL中缩放、平移和旋转的几个示例:

glScalef(2,2,2);        // 以2为因子进行均匀缩放。

glScalef(0.5,1,1);      // 只在x方向上缩小一半。

glScalef(-1,1,1);       // 通过yz平面反射。
                        // 将正x轴反射到负x轴上。

glTranslatef(5,0,0);    // 在正x方向移动5个单位。

glTranslatef(3,5,-7.5); // 将每个点(x,y,z)移动到(x+3, y+5, z-7.5)。

glRotatef(90,1,0,0);    // 围绕x轴旋转90度。
                        // 将+y轴移动到+z轴上
                        //    和+z轴移动到-y轴上。

glRotatef(-90,-1,0,0);  // 与上一个旋转具有

相同的效果

glRotatef(90,0,1,0);    // 围绕y轴旋转90度。
                        // 将+z轴移动到+x轴上
                        //    和+x轴移动到-z轴上。

glRotatef(90,0,0,1);    // 围绕z轴旋转90度。
                        // 将+x轴移动到+y轴上
                        //    和+y轴移动到-x轴上。

glRotatef(30,1.5,2,-3); // 围绕通过点(0,0,0)和(1.5,2,-3)的线旋转30度。

记住,变换应用于在变换函数调用之后绘制的对象,并且变换按照它们在代码中出现的相反顺序应用于对象。

当然,OpenGL 既可以在二维中绘制,也可以在三维中绘制。在OpenGL中进行二维绘制时,你可以在xy平面上绘制,将 z 坐标设为零。在进行二维绘制时,你可能希望应用二维版本的旋转、缩放和平移。OpenGL 没有二维变换函数,但你可以使用带有适当参数的三维版本:

  • 对于二维中的平移 (dx, dy),使用 glTranslatef(dx, dy, 0)。在 z 方向上的零平移意味着变换不改变 z 坐标,因此它将 xy 平面映射到自身。(当然,你也可以使用 glTranslated 代替 glTranslatef。)
  • 对于二维中的缩放 (sx, sy),使用 glScalef(sx, sy, 1),这样只在 x 和 y 方向上进行缩放,不改变 z 坐标
  • 对于围绕原点的角度为 r 的二维旋转,使用 glRotatef(r, 0, 0, 1)。这是围绕 z 轴的旋转,它将 xy 平面旋转到自身。在通常的OpenGL坐标系统中,z 轴指向屏幕外,右手定则表示正角度的旋转将顺时针方向在 xy 平面上进行。由于 x 轴指向右侧,y 轴指向上方,顺时针旋转将正 x 轴旋转到正 y 轴方向。这与我们之前用于旋转正方向的约定相同。

The basic transforms in 3D are extensions of the basic transforms that you are already familiar with from 2D: rotation, scaling, and translation. We will look at the 3D equivalents and see how they affect objects when applied as modeling transforms. We will also discuss how to use the transforms in OpenGL.

Translation is easiest. In 2D, a translation adds some number onto each coordinate. The same is true in 3D; we just need three numbers, to specify the amount of motion in the direction of each of the coordinate axes. A translation by (dx,dy,dz) transforms a point (x,y,z) to the point (x+dx, y+dy, z+dz). In OpenGL, this translation would be specified by the command

glTranslatef( dx, dy, dz );

or by the command

glTranslated( dx, dy, dz );

The translation will affect any drawing that is done after the command is given. Note that there are two versions of the command. The first, with a name ending in "f", takes three float values as parameters. The second, with a name ending in "d", takes parameters of type double. As an example,

glTranslatef( 0, 0, 1 );

would translate objects by one unit in the z direction.

Scaling works in a similar way: Instead of one scaling factor, you need three. The OpenGL command for scaling is glScale*, where the "*" can be either "f" or "d". The command

glScalef( sx, sy, sz );

transforms a point (x,y,z) to (xsx, ysy, z*sz). That is, it scales by a factor of sx in the x direction, sy in the y direction, and sz in the z direction. Scaling is about the origin; that is, it moves points farther from or closer to the origin, (0,0,0). For uniform scaling, all three factors would be the same. You can use scaling by a factor of minus one to apply a reflection. For example,

glScalef( 1, 1, -1 );

reflects objects through the xy-plane by reversing the sign of the z coordinate. Note that a reflection will convert a right-handed coordinate system into a left-handed coordinate system, and vice versa. Remember that the left/right handed distinction is not a property of the world, just of the way that one chooses to lay out coordinates on the world.

Rotation in 3D is harder. In 2D, rotation is rotation about a point, which is usually taken to be the origin. In 3D, rotation is rotation about a line, which is called the axis of rotation. Think of the Earth rotating about its axis. The axis of rotation is the line that passes through the North Pole and the South Pole. The axis stays fixed as the Earth rotates around it, and points that are not on the axis move in circles about the axis. Any line can be an axis of rotation, but we generally use an axis that passes through the origin. The most common choices for axis of rotation are the coordinates axes, that is, the x-axis, the y-axis, or the z-axis. Sometimes, however, it's convenient to be able to use a different line as the axis.

There is an easy way to specify a line that passes through the origin: Just specify one other point that is on the line, in addition to the origin. That's how things are done in OpenGL: An axis of rotation is specified by three numbers, (ax,ay,az), which are not all zero. The axis is the line through (0,0,0) and (ax,ay,az). To specify a rotation transformation in 3D, you have to specify an axis and the angle of rotation about that axis.

We still have to account for the difference between positive and negative angles. We can't just say clockwise or counterclockwise. If you look down on the rotating Earth from above the North pole, you see a counterclockwise rotation; if you look down on it from above the South pole, you see a clockwise rotation. So, the difference between the two is not well-defined. To define the direction of rotation in 3D, we use the right-hand rule, which says: Point the thumb of your right hand in the direction of the axis — from the point (0,0,0) towards the point (ax,ay,az) that determines the axis. Then the direction of rotation for positive angles is given by the direction in which your fingers curl. I should emphasize that the right-hand rule only works if you are working in a right-handed coordinate system. If you have switched to a left-handed coordinate system, then you need to use a left-hand rule to determine the positive direction of rotation.

You can use the following demo to help you understand rotation about an axis in three-dimensional space. Use the buttons labeled "+X", "-X", and so on to make the cube rotate about the coordinate axes, or enter any (x,y,z) point and click "Set". Drag your mouse on the image to rotate the scene.

The rotation function in OpenGL is glRotatef(r,ax,ay,az). You can also use glRotated. The first parameter specifies the angle of rotation, measured in degrees. The other three parameters specify the axis of rotation, which is the line from (0,0,0) to (ax,ay,az).

Here are a few examples of scaling, translation, and scaling in OpenGL:

glScalef(2,2,2);        // Uniform scaling by a factor of 2.

glScalef(0.5,1,1);      // Shrink by half in the x-direction only.

glScalef(-1,1,1);       // Reflect through the yz-plane.
                        // Reflects the positive x-axis onto negative x.

glTranslatef(5,0,0);    // Move 5 units in the positive x-direction.

glTranslatef(3,5,-7.5); // Move each point (x,y,z) to (x+3, y+5, z-7.5).

glRotatef(90,1,0,0);    // Rotate 90 degrees about the x-axis.
                        // Moves the +y axis onto the +z axis
                        //    and the +z axis onto the -y axis.

glRotatef(-90,-1,0,0);  // Has the same effect as the previous rotation.

glRotatef(90,0,1,0);    // Rotate 90 degrees about the y-axis.
                        // Moves the +z axis onto the +x axis
                        //    and the +x axis onto the -z axis.

glRotatef(90,0,0,1);    // Rotate 90 degrees about the z-axis.
                        // Moves the +x axis onto the +y axis
                        //    and the +y axis onto the -x axis.

glRotatef(30,1.5,2,-3); // Rotate 30 degrees about the line through
                        //    the points (0,0,0) and (1.5,2,-3).

Remember that transforms are applied to objects that are drawn after the transformation function is called, and that transformations apply to objects in the opposite order of the order in which they appear in the code.

Of course, OpenGL can draw in 2D as well as in 3D. For 2D drawing in OpenGL, you can draw on the xy-plane, using zero for the z coordinate. When drawing in 2D, you will probably want to apply 2D versions of rotation, scaling, and translation. OpenGL does not have 2D transform functions, but you can just use the 3D versions with appropriate parameters:

  • For translation by (dx,dy) in 2D, use glTranslatef(dx, dy, 0). The zero translation in the z direction means that the transform doesn't change the z coordinate, so it maps the xy-plane to itself. (Of course, you could use glTranslated instead of glTranslatef.)
  • For scaling by (sx,sy) in 2D, use glScalef(sx, sy, 1), which scales only in the x and y directions, leaving the z coordinate unchanged.
  • For rotation through an angle r about the origin in 2D, use glRotatef(r, 0, 0, 1). This is rotation about the z-axis, which rotates the xy-plane into itself. In the usual OpenGL coordinate system, the z-axis points out of the screen, and the right-hand rule says that rotation by a positive angle will be in the counterclockwise direction in the xy-plane. Since the x-axis points to the right and the y-axis points upwards, a counterclockwise rotation rotates the positive x-axis in the direction of the positive y-axis. This is the same convention that we have used previously for the positive direction of rotation.

3.2.3 层次建模

Hierarchical Modeling

建模变换经常用于分层建模,允许将复杂对象由简单对象组合而成。参见第 2.4 节。简要回顾一下:在分层建模中,一个对象可以在其自然坐标系中定义,通常使用 (0,0,0) 作为参考点。然后,可以对该对象进行缩放、旋转和平移,以将其放置到世界坐标或更复杂的对象中。为了实现这一点,我们需要一种方法来将建模变换的效果限制在一个对象或一个对象的一部分上。这可以通过使用变换堆栈来实现。在绘制一个对象之前,将当前变换的副本推送到堆栈上。在绘制对象及其子对象之后,使用任何必要的临时变换,通过从堆栈中弹出来恢复先前的变换。

OpenGL 1.1 维护一个变换堆栈,并提供了操作该堆栈的函数。(实际上,它有几个用于不同目的的变换堆栈,这引入了一些复杂性,我们将在下一节中推迟讨论。)由于变换被表示为矩阵,所以堆栈实际上是矩阵的堆栈。在OpenGL中,操作堆栈的函数命名为 glPushMatrix()glPopMatrix()

这些函数不接受参数,也不返回值。OpenGL 会跟踪一个当前矩阵,它是所有已应用的变换的组合。调用诸如 glScalef 的函数只是修改当前矩阵。当绘制一个对象时,使用 glVertex* 函数指定的坐标将被当前矩阵进行变换。还有另一个影响当前矩阵的函数:glLoadIdentity()。调用 glLoadIdentity 将当前矩阵设置为单位变换,表示坐标没有任何变化,这是一系列变换的常见起点。

当调用 glPushMatrix() 函数时,当前矩阵的副本将被推送到堆栈上。请注意,这不会更改当前矩阵;它只是在堆栈上保存了一个副本。当调用 glPopMatrix() 时,堆栈顶部的矩阵将从堆栈中弹出,并替换当前矩阵。请注意,glPushMatrixglPopMatrix 必须始终成对出现;glPushMatrix 保存当前矩阵的副本,相应的 glPopMatrix 调用恢复该副本。在调用 glPushMatrix 和相应的 glPopMatrix 之间,可以额外调用这些函数的成对调用,只要它们被正确地配对即可。通常,你会在绘制一个对象之前调用 glPushMatrix,并在完成该对象之后调用 glPopMatrix。在此期间,绘制子对象可能需要额外的这些函数的成对调用。

举例来说,假设我们想要绘制一个立方体。使用 glBegin/glEnd 来绘制每个面并不难,但让我们使用变换来实现。我们可以从一个绘制正面的正方形的函数开始。对于大小为 1 的立方体,正面将位于屏幕前半个单位处,在 z = 0.5 平面上,并且其顶点分别为 (-0.5, -0.5, 0.5)、(0.5, -0.5, 0.5)、(0.5, 0.5, 0.5) 和 (-0.5, 0.5, 0.5)。以下是绘制正方形的函数。函数的参数是在范围 0.0 到 1.0 内的浮点数,表示正方形的 RGB 颜色:

void square(float r, float g, float b) {
    glColor3f(r, g, b);  // 设置正方形的颜色。
    glBegin(GL_TRIANGLE_FAN);
    glVertex3f(-0.5, -0.5, 0.5);
    glVertex3f(0.5, -0.5, 0.5);
    glVertex3f(0.5, 0.5, 0.5);
    glVertex3f(-0.5, 0.5, 0.5);
    glEnd();
}

要创建一个红色的立方体正面,我们只需调用 square(1,0,0)。现在,考虑右侧面,它垂直于 x 轴,在 x = 0.5 平面上。要创建一个右侧面,我们可以从一个正面开始,然后绕着 y 轴旋转 90 度。考虑将正面(红色)绕着 y 轴旋转到右侧面(绿色)位置的操作:

123

因此,我们可以使用以下代码绘制一个绿色的立方体右侧面:

glPushMatrix();
glRotatef(90, 0, 1, 0);
square(0, 1, 0);
glPopMatrix();

调用 glPushMatrixglPopMatrix 确保对正方形应用的旋转不会延续到稍后绘制的对象上。其他四个面可以用类似的方式制作,方法是围绕坐标轴旋转正面。您应该尝试在每种情况下可视化所需的旋转。我们可以将所有这些组合到一个绘制立方体的函数中。为了增加趣味性,立方体的大小是一个参数:

void cube(float size) {  // 绘制边长为 size 的立方体。

    glPushMatrix();  // 保存当前矩阵的副本。
    glScalef(size, size, size); // 将单位立方体缩放到所需大小。

    square(1, 0, 0); // 红色正面

    glPushMatrix();
    glRotatef(90, 0, 1, 0);
    square(0, 1, 0); // 绿色右侧面
    glPopMatrix();

    glPushMatrix();
    glRotatef(-90, 1, 0, 0);
    square(0, 0, 1); // 蓝色顶部面
    glPopMatrix();

    glPushMatrix();
    glRotatef(180, 0, 1, 0);
    square(0, 1, 1); // 青色背面
    glPopMatrix();

    glPushMatrix();
    glRotatef(-90, 0, 1, 0);
    square(1, 0, 1); // 品红色左侧面
    glPopMatrix();

    glPushMatrix();
    glRotatef(90, 1, 0, 0);
    square(1, 1, 0); // 黄色底部面
    glPopMatrix();

    glPopMatrix(); // 将矩阵恢复到调用 cube() 之前的状态。

}

示例程序 glut/unlit-cube.c 使用此函数绘制立方体,并允许您通过按箭头键来旋转立方体。Java 版本是 jogl/UnlitCube.java,Web 版本是 glsim/unlit-cube.html。下面是立方体的图像,绕 x 轴旋转了 15 度,绕 y 轴旋转了 -15 度,以显示顶部和右侧:

123

对于具有 glPushMatrix 和 glPopMatrix 的层次建模的更复杂示例,请查看子节 2.4.1中作为示例使用的“小车和风车”动画的 OpenGL 等效版本。该示例的三个版本分别是:glut/opengl-cart-and-windmill-2d.cjogl/CartAndWindmillJogl2D.javaglsim/opengl-cart-and-windmill.html。此程序是 OpenGL 中层次 2D 图形的示例。


请记住,像 glRotated()glPushMatrix() 这样的变换和矩阵函数是老式的 OpenGL。在 WebGL 和其他现代图形 API 中,您将负责自行管理变换和矩阵。您很可能会使用软件库来完成这项工作,该库提供了与 OpenGL 1.1 内置函数非常相似的函数。

Modeling transformations are often used in hierarchical modeling, which allows complex objects to be built up out of simpler objects. See Section 2.4. To review briefly: In hierarchical modeling, an object can be defined in its own natural coordinate system, usually using (0,0,0) as a reference point. The object can then be scaled, rotated, and translated to place it into world coordinates or into a more complex object. To implement this, we need a way of limiting the effect of a modeling transformation to one object or to part of an object. That can be done using a stack of transforms. Before drawing an object, push a copy of the current transform onto the stack. After drawing the object and its sub-objects, using any necessary temporary transformations, restore the previous transform by popping it from the stack.

OpenGL 1.1 maintains a stack of transforms and provides functions for manipulating that stack. (In fact it has several transform stacks, for different purposes, which introduces some complications that we will postpone to the next section.) Since transforms are represented as matrices, the stack is actually a stack of matrices. In OpenGL, the functions for operating on the stack are named glPushMatrix() and glPopMatrix().

These functions do not take parameters or return a value. OpenGL keeps track of a current matrix, which is the composition of all transforms that have been applied. Calling a function such as glScalef simply modifies the current matrix. When an object is drawn, using the glVertex* functions, the coordinates that were specified for the object are transformed by the current matrix. There is another function that affects the current matrix: glLoadIdentity(). Calling glLoadIdentity sets the current matrix to be the identity transform, which represents no change of coordinates at all and is the usual starting point for a series of transformations.

When the function glPushMatrix() is called, a copy of the current matrix is pushed onto the stack. Note that this does not change the current matrix; it just saves a copy on the stack. When glPopMatrix() is called, the matrix on the top of the stack is popped from the stack, and that matrix replaces the current matrix. Note that glPushMatrix and glPopMatrix must always occur in corresponding pairs; glPushMatrix saves a copy of the current matrix, and a corresponding call to glPopMatrix restores that copy. Between a call to glPushMatrix and the corresponding call to glPopMatrix, there can be additional calls of these functions, as long as they are properly paired. Usually, you will call glPushMatrix before drawing an object and glPopMatrix after finishing that object. In between, drawing sub-objects might require additional pairs of calls to those functions.

As an example, suppose that we want to draw a cube. It's not hard to draw each face using glBegin/glEnd, but let's do it with transformations. We can start with a function that draws a square in the position of the front face of the cube. For a cube of size 1, the front face would sit one-half unit in front of the screen, in the plane z = 0.5, and it would have vertices at (-0.5, -0.5, 0.5), (0.5, -0.5, 0.5), (0.5, 0.5, 0.5), and (-0.5, 0.5, 0.5). Here is a function that draws the square. The function's parameters are floating point numbers in the range 0.0 to 1.0 that give the RGB color of the square:

void square( float r, float g, float b ) {
    glColor3f(r,g,b);  // Set the color for the square.
    glBegin(GL_TRIANGLE_FAN);
    glVertex3f(-0.5, -0.5, 0.5);
    glVertex3f(0.5, -0.5, 0.5);
    glVertex3f(0.5, 0.5, 0.5);
    glVertex3f(-0.5, 0.5, 0.5);
    glEnd();
}

To make a red front face for the cube, we just need to call square(1,0,0). Now, consider the right face, which is perpendicular to the x-axis, in the plane x = 0.5. To make a right face, we can start with a front face and rotate it 90 degrees about the y-axis. Think about rotating the front face (red) to the position of the right face (green) in this illustration by rotating the red square about the y-axis:

123

So, we can draw a green right face for the cube with

glPushMatrix();
glRotatef(90, 0, 1, 0);
square(0, 1, 0);
glPopMatrix();

The calls to glPushMatrix and glPopMatrix ensure that the rotation that is applied to the square will not carry over to objects that are drawn later. The other four faces can be made in a similar way, by rotating the front face about the coordinate axes. You should try to visualize the rotation that you need in each case. We can combine it all into a function that draws a cube. To make it more interesting, the size of the cube is a parameter:

void cube(float size) {  // Draws a cube with side length = size.

    glPushMatrix();  // Save a copy of the current matrix.
    glScalef(size,size,size); // Scale unit cube to desired size.

    square(1, 0, 0); // red front face

    glPushMatrix();
    glRotatef(90, 0, 1, 0);
    square(0, 1, 0); // green right face
    glPopMatrix();

    glPushMatrix();
    glRotatef(-90, 1, 0, 0);
    square(0, 0, 1); // blue top face
    glPopMatrix();

    glPushMatrix();
    glRotatef(180, 0, 1, 0);
    square(0, 1, 1); // cyan back face
    glPopMatrix();

    glPushMatrix();
    glRotatef(-90, 0, 1, 0);
    square(1, 0, 1); // magenta left face
    glPopMatrix();

    glPushMatrix();
    glRotatef(90, 1, 0, 0);
    square(1, 1, 0); // yellow bottom face
    glPopMatrix();

    glPopMatrix(); // Restore matrix to its state before cube() was called.

}

The sample program glut/unlit-cube.c uses this function to draw a cube, and lets you rotate the cube by pressing the arrow keys. A Java version is jogl/UnlitCube.java, and a web version is glsim/unlit-cube.html. Here is an image of the cube, rotated by 15 degrees about the x-axis and -15 degrees about the y-axis to make the top and right sides visible:

123

For a more complex example of hierarchical modeling with glPushMatrix and glPopMatrix, you can check out an OpenGL equivalent of the "cart and windmills" animation that was used as an example in Subsection 2.4.1. The three versions of the example are: glut/opengl-cart-and-windmill-2d.c, jogl/CartAndWindmillJogl2D.java, and glsim/opengl-cart-and-windmill.html. This program is an example of hierarchical 2D graphics in OpenGL.


Keep in mind that transformation and matrix functions such as glRotated() and glPushMatrix() are old-fashioned OpenGL. In WebGL and other modern graphics APIs, you will be responsible for managing transforms and matrices on your own. You are quite likely to do that using a software library that provides functions very similar to those that are built into OpenGL 1.1.