OpenGL实现3D游戏编程【连载4】——纹理加载常用的三种方式
欢迎来到zhooyu的专栏。
个人主页:【zhooyu】
文章专栏:【OpenGL实现3D游戏编程】
本专栏内容:
我们从游戏的角度出发,用C++去了解一下游戏中的功能都是怎么实现的。这一切还是要从自己玩游戏开始说起,此前就玩过一下3D游戏,当时就被游戏里的一些画面和设置深深的吸引了,同时游戏里还有很多很有趣的设定,比如,玩家的视角是怎么移动的?崎岖不平的地图是怎样制作的?人物和物体、地面的碰撞是怎样检测的?鼠标是怎样选中眼前的物体的?魔法技能是怎样释放的?不用加载进度条的无缝世界地图是怎么实现的?带着这些疑问,我们走进了一个OpenGL世界的3D世界。
1、什么是纹理
如果我们想让物体更加真实,就得有足够多的顶点,且指定足够多的颜色,这样会产生很多额外开销,因为每个模型需要很多顶点,而每个顶点又需要颜色属性。为了方便且减少开销,我们更习惯使用纹理来绘制更多的模型,纹理就像一张2D的贴图,把它贴在图像或模型上来增加物体的细节。这样物体细节取决于设计师的美工,而程序员则不必指定更多的顶点。
OpenGL支持多种图像格式,如BMP、JPEG、PNG等。根据实际需求选择合适的格式,当然也可以以上格式都使用。
2、图片的加载
在OpenGL中,加载图像作为纹理的一般步骤如下:
- 加载图像数据:使用系统自带的图像加载函数(方法一)、或合适的图像处理库(方法二)、或者自己编写的加载函数(方法三)加载图像数据到内存中。个纹理对象,并绑定到一个纹理单元上。
- 设置纹理参数:通过设置纹理参数来控制纹理的采样方式。例如,设置纹理过滤方式、纹理环绕方式等。
- 将图像数据传输到纹理对象:使用glTexImage2D函数将图像数据传输到纹理对象中。这需要指定图像的宽度、高度、像素格式等信息。
- 释放图像数据:在纹理对象创建成功后,可以释放已加载的图像数据,以节省内存空间。
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、小结
以上介绍的三种图片加载方式各有利弊,但基本满足我们一些涉及需求,我们这节做好的图像数据加载的铺垫工作,下一节我们就可以使用纹理进行贴图,进入到一个真实的三维世界。