近期学习用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函数了。
五角星粒子系统效果:
雪粒子效果