代码改变世界

OpenGL基础知识

2010-12-01 15:46  bingcaihuang  阅读(585)  评论(0)    收藏  举报

OpenGL是个大的状态机。我们就是通过改变它的状态来实现场景的渲染的。

1.最基本的矩阵变换:

模型变换:移动和变换场景中的模型;

投影变换 :对视见空间进行裁剪和扭曲;

视角变换: 对最终输出进行缩放。

矩阵变换

通过前三章的学习,我们知道了如何使用OpenGL在3D空间中绘制基本图元,并把使用图元组成模型。然而,在我们绘制完一个物体或一个场景之后,我们总希望从多个角度观察这个物体,或者在场景中走动。这时,我们需要OpenGL的另一个功能:变换。

OpenGL为我们提供了许多方面和类型的变换。你可以对投影方式进行变换,也可以对物体/模型 进行变换。你可以改变自己的位置和方向,也可以改变物体的大小和角度。学习本章内容,你将了解:

OpenGL中变换的种类

使用矩阵描述一个变换

基本变换

定义和使用自己的变换

4.1 OpenGL中的变换

变换(Transform),可以使3D空间中的物体投影到2D平面上。使用变换,你可以移动、旋转、缩放甚至弯曲一个物体。然而变换并没有直接修改顶点数据,取而代之,变换修改了坐标系。如果旋转一个坐标系,然后再在旋转后的坐标系里绘图,绘制后的图形就好像被旋转了。在基本OpenGL渲染流程中,将进行以下变换:

视图变换 :用于指定观察者的位置和方向;

模型视图变换:移动和变换场景中的模型;

投影变换 :对视见空间进行裁剪和扭曲;

视见区变换:对最终输出进行缩放。

4.1.1 视图变换

在一个场景中,我们希望改变观察者的位置和观察角度。用于改变观察者方位和角度的变换,就是视图变换。默认情况下(没有执行任何变换时),观察者位于点(0,0,0),且视线朝着-Z方向。也就是说,只有在z<0的地方绘图,才有可能被观察到。

4.1.2 模型视图变换

此变换用于移动和旋转场景中的物体。使用模型视图变换完全可以代替视图变换。道理是很简单的:比如你想使用视图变换将观察者向-Z轴移动10个单位,此时场景中所有的物体都向+Z轴移动了10个单位。这跟你直接使用模型视图变换将场景中所有物体向+Z方向移动10个单位的效果是完全一样的。

4.1.3 投影变换

要把3D场景投影到2D平面上,就必须执行投影变换。投影变换有两种形式,即平行投影变换和透视投影变换。关于平行投影和透视投影,在第3中已进行了具体的介绍,这里不在复述。现在要强调的是,投影也是一种变换,实现投影,本质上是对场景中所有物体进行特殊的变换,使得它们能够被画在一个平面上。比如透视投影变换会将场景中所有物体按照远近不同进行缩放和扭曲,使它们看起来具有立体感。

4.1.4 视见区变换

这里又回到了第二章中的主题。视见区变换就是对投影后的2D图象进行缩放和剪裁,使它能够被正确地显示在窗口上。你可以回到第二章了解视见区的具体概念。

4.2 矩阵

矩阵(Matrix)是那样的强大以至于几乎所有的变换都可以由矩阵来表达。矩阵是又n行m列的数组成的一个阵列(m、n≥1)。通过矩阵的乘法运算就可以运用各种变换。在OpenGL中,统一使用大小为4×4的矩阵。

由于矩阵的运算法则和具体数学内容,和OpenGL这一主题并没有太大关系(使用OpenGL并不需要了解矩阵是怎样运算的,因为OpenGL会帮你完成一切),所以这里不再介绍。但这并不代表这些数学知识是不重要的,灵活地运用矩阵,可以自己创造出许多OpenGL没有提供的变换,并提高运算速度。你可以参看《线性代数》了解更多内容。

在OpenGL进行变换操作时,会首先把顶点转换为1×4的矩阵(第1-3行分别存放顶点的x、y、z坐标,第4行存放w坐标,即缩放因子,一般总为1.0),然后将这个点依次乘以模型视图变换矩阵、投影矩阵、视见区变换矩阵......最后得到因出现在屏幕上的2D屏幕坐标,完成变换。幸运的是,你不需要任何数学基础,哪怕你对矩阵一无所知,也能顺利地完成这一流程。因为OpenGL已经封装了高级函数,这使得你不用自己动手写矩阵,就能完成所有的基本变换。稍后就将介绍这些基本函数。

4.3 基本变换

对于每一种变换,OpenGL都有自己的函数用来生成这些变换的矩阵并应用它们。下面将一一介绍。

4.3.1 模型变换矩阵

这是本章最重要的内容。使用模型变换,你就可以完成物体的旋转和移动,并产生移动观察者的效果。这正是本章的主题。为了能够完成我们的示例,我们定义以下函数在原点绘制一个球体。以下函数涉及到二次曲面的内容,这是OpenGL的另一个高级主题,我们将在今后的章节中具体讲解,现在我们只用它来绘制球体:

procedure DrawSphere(R:Single); //R代表球体的半径

var SpObj:GLUQuadricObj;

begin

spObj:=gluNewQuadric;

gluQuadricNormals(SpObj,GLU_SMOOTH);

gluQuadricOrientation(SpObj,GLU_OUTSIDE);

gluSphere(SpObj,R,50,50);

gluDeleteQuadric(spObj);

end;

4.3.1.1 平移

当我们调用DrawSphere时,会在原点绘制一个球体。现在我们想在点(0,10,0)上绘制这个球体,就必须在绘制之前将坐标系沿+Y方向平移10个单位。于是我们会写出这样的代码:

//建立一个将坐标系沿+Y方向平移10个单位的矩阵:

....

//用当前模型视图矩阵乘以这个矩阵:

...

DrawSphere(5);//绘制一个半径为5的球体

但事实上,我们不需要这么麻烦。OpenGL为我们提供了这样一个函数:

glTranslatef(x,y,z:Single);

其中,x,y,z分别表示在X、Y、Z轴上平移的量。调用这个函数之后,OpenGL会自动生成一个平移矩阵,然后应用这个矩阵。因此,我们可以这样写代码:

glTranslatef(0,10,0);

DrawSphere(5);

这样就能在(0,10,0)上绘制一个球体了。

4.3.1.2 旋转

与平移类似,OpenGL也为我们提供了一个高级函数用于旋转物体:

glRotatef(Angle,x,y,z:Single);

这个函数将生成并应用一个将坐标系以向量(x,y,z)为轴,旋转angle个角度的矩阵。如果我们想将一个球体以Y轴自转50度,就可以调用:

glRotatef(50,0,1,0);

DrawSphere(5);

4.3.1.3 缩放

缩放变换其实是将坐标系的x、y、z轴按不同的缩放因子展宽,从而实现缩放效果。函数

glScalef(x,y,z:Single);

把坐标系的X、Y、Z轴分别缩放x、y、z倍。例如:

glScalef(2,2,2);

DrawSphere(5);

将绘制一个半径为10的球体。

4.3.1.4 变换的叠加性质

使用变换时,我们应该注意的是,变换是叠加在上次变换的基础上的。也就是说,变换的效果会累积。每次调用变换函数时,会生成一个新的函数来乘以当前的模型视图矩阵,随后,新的矩阵将成为当前的模型变换矩阵,在下次执行变换时,会被新的矩阵相乘,因此作用效果将不断累积。举个例子就能很明白地说明这一点。

例如,你想在(0,10,0)上绘制一个球体,完后在(10,0,0)上绘制另一个,得到如图4.3-1所示的图形:


图4.3-1
你可能会写出如下代码:

//沿Y轴向上平移10个单位

glTranslatef(0,10,0);

//画第一个球体

DrawSphere(5);

//沿X轴向左平移10个单位

glTranslatef(10,0,0);

//画第二个球体

DrawSphere(5);

然而,你不应该忘记,变换的作用效果是累积的。在绘制第二个球体时,由于此时坐标系已经向Y轴移动了10个单位,再向X方向移动10个单位之后,新的坐标系的原点应是绝对坐标系中的点(10,10)。因此,上述程序将绘制出如图4.3-2所示的图形。


图4.3-2
你可能会在绘制第二个球体之前调用glTranslatef(0,-10,0);把坐标系往回移动10个单位。但这样会降低代码的可读性,还会给CPU增加额外的运算。这个时候,我们可以使用单位矩阵。

我们可以调用glLoadIdentity();函数将当前模型视图变换矩阵重置到初始状态,再进行新的绘制:

procedure RenderScene();

begin

glMatrixMode(GL_MODELVIEW);

//沿Y轴向上平移10个单位

glTranslatef(0,10,0);

//画第一个球体

DrawSphere(5);

//加载单位矩阵

glLoadIdentity;

//沿X轴向上平移10个单位

glTranslatef(10,0,0);

//画第二个球体

DrawSphere(5);

end;

请看第一行代码。这里调用了glMatrixMode函数。这个函数的作用是通知OpenGL我们将对模型视图变换矩阵进行操作。也就是要进行模型视图变换。glMatrixMode可用参数如下:

GL_PROJECTION :用于修改投影矩阵

GL_MODELVIEW :用于修改模型视图变换矩阵

4.3.1.5 矩阵堆栈

如果每次变换前都把当前矩阵恢复到单位矩阵,也比较麻烦。更多时候,我们希望保存当前矩阵,执行一些变换之后,把当前矩阵恢复到上次保存时的状态。

OpenGL为我们提供了一个"矩阵堆栈"满足我们的这种要求。我们可以把当前矩阵压入堆栈中,然后执行一些变换,再弹出刚才压入的矩阵,从而把当前矩阵恢复到上次变换之前的状态。我们调用

glPushMatrix();

把当前矩阵压入矩阵堆栈,调用

glPopMatrix();

弹出矩阵。我们还可以分别调用

glGet(GL_MAX_MODELVIEW_STACK_DEPTH);

glGet(GL_MAX_PROJECTION_STACK_DEPTH);

来获取模型视图矩阵堆栈和投影矩阵堆栈的最大堆栈深度。一般情况下(在Windows平台上),模型视图的最大堆栈深度是32,而投影堆栈的最大深度是2。

使用矩阵堆栈,4.3.1.4节中的程序可以改写为:

procedure RenderScene();

begin

glMatrixMode(GL_MODELVIEW);

//推入矩阵堆栈

glPushMatrix;

//沿Y轴向上平移10个单位

glTranslatef(0,10,0);

//画第一个球体

DrawSphere(5);

//恢复到上次保存时的状态

glPopMatrix;

//沿X轴向左平移10个单位

glTranslatef(10,0,0);

//画第二个球体

DrawSphere(5);

end;

4.3.2 投影矩阵

设置投影矩阵往往在OpenGL绘图和模型视图变换之前。一般情况下,我们调用

glMatrixMode(GL_PROJECTION);

将当前矩阵设置为投影矩阵。再调用

glOrtho 或 gluPerspective 来创建平行或透视投影。创建完后,再调用

glMatrixMode(GL_MODELVIEW);

将当前变换矩阵设置为模型视图变换矩阵。

至此,你应该能够理解前面章节的示例程序中的 SetView 过程的意义了吧。请再看一次SetView过程:

procedure TfrmMain.SetView;

begin

glClearColor(0,0,0,0);//设置背景颜色为黑色

glViewPort(0,0,ClientWidth,ClientHeight);//指定OpenGL在此区域内绘图。

glMatrixMode(GL_PROJECTION);//设置视图投影变换矩阵

glLoadIdentity;//加载单位矩阵。

glOrtho(0,ClientWidth,ClientHeight,0,1,-1);//创建平行投影。

glMatrixMode(GL_MODELVIEW);//将矩阵变换对象切换为模型视图变换。

end;

4.4 定义和使用自己的变换

除了使用OpenGL为我们提供的几个高级变换函数之外,我们还可以自己创建一个矩阵,并使用当前矩阵乘以该矩阵来进行特殊的变换。你可以创建一个4×4的二维数组用于描述一个矩阵。如:

M:array[1..4] of array[1..4] of Single;

其中M[j,i]表示矩阵M的第j行,第i列的数据。

你也可以创建一个一维数组:

M:array[1..16]of Single;

无论是2维数组还是一维数组,都是按照列优先的顺序保存的。

矩阵定义完后,调用

glLoadMatrix(M);

可以用矩阵M替换当前矩阵,调用

glMultMatrix(M);

用当前矩阵乘以矩阵M。

要说明的是,使用glLoadMatrix或glMultMatrix的速度没有OpenGL的高级变换函数快。所以如果不是高级变换函数完成不了的变换,就不要使用glLoadMatrix或者glMultMatrix。

4.5 示例程序

这是一个经典的示例程序。它演示了太阳系中地月系与太阳之间的运动关系:月球饶地球转,整个地月系饶太阳转,所有的星球都自转。这个例子很好地展示了矩阵变换的性质和矩阵堆栈的作用。为了增加视觉效果,本程序中加入了光照渲染。同时,我们也加入了纹理贴图,这是为了能看星球自转的景象。有关光照和纹理贴图的详细内容,我们都将会在今后的章节中具体讲解。

以下是渲染过程的代码:

procedure TfrmMain.RenderScene;

begin

glEnable(GL_CULL_FACE);

glClear(GL_COLOR_BUFFER_BIT OR GL_DEPTH_BUFFER_BIT);

glLoadIdentity;

glTranslatef(0,0,-110);

glRotatef(yDeg,0,1,0);

glRotatef(xDeg,1,0,1);

RenderLights;//光照处理

//绘制太阳

glColor3ub(255,100,64);

glPushMatrix;

glRotate(SunSelfAng,0,1,0);

DrawSphere(10);

glPopMatrix;

glPushMatrix;//推入当前矩阵

//绘制地月系

glRotatef(EarthCommonAng,0,1,0);

glTranslatef(50,0,0);

glPushMatrix;//绘制地球

glRotatef(EarthSelfAng,0,1,0);

glColor3ub(20,50,255);

DrawSphere(5);

glPopMatrix;

glPushMatrix;//绘制月球

glColor3ub(200,200,200);

glRotatef(MoonAng,0,1,0);

glTranslatef(10,0,0);

glRotatef(MoonSelfAng,0,1,0);

DrawSphere(2);

glPopMatrix;

glPopMatrix;//弹出矩阵

SwapBuffers(wglGetCurrentDC);

end;

2. OpenGL中分为3种材质:环境光,漫反射光,镜面反射光

设置环境光强度:

GLfloat LightAmbient[]= { 0.5f, 0.5f, 0.5f, 1.0f };

为光源GL_LIGHT1赋予环境光属性:

glLightfv(GL_LIGHT1, GL_AMBIENT, LightAmbient);

为光源GL_LIGHT1设置位置:

glLightfv(GL_LIGHT1, GL_POSITION, LightPosition);