第三章 使用OpenGL绘图

来源:互联网 发布:安东尼 知乎 编辑:程序博客网 时间:2024/06/11 18:44

第三章  使用OpenGL绘图

    从本章开始,我们将正式开始使用OpenGL来绘制图形。学习本章内容,你将发现使用计算机绘制3D图形原来如此容易。你将了解:

  • 设置可视区域创建投影

  • 在3D空间中绘制基本图元

  • 使用深度测试

  • 使用背面剔除提高渲染速度

  • 将绘制的图形输出到屏幕上

3.1 绘制之前的必要工作

    从章节2.1中,你应该了解到,在使用OpenGL绘图之前,我们应该决定使用何种投影方式,设置渲染后的图形应出现在窗口的位置等等。本节中,我们将了解这些步骤的具体实现方法。

3.1.1 设置窗体的视见区域 (View Port)

    在OpenGL初始化完成之后,我们应该进行一些视图设置。首先是设定视见区域,即告诉OpenGL应把渲染之后的图形绘制在窗体的哪个部位。当视见区域是整个窗体时,OpenGL将把渲染结果绘制到整个窗口。我们调用glViewPort函数来决定视见区域:

    procedure glViewPort(x:GLInt;y:GLInt;Width:GLSizei;Height:GLSizei);

    其中,参数X,Y指定了视见区域的左下角在窗口中的位置,一般情况下为(0,0),Width和Height指定了视见区域的宽度和高度。注意OpenGL使用的窗口坐标和WindowsGDI使用的窗口坐标是不一样的。图3.1-1表示了在WindowsGDI中的窗口坐标,而图3.1-2则是OpenGL所定义的窗口坐标。

图3.1-1 WindowsGDI下的窗体坐标

图3.1-2 OpenGL所定义的窗体坐标

    

    例如,要设置如图3.1-3中的视见区域,我们应该调用函数:

      glViewPort(100,100,Width,Height);

图3.1-3

3.1.2 创建投影变换

    接下来,我们要设置一种投影变换。投影变换分为平行投影和透视投影。平行投影中,物体无论远近,大小都是一样的,而透视投影则相反。因此,透视投影更像是我们眼睛所看到的景物。但在某些特殊的时候,平行投影还是有它的作用的,比如3D建摸程序。图3.1-4是甲烷分子模型在平行投影下的渲染结果,而图3.1-5是在透视投影下的渲染结果。可以看到,平行投影下,四个氢原子(绿色的球体)大小是一样的,而在透视投影下,远处的氢原子要小一些。

图3.1-4 平行投影下的甲烷分子模型

图3.1-5 透视投影下的甲烷分子模型

    值得注意的是,本节所讲的内容涉及矩阵变换这一主题。关于OpenGL中的矩阵,我们将在下一章中作具体说明。因此本章不讨论矩阵变换的原理。

3.1.2.1 创建平行投影

    调用glOrtho函数,可以创建一个平行投影:

       procedure glOrtho(left, right, bottom, top, zNear, zFar: GLdouble);

    其中,left指定了该平行投影最左边的平面;
          right指定了该平行投影最右边的平面;
          bottom指定了该平行投影最下边的平面;
          top指定了该平行投影最上边的平面;
          zNear,zFar指定了近修剪平面和远修建平面。

    也就是说,仅当顶点v(x,y,z)满足条件 x>left and xbottom and yzNear and z

    我们使用下面的代码创建一个平行投影:

      glMatrixMode(GL_PROJECTION);

      glOrtho(-ClientWidth div 2,ClientWidth div 2,-ClientHeight div 2,ClientHeight div 2,1,100);

      glMatrixMode(GL_MODELVIEW);

    在上面的代码中,你看到了一个陌生的函数:glMatrixMode。它的作用是告诉OpenGL接下来我们将要设置投影变换矩阵。这涉及到下一章的主题,这里就不多讨论了。现在你可以暂时把上面的代码当作固定代码使用。

3.1.2.2 创建透视投影

    透视投影对远处的物体根据距离进行缩短或压缩变换。这使得远处的物体看起来小些,从而更加真实。因为远处的景物更小,所以随着距离的增加,观察者应该能看到更多的景物。因此,透视投影的可视区域应是一个被称为平截头体的几何形状。如图3.1-6所示。

图3.1-6 透视投影

    和平行投影相似,只要把函数glOrtho的调用该为glFustum或者gluPerspective。

    调用glFrustum函数,可以指定一个平截头体。 

        procedure glFrustum (left, right, bottom, top, zNear, zFar: GLdouble);

     可以看到,glFrustum的参数和glOrtho完全一样。

     但对于平截头体的性质来说,使用glFrustum总是不太直观。因此gluPerspective反而更常用:

       procedure gluPerspective(fovy, aspect, zNear, zFar: GLdouble);

    其中,fovy为垂直方向上可见区域的角度(即上修剪平面和下修剪平面的二面角);
          aspect为高度与宽度的纵横比(即 Width/Height 的比值);
          zNear和zFar为近、远修剪平面。

    如图3.1-7所示。

图3.1-7 由gluPerspective定义的平截头体

     我们用下面的代码定义透视投影:

      glMatrixMode(GL_PROJECTION);

      gluPerspective(60,ClientWidth/ClientHeight,1,zFar);

      glMatrixMode(GL_MODELVIEW);

     其中,zFar根据要绘制场景的大小设置不同的值。

3.1.3  设置背景颜色

    这一步是可选的。我们可以调用glClearColor函数来设置用于清空屏幕和缓冲区的颜色。

    glClearColor(R,G,B,A:GLFloat);

    其中,R,G,B,A分别表示颜色的R、G、B分值和透明度值。取值范围均为0-1之间。例如,下面的代码把背景色设置为黑色:

    glClearColor(0,0,0,1);

3.1.4 绘制之前,清空屏幕和缓冲区

   一般地,我们把所有绘制函数的调用写在RenderScene过程中。在每次绘制之前,我们都应该清空屏幕和缓冲区。下面的代码用指定的清空颜色清空它们:

   procedure RenderScene;

   begin

     glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT);

     ...图形绘制...

   end;

3.1.5 整理代码

   章节3.1.1-3.1.3中,所提到的所有代码在整个渲染流程中只需被执行一次。因此,我们可以向窗体添加一个过程,将上述代码放到该过程中。并且在OpenGL初始化工作完毕之后执行它。

TfrmMain = class(TForm)
   ...
   procedure FormCreate(Sender: TObject);
private
  procedure SetView;
  procedure RenderScene;  //渲染函数
  procedure InitializeOpenGL; //用于初始化OpenGL渲染环境
public
  { public declarations }
end;

   然后,在FormCreate过程中添加对过程SetView的调用:

procedure FormCreate(Sender: TObject);
begin
  InitializeOpenGL;
  SetView;
end;

   我们的SetView过程看起来应该是这样的:

procedure 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;

3.2 使用OpenGL绘制基本图元

    一切复杂的东西都是由简单而基本的元素构成的。在OpenGL中,组成复杂图形的基本元素被成为图元。掌握了基本图元的绘制方法,就能绘制出任何复杂的物体。

    OpenGL为我们提供了以下几种图元:

  • 线

  • 连续线

  • 封闭线

  • 三角形

  • 三角条形

  • 三角扇形

  • 四边形

  • 多边形

3.2.1 绘制三角形

    绘制三角形是非常简单的。我们只需通过glVertex的调用传给OpenGL必要的顶点值即可。

    在调用glVertex之前和之后,我们需要调用glBegin和glEnd这两个函数来标识图元的开始和结束。

    在调用glBegin函数时,我们需要传入一个参数,以告诉OpenGL我们将绘制什么类型的图元。传入GL_TRIANGLES表明我们将要绘制三角形。例如:

    glBegin(GL_TRIANGLES);

      glVertex(1,0,1);

      glVertex(0,1,0);

      glVertex(1,1,0);

    glEnd;

    将绘制一个以点(1,0,1)、(0,1,0)、(1,1,0)为顶点的三角形。

3.2.2 绘制三角条形

    和绘制三角形相同,只要把glBegin的参数该为GL_TRIANGLE_STRIP即可。例如:

    glBegin(GL_TRIANGLE_STRIP);

      glVertex(1,0,1);

      glVertex(0,1,0);

      glVertex(1,1,0);

      glVertex(1,3,0);

      glVertex(4,8,2);

    glEnd;

3.2.3 绘制三角扇形

    和绘制三角形相同,只要把glBegin的参数该为GL_TRIANGLE_FAN即可。由于绘制方法大致相同,这里不再举例了。

3.2.4 绘制点、线、连续线、封闭线

    下面列举了绘制这些图元应传给glBegin的值。

    点:GL_POINTS

    线:GL_LINES

    连续线:GL_LINE_STRIP

    封闭线:GL_LINE_LOOP

    值得说明的是图元GL_LINE_STRIP和GL_LINE_LOOP。

    GL_LINE_STRIP和GL_TRIANGLE_STRIP原理是一样的。也就是从第2个顶点开始,第n个顶点与第n-1个顶点构成一条直线。例如:

    glBegin(GL_LINE_STRIP);

      glVertex(A.x,A.y,A.z);

      glVertex(B.x,B.y,B.z);

      glVertex(C.x,C.y,C.z);

    glEnd;

    将绘制出两条线段:线段AB和线段BC。

    图元GL_LINE_LOOP建立在GL_LINE_STRIP的基础之上。与GL_LINE_STRIP不同的是,GL_LINE_LOOP会在最后一个顶点和第一个顶点之间再连一根直线,构成一个封闭图形。如果把上述代码的GL_LINE_STRIP参数该为GL_LINE_LOOP,那么将绘制出三条线段:线段AB、BC和CA。

3.2.5 绘制四边形和多边形

    四边形(QUAD)也属于使用几率较高的图元。只要把glBegin的参数改为GL_QUADS,就可以绘制四边形。把参数改为GL_POLYGON,则可以绘制一个多边形。

    然而,仔细观察你将发现,无论是四边形还是多边形,只要花一点工夫,他们都可以使用三角形来表示。而且,由于现在的显卡都对三角形的绘制做了大量的优化,使得绘制三角形的速度比绘制多边形的速度快得多。因此请尽量不要使用多边形这种图元以提高渲染速度。

    如果你要使用四边形或多边形,请注意以下几点:

    1.使用OpenGL绘制的多边形,必须是凸多边形;

    2.绘制的多边形的所有顶点都必须处在同一个平面上。

    由于这些限制,使得绘制多边形这种图元显得不怎么方便。这也突出了使用三角形的优点——你永远也不用担心绘制出来的三角形是无效的。你可以尽情地使用三角形绘制各种复杂多边形。

3.3 使用深度测试

    我们知道,当一个平面或物体挡住了另一个物体时,后面的物体是不可见的。此时,我们应该避免绘制后面的物体。这个时候,我们可以使用OpenGL的一个功能:深度测试(Depth Test,也称深度缓冲(z-Buffer))来剔除这些被挡住的表面。深度测试就是在绘制像素时,计算该像素所代表的物体离观察者的距离,称为z值。如果该值在同一个像素上所有的z值中是最小的,就绘制该像素,否则就跳过。这是一个解决深度问题的有效方法。我们只需要调用函数

   glEnable(GL_DEPTH_TEST);

   就可以开启深度测试。调用

   glDisable(GL_DEPTH_TEST);

   关闭深度测试。

3.4 背面剔除

    如果你将要绘制一些实心的物体,那么这个实心物体内部的表面将永远是不可见的。而OpenGL并不知道这些面不可见,它会照样对他们进行计算和绘制。虽然最后还是没有将这些背面绘制在屏幕上,但是浪费了许多不必要的时间。因此,我们应该开启背面隐藏功能剔除这些不可见的表面。

绕法

    如果我们把一个物体朝着外面的表面都按照逆时针的顺序传给OpenGL,那么OpenGL就会认为这个面是朝外面的。这个时候,我们开启背面剔除就不会有什么影响。但如果你没有遵守这个规定就开启了背面剔除,将得不到正确的渲染结果。我们可以通过函数glFrontFace的调用来改变这一规则。例如:

   glFrontFace(GL_CCW);

   将让OpenGL认为所有逆时针缠绕的表面是正面,如果把 GL_CCW 改为 GL_CW ,那么OpenGL将认为所有逆时针缠绕的面是正面。

 开启表面剔除

   在渲染之前,添加下面的代码来打开表面剔除:

   glCullFace(GL_BACK);//隐藏背面,如果把参数改为GL_FRONT则隐藏正面。

   glEnable(GL_CULL_FACE);

 关闭表面剔除

   有些物体,无论是正面还是背面都有可能是可见的(比如一张纸,既有正面又有背面),在渲染这些物体的时候,我们应该关闭表面剔除。只需添加以下代码:

   glDisable(GL_CULL_FACE);

3.5 将渲染结果显示到屏幕上

   在所有物体绘制完成之后,我们要调用函数SwapBuffers来显示渲染结果:

   procedure RenderScene;

   begin

     glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT);

     glBegin(GL_TRIANGLE_STRIP);

       ...

     glEnd;

     ...

     SwapBuffers(wglGetCurrentDC);

   end;

   请注意SwapBuffers的参数总是wglGetCurrentDC。wglGetCurrentDC也是一个函数,它返回当前OpenGL的渲染环境句柄。

 
原创粉丝点击