OpenGL3D迷宫场景设计「建议收藏」

OpenGL3D迷宫场景设计「建议收藏」OpenGL实现的3D迷宫场景,五角星粒子系统和雨雪粒子系统

大家好,又见面了,我是你们的朋友全栈君。

最近学习用opengl库来构建一个3D场景,以及实现场景漫游、粒子系统等效果,最终算是是做了一个3D走迷宫游戏吧。感觉最近学了好多东西,所以有必要整理整理。

一 实现效果

OpenGL3D迷宫场景设计「建议收藏」OpenGL3D迷宫场景设计「建议收藏」

二 实现过程详解

   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迷宫场景设计「建议收藏」

雪粒子效果

OpenGL3D迷宫场景设计「建议收藏」

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/134542.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)


相关推荐

  • nginx做正向代理_反向代理和正向代理

    nginx做正向代理_反向代理和正向代理Nginx正向代理四种方式为什么需要正向代理案例配置方式第一种生成一个适合你的列表创建一个表格设定内容居中、居左、居右SmartyPants创建一个自定义列表如何创建一个注脚注释也是必不可少的KaTeX数学公式新的甘特图功能,丰富你的文章UML图表FLowchart流程图导出与导入导出导入为什么需要正向代理如果我们的服务部署在公司内网环境,不能直接访问互联网服务,就需要通过可以访问互联网的代理服务器来实现访问互联网的服务。此处我们使用Nginx作为代理服务器。案例互联网上的接口:https://

  • Linux下rpm包x86、i386、i486、i586、i686和x86_64这些后缀含义

    Linux下rpm包x86、i386、i486、i586、i686和x86_64这些后缀含义iamlaosong评:虽然rpm包版本很多,不过目前的新机器都可以使用x86_64版本,而且也应该使用这个版本,除非一些特殊场合,比如为了使用一些老版程序。有些功能没有x86_64版本,那也只好用i386了。现在的发行包,一般也就提供i386和x86_64两个版本,即32位版本和64位版本,有些甚至已经不提供i386版本了。1、i386、i586、i686与Noarchi386—几乎所有的X…

  • Microsoft Office 2007 中文专业版密钥

    Microsoft Office 2007 中文专业版密钥MicrosoftOffice2007中文专业版(微软原版)正版密钥MicrosoftOfficeVisio2007简体中文专业版:简介:    便于IT和商务专业人员就复杂信息、系统和流程进行可视化处理、分析和交流。使用具有专业外观的OfficeVisio2007图表,可以促进对系统和流程的了解,深入了解复杂信息并利用这些知识做出更好的业务决策。迅雷下载    …

  • jsp+ajax_javascriptjavascript日

    jsp+ajax_javascriptjavascript日明后两天梁言兵老师来讲Ajax及其最近作过的一个真实的Ajax项目,所以,我今天讲解梁老师的课程所需要的一些前置知识。因为大家对Javascript不是很熟悉,所以我首先讲解Javascript的DHTML功能。本来入学考试要求大家很好地掌握Javascript的,但是大家都不能理解我们的苦衷,并没有专心去对待Javascript。想想我前两年强调javascript和css的重要性时,一些培训中

  • sql 2003错误 ERROR 2003: Can’t connect to MySQL server on ‘localhost’ (10061)

    sql 2003错误 ERROR 2003: Can’t connect to MySQL server on ‘localhost’ (10061)系统把你SQL停了,在键盘上的win+R键打开运行,输入services.msc打开服务面板找到SQL左上脚启动,再重启SQL,搞定。

  • unity 的Cinemachine组件运用

    unity 的Cinemachine组件运用1.第三人称视角控制通过PackageManager安装CineMachine1) 最简单的方法使用freeLook虚拟相机常用的调整为:1.观察目标:将要看的目标放在这里。2输入控制:把你想用来控制的虚拟轴(就是InputManager里的)的名字输入进去就行。默认是填mouse那个输入轴。注意:似乎不支持NewInputSystem。所以在用NewInputSystem时要么用在projectSetting/player里改成both设置。要么自己写脚本去调用这个组件中的

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号