/LGC图形渲染/基于 OpenGL 进行 3D 图形开发

基于 OpenGL 进行 3D 图形开发
作者: 刘鹏
日期: 2010-05-10
本文介绍了基于 OpenGL 进行 3D 开发的基本思路,帮助初学者快速理清思路。

3D 开发的一般思路

3D 开发主要分为三个步骤:建模、渲染、逻辑控制。

建模,指通过一些基本图元如点、线、三角形、多边形将物体画出来,一般使用 3DMax、MilkShape 等建模工具来完成。具体来说,建模涉及模型的构建、贴纹 理、制作动画等。

渲染,即使用 OpenGL 图形接口将模型在计算机上画出来。

逻辑控制,若要模型动起来,需要根据时间计算模型各个顶点的坐标,这个通过 程序逻辑来控制。

在 3D 开发中有一个常用的速度指标,即帧每秒(f/s),其计算方法如下:

fps = numFrame / Interval
  • numFrame 是当前的 frame 总和,从程序运行起,每运行一个 frame 这个就加1;
  • Interval 指从程序运行起的时间总和。

建模

用 3dMax 等工具建模的一般思路是:

  1. 用基本形状,如三角形、多边形、曲面、球体、立方体等,把物体轮廓构件出来
  2. 通过拉伸、调整顶点等方法对细节进行处理

建模工具有很多,如 3DMax、Maya、Blender、MilkShape、AC3D 等,其中 MilkShape 短小精悍,容易上手比较推荐新手使用。

Milkshape :建模、贴纹理、加骨骼、做动画

  1. mode: 做模型
  2. group: 构成模型的各个子部分
  3. material:往各个子部分上贴材质
  4. joint:骨骼结点,现在还没想清楚这些 joint 的作用是什么
  5. animation:设置关键祯,生成动画
  6. 生成的模型格式是 .ms3d

Milkshape 的教程可在这里 下载。

保存模型有很多种格式,如 md2、ms3d、3ds、obj等。

  1. md2 速度快,但数据量大,典型的以空间换时间; ms3d 保存的数据量小,但需要的计算比较多
  2. md2 的动画可能不如 ms3d 细腻

OpenGL based Graphics

It's a state machine - Setup the stage, lighting, actors... Then draw it!

使用 OpenGL 进行渲染的总体思路

  • clear the screen:glClear (XXX | XXX)
  • Reset the view:glLoadIdentity
  • move / rotate axis:glTranslatef (x, y, z), glRotatef ()
  • draw scene: glBegin, ..., glEnd
  • glDrawArray/glDrawElements 完成点、线、多边形等基本图元的绘制

贴纹理

生成纹理

OpenGL 中的纹理通过一个唯一号引用,通过函数 glBindTexture() 实现。你 可以自己指定这个唯一号,或者通过调用 glGenTextures () 函数生成一个唯一 号。

GLuint        texture[1];
glGenTextures(1, &texture[0]);
glBindTexture(GL_TEXTURE_2D, texture[0]);
glTexImage2D(GL_TEXTURE_2D, 0, 3, sizeX, sizeY, 0, GL_RGB, GL_UNSIGNED_BYTE, pData);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
此时 pdata 里的数据就可以释放了

纹理管理器

在实际开发时,往往需要一个纹理管理器提供纹理的装载、释放、获取等功能, 下面的文章介绍了一个纹理管理器的设计和实现。

A Singleton Texture Manager for OpenGL

如何贴纹理

以上图片来自:http://blogs.msdn.com/danlehen/

核心绘制代码如下所示:

glVertexPointer(3, GL_FLOAT, 0, m_vertices);
glNormalPointer(GL_FLOAT, 0, m_normals);

glBindTexture(GL_TEXTURE_2D, m_textureId);
glTexCoordPointer(2, GL_FLOAT, 0, m_uvs);

glDrawElements(GL_TRIANGLES, m_triangleNums * 3, GL_UNSIGNED_SHORT,m_indices);

或者

glDrawArrays(GL_TRIANGLES, 0, m_triangleNums * 3);

在嵌入式设备上的用 OpenGL ES 贴纹理,以 Androiod 为例

在 android 平台上,纹理图的长宽必须是 2 的幂,纹理图尺寸超过 512x512 时 fps 会显著降低。一般的做法是将所有纹理组合到成一张 256x256 或者 512x512 的大图。

使用纹理有两种方法,一是直接用建模工具映射好,二是通过动态调整纹理坐标 来获取需要的纹理。前者不做介绍,重点介绍下后者。

调整纹理坐标的方法有两种:一是手工修改现有纹理代码,二是通过 glMatrixMode (glTexture) 对纹理坐标进行操作。

glMatrixMode (glTexture) 调整纹理坐标的代码如下:

    // move texture
glMatrixMode(GL_TEXTURE);
glPushMatrix();
glLoadIdentity();

// move the texture - this does not work
glTranslatef(offset, 0.0f, 0.0f);
glTranslatef(0.0f, offset, 0.0f);
glTranslatef(0.0f, 0.0f, offset);

glPopMatrix();


注意:

  • 纹理坐标的偏移值范围为:-1~1
  • 纹理坐标的原点在纹理图的左下角,若沿 Y 轴向下偏移,偏移量为负
  • glMatrixMode (GL_TEXTURE)往往与 glMatrixMode (GL_MODELVIEW)混用,注意保存好现场。

gluPerspective 和 gluLookAt 1

函数原型如下所示:

gluLookAt(GLdoble eyex,GLdouble eyey,GLdouble eyez,
GLdouble centerx,GLdouble centery,GLdouble centerz,
GLdouble upx,GLdouble upy,GLdouble upz);

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

gluPerspective

首先得设置 gluPerspective,来看看它的参数都表示什么意思:

  • fovy,可以理解成眼睛睁开的角度,即,视角的大小,如果设置为0,相当你闭上眼睛了,所以什么也看不到,如果为180,那么可以认为你的视界很广阔;
  • aspect,实际窗口的纵横比,即x/y;
  • zNear,表示你近处的裁面;
  • zFar,表示远处的裁面。

我们知道,远处的东西看起来要小一些,近处的东西看起来会大一些,这就是透视 (perspective)原理,如下图所示:

假设那两条线表示公路,理论上讲,它们的两条边是平行的,但现实情况中,它 们在远方(可以无限远)总要相交于一点,实际线段 AB 的长度等于 CD 的长度, 只是在此例中使用了透视角,故会有如上的效果,是不是很接近现实的情况?

结合我们刚才这两个函数:

  • zNear,眼睛距离近处的距离,假设为10米远,请不要设置为负值,OpenGl 就傻了,不知道怎么算了;
  • zFar,表示远处的裁面,假设为1000米远;

就是这两个参数的意义了。

再解释下那个"眼睛睁开的角度"是什么意思,首先假设我们现在距离物体有50个 单位距离远的位置,在眼睛睁开角度设置为 45 时,请看大屏幕:

我们可以看到,在远处一个球;现在我们将眼睛再张开点看,将“眼睛睁开的角 度”设置为 178 (180度表示平角,那时候我们将什么也看不到,眼睛睁太大了,眼 大无神)。

我们只看到一个点,因为我们看的范围太大了,这个球本身大小没有改变,但是 它在我们的“视界”内太小了。

反之,我们将眼睛闭小些,改为 1 度看看会出现什么情况呢?

在我们距离该物体 3000 距离远,“眼睛睁开的角度”为 1 时,我们似乎走进了 这个球内,这个是不是类似于相机的焦距?

当我们将“透视角”设置为 0 时,我们相当于闭上双眼,这个世界清静了,我们 什么也看不到了。

gluLookAt

现在来看 gluLookAt 函数。

它共接受三对坐标,分别为 eye,center,up。

  • eye, 表示我们眼睛在“世界坐标系”中的位置;
  • center, 表示眼睛“看”的那个点的坐标;
  • up,表示观察者本身的方向。

如果将观察点比喻成我们的眼睛,那么这个 up 则表示我们是正立还是倒立。从 不同角度看,所看的影像大不相同。若需要指明我们现在正立,那么 X,Z 轴为 0,Y 轴为正即可,通常将其设置为 1,只要表示一个向上的向量(方向)即可。 我们指定 0.1f 或 0.00001f 或 1000.0f,效果是一样的,只要能表示方向即可。

球是画在世界坐标系的原点上的,即 O(0,0,0) 坐标上,我们的眼睛位于观察点 A(0,0,100),Z 轴向屏幕里看去的方向为负,屏幕外我们的位置,Z 轴为正值, 其实很好理解,即我们距离原点的距离,设置 100,将观察到如下图所示的影像。

如果我们向前或向后移动,则相应的图像会变大或变小,这里其实就是运用了透 视原理,近处的物体大,远处的物体小,实际物体的大小是不变的。

同理改变 center 坐标(眼睛看去的那个点,可简单理解为视线的终点)也会影响 球的大小,同样可以认为是改变了物体与观察点的距离所致。

测试

以上理解了之后,来做一个测试。

透视图不变,最远处仍为 3000,近处为 0.1。

gluPerspective                    // 设置透视图
(45, // 透视角设置为 45 度,在Y方向上以角度为单位的视野
(GLfloat)x/(GLfloat)y, // 窗口的宽与高比
0.1f, // 视野透视深度:近点1.0f
3000.0f // 视野透视深度:始点0.1f远点1000.0f
);

将我们的观察点置于 A(0,10,0),将观察位置(视线终点)坐标置于(0,0,0),然 后在原点开始绘图,画一个 V 字形,并将 Z 轴的值从 -1000 递增加到 +1000, 增量为10,代码如下:

    glColor3f(0.5f, 0.7f, 1.0f);

glBegin(GL_LINES);
for(int i=-1000;i<=1000;i+=10)
{
glVertex3f(0,0,i);
glVertex3f(10,10,i);

glVertex3f(0,0,i);
glVertex3f(-10,10,i);
}
glEnd();

效果图如下所示:

OpenGL 的坐标系统变换

OpenGL 的重要功能之一是将三维的世界坐标经过变换、投影等计算,最终算出它在显示设备上对应的位置。

每次计算屏幕输出时,OpenGL 采取这样的方式进行:

    屏幕坐标点 = 3D 模型点 * 几何变换栈矩阵(n...1) * 投影变换栈矩阵    (n...1)。其中 n...1 表示从栈顶到栈底。

更复杂的流程是:

    屏幕坐标点 = 3D 模型坐标 -〉 (几何变换矩阵) -〉 人眼坐标 -〉(投影变换矩阵) -〉 正则设备坐标(相对于 OpenGL 屏幕坐标原点)
-〉 校正成窗口坐标(相对于窗口坐标)

简单的讲,OpenGL 中从三维场景到屏幕图形要经历如下所示的变换过程:

模型坐标-〉世界坐标-〉观察坐标-〉投影坐标-〉设备坐标

其中四种坐标经常要在程序中用到:物体坐标(也叫模型坐标、局部坐标),世界坐标,眼坐标(也叫观察坐标)和设备坐标.

物体坐标:

以物体某一点为原点而建立的“世界坐标”,该坐标系仅对该物体适用,用来简化对物体各部分坐标的描述。物体放到场景中时,各部分经历的坐标变换相同,相对位置不变,所以可视为一个整体,与人类的思维习惯一致;

世界坐标:

是OpenGL中用来描述场景的坐标,Z+轴垂直屏幕向外,X+从左到右,Y+轴从下到上,是右手笛卡尔坐标系统。我们用这个坐标系来描述物体及光 源的位置。 OpenGL 中有一个坐标转换矩阵栈 (Modeview),栈顶就是当前坐标变换矩阵,进入 OpenGL 管道的每个坐标 (齐次坐标)都会乘上这个矩阵,结果才是对应点在场景中的世界坐标。将物体放到场景中也就是将物体平移到特定位置、旋转一定角度,这些操作都是坐标变换。

眼坐标:

是以视点为原点,以视线的方向为Z+轴正方向的坐标系中的方向。OpenGL管道会将世界坐标先变换到眼坐标,然后进行裁剪,只有在视线范围(视见 体)之内的场景才会进入下一阶段的计算。 OpenGL 有个投影变换矩阵栈 (Projection),栈顶矩阵就是当前投影变换矩阵,负责将场景各坐标变换到眼坐标。由于所得到的结果是裁剪后的场景部分,称为裁剪坐标。

设备坐标:

OpenGL的重要功能之一就是将三维的世界坐标经过变换、投影等计算,最终算出它在显示设备上对应的位置,这个位置就称为设备坐标。在屏幕、打印机等设备上的坐标是二维坐标。

几何变换:平移、旋转、缩放

  • glTranslatef (x, y, z)
  • glRotatef (alpha, x, y, z);
  • glScalef (x, y, z);

几何变换栈:为了维护几何变换, OpenGL 维护一个几何变换栈,每次几何变换, 都用相应的矩阵乘对应的栈顶元素,并替换掉上次的栈顶。

对于几何变换栈,有两个操作可以使用:

  • glPushMatrix(): 保存当前坐标系 ,复制当前栈顶,并把复制的内容再次放到栈顶,能够保护栈上以前的内容;
  • glPopMatrix(): 恢复当前坐标系。

投影变换 : 正交投影 (正射投影、平行投影),透视投影

  • 正交投影:将 3D 模型平行的映射到平面上,glOrtho(xleft, xright, ybottom, ytop, znear, zfar);
  • 透视投影:将 3D 模型映射到相对某个观察点的平面上, gluPerspective(fovy, aspect, znear, zfar);

深入探讨透视投影变换:http://www.cppblog.com/zmj/archive/2008/08/28/58936.html

glOrtho 函数的意义: http://flyingliang.spaces.live.com/blog/cns!9112491EC93C817B!1228.entry

正交投影的特点:

无论物体距离相机多远,投影后的物体大小尺寸不变。这种投影通常用在建筑蓝图绘制和计算机辅助设计等方面,这些行业要求投影后的物体尺寸及相互间的角度不变,以便施工或制造时物体比例大小正确。

透视投影的特点:离观察者越远的对象越小。

切换当前栈

  • 切换当前操作的栈为投影变换栈:glMatrixMode(GL_PROJECTION);
  • 切换当前操作的栈为几何变换栈:glMatrixMode(GL_MODELVIEW);
  • 清除当前操作栈的内容:glLoadIdentity();

变化观察模型的角度,可以在切换到几何变换栈的时候进行:

  • glMatrixMode(GL_MODELVIEW); // 模型视图矩阵
  • glLoadIdentity();
  • gluLookAt(6.0, 8.0, 10.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0); // 旋转视点

一段典型的 OpenGL 代码

    // Init *
glClearColor (0.0f, 0.0f, 0.0f, 0.0f); /* Set background color*/
glClearDepth (1.0);

glShadeModel (GL_SMOOTH);

glMatrixMode (GL_PROJECTION); /* 设置投影变换 */
glLoadIdentity ();
gluPerspective (......);
glMatrixMode (GL_MODEVIEW);

//* 清屏 *
glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glLoadIdentity ();

glTranslatef (x, y, z); /* 平移 */
glRotatef (angle, x, y, z); /* 旋转 */

glBegin (GL_POLYGON);
glColor3f (...);
glVertex3f (...);
... ...
glEnd ();

/* ReSizeScene */
glViewport (0, 0, width, height);
glMatrixMode (GL_PROJECTION);
glLoadIdentity();
gluPerspective(... ...);
glMatrixMode(GL_MODELVIEW);

How to walk around and explore the world

  • move camera around and draw the 3D environment relative to the camera positon. But it is slow and hard to code.
  • Rotate / translate the world in the opposite manner.
    1. Rotate and translate the camera position according to user commands
    2. Rotate the world around the origin in the opposite direction of the camera rotation (giving the illusion that the camera has been rotated)
    3. Translate the world in the opposite manner that the camera has been translated (again, giving the illusion that the camera has moved)

Reference

  1. 终于搞明白 gluPerspective 和 gluLookAt 的关系了

你可能感兴趣的:(GT开发,friendly,software)