OpenGL实现3D游戏编程【连载4】——纹理加载常用的三种方式

OpenGL实现3D游戏编程【连载4】——纹理加载常用的三种方式

欢迎来到zhooyu的专栏。
个人主页:【zhooyu】
文章专栏:【OpenGL实现3D游戏编程】

本专栏内容:

我们从游戏的角度出发,用C++去了解一下游戏中的功能都是怎么实现的。这一切还是要从自己玩游戏开始说起,此前就玩过一下3D游戏,当时就被游戏里的一些画面和设置深深的吸引了,同时游戏里还有很多很有趣的设定,比如,玩家的视角是怎么移动的?崎岖不平的地图是怎样制作的?人物和物体、地面的碰撞是怎样检测的?鼠标是怎样选中眼前的物体的?魔法技能是怎样释放的?不用加载进度条的无缝世界地图是怎么实现的?带着这些疑问,我们走进了一个OpenGL世界的3D世界。

在这里插入图片描述

1、什么是纹理

如果我们想让物体更加真实,就得有足够多的顶点,且指定足够多的颜色,这样会产生很多额外开销,因为每个模型需要很多顶点,而每个顶点又需要颜色属性。为了方便且减少开销,我们更习惯使用纹理来绘制更多的模型,纹理就像一张2D的贴图,把它贴在图像或模型上来增加物体的细节。这样物体细节取决于设计师的美工,而程序员则不必指定更多的顶点。

在这里插入图片描述
OpenGL支持多种图像格式,如BMP、JPEG、PNG等。根据实际需求选择合适的格式,当然也可以以上格式都使用。

2、图片的加载

在OpenGL中,加载图像作为纹理的一般步骤如下:

  1. 加载图像数据:使用系统自带的图像加载函数(方法一)、或合适的图像处理库(方法二)、或者自己编写的加载函数(方法三)加载图像数据到内存中。个纹理对象,并绑定到一个纹理单元上。
  2. 设置纹理参数:通过设置纹理参数来控制纹理的采样方式。例如,设置纹理过滤方式、纹理环绕方式等。
  3. 将图像数据传输到纹理对象:使用glTexImage2D函数将图像数据传输到纹理对象中。这需要指定图像的宽度、高度、像素格式等信息。
  4. 释放图像数据:在纹理对象创建成功后,可以释放已加载的图像数据,以节省内存空间。

3、三种加载图片方式的特点

图片加载方法分类加载方式的特点
系统自带的图像加载函数(方法一)系统自带的图像加载函数(方法一)使用最方便,无需加载额外的运行库或设置,直接在程序中使用即可,但仅能读取BMP文件;
合适的图像处理库(方法二)其中方法二合适的图像处理库(如StbImage、FreeImage、Soil等)提供了方便的函数来读取不同格式的图像文件,可以方便的读取多种格式,如BMP、JPEG、PNG等;
自己编写的加载函数(方法三)自己编写的加载函数难度相对复杂些,还要根据不同格式的图片进行不同的加载处理,这节我们仅仅介绍最简单的BMP图像文件的读取。但是这种办法的优点也很明显,由于以文件方式打开并读取图片信息,在进行系统后台无缝地图的加载过程中,可以进行自定义的过程控制,不会出现由于纹理图片过大导致的程序卡顿问题。

4、系统自带的图像加载函数(方法一)

我们使用系统自带的auxDIBImageLoad函数来加载图片资源,具体代码如下。该方法调用了glaux.h库文件中的auxDIBImageLoad函数,其实它是一个宏,函数原型为auxRGBImageLoadW(LPCWSTR)或者auxRGBImageLoadA(LPCSTR),可以在该库文件中找到它的定义。宏auxDIBImageLoad实现的功能就是:根据指定的位图名称,将该位图的信息加载到内存中,以便用来创建成为纹理。本节中用到的路径均可以为相对路径。


 //加载纹理函数,读取一个BMP文件作为纹理,如果失败返回-1,如果成功返回纹理编号
 
 GLuint	Texture_LoadFromFile_1(char* filename,GLuint iTempTextureId)
 {

	//检测文件是否存在
	 
	FILE *tempFile=fopen(filename,"r");
	 
	//文件打开成功则继续后续操作,失败后直接返回
	 
	if(tempFile)
	{
		 
		fclose(tempFile);
		 
	}
	else
	{
		 
		return -1;
		 
	}
	//生成纹理编号
	 
	GLuint tex_id=-1;
		
	glGenTextures(1,&tex_id);
	 
	//绑定纹理
	 
	glBindTexture(GL_TEXTURE_2D,tex_id);
	 
	//纹理过滤函数
	 
	glTexParameterf(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_REPEAT);
	 
	glTexParameterf(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,GL_REPEAT);
	 
	glTexParameterf(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
	 
	glTexParameterf(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
	 
	//指定纹理贴图和材质混合的方式,默认为GL_REPLACE模式
	 
	glTexEnvf(GL_TEXTURE_ENV,GL_TEXTURE_ENV_MODE,GL_REPLACE);

	//标准化加载文件名称

	if(true)
	{	
		
		//加载纹理资源
		
		AUX_RGBImageRec *m_bmpTexture; 
		
		//加载图片资源
		
		m_bmpTexture=auxDIBImageLoad(filename);
		
		//生成纹理(目标纹理,执行细节级别,颜色组件,纹理宽度,纹理高度, 边框宽度必须为,颜色格式,数据类型,指向图像数据的指针)
		
		glTexImage2D(GL_TEXTURE_2D,0,3,m_bmpTexture->sizeX,m_bmpTexture->sizeY,0,GL_RGB,GL_UNSIGNED_BYTE,m_bmpTexture->data);
		
		//释放相关资源,防止内存泄露
		
		if(m_bmpTexture)
		{
			
			if(m_bmpTexture->data)
			{
				
				free(m_bmpTexture->data);
				
			}
			
			free(m_bmpTexture);
			
		}
		
	}
	 
	return tex_id;

}

这里要注意2点:

1、一定要进行文件是否存在检测:加载文件时一定要文件是否存在的检测,如果文件不存在,程序运行时直接会出现崩溃并退出。同时已打开的文件才用fclose(tempFile)关闭文件,如果打开失败则无需关闭文件,此时强行fclose(tempFile)关闭会出现资源溢出。

2、一定要进行合理的内存释放:要及时释放相关内存资源,防止内存泄露。使用完m_bmpTexture->data和m_bmpTexture后均要使用free函数释放内存资源,切释放的先后顺序不能错,否则会出现内存泄漏。

通过前面的过程,已经将位图加载并创建和加载纹理成功,并返回纹理编号,后期就可以通过纹理编号从内存中取出指定的纹理信息,将其映射到立方体的指定的面上,具体的纹理贴图我们下一章节在讨论。

5、合适的图像处理库(方法二)

在这里,介绍一个简单易用的图像库:stb_image 。Github 地址为:https://github.com/nothings/stb ,我们仅仅使用其中stb_image.h这个文件即可,它的使用非常简单。

看看它的源码,你会发现是 .h为后缀的文件。这就是它的强大之处在于,仅需在工程中加入头文件就可以使用相应的函数解析、加载图像了,实际上是函数实现等内容都放在头文件中。

大家可以直接在以上网站下载,或在我的下载中进行下载,点击stb_image.h进行下载。

首先,我们需要声明并引用头文件。

这里我们要注意,第一个STBI_NO_SIMD这个声明并非必要,有时可以删除不要,主要是根据大家电脑设置不同,有些时候没有该声明系统无法通过编译。



//加载图片处理头文件,进行必要声明

#define		STBI_NO_SIMD

//加载图片处理头文件,进行必要声明

#define		STB_IMAGE_IMPLEMENTATION

//加载图片处理头文件

#include	"gl/stb_image.h"

接下来,我们就可以直接在系统中调用stb_image 的相应函数stbi_load函数。我们发现这种方法和方法一种的内容基本一样,主要是方法二使用了stbi_load函数来加载图像数据。


 //加载纹理函数,读取一个BMP、JPG、PNG文件作为纹理,如果失败返回-1,如果成功返回纹理编号
 
 GLuint	Texture_LoadFromFile_2(char* filename,GLuint iTempTextureId)
 {
	 
	//检测文件是否存在
	 
	FILE *tempFile=fopen(filename,"r");
	 
	//文件打开成功则继续后续操作,失败后直接返回
	 
	if(tempFile)
	{
		 
		//打开文件才用关闭文件,如果打开失败则无需关闭文件(此时强行关闭会出现资源溢出)
		 
		fclose(tempFile);
		 
	}
	else
	{
		 
		return -1;
		 
	}

	//生成纹理编号
	 
	GLuint tex_id=-1;
		
	glGenTextures(1,&tex_id);
	 
	//绑定纹理
	 
	glBindTexture(GL_TEXTURE_2D,tex_id);
	 
	//纹理过滤函数
	 
	glTexParameterf(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_REPEAT);
	 
	glTexParameterf(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,GL_REPEAT);
	 
	glTexParameterf(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
	 
	glTexParameterf(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
	 
	//指定纹理贴图和材质混合的方式,默认为GL_REPLACE模式
	 
	glTexEnvf(GL_TEXTURE_ENV,GL_TEXTURE_ENV_MODE,GL_REPLACE);

	//标准化加载文件名称

	if(true)
	{	
		
		int width,height,nrChannels;

		unsigned char *data=stbi_load(filename,&width,&height,&nrChannels,0);

		//判断是否加载成功,这里要注意图片通nrChannels道数,否则异常错误导致系统崩溃

		if(data)
		{

			glTexImage2D(GL_TEXTURE_2D,0,(nrChannels==3?GL_RGB:GL_RGBA),width,height,0,(nrChannels==3?GL_RGB:GL_RGBA),GL_UNSIGNED_BYTE,data);
			
			stbi_image_free(data);

		}
		else
		{

			char szTemp[1024]="";

			sprintf(szTemp,"纹理文件%s加载失败",filename);

			InsertLog(szTemp);

		}

	}
	 
	return tex_id;

}

但这里要特别注意,由于该函数能够加载包括BMP、JPG、PNG图像的数据,就会涉及到图像像素的通道数。比如说,后缀为PNG的文件中读取的每一个像素除了RGB三种颜色外,还会涉及到RGBA中的Alpha数据,stbi_load函数会返回nrChannels值,我们随后再使用glTexImage2D时,一定要对GL_RGB或GL_RGBA进行判断以进行匹配,否则系统会出现意外。

同时,我们要注意到内存的释放要使用图像库中stbi_image_free(data)函数进行释放,不能使用free进行释放,否则系统也会出现内存泄漏,这里一定要注意。

6、自己编写的加载函数(方法三)

自己编写的加载函数难度相对复杂些,还要根据不同格式的图片进行不同的加载处理,这节我们仅仅介绍最简单的BMP图像文件的读取。但是这种办法的优点也很明显,由于以文件方式打开并读取图片信息,在进行系统后台无缝地图的加载过程中,可以进行自定义的过程控制,不会出现由于纹理图片过大导致的程序卡顿问题。


//设置图片头文件信息
 
#define BMP_Header_Length 54
 
 //检查一个整数是否为2的整数次方,如果是,返回1,否则返回0
 
 int IsPowerOfTwo(int n)
 {
	 
	 if(n<=0)return 0;
	 
	 return (n&(n-1))==0;
	 
 }

//读取一个BMP文件作为纹理,如果失败,返回-1,如果成功,返回纹理编号

 GLuint Texture_LoadFromFile_3(const char* filename)
 {
	 
	 GLint width,height,total_bytes;
	 
	 GLubyte* pixels=0;
	 
	 GLint last_tex_id;
	 
	 GLuint tex_id=-1;
	 
	 //打开文件,如果失败,返回
	 
	 FILE* pFile=fopen(filename,"rb");
	 
	 if(pFile==0)return 0;
	 
	 //读取文件中图象的宽度和高度
	 
	 fseek(pFile,0x0012,SEEK_SET);
	 
	 fread(&width,4,1,pFile);
	 
	 fread(&height,4,1,pFile);
	 
	 fseek(pFile,BMP_Header_Length,SEEK_SET);
	 
	 //计算每行像素所占字节数,并根据此数据计算总像素字节数

	 if(true)
	 {
		 
		 GLint line_bytes=width * 3;
		 
		 while( line_bytes % 4 != 0 )++line_bytes;
		 
		 total_bytes=line_bytes*height;
		 
     }
	 
	 // 根据总像素字节数分配内存
	 
	 pixels=(GLubyte*)malloc(total_bytes);
	 
	 if(pixels==0)
	 {
		 
		 fclose(pFile);
		 
		 return 0;
		 
	 }
	 
	 // 读取像素数据
	 
	 if(fread(pixels,total_bytes,1,pFile)<=0)
	 {
		 
		 free(pixels);
		 
		 fclose(pFile);
		 
		 return 0;
		 
	 }
	 
	 //如果图象的宽度和高度不是的整数次方,或图象的宽度和高度超过当前OpenGL实现所支持的最大值时,要进行缩放
	 
	 if(true)
	 {
		 
		 GLint max;
		 
		 glGetIntegerv(GL_MAX_TEXTURE_SIZE,&max);
		 
		 if(!IsPowerOfTwo(width) || !IsPowerOfTwo(height) || width>max || height>max)
		 {
		 
			 // 规定缩放后新的大小为边长的正方形
			 
			 const GLint new_width=256;
			 
			 const GLint new_height=256; 
			 
			 GLint new_line_bytes,new_total_bytes;
			 
			 GLubyte* new_pixels=0;
			 
			 //计算每行需要的字节数和总字节数
			 
			 new_line_bytes=new_width*3;
			 
			 while( new_line_bytes%4!=0)++new_line_bytes;
			 
			 new_total_bytes=new_line_bytes*new_height;
			 
			 //分配内存
			 
			 new_pixels=(GLubyte*)malloc(new_total_bytes);
			 
			 if(new_pixels==0)
			 {
				 
				 free(pixels);
				 
				 fclose(pFile);
				 
				 return 0;
				 
			 }
			 
			 //进行像素缩放
			 
			 gluScaleImage(GL_RGB,width,height,GL_UNSIGNED_BYTE,pixels,new_width,new_height,GL_UNSIGNED_BYTE,new_pixels);
			 
			 //释放原来的像素数据,把pixels指向新的像素数据,并重新设置width和height
			 
			 free(pixels);
			 
			 pixels=new_pixels;
			 
			 width=new_width;
			 
			 height=new_height;
			 
		 }
		 
	 }
	 
	 // 分配一个新的纹理编号
	 
	 glGenTextures(1,&tex_id);
	 
	 if(tex_id==0)
	 {     
		 
		 free(pixels);
		 
		 fclose(pFile);
		 
		 return -1;
		 
     }
	 
     //绑定新的纹理,载入纹理并设置纹理参数;在绑定前,先获得原来绑定的纹理编号,以便在最后进行恢复
	 
     glGetIntegerv(GL_TEXTURE_BINDING_2D,&last_tex_id);
	 
	 glBindTexture(GL_TEXTURE_2D,tex_id);
	 
	 glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
	 
	 glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
	 
	 glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_REPEAT);
	 
	 glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,GL_REPEAT);
	 
	 glTexEnvf(GL_TEXTURE_ENV,GL_TEXTURE_ENV_MODE,GL_REPLACE);
	 
	 glTexImage2D(GL_TEXTURE_2D,0,GL_RGB,width,height,0,GL_BGR_EXT,GL_UNSIGNED_BYTE,pixels);
     
	 glBindTexture(GL_TEXTURE_2D,last_tex_id);
	 
	 //之前为pixels分配的内存可在使用glTexImage2D以后释放
	 
	 free(pixels);
	 
	 return tex_id;
	 
}

我们可以看到这个函数进行了部分优化,如果纹理图片较大,我们在加载时进行了进行像素缩放,这样可以有效减小内存压力;同时通过文件进行读取像素,还可以有效控制加载节奏。如果待加载文件较大,我们就可以分多次少量加载,减少CPU开支,这在无缝地图的纹理加载时极为重要。我们将在后期实现相关功能,如果大家有需要可以自行了解一下JPG和PNG的文件读取方式。

7、小结

以上介绍的三种图片加载方式各有利弊,但基本满足我们一些涉及需求,我们这节做好的图像数据加载的铺垫工作,下一节我们就可以使用纹理进行贴图,进入到一个真实的三维世界。

  • 8
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zhooyu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值