摘要
本文介绍了运用OpenGL,通过读取MD2文件开发复杂三维动画的方法,从而实现OpenGL与其它软件相结合创建复杂场景的能力,为快速开发复杂三维动画程序提供了捷径,并给出了一个实例。
关键词
开放图形库; 文件格式;三维建模
1
前言
OpenGL被严格定义为“一种到图形硬件的软件接口”。从本质上说,它是一个完全可移植并且速度很快的3D图形和建模库。它提高了二维和三维建模、变换处理、光照处理、动画和实时交互等能力,是绘制真实感图像、建立三维交互场景的高性能的3D图形开发API。使用OpenGL,可以创建视觉质量接近射线跟踪程序的精致漂亮的3D图形。使用OpenGL的最大好处是它比射线跟踪程序要快好几个数量级,它使用由Silicon Graphics(SGI)公司精心开发和优化的算法。
尽管OpenGL具有这么多的好处,但作为开发者来说,如何用它显示复杂的三维动画是个不可避免的问题。我们知道,对简单的三维图形,可以自己构造,然后在编程时引用构造数据画图来实现,但对复杂物体如人体,要构造数据的话,就不是件容易的事。为此要与其它软件结合起来。
MD2文件是QuakeП专用的文件格式,QuakeП中复杂的三维场景就是通过对它的读取产生的。从其它软件产生的文件中获得复杂的三维模型信息并显示出来,是三维建模常用的方法,这可以大大省却开发者的时间。在MD2文件中,还包含有多帧的三维物体信息,只要把它们都读取出来,就能显示连续的动画了。此外,文件中还有物体的纹理坐标,只要从别的图像文件中导入纹理数据,在显示时对物体贴上相应纹理,就能显示更具真实感的3D图形了。最后,由于MD2采用的压缩算法,使其文件体积并不臃肿。
2 MD2
格式说明
MD2文件建模的原理是用三角形来描述复杂的物体,即所有的物体都是三角形组成的。因此要从文件中读取各三角形的点的位置、纹理坐标来显示图形。
MD2文件组成分为五部分:文件头、纹理坐标集合、三维对象组成点集合、组成三角形的点的索引集合、三角形的纹理坐标的索引集合。
文件信息头部分包括17种数据,其具体定义如表1所示。
表
1 文件信息头的定义
数据类型
|
意义
|
备注
|
整型
|
MD2文件的标志位
|
固定为 "IDP2",用以标示为文件类型为MD2文件
|
整型
|
文件版本号
|
|
整型
|
纹理宽度
|
|
整型
|
纹理高度
|
|
整型
|
每一帧的字节数;
|
|
整型
|
纹理数
|
|
整型
|
每一帧点的个数,注意是每一帧的
|
|
整型
|
纹理坐标数
|
|
整型
|
三角形个数
|
|
整型
|
无用
|
|
整型
|
总帧数
|
|
整型
|
纹理到文件头的偏移
|
|
整型
|
纹理坐标到文件头的偏移
|
|
整型
|
三角形索引到文件头的偏移
|
|
整型
|
第一帧开始处到文件头的偏移;
|
|
整型
|
OpenGL命令类型
|
|
整型
|
到文件尾的字节数
|
|
利用以上这些信息,就可以编制相应算法,定位到需要的地方。
编程原理是:三角形的每个点在点坐标的集合中有个索引值,通过索引可以得到每个三角形的组成点的位置。同理,三角形的纹理坐标在纹理坐标集合中也有索引,通过索引可以获得纹理坐标值。绘制三角形时,要先通过索引指向点集合中和纹理坐标集合中的相应位置,再取出数据绘制。所以要准备几个缓存:存放三角形组成点在点集合中索引的缓存、存放三角形纹理在纹理集合中索引的缓存、存放点集合的缓存、存放纹理坐标集合的缓存。有了以上几个缓存,就可以显示场景并对其进行渲染。其示意图如图1所示。
图1 渲染示意图
如要显示动画的话,就要求每隔一定时间就对画面进行刷新,所以在程序中先开个定时器,每隔一定时间刷新页面,刷新时做的工作就是读入下一帧的数据,进行显示,从而实现连续的动画效果。
3
读取MD2文件方法的实现
读取MD2文件的关键在于定义合理的数据结构和使用优秀的算法。首先,要把构成场景模型的所有点的坐标读入缓存区,由于文件中有多帧信息,所以缓存大小的定义有两种选择:一、大小为一个关键帧的点的数目,每次要显示下一帧时再清空缓存,读入下一帧数据。这样可以节省内存。二、大小为所有关键帧数目乘以每帧的点数。这样每次要显示下一帧的时候不用到文件中去读,直接到缓存中去取就可以了。这样可以提升速度,但相对来说耗内存。由于OpenGL是显示动画,要不停的实时刷新画面,对速度的要求很高,所以读入的点数应该是关键帧数目乘以每帧的点数,即通过牺牲系统性能换取速度提升。另外,由于每个三角形的纹理坐标是始终不变的,所以存放纹理坐标的缓存大小不用多个关键帧的数目乘以每帧的纹理坐标数。因为纹理数据的获得还要从别的文件中获取,限于篇幅,本文就不介绍纹理坐标的读取了。
下面我们为各缓存分配空间。我们定义一个结构mesh_t:
struct
{
unsigned short meshIndex[3]; // 点的索引值。
unsigned short stIndex[3]; // 纹理坐标索引。
} mesh_t;
坐标点的获得不是直接从文件中得到的,结构为表2:
类型
|
意义
|
备注
|
Float
|
缩放比例
|
三个浮点数,分别对应点的x,y,z
|
Float
|
偏移量
|
三个浮点数,分别对应点的x,y,z
|
Char
|
帧的名字
|
16个字节大小。
|
Char
|
点数据
|
大小每一帧包含点数目乘以3
|
表2
每个点的三维坐标是由下式得到的:
x = 缩放比例 * v[0] + 偏移量;
y = 缩放比例 * v[1] + 偏移量;
z = 缩放比例 * v[2] + 偏移量;
其中v[i](i = 0,1,2)是点数据的值。
4
程序实例
下面我们来实现三维动画的编程。首先用VC建立一个可以运行的SDK程序,名字叫做MODAL,然后在Project->Setting->Link的Library module里加入OpenGL32.lib glut.lib两个库,这是OpenGL提供的win32实现的标准导入库。一般在VC中,OpenGL的头文件是存放在系统头文件目录的子目录GL中的,所以在指定包含的时候要指定一下相对路径。gl.h是基本头文件,glu.h是应用头文件,大多数应用程序都需要同时包含这两个头文件。把头文件包含进来,就可以着手编了。
首先要从MD2文件中得到文件头信息。先用二进制方式打开一个文件“tris.MD2”。然后把整个文件一股脑读到一个buffer缓存区去。文件一开始就是头信息,利用头文件信息得到点数据到文件头的偏移量,定位到点数据开始的地方。然后定义数组:
pointList = (vector_t*)numXYZ * numFrames;
其中numXYZ是每一帧的点数目,numFrames是关键帧数目,vector_t是包含点的x,y,z坐标的结构。
//以下循环读取各个帧的点信息。
for(j = 0; j < numFrames; j++)
{
frame=(frame_t*)&buffer[offsetFrames+ framesize*j];// framesize 是每一帧大小,offsetFrames 是点数据到文件头的偏移。
pointListPtr = (vector_t*)&model->pointList[modelHeader->numXYZ * j];}
// pointListPtr根据头信息内容定位到pointList[]开始写每帧数据的地方。
//下面代码开始写入点数据到pointList[]数组中。
for(i = 0; i < modelHeader->numXYZ; i++)
{
pointListPtr[i].point[0] = frame->scale[0] * frame->fp[i].v[0] + frame->translate[0];
pointListPtr[i].point[1] = frame->scale[1] * frame->fp[i].v[1] + frame->translate[1];
pointListPtr[i].point[2] = frame->scale[2] * frame->fp[i].v[2] + frame->translate[2];
}
}
接着再根据各三角形的点的索引值把在pointList[]查到三角形组成点的坐标,就可以画出复杂三维场景了。
由于文件中是关键帧的数据,如果每隔一定时间显示一个关键帧的图像,动画会极不连续,为此我们引入一个比例因子,目的在于在两个相邻关键帧之间线性插值插入若干帧,使画面流畅,代码如下,插值因子大小为0.1:
glBegin(GL_TRIANGLES);
for (i = 0; i < numTriangles; i++)
{
x1 = pointList[Tri->pt1].point[0];
y1 = pointList[Tri->pt1].point[1]; // 得到当前帧某个三角形的第一个点。
z1 = pointList[Tri->pt1].point[2];
x2 = nextPointList[Tri->pt1].point[0];
y2 = nextPointList[Tri->pt1].point[1]; // 得到下一帧对应三角形的第一个点。
z2 = nextPointList[Tri->pt1].point[2];
// 存储插值后的三维坐标
vertex[0].point[0] = x1 + interpol * (x2 - x1);
vertex[0].point[1] = y1 + interpol * (y2 - y1);
vertex[0].point[2] = z1 + interpol * (z2 - z1);
// 得到当前帧某个三角形的第二个点。
x1 = pointList[Tri->pt2].point[0];
y1 = pointList[Tri->pt2].point[1];
z1 = pointList[Tri->pt2].point[2];
// 得到下一帧对应三角形的第二个点。
x2 = nextPointList[Tri->pt2].point[0];
y2 = nextPointList[Tri->pt2].point[1];
z2 = nextPointList[Tri->pt2].point[2];
// 存储插值后的三维坐标
vertex[2].point[0] = x1 + model->interpol * (x2 - x1);
vertex[2].point[1] = y1 + model->interpol * (y2 - y1);
vertex[2].point[2] = z1 + model->interpol * (z2 - z1);
// 得到当前帧某个三角形的第三个点。
x1 = pointList[Tri->pt3].point[0];
y1 = pointList[Tri->pt3].point[1];
z1 = pointList[Tri->pt3].point[2];
// 得到下一帧对应三角形的第三个点。
x2 = nextPointList[Tri->pt3].point[0];
y2 = nextPointList[Tri->pt3].point[1];
z2 = nextPointList[Tri->pt3].point[2];
// 存储插值后的三维坐标
vertex[1].point[0] = x1 + interpol * (x2 - x1);
vertex[1].point[1] = y1 + interpol * (y2 - y1);
vertex[1].point[2] = z1 + interpol * (z2 - z1);
//绘制三角形
glVertex3fv(vertex[0].point);
glVertex3fv(vertex[1].point);
glVertex3fv(vertex[2].point);
}
glEnd();
interpol += 0.1; 增加插值因子。
程序运行效果如图2所示。
如果读取了纹理坐标信息,再对其贴上纹理,那么就真实的显示三维图形了。这里限于篇幅不再给出代码,只给出效果图。
图2 程序效果图
5
结束语
本文讨论了将OpengGL和MD2文件结合起来创建复杂场景的方法,并给出了一个实际开发的例子,应用程序代码在VC6.0下调试通过。程序实时性好,逼真程度高,具有实用意义。
参考文献
[1] 和平鸽工作室 OpenGL高级编程与可视化系统开发[M]
中国水利水电出版社 2003
[2] 郭启全 计算机图形学教程[M] 机械工业出版社 2001
[3] 孙家广等 计算机图形学[M] 清华大学出版社 2003
[4] 杨武功等 OpenGL三维动画程序设计[M]
清华大学出版社 2000
[5] Richard S.Wright,Jr.Michael Sweet OpenGL 超级宝典[M]
人民邮电出版社 2001
[6] Mason Woo,Jackie Neider,Tom Davis OpenGL编程权威指南(第二版)[M]
中国电力出版社2000