OpenGL入门学习[十一]
我们在前一课中,学习了简单的像素操作,这意味着我们可以使用各种各样的BMP文件来丰富程序的显示效果,于是我们的OpenGL图形程序也不再像以前总是只显示几个多边形那样单调了。——但是这还不够。虽然我们可以将像素数据按照矩形进行缩小和放大,但是还不足以满足我们的要求。例如要将一幅世界地图绘制到一个球体表面,只使用glPixelZoom这样的函数来进行缩放显然是不够的。OpenGL纹理映射功能支持将一些像素数据经过变换(即使是比较不规则的变换)将其附着到各种形状的多边形表面。纹理映射功能十分强大,利用它可以实现目前计算机动画中的大多数效果,但是它也很复杂,我们不可能一次性的完全讲解。这里的课程只是关于二维纹理的简单使用。但即使是这样,也会使我们的程序在显示效果上迈出一大步。
下面几张图片说明了纹理的效果。前两张是我们需要的纹理,后一张是我们使用纹理后,利用OpenGL所产生出的效果。
纹理的使用是非常复杂的。因此即使是入门教程,在编写时我也多次进行删改,很多东西都被精简掉了,但本课的内容仍然较多,大家要有一点心理准备~
1、启用纹理和载入纹理
就像我们曾经学习过的OpenGL光照、混合等功能一样。在使用纹理前,必须启用它。OpenGL支持一维纹理、二维纹理和三维纹理,这里我们仅介绍二维纹理。可以使用以下语句来启用和禁用二维纹理:
使用纹理前,还必须载入纹理。利用glTexImage2D函数可以载入一个二维的纹理,该函数有多达九个参数(虽然某些参数我们可以暂时不去了解),现在分别说明如下:
第一个参数为指定的目标,在我们的入门教材中,这个参数将始终使用GL_TEXTURE_2D。
第二个参数为“多重细节层次”,现在我们并不考虑多重纹理细节,因此这个参数设置为零。
第三个参数有两种用法。在OpenGL 1.0,即最初的版本中,使用整数来表示颜色分量数目,例如:像素数据用RGB颜色表示,总共有红、绿、蓝三个值,因此参数设置为3,而如果像素数据是用RGBA颜色表示,总共有红、绿、蓝、alpha四个值,因此参数设置为4。而在后来的版本中,可以直接使用GL_RGB或GL_RGBA来表示以上情况,显得更直观(并带来其它一些好处,这里暂时不提)。注意:虽然我们使用Windows的BMP文件作为纹理时,一般是蓝色的像素在最前,其真实的格式为GL_BGR而不是GL_RGB,在数据的顺序上有所不同,但因为同样是红、绿、蓝三种颜色,因此这里仍然使用GL_RGB。(如果使用GL_BGR,OpenGL将无法识别这个参数,造成错误)
2、纹理坐标
我们先来回忆一下之前学过的一点内容:
当我们绘制一个三角形时,只需要指定三个顶点的颜色。三角形中其它各点的颜色不需要我们指定,这些点的颜色是OpenGL自己通过计算得到的。
在我们学习OpneGL光照时,法线向量、材质的指定,都是只需要在顶点处指定一下就可以了,其它地方的法线向量和材质都是OpenGL自己通过计算去获得。
纹理的使用方法也与此类似。只要指定每一个顶点在纹理图象中所对应的像素位置,OpenGL就会自动计算顶点以外的其它点在纹理图象中所对应的像素位置。
这听起来比较令人迷惑。我们可以这样类比一下:
在绘制一条线段时,我们设置其中一个端点为红色,另一个端点为绿色,则OpenGL会自动计算线段中其它各像素的颜色,如果是使用glShadeMode(GL_SMOOTH);,则最终会形成一种渐变的效果(例如线段中点,就是红色和绿色的中间色)。
类似的,在绘制一条线段时,我们设置其中一个端点使用“纹理图象中最左下角的颜色”作为它的颜色,另一个端点使用“纹理图象中最右上角的颜色”作为它的颜色,则OpenGL会自动在纹理图象中选择合适位置的颜色,填充到线段的各个像素(例如线段中点,可能就是选择纹理图象中央的那个像素的颜色)。
我们在类比时,使用了“纹理图象中最左下角的颜色”这种说法。但这种说法在很多时候不够精确,我们需要一种精确的方式来表示我们究竟使用纹理中的哪个像素。纹理坐标也就是因为这样的要求而产生的。以二维纹理为例,规定纹理最左下角的坐标为(0, 0),最右上角的坐标为(1, 1),于是纹理中的每一个像素的位置都可以用两个浮点数来表示(三维纹理会用三个浮点数表示,一维纹理则只用一个即可)。
使用glTexCoord*系列函数来指定纹理坐标。这些函数的用法与使用glVertex*系列函数来指定顶点坐标十分相似。例如:glTexCoord2f(0.0f, 0.0f);指定使用(0, 0)纹理坐标。
通常,每个顶点使用不同的纹理,于是下面这样形式的代码是比较常见的。
glTexCoord2f( /* ... */ ); glVertex3f( /* ... */ );
glTexCoord2f( /* ... */ ); glVertex3f( /* ... */ );
/* ... */
glEnd();
当我们用一个坐标表示顶点在三维空间的位置时,可以使用glRotate*等函数来对坐标进行转换。纹理坐标也可以进行这种转换。只要使用glMatrixMode(GL_TEXTURE);,就可以切换到纹理矩阵(另外还有透视矩阵GL_PROJECTION和模型视图矩阵GL_MODELVIEW,详细情况在第五课有讲述),然后glRotate*,glScale*,glTranslate*等操作矩阵的函数就可以用来处理“对纹理坐标进行转换”的工作了。在简单应用中,可能不会对矩阵进行任何变换,这样考虑问题会比较简单。
3、纹理参数
到这里,入门所需要掌握的所有难点都被我们掌握了。但是,我们的知识仍然是不够的,如果仅利用现有的知识去使用纹理的话,你可能会发现纹理完全不起作用。这是因为在使用纹理前还有某些参数是必须设置的。
使用glTexParameter*系列函数来设置纹理参数。通常需要设置下面四个参数:
GL_TEXTURE_MAG_FILTER:指当纹理图象被使用到一个大于它的形状上时(即:有可能纹理图象中的一个像素会被应用到实际绘制时的多个像素。例如将一幅256*256的纹理图象应用到一个512*512的正方形),应该如何处理。可选择的设置有GL_NEAREST和GL_LINEAR,前者表示“使用纹理中坐标最接近的一个像素的颜色作为需要绘制的像素颜色”,后者表示“使用纹理中坐标最接近的若干个颜色,通过加权平均算法得到需要绘制的像素颜色”。前者只经过简单比较,需要运算较少,可能速度较快,后者需要经过加权平均计算,其中涉及除法运算,可能速度较慢(但如果有专门的处理硬件,也可能两者速度相同)。从视觉效果上看,前者效果较差,在一些情况下锯齿现象明显,后者效果会较好(但如果纹理图象本身比较大,则两者在视觉效果上就会比较接近)。
GL_TEXTURE_MIN_FILTER:指当纹理图象被使用到一个小于(或等于)它的形状上时(即有可能纹理图象中的多个像素被应用到实际绘制时的一个像素。例如将一幅256*256的纹理图象应用到一个128*128的正方形),应该如何处理。可选择的设置有GL_NEAREST,GL_LINEAR,GL_NEAREST_MIPMAP_NEAREST,GL_NEAREST_MIPMAP_LINEAR,GL_LINEAR_MIPMAP_NEAREST和GL_LINEAR_MIPMAP_LINEAR。其中后四个涉及到mipmap,现在暂时不需要了解。前两个选项则和GL_TEXTURE_MAG_FILTER中的类似。此参数似乎是必须设置的(在我的计算机上,不设置此参数将得到错误的显示结果,但我目前并没有找到根据)。
GL_TEXTURE_WRAP_S:指当纹理坐标的第一维坐标值大于1.0或小于0.0时,应该如何处理。基本的选项有GL_CLAMP和GL_REPEAT,前者表示“截断”,即超过1.0的按1.0处理,不足0.0的按0.0处理。后者表示“重复”,即对坐标值加上一个合适的整数(可以是正数或负数),得到一个在[0.0, 1.0]范围内的值,然后用这个值作为新的纹理坐标。例如:某二维纹理,在绘制某形状时,一像素需要得到纹理中坐标为(3.5, 0.5)的像素的颜色,其中第一维的坐标值3.5超过了1.0,则在GL_CLAMP方式中将被转化为(1.0, 0.5),在GL_REPEAT方式中将被转化为(0.5, 0.5)。在后来的OpenGL版本中,又增加了新的处理方式,这里不做介绍。如果不指定这个参数,则默认为GL_REPEAT。
GL_TEXTURE_WRAP_T:指当纹理坐标的第二维坐标值大于1.0或小于0.0时,应该如何处理。选项与GL_TEXTURE_WRAP_S类似,不再重复。如果不指定这个参数,则默认为GL_REPEAT。
设置参数的代码如下所示:
4、纹理对象
前面已经提到过,载入一幅纹理所需要的时间是比较多的。因此应该尽量减少载入纹理的次数。如果只有一幅纹理,则应该在第一次绘制前就载入它,以后就不需要再次载入了。这点与glDrawPixels函数很不相同。每次使用glDrawPixels函数,都需要把像素数据重新载入一次,因此用glDrawPixels函数来反复绘制图象的效率是较低的(如果只绘制一次,则不会有此问题),使用纹理来反复绘制图象是可取的做法。
但是,在每次绘制时要使用两幅或更多幅的纹理时,这个办法就行不通了。你可能会编写下面的代码:
// 使用第一幅纹理
glTexImage2D( /* ... */ ); // 载入第二幅纹理
// 使用第二幅纹理
// 当纹理的数量增加时,这段代码会变得更加复杂。
在绘制动画时,由于每秒钟需要将画面绘制数十次,因此如果使用上面的代码,就会反复载入纹理,这对计算机是非常大的负担,以目前的个人计算机配置来说,根本就无法让动画能够流畅的运行。因此,需要有一种机制,能够在不同的纹理之间进行快速的切换。
纹理对象正是这样一种机制。我们可以把每一幅纹理(包括纹理的像素数据、纹理大小等信息,也包括了前面所讲的纹理参数)放到一个纹理对象中,通过创建多个纹理对象来达到同时保存多幅纹理的目的。这样一来,在第一次使用纹理前,把所有的纹理都载入,然后在绘制时只需要指明究竟使用哪一个纹理对象就可以了。
使用纹理对象和使用显示列表有相似之处:使用一个正整数来作为纹理对象的编号。在使用前,可以调用glGenTextures来分配纹理对象。该函数有两种比较常见的用法:
glGenTextures(1, &texture_ID); // 分配一个纹理对象的编号
或者:
glGenTextures(5, texture_ID_list); // 分配5个纹理对象的编号
零是一个特殊的纹理对象编号,表示“默认的纹理对象”,在分配正确的情况下,glGenTextures不会分配这个编号。与glGenTextures对应的是glDeleteTextures,用于销毁一个纹理对象。
在分配了纹理对象编号后,使用glBindTexture函数来指定“当前所使用的纹理对象”。然后就可以使用glTexImage*系列函数来指定纹理像素、使用glTexParameter*系列函数来指定纹理参数、使用glTexCoord*系列函数来指定纹理坐标了。如果不使用glBindTexture函数,那么glTexImage*、glTexParameter*、glTexCoord*系列函数默认在一个编号为0的纹理对象上进行操作。glBindTexture函数有两个参数,第一个参数是需要使用纹理的目标,因为我们现在只学习二维纹理,所以指定为GL_TEXTURE_2D,第二个参数是所使用的纹理的编号。
使用多个纹理对象,就可以使OpenGL同时保存多个纹理。在使用时只需要调用glBindTexture函数,在不同纹理之间进行切换,而不需要反复载入纹理,因此动画的绘制速度会有非常明显的提升。典型的代码如下所示:
glGenTextures( /* ... */ );
glBindTexture(GL_TEXTURE_2D, texture_ID_1);
// 载入第一幅纹理
glBindTexture(GL_TEXTURE_2D, texture_ID_2);
// 载入第二幅纹理
glBindTexture(GL_TEXTURE_2D, texture_ID_1); // 指定第一幅纹理
// 使用第一幅纹理
glBindTexture(GL_TEXTURE_2D, texture_ID_2); // 指定第二幅纹理
// 使用第二幅纹理
提示:纹理对象是从OpenGL 1.1版开始才有的,最旧版本的OpenGL 1.0并没有处理纹理对象的功能。不过,我想各位的机器不会是比OpenGL 1.1更低的版本(Windows 95就自带了OpenGL 1.1版本,遗憾的是,Microsoft对OpenGL的支持并不积极,Windows XP也还采用1.1版本。据说Vista使用的是OpenGL 1.4版。当然了,如果安装显卡驱动的话,现在的主流显卡一般都附带了适用于该显卡的OpenGL 1.4版或更高版本),所以这个问题也就不算是问题了。
5、示例程序
纹理入门所需要掌握的知识点就介绍到这里了。但是如果不实际动手操作的话,也是不可能真正掌握的。下面我们来看看本课开头的那个纹理效果是如何实现的吧。
因为代码比较长,我把它拆分成了三段,大家如果要编译的话,应该把三段代码按顺序连在一起编译。如果要运行的话,除了要保证有一个名称为dummy.bmp,图象大小为1*1的24位BMP文件,还要把本课开始的两幅纹理图片保存到正确位置(一幅名叫ground.bmp,另一幅名叫wall.bmp。注意:我为了节省网络空间,把两幅图片都转成jpg格式了,读者把图片保存到本地后,需要把它们再转化为BMP格式。可以使用Windows XP带的画图程序中的“另存为”功能完成这一转换)。
第一段代码如下。其中的主体——grab函数,是我们在第十课介绍过的,这里仅仅是抄过来用一下,目的是为了将最终效果图保存到一个名字叫grab.bmp的文件中。(当然了,为了保证程序的正确运行,那个大小为1*1的dummy.bmp文件仍然是必要的,参见第十课)
#define WindowHeight 400
#define WindowTitle "OpenGL纹理测试"
#include <gl/glut.h>
#include <stdio.h>
#include <stdlib.h>
/* 函数grab
* 抓取窗口中的像素
* 假设窗口宽度为WindowWidth,高度为WindowHeight
*/
#define BMP_Header_Length 54
void grab(void)
{
FILE* pDummyFile;
FILE* pWritingFile;
GLubyte* pPixelData;
GLubyte BMP_Header[BMP_Header_Length];
GLint i, j;
GLint PixelDataLength;
// 计算像素数据的实际长度
i = WindowWidth * 3; // 得到每一行的像素数据长度
while( i%4 != 0 ) // 补充数据,直到i是的倍数
++i; // 本来还有更快的算法,
// 但这里仅追求直观,对速度没有太高要求
PixelDataLength = i * WindowHeight;
// 分配内存和打开文件
pPixelData = (GLubyte*)malloc(PixelDataLength);
if( pPixelData == 0 )
exit(0);
pDummyFile = fopen("dummy.bmp", "rb");
if( pDummyFile == 0 )
exit(0);
pWritingFile = fopen("grab.bmp", "wb");
if( pWritingFile == 0 )
exit(0);
// 读取像素
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
glReadPixels(0, 0, WindowWidth, WindowHeight,
GL_BGR_EXT, GL_UNSIGNED_BYTE, pPixelData);
// 把dummy.bmp的文件头复制为新文件的文件头
fread(BMP_Header, sizeof(BMP_Header), 1, pDummyFile);
fwrite(BMP_Header, sizeof(BMP_Header), 1, pWritingFile);
fseek(pWritingFile, 0x0012, SEEK_SET);
i = WindowWidth;
j = WindowHeight;
fwrite(&i, sizeof(i), 1, pWritingFile);
fwrite(&j, sizeof(j), 1, pWritingFile);
// 写入像素数据
fseek(pWritingFile, 0, SEEK_END);
fwrite(pPixelData, PixelDataLength, 1, pWritingFile);
// 释放内存和关闭文件
fclose(pDummyFile);
fclose(pWritingFile);
free(pPixelData);
}
第二段代码是我们的重点。它包括两个函数。其中power_of_two比较简单,虽然实现手段有点奇特,但也并非无法理解(即使真的无法理解,读者也可以给出自己的解决方案,用一些循环以及多使用一些位操作也没关系。反正,这里不是重点啦)。另一个load_texture函数却是重头戏:打开BMP文件、读取其中的高度和宽度信息、计算像素数据所占的字节数、为像素数据分配空间、读取像素数据、对像素图象进行缩放(如果必要的话)、分配新的纹理编号、填写纹理参数、载入纹理,所有的功能都在同一个函数里面完成了。为了叙述方便,我把所有的解释都放在了注释里。
* 检查一个整数是否为2的整数次方,如果是,返回1,否则返回0
* 实际上只要查看其二进制位中有多少个,如果正好有1个,返回1,否则返回0
* 在“查看其二进制位中有多少个”时使用了一个小技巧
* 使用n &= (n-1)可以使得n中的减少一个(具体原理大家可以自己思考)
*/
int power_of_two(int n)
{
if( n <= 0 )
return 0;
return (n & (n-1)) == 0;
}
/* 函数load_texture
* 读取一个BMP文件作为纹理
* 如果失败,返回0,如果成功,返回纹理编号
*/
GLuint load_texture(const char* file_name)
{
GLint width, height, total_bytes;
GLubyte* pixels = 0;
GLuint last_texture_ID, texture_ID = 0;
// 打开文件,如果失败,返回
FILE* pFile = fopen(file_name, "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);
// 计算每行像素所占字节数,并根据此数据计算总像素字节数
{
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中
// 如果图象的宽度和高度不是的整数次方,则需要进行缩放
// 这里并没有检查OpenGL版本,出于对版本兼容性的考虑,按旧版本处理
// 另外,无论是旧版本还是新版本,
// 当图象的宽度和高度超过当前OpenGL实现所支持的最大值时,也要进行缩放
{
GLint max;
glGetIntegerv(GL_MAX_TEXTURE_SIZE, &max);
if( !power_of_two(width)
|| !power_of_two(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, &texture_ID);
if( texture_ID == 0 )
{
free(pixels);
fclose(pFile);
return 0;
}
// 绑定新的纹理,载入纹理并设置纹理参数
// 在绑定前,先获得原来绑定的纹理编号,以便在最后进行恢复
glGetIntegerv(GL_TEXTURE_BINDING_2D, &last_texture_ID);
glBindTexture(GL_TEXTURE_2D, texture_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_texture_ID);
// 之前为pixels分配的内存可在使用glTexImage2D以后释放
// 因为此时像素数据已经被OpenGL另行保存了一份(可能被保存到专门的图形硬件中)
free(pixels);
return texture_ID;
}
第三段代码是关于显示的部分,以及main函数。注意,我们只在main函数中读取了两幅纹理,并把它们保存在各自的纹理对象中,以后就再也不载入纹理。每次绘制时使用glBindTexture在不同的纹理对象中切换。另外,我们使用了超过1.0的纹理坐标,由于GL_TEXTURE_WRAP_S和GL_TEXTURE_WRAP_T参数都被设置为GL_REPEAT,所以得到的效果就是纹理像素的重复,有点向地板砖的花纹那样。读者可以试着修改“墙”的纹理坐标,将5.0修改为10.0,看看效果有什么变化。
*/
GLuint texGround;
GLuint texWall;
void display(void)
{
// 清除屏幕
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// 设置视角
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective(75, 1, 1, 21);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
gluLookAt(1, 5, 5, 0, 0, 0, 0, 0, 1);
// 使用“地”纹理绘制土地
glBindTexture(GL_TEXTURE_2D, texGround);
glBegin(GL_QUADS);
glTexCoord2f(0.0f, 0.0f); glVertex3f(-8.0f, -8.0f, 0.0f);
glTexCoord2f(0.0f, 5.0f); glVertex3f(-8.0f, 8.0f, 0.0f);
glTexCoord2f(5.0f, 5.0f); glVertex3f(8.0f, 8.0f, 0.0f);
glTexCoord2f(5.0f, 0.0f); glVertex3f(8.0f, -8.0f, 0.0f);
glEnd();
// 使用“墙”纹理绘制栅栏
glBindTexture(GL_TEXTURE_2D, texWall);
glBegin(GL_QUADS);
glTexCoord2f(0.0f, 0.0f); glVertex3f(-6.0f, -3.0f, 0.0f);
glTexCoord2f(0.0f, 1.0f); glVertex3f(-6.0f, -3.0f, 1.5f);
glTexCoord2f(5.0f, 1.0f); glVertex3f(6.0f, -3.0f, 1.5f);
glTexCoord2f(5.0f, 0.0f); glVertex3f(6.0f, -3.0f, 0.0f);
glEnd();
// 旋转后再绘制一个
glRotatef(-90, 0, 0, 1);
glBegin(GL_QUADS);
glTexCoord2f(0.0f, 0.0f); glVertex3f(-6.0f, -3.0f, 0.0f);
glTexCoord2f(0.0f, 1.0f); glVertex3f(-6.0f, -3.0f, 1.5f);
glTexCoord2f(5.0f, 1.0f); glVertex3f(6.0f, -3.0f, 1.5f);
glTexCoord2f(5.0f, 0.0f); glVertex3f(6.0f, -3.0f, 0.0f);
glEnd();
// 交换缓冲区,并保存像素数据到文件
glutSwapBuffers();
grab();
}
int main(int argc, char* argv[])
{
// GLUT初始化
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA);
glutInitWindowPosition(100, 100);
glutInitWindowSize(WindowWidth, WindowHeight);
glutCreateWindow(WindowTitle);
glutDisplayFunc(&display);
// 在这里做一些初始化
glEnable(GL_DEPTH_TEST);
glEnable(GL_TEXTURE_2D);
texGround = load_texture("ground.bmp");
texWall = load_texture("wall.bmp");
// 开始显示
glutMainLoop();
return 0;
}
小结:
本课介绍了OpenGL纹理的入门知识。
利用纹理可以进行比glReadPixels和glDrawPixels更复杂的像素绘制,因此可以实现很多精彩的效果。
本课只涉及了二维纹理。OpenGL还支持一维和三维纹理,其原理是类似的。
在使用纹理前,要启用纹理。并且,还需要将像素数据载入到纹理中。注意纹理的宽度和高度,目前很多OpenGL的实现都还要求其值为2的整数次方,如果纹理图象本身并不满足这个条件,可以使用gluScaleImage函数来进行缩放。为了正确的使用纹理,需要设置纹理参数。
载入纹理所需要的系统开销是比较大的,应该尽可能减少载入纹理的次数。如果程序中只使用一幅纹理,则只在第一次使用前载入,以后不必重新载入。如果程序中要使用多幅纹理,不应该反复载入它们,而应该将每个纹理都用一个纹理对象来保存,并使用glBindTextures在各个纹理之间进行切换。
本课还给出了一个程序(到目前为止,它是这个OpenGL教程系列中所给出的程序中最长的)。该程序演示了纹理的基本使用方法,本课程涉及到的几乎所有内容都被包括其中,这是对本课中文字说明的一个补充。如果读者有什么不明白的地方,也可以以这个程序作为参考。