最近学习用opengl库来构建一个3D场景,以及实现场景漫游、粒子系统等效果,最终算是是做了一个3D走迷宫游戏吧。感觉最近学了好多东西,所以有必要整理整理。
一 实现效果
二 实现过程详解
1、3d场景构建
1)光照与材质
通过设置光照与材质,使得场景的显示效果更真实。opengl加光源的方法:
GLfloat light_position[] = {0.0, 80.0, 0.0}; GLfloat light_diffuse[] = {1.0, 1.0, 1.0, 1.0}; glLightfv(GL_LIGHT0, GL_POSITION, light_position); glLightfv(GL_LIGHT0, GL_AMBIENT_AND_DIFFUSE, light_diffuse); glEnable(GL_LIGHTING); glEnable(GL_LIGHT0);
加一个光源至少要上面这些代码,通过glLightfv()函数给光源设置位置以及颜色,可以加环境光G_AMBIENT、漫射光GL_DIFFUSE或镜面光GLSPECULAR,可以同时加8个光源,上面的光源时GL_LIGHT0,其他的就是GL_LIGHT1、2等。
场景中一旦加了光源,物体就会根据自己的材质对RGB光成分反射程度而显示不同的颜色。OpenGL给一个物体设置材质的方法
GLfloat diffuse[] = {1.0, 0.9, 0.9}; glMaterialfv(GL_FRONT, GL_AMBIENT_AND_DIFFUSE, diffuse);
设置材质参数后接下来画的物体就具备了这种材质。通常使用参数GL_AMBIENT_AND_DIFFUSE给环境光和漫射光设置相同的反射程度。
2)纹理映射与多重纹理映射
给模型加纹理是为了在表面形成复杂图案,因为设置材质只是控制表面的显示颜色,实际上物体的表面信息要更复杂些。opengl提供了给物体贴纹理图的方法,实际上有多种方法,但我用的是 glaux库。
struct IMAGE { GLuint sizeX; GLuint sizeY; signed char* data; }; IMAGE *Image[3]; GLuint Texture[3]; bool loadTexture()//设置各种纹理,从bmp图像读取 { FILE* myFile; if(!(myFile = fopen("wall.bmp", "r"))) return false; Image[0] = (IMAGE*)auxDIBImageLoad("wall.bmp"); glGenTextures(3, &Texture[0]); glBindTexture(GL_TEXTURE_2D, Texture[0]); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexImage2D(GL_TEXTURE_2D, 0, 3, Image[0]->sizeX, Image[0]->sizeY, 0, GL_RGB, GL_UNSIGNED_BYTE, Image[0]->data); if(!(myFile = fopen("floor.bmp", "r"))) return false; Image[1] = (IMAGE*)auxDIBImageLoad("floor.bmp"); glBindTexture(GL_TEXTURE_2D, Texture[1]); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexImage2D(GL_TEXTURE_2D, 0, 3, Image[1]->sizeX, Image[1]->sizeY, 0, GL_RGB, GL_UNSIGNED_BYTE, Image[1]->data); if(!(myFile = fopen("water.bmp", "r"))) return false; Image[2] = (IMAGE*)auxDIBImageLoad("water.bmp"); glBindTexture(GL_TEXTURE_2D, Texture[2]); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexImage2D(GL_TEXTURE_2D, 0, 3, Image[2]->sizeX, Image[2]->sizeY, 0, GL_RGB, GL_UNSIGNED_BYTE, Image[2]->data); //释放内存 if(Image[0]) { if(Image[0]->data) free(Image[0]->data); free(Image[0]); } if(Image[1]) { if(Image[1]->data) free(Image[1]->data); free(Image[1]); } if(Image[2]) { if(Image[2]->data) free(Image[2]->data); free(Image[2]); } return true; }
上面的代码生成了三种纹理。语句glGenTextures(3, &Texture[0])生成了三个纹理索引,存在了数组Texture中,以后每次要设置纹理信息或是想应用纹理,通过函数glBindTexture(GL_TEXTURE_2D, textureIndex)就可以取到对应的纹理,其中第二个参数就是纹理索引。
绑定纹理到物体表面:
void drawPolygon(GLfloat a[3], GLfloat b[3], GLfloat c[3], GLfloat d[3])//根据四个点画一个面 { glEnable(GL_TEXTURE_2D); glBindTexture(GL_TEXTURE_2D, Texture[0]); glBegin(GL_POLYGON); glTexCoord2f(0.0f, 0.0f); glVertex3fv(a); glTexCoord2f(1.0f, 0.0f); glVertex3fv(b); glTexCoord2f(1.0f, 1.0f); glVertex3fv(c); glTexCoord2f(0.0f, 1.0f); glVertex3fv(d); glEnd(); }
多重纹理就是在物体表面贴上多个纹理的方法,要使用多重纹理需要用到另一个库glext库。根据下面的代码就可以给物体表面贴上两种纹理:
PFNGLMULTITEXCOORD2FARBPROC glMultiTexCoord2fARB=NULL; PFNGLACTIVETEXTUREARBPROC glActiveTextureARB=NULL; bool canMultiTexture = true; void multiTextureInit()//多重纹理的初始化 { glActiveTextureARB = (PFNGLACTIVETEXTUREARBPROC)wglGetProcAddress("glActiveTextureARB"); glMultiTexCoord2fARB = (PFNGLMULTITEXCOORD2FARBPROC)wglGetProcAddress("glMultiTexCoord2fARB"); if(glActiveTextureARB == NULL) canMultiTexture = false; } void multiTextureBegin()//多重纹理绑定 { glEnable(GL_TEXTURE_2D); glActiveTextureARB(GL_TEXTURE0_ARB); glBindTexture(GL_TEXTURE_2D, Texture[0]);//纹理1 glActiveTextureARB(GL_TEXTURE1_ARB); glEnable(GL_TEXTURE_2D); glBindTexture(GL_TEXTURE_2D, Texture[1]);//纹理2 }
值得注意的是,多重纹理对电脑设备有要求,主要是显示器要支持,所以一旦不支持,上面初始化的时候glActivetextureARB就会为null,这时候要做另外处理,不然后面使用这个为null的变量程序就会出错。
3)显示列表
显示列表是OpenGL提供的一种方便反复调用相同的显示函数的方法,比如你的程序中需要反复的描绘一个物体,你就最好用显示列表来调用,这样做能够大大优化性能。
调用显示列表是通过glCallList(列表索引)函数调用的,显然没一个显示列表都有一个对应的索引,通过这个索引去调用显示列表中的显示操作。下面的代码生成了一个画五角星的显示列表:
GLuint display_list;//一个五角星的显示列表索引
GLuint createDL()//创建一个五角星显示列表 { GLuint DL; DL = glGenLists(1); glNewList(DL,GL_COMPILE); drawFive();//画一个五角星 glEndList(); return DL; }
当需要画一个五角星的时候调用glCallList(display_list);即可。
2 场景漫游
我实现的是模拟人在迷宫中走动寻找出口的情形,通过键盘的上下左右键控制视线的改变以及位置的移动。先理解一下gluLookAt函数,我的程序里参数是这样的gluLookAt(x, y, z, x + lx,y + ly,z + lz,0.0f,1.0f,0.0f) 总共有9个参数,前三个参数代表了照相机的位置,所以这里照相机的位置是(x,y,z),接下来三个参数是目标的中心位置,即(x+lx, y+ly,z+lz),后面三个参数一般设为0, 1, 0,表示的是照相机头部的方向,如果把照相机看错人眼,那照相机头部的方向也就是我们头的方向(所以一般向上)。因为要控制向前/后移动,所以需要知道此时视线的方向向量,实际上就是(lx,
ly, lz),当改变视角是其实就是改变(lx, ly, lz)的值,所以当左右键事件发生时,进行以下计算:
void orientMe(float ang) //计算由于左右键盘操作而改变视点方向,使用左右方向键旋转照相机 { lx = sin(ang); lz = -cos(ang); glLoadIdentity(); gluLookAt(x, y, z, x + lx,y + ly,z + lz, 0.0f,1.0f,0.0f); }
可以注意到照相机位置还是不变的,因为只是改变了视线。当上下键事件发生时,改变的就是照相机的位置了。
void moveMeFlat(int direction) //计算视点由于上下键盘操作而移动的量,上下方向键使照相机沿视线前后移动 { int prev_x = x, prev_z = z; x = x + direction*(lx)*0.1; z = z + direction*(lz)*0.1; glLoadIdentity(); if(isWall[(int)(x + 93)][(int)(z + 93)]) { x = prev_x; z = prev_z; } gluLookAt(x, y, z, x + lx,y + ly,z + lz,0.0f,1.0f,0.0f); }
3 粒子系统的实现
粒子系统不是什么具体的东西,而是是一个很好的编程设计思想,通常用来模拟雨、雪、雾、烟花等效果。粒子系统的实现主要问题就是如何设计粒子的行为以及如何渲染粒子以达到真实的效果。我的程序里粒子系统是最后加的,跟走迷宫没什么练习,只是觉得粒子系统挺神奇的,就试着实现各种五角星漫天飞扬的效果。这时粒子的类,定义了一个粒子的所有行为:
//次类是粒子类,实现粒子的一系列行为 #include<stdlib.h> #include<GL\glut.h> #include<time.h> #define PI 3.1415 class particle { private: GLfloat x;//位置x坐标 GLfloat y;//y坐标 GLfloat z;//z坐标 GLfloat v[3];//控制速度 GLfloat rotate[3];//控制旋转方向 GLfloat angle;//旋转的角度 GLfloat color[3];//五角星显示的颜色 GLuint display_list;//一个五角星的显示列表索引 public: GLuint createDL()//创建一个五角星显示列表 { GLuint DL; DL = glGenLists(1); glNewList(DL,GL_COMPILE); drawFive();//画一个五角星 glEndList(); return DL; } void init()//随机初始化位置以及方向等信息 { display_list = createDL(); angle = 0; y = rand() % 40; x = rand() % 181 - 90; z = rand() % 181 - 90; v[0] = (float)(rand() % 8) / (float)10 - 0.4; v[1] = (float)(rand() % 8) / (float)10 - 0.4; v[2] = (float)(rand() % 8) / (float)10 - 0.4; rotate[0] = (float)(rand() % 7) / (float)7 + 5; rotate[1] = (float)(rand() % 7) / (float)7 + 5; rotate[2] = (float)(rand() % 7) / (float)7 + 5; color[0] = (float)(rand() % 5) / (float)5 + 0.2; color[1] = (float)(rand() % 5) / (float)5 + 0.2; color[2] = (float)(rand() % 5) / (float)5 + 0.2; } void drawFive()//画五角星 { GLfloat out_length = sqrt(1.0 / (2 - 2 * cos(72 * PI / 180))), bx = out_length * cos(18 * PI / 180), by = out_length * sin(18 * PI / 180), cx = out_length * sin(36 * PI / 180), cy = -out_length * cos(36 * PI / 180); GLfloat fx = cx * (by - out_length) / (cy - out_length), fy = by, in_length = sqrt(fx * fx + fy * fy), gx = in_length * cos(18 * PI / 180), gy = -in_length * sin(18 * PI / 180); GLfloat point_a[2] = {0, out_length}, point_b[2] = {bx, by}, point_c[2] = {cx, cy}, point_d[2] = {-cx, cy}, point_e[2] = {-bx, by}, point_f[2] = {fx, fy}, point_g[2] = {gx, gy}, point_h[2] = {0, -in_length}, point_i[2] = {-gx, gy}, point_j[2] = {-fx, fy}; glBegin(GL_TRIANGLE_FAN); glVertex2f(0.0f, 0.0f); glVertex2f(point_a[0], point_a[1]); glVertex2f(point_f[0], point_f[1]); glVertex2f(point_b[0], point_b[1]); glVertex2f(point_g[0], point_g[1]); glVertex2f(point_c[0], point_c[1]); glVertex2f(point_h[0], point_h[1]); glVertex2f(point_d[0], point_d[1]); glVertex2f(point_i[0], point_i[1]); glVertex2f(point_e[0], point_e[1]); glVertex2f(point_j[0], point_j[1]); glVertex2f(point_a[0], point_a[1]); glEnd(); } void draw()//在(x, y, z)显示五角星 { GLfloat diffuse[] = {color[0], color[1], color[2]}; glMaterialfv(GL_FRONT, GL_AMBIENT_AND_DIFFUSE, diffuse); glPushMatrix(); glTranslatef(x, y, z); glRotatef(angle, rotate[0], rotate[1], rotate[2]); glCallList(display_list); glPopMatrix(); } void move(float slowdown)//改变粒子位置及角度等信息 { x += v[0] / slowdown; y += v[1] / slowdown; z += v[2] / slowdown; angle += 10 / slowdown; if(!(x >= -90 && x <= 90)) die(); else if(!(z >= -90 && z <= 90)) die(); else if(!(y >= 0 && y <= 50)) die(); } void die()//粒子死亡,消失,重新初始化 {//可以加其他操作 init(); } };
另外也可以对比一下我实现下雨效果的粒子类:
#include<stdlib.h> #include<GL\glut.h> #include<time.h> class rain { private: GLfloat position[3];//粒子的位置 GLfloat v0;//粒子的初速度 GLfloat g;//重力加速度 GLfloat size;//雨滴的大小 GLfloat sizeSet[4]; GLfloat gSet[4]; GLuint display_list; public: rain() { sizeSet[0] = 0.40; sizeSet[1] = 0.45; sizeSet[2] = 0.50; sizeSet[3] = 0.55; gSet[0] = 0.5; gSet[1] = 0.52; gSet[2] = 0.54; gSet[3] = 0.56; } GLuint createDL() { GLuint DL; DL = glGenLists(1); glNewList(DL,GL_COMPILE); GLUquadricObj *qobj = gluNewQuadric(); gluQuadricTexture(qobj,GL_TRUE); gluSphere(qobj, size, 20, 20);//画一个小球 glEndList(); return DL; } void init()//粒子初始化 { display_list = createDL(); position[0] = rand() % 181 - 90; position[1] = 50; position[2] = rand() % 181 - 90; int sizeIndex = rand() % 4; size = sizeSet[sizeIndex]; g = gSet[sizeIndex];//随机加速度 v0 = (float)(rand() % 6) / (float)20;//随机初始化初速度 } void draw() { GLfloat diffuse[3] = {1.0, 1.0, 1.0}; glMaterialfv(GL_FRONT, GL_AMBIENT_AND_DIFFUSE, diffuse); glPushMatrix(); glTranslatef(position[0], position[1], position[2]); glCallList(display_list); glPopMatrix(); } void move() { position[1] -= v0; v0 += g; if(position[1] <= 0) die(); } void die() { init(); } };
雨水粒子我设计得比较简单,初始化的时候分配它随机一个初速度、一个初位置、加速度、大小等,每次显示过后根据速度和加速度改变位置以实现“加速落下”的效果,还有渲染的时候需要用到雨水的纹理图。
设计好了粒子类之后,就可以再写一个类实现对粒子数的控制,以及对所有粒子进行初始化和显示。
反复调用的实现
通过理解粒子系统,我知道它是反复地调用显示所有粒子的函数,因为每次粒子的位置都会改变,所以就形成了粒子的运动。那怎么反复调用显示函数呢?看一下glut库里的函数(当然如果用windows库的话也能实现反复调用,这里只是glut库的):
glutDisplayFunc(renderScene);每次窗口重绘时指定调用函数
glutReshapeFunc(changeSize); 每次窗口大小改变时制定调用函数
一开始想通过这两个函数想反复调用renderScene函数,但是没办法,它们指定的函数只能在特定情况下被调用;
然后我就找到了glutIdleFunc(renderScene)函数,作用是设置全局的默认调用函数。当函数glutMainLoop()进行了无限等待时间循环时,如果没有窗口事件发生,就默认调用glutIdelFunc指定的函数,这样就可以反复调用renderScene函数了。
五角星粒子系统效果:
OpenGL3D迷宫场景设计