摘 要:本文通过一个完整的OpenGL动画程序,介绍并演示了在C++Builder环境下开发OpenGL动画的方法。
关键词:OpenGL,全景图像,三维模型,数据格式转换
一、前言
科学可视化、计算机动画和虚拟现实已经成为近年来计算机图形学的三大热门话题,而这三大热门话题的技术核心均为三维图形.当前,三维图形在军事、航空、航天、医学、地质勘探、文化娱乐和艺术造型等方面有着十分广泛的应用。
1992年,OpenGL正式成为各种计算机环境下的三维应用程序接口(3D API)。目前,它已成为国际上通用的开放式三维图形标准。Compaq,IBM,Intel,Microsoft,DEC,HP和SUN等在计算机界具有主导地位的公司纷纷采用OpenGL图形标准。逼真的3D游戏让人目不暇接,令人身临其境的虚拟现实也正向人们走来。这一切都必然吸引着更多的人加入到图形这一行列中。然而,国内介绍OpenGL的书籍和文章并不多见,而介绍在C++Builder环境下开发基于OpenGL的书籍和文章就是在国际上也可谓少而又少。
事实上,C++Builder是一个性能优良的真正面向对象的C++语言开发环境。它的可视化编程组件具有非常优秀的可重用性和可扩展性。与其它C++语言开发环境相比,使用C++Builder可显著地简化软件开发工作,提高开发效率。
本文通过一个描述飞行器飞行画面的实例,简要介绍了C++Builder环境下开发基于OpenGL的动画程序的方法。
二、编程方法
2.1 安装OpenGL组件
2.1.1 进入C++Builder,关闭所有已经打开的文件和工程(File | Close All)。
2.1.2 选择菜单Component | Install Package。
2.1.3 单击“Add”,选择TopenGL目录中的TopenGL.bpl文件,并单击“打开”按钮。
2.1.4 选中“OpenGL Package”前面的单选按钮,禁选“Build With runtime packages”前的单选按钮。
2.1.5 单击“OK”完成安装。
接下来进一步设置C++Builder的编译搜索路径。方法是:
(1) 关闭所有已经打开的文件和工程(File | Close All)。
(2) 选择Project下的Options子菜单。
(3) 选择Directories/Conditionals页,在Include path、Library path和Debug source path中增加路径C:\TopenGL。
(4) 单击“OK”完成设置。
安装和设置完成后,C++Builder的组件工具栏终究会增加新的一页“OpenGL”,这一页中含有OpenGL组件库中的所有组件。如图1所示。
图1 OpenGL组件页
2.2 全景图像的制作
全景图像是利用一系列局部图像拼接起来的、能够进行360O环视漫游的图像环境,它能够比较完整地表达周围环境的信息,相当于观察者站在一个固定的观察点向四周转一圈看到的景象。全景图像是视点空间的关键性基础数据。
全景图像是由一系列局部图像经过无缝拼接而成的。要得到一幅全景图像,必须经过下面的三个步骤。
2.2.1 获取局部图像
在现实世界中选择好视点后,将一个可水平旋转的支架放在视点处,再将照相机固定在支架上,然后水平转动支架,每个一定的角度拍摄一张照片,直到支架转过360O为止。图2是在山区拍摄的一组照片。
(a)第一幅照片 (b)第二幅照片 (c)第三幅照片(d)第四幅照片 (e)第十八幅照片
图2 局部图像
2.2.2 预处理局部图像
2.2.3 拼接全景图像
全景图像可用手工方式拼接,也可以利用计算机实现自动拼接。自动拼接是利用两幅图像重叠部分的像素相关性实现的,一般有面积和特征等方法。
图3是作者利用全景图像制作软件Ulead COOL 360制作的全景图像。
图3 全景图像
2.3 3D数据模型转换
在三维图形程序中,经常要绘制许多复杂的图形,如一个非常精细的武器模型或一个逼真的地形场景等,单纯利用OpenGL的实例库提供的基本集合体构造着实困难。而且也不可能一次性地在内存中编写绘图语句。必须考虑一些合适的三维数据存储结构,使之可以将复杂的三维模型保存下来。这里采用的保存三维模型的方法是用多边形逼近的方法,即用许多小多边形拼出模型的外观,文件中保存这些多边形的信息。由于OpenGL中提供了最基本的由多边形构造三维模型的方法,因此从三维图形数据文件中读取模型数据,在OpenGL中进行绘制就非常容易了。
利用TopenGL目录下的3DSTOGL工具,可以将*.3ds格式的模型转换为*.gl文件(数据文件)和*.h文件(头文件),这两个文件的主文件名与模型文件的主文件名相同。在宿主程序中包含这个文件的头文件并条用其中的函数就可以实现3DS模型的显示。图4是用3DS Max R3制作的飞机和巡航导弹的3DS模型。
(a)飞机模型 (b)巡航导弹模型
图4 飞机和巡航导弹的3DS模型
2.3 全景图像背景和飞行器的绘制
2.3.1 全景图像背景的有关函数
首先应新建一个应用,然后在Form1中加入Tscene和Tlight组件,再在Unite1. Cpp的尾部插入如下代码:
#include "bomb01.h"
#include "Shellnew.h"
GLint ListNum;
接下来编写PanoSphere()函数创建显示列表所银号为1的球面片曲面。函数先计算球面片曲面的控制点坐标,然后计算二维网格的顶点坐标。由于要对这个曲面进行纹理映射,因此同时还要生成纹理坐标,纹理坐标控制点的坐标存放在texpt数组中。最后调用glEvalMesh2()函数创建球面片(样条曲面)。
void PanoSphere(void)
{
float alh,bet,ang,xa,TexX;
int i,j,k;
GLfloat texpts[8];
TexX = 0.5;
texpts [0] = TexX;
texpts [1] = 0;
texpts [2] = TexX;
texpts [3] = 1.0;
texpts [4] = 0,
texpts [5] = 0;
texpts [6] = 0;
texpts [7] = 1.0;
ang = FAngle /180.0*pi;
xa = ang/8;
k=0;
for (i=1;i<=5;i++) {
for (j=1;j<=9;j++) {
switch (j) {
case 5:
alh=pi/2;break;
case 1:
alh =(pi+ang)/2;break;
case 9:
alh = (pi-ang)/2;break;
default:
alh =(pi+ang)/2-xa*j+xa;
}
switch (i) {
case 3:
bet=pi/2;break;
case 1:
bet =(pi+ang/2)/2;break;
case 5:
bet = (pi-ang/2)/2;break;
default:
bet =(pi+ang/2)/2-xa*i+xa;
}
cps[k++]=FRadius*cos(alh)*sin(bet);
cps[k++]=FRadius*cos(bet);
cps[k++]=-FRadius*sin(alh)*sin(bet);
}
}
glMap2f(GL_MAP2_VERTEX_3, 0, 1, 3, 9,
0, 1, 27, 5, &cps[0]);
glMap2f(GL_MAP2_TEXTURE_COORD_2, 0, 1, 2, 2,
0, 1, 4, 2, &texpts[0]);
glEnable(GL_MAP2_TEXTURE_COORD_2);
glEnable(GL_MAP2_VERTEX_3);
glMapGrid2f(20, 0.0, 1.0, 20, 0.0, 1.0);
if (glIsList(1))
glDeleteLists(1,1);
glNewList(1,GL_COMPILE);
glEvalMesh2(GL_FILL, 0, 20, 0, 20);
glEndList();
}
PanLoad()函数的功能是装入全景图像,将其格式转换为纹理映射所要求的格式,并根据全景图像的尺寸计算一些重要的控制参数(如纹理尺寸等)。
bool PanoLoad(void)
{
Graphics::TBitmap *FBitmap = new Graphics::TBitmap;
try {
// FBitmap->LoadFromFile("Beach.bmp");//画海滩背景
FBitmap->LoadFromFile("quzhounan.bmp");//画山区背景
}
catch (...) {
return false;
}
if (FTexData)
delete FTexData;
FPanoWidth = FBitmap->Width;
FPanoHeight = FBitmap->Height;
int wh;
if (FPanoHeight>FPanoWidth)
wh = FPanoWidth;
else
wh = FPanoHeight;
FTexHeight = 1;
while (wh>1) {wh = wh>>1;FTexHeight=FTexHeight<<1;};
if (FTexHeight*2>FPanoWidth)
FTexWidth = FTexHeight;
else
FTexWidth = FTexHeight<<1;
long sz,sy;
sz = FPanoWidth;
sy = FPanoHeight;
sz = sz *sy*3;
sy = FTexHeight*FTexWidth*3;
FPanoX =(FPanoHeight-FTexHeight)/2;
try {
FTexData = new BYTE[sz];
FCurrTex = new BYTE[sy];
} catch (...) {
return false;
}
if (!FTexData)
return false;
union {
TColor c;
struct { BYTE red; BYTE green; BYTE blue; BYTE x;} b;
} co;
int I, J;
long c;
for (I=0;I<FBitmap->Width;I++) {
for (J=0;J<FBitmap->Height;J++) {
co.c = FBitmap->Canvas->Pixels[I][J];
c = I*FBitmap->Height*3+J*3;
FTexData[c++]= co.b.red;
FTexData[c++]= co.b.green;
FTexData[c]= co.b.blue;
}
}
FBitmap->Free();
return true;
}
PanoMove1(int ms)的功能是移动视点。Ms为偏移量,360O环视、仰视和俯视等动作就是用这个函数实现的。同时,设置一个定时器Timer1以实现偏移量的测量。
void __fastcall TForm1::Timer1Timer(TObject *Sender)
{
int A3;
A2=A1,E2=E1;
A1=A,E1=E;
A3=int(A)%2870;
Edit1->Text=IntToStr(int(A3));
Edit2->Text=IntToStr(int(E));
if((A1==A2))
{
OldA=A;
}
if(E1==E2)
OldE=E;
if((A1!=A2)||(E1!=E2))
{
float pp;
if ((OldE<50) || (OldE>Height-50))
{
if ((OldE-E)>0)
PanoMove1(OldE-E);
else
PanoMove1(E-OldE);
}
if ((OldA-A)>0)
PanoMove1(OldA-A);
else
PanoMove1(A-OldA);
pp = E-OldE;
pp = pp/50.0;
Scene1->Projection->Height +=pp;
Scene1->Projection->Width = Scene1->Projection->Height*1.94;
Texture1->TexEnable();
Scene1->Refresh(NULL);
OldA = A;
OldE = E;
}
}
2.3.2 全景图像和飞行器的绘制
响应Scence1的OnInital事件,并在该事件的处理函数中插入下面的代码。
void __fastcall TForm1::Scene1Initial(TObject *Sender)
{
Scene1->Projection->Width = 9.7;
Scene1->Projection->Height = 5;
Scene1->ProjectionMode = pmOrtho;
Scene1->MouseFunction = mfNormal;
PanoSphere();
PanoLoad();
PanoMove1(0);
Scene1->AddLight(Light1);
Scene1->AddLight(Light2);
glEnable(GL_COLOR_MATERIAL);
// ListNum=GL3DS_initialize_Bomb01();//画飞机
ListNum=GL3DS_initialize_shellnew();//画巡航导弹
Texture1->TexEnable();
}
响应Scence1的OnPaint事件,并在该事件的处理函数中插入下面的代码。
void __fastcall TForm1::Scene1Paint(TObject *Sender)
{
//画全景
glPushMatrix();
glCallList(1);
glBegin(GL_QUADS);
glNormal3f(0, 0, 1);
glTexCoord2f(0.5,0.0); glVertex3f(-4.85, -2.45, -45);
glTexCoord2f(0.5,1.0); glVertex3f(4.85, -2.45, -45);
glTexCoord2f(0.0,1.0); glVertex3f(4.85,2.45, -45);
glTexCoord2f(0.0,0.0); glVertex3f(-4.85,2.45, -45);
glEnd();
glPopMatrix();
Texture1->TexDisable();
//画飞机
glPushMatrix();
glScalef(i,i,i);
glTranslatef(X,Y,-15);
glRotatef(AngX,1.0,0,0);
glRotatef(AngY,0,1.0,0);
glRotatef(AngZ,0,0,1.0);
glCallList(ListNum);
glTranslatef(0,0,0);
glPopMatrix();
}
2.3.3 全景图像和飞行器的控制
设置Form1的ActiveControl属性值为Scence1,使Scence1能处理键盘事件,响应Scence1的OnKeyPress事件,并在该事件的处理函数中插入下面的代码。
void __fastcall TForm1::Scene1KeyPress(TObject *Sender, char &Key)
{
switch(Key){
case 'e':
X+=5;
Texture1->TexEnable();
break;
case 'f':
X-=5;
Texture1->TexEnable();
break;
case 'g':
A+=10;
Texture1->TexEnable();
break;
case 'h':
A-=10;
Texture1->TexEnable();
break;
case 'i':
E+=5;
Texture1->TexEnable();
break;
case 'j':
E-=5;
Texture1->TexEnable();
break;
case 'x':
AngX++;
Texture1->TexEnable();
Scene1->Refresh(NULL);
break;
case 'y':
AngY++;
Texture1->TexEnable();
Scene1->Refresh(NULL);
break;
case 'z':
AngZ++;
Texture1->TexEnable();
Scene1->Refresh(NULL);
break;
}
Scene1->Refresh(NULL);
}
上述应用的运行结果如图5所示。
图5 飞行器与全景图像背景
按x键可使飞行器做俯仰运动,按y键可使飞行器做转弯运动,按z键可使飞行器做侧倾运动,按a键可将飞行器拉近,按b键可将飞行器推远,按e键可使飞行器做前进运动,按f键可使飞行器做后腿运动,按e键可使飞行器做前进运动,按g键可使背景在飞行器静止的情况下顺时针转动,按h键可使背景在飞行器静止的情况下逆时针转动,按i键可在飞行器静止的情况下仰视背景,按j键可在飞行器静止的情况下俯视背景。
3、重点与难点分析
3.1 这个小程序显示的飞行器是“bomb01.3ds”和“shellnew.3ds”文件,运行3DSTOGL程序,读取这两个3DS文件,生成一个头文件(.h)和一个数据文件(.gl)。
头文件(.h)的内容包括模型所使用的材质(GL3DS_MATERIAL_shellnew数组)、顶点(GL3DS_VERTEX_shellnew数组)、顶点索引(Gluint GL3DS_INDEX_shellnew数组)以及一个为模型创建显示列表的函数(GL3DS_initialize_shellnew())。以下是Shellnew.h的部分内容:
#include "GL/gl.h"
//材质
static GLfloat GL3DS_MATERIAL_shellnew[48] = {
0.64843750000000000000, 0.85937500000000000000, 0.82031250000000000000, 1.00000000000000000000,
… …
0. 93750000000000000000, 0.93750000000000000000, 0.93750000000000000000, 1.00000000000000000000};
//顶点
static GLfloat GL3DS_VERTEX_shellnew[17968];
//顶点索引
static GLuint GL3DS_INDEX_shellnew[] = {
11,9,1127,
… …
2092,2096,2222,
975,1105,979};
//读取顶点坐标并创建显示列表
int GL3DS_initialize_shellnew() {
int ReturnVal;
FILE *in;
//打开数据文件
if (!(in = fopen("shellnew.gl", "rb")))
return(-1);
//读取数据文件
if (fread(GL3DS_VERTEX_shellnew, 71872, 1, in) != 1)
return(-1);
//关闭数据文件
fclose(in);
//创建显示列表
ReturnVal = glGenLists(1);
glInterleavedArrays(0x2A2B, 0, GL3DS_VERTEX_shellnew);
glNewList(ReturnVal, GL_COMPILE);
glFrontFace(GL_CCW);
glEnable(GL_CULL_FACE);
glCullFace(GL_BACK);
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
glMaterialfv(GL_FRONT, GL_AMBIENT, (GLfloat *) &GL3DS_MATERIAL_shellnew[0]);
glMaterialfv(GL_FRONT, GL_DIFFUSE, (GLfloat *) &GL3DS_MATERIAL_shellnew[4]);
glMaterialfv(GL_FRONT, GL_SPECULAR, (GLfloat *) &GL3DS_MATERIAL_shellnew[8]);
glDrawElements(GL_TRIANGLES, 4464, GL_UNSIGNED_INT, &GL3DS_INDEX_shellnew[0]);
glMaterialfv(GL_FRONT, GL_AMBIENT, (GLfloat *) &GL3DS_MATERIAL_shellnew[12]);
glMaterialfv(GL_FRONT, GL_DIFFUSE, (GLfloat *) &GL3DS_MATERIAL_shellnew[16]);
glMaterialfv(GL_FRONT, GL_SPECULAR, (GLfloat *) &GL3DS_MATERIAL_shellnew[20]);
glDrawElements(GL_TRIANGLES, 240, GL_UNSIGNED_INT, &GL3DS_INDEX_shellnew[4464]);
glMaterialfv(GL_FRONT, GL_AMBIENT, (GLfloat *) &GL3DS_MATERIAL_shellnew[24]);
glMaterialfv(GL_FRONT, GL_DIFFUSE, (GLfloat *) &GL3DS_MATERIAL_shellnew[28]);
glMaterialfv(GL_FRONT, GL_SPECULAR, (GLfloat *) &GL3DS_MATERIAL_shellnew[32]);
glDrawElements(GL_TRIANGLES, 96, GL_UNSIGNED_INT, &GL3DS_INDEX_shellnew[4704]);
glMaterialfv(GL_FRONT, GL_AMBIENT, (GLfloat *) &GL3DS_MATERIAL_shellnew[36]);
glMaterialfv(GL_FRONT, GL_DIFFUSE, (GLfloat *) &GL3DS_MATERIAL_shellnew[40]);
glMaterialfv(GL_FRONT, GL_SPECULAR, (GLfloat *) &GL3DS_MATERIAL_shellnew[44]);
glDrawElements(GL_TRIANGLES, 2460, GL_UNSIGNED_INT, &GL3DS_INDEX_shellnew[4800]);
glEndList();
return (ReturnVal);
}
3.2 应用程序首先调用GL3DS_initialize_shellnew()函数,然后通过glCallList(1)调用显示列表绘制飞行器模型。
3.3 绘制背景时,为了避免将背景纹理到飞行器模型上,首先绘制一个与Sence1大小相同的面(GL_QUADS),将背景投影到这个面上,然后再绘制飞行器模型。
参考文献:
1、 彭小明、王坚编著.OpenGL深入编程与实例揭密.北京:人民邮电出版社,1999
2、 乔林、费广政编著. OpenGL程序设计.北京:清华大学出版社,2000
3、 吴斌、毕丽蕴编著. OpenGL编程实例与技巧.北京:人民邮电出版社,1999
4、Borland C++Builder4.0开发技巧与实例教程.北京:人民邮电出版社,2000
|