NeHe OpenGL教程第二十四课,DancingWind翻译

Nehe SDK

第24课

扩展,剪裁和TGA图像文件的加载:

在这一课里,你将学会如何读取你显卡支持的OpenGL的扩展,并在你指定的剪裁区域把它显示出来。

这个教程有一些难度,但它会让你学到很多东西。我听到很多朋友问我扩展方面的内容和怎样找到它们。这个教程将交给你这
一切。
我将教会你怎样滚动屏幕的一部分和怎样绘制直线,最重要的是从这一课起,我们将不使用AUX库,以及*.bmp文件。我将告诉你如何使用Targa(TGA)图像文件。因为它简单并且支持alpha通道,它可以使你更容易的创建酷的效果。

接下来我们要做的第一件事就是不包含glaux.h头文件和glaux.lib库。另外,在使用glaux库时,经常会发生一些可疑的警告,现在我们可以测定告别它了。

#include	<stdarg.h>								// 处理可变参数的函数的头文件
#include	<string.h>								// 处理字符串的头文件
接下来我们添加一些变量,第一个为滚动参数。第二给变量记录扩展的个数,swidth和sheight记录剪切矩形的大小。base为字体显示列表的开始值。
int		scroll;									// 用来滚动屏幕
int		maxtokens;								// 保存扩展的个数
int		swidth;									// 剪裁宽度
int		sheight;									// 剪裁高度

GLuint		base;									// 字符显示列表的开始值
现在我们创建一个数据结构用来保存TGA文件,接着我们使用这个结构来加载纹理。
typedef	struct										// 创建加载TGA图像文件结构
{
	GLubyte	*imageData;								// 图像数据指针
	GLuint	bpp;									// 每个数据所占的位数(必须为24或32)
	GLuint	width;									// 图像宽度
	GLuint	height;									// 图像高度
	GLuint	texID;									// 纹理的ID值
} TextureImage;										// 结构名称

TextureImage	textures[1];								// 保存一个纹理
这个部分的代码将要加载一个TGA文件并把它转换为纹理。必须注意的是这部分代码只能加载24/32位的不压缩的TGA文件。
这个函数包含两个参数,一个保存载入的图像,一个为将载入的文件名。
TGA文件包含一个12个字节的文件头,载入图像后,我们用type来设置图像中像素格式在OpenGL中的对应。如果是24位的图像我们使用GL_RGB,如果是32位的图像我们使用GL_RGBA。
bool LoadTGA(TextureImage *texture, char *filename)					// 把TGA文件加载入内存
{
	GLubyte		TGAheader[12]={0,0,2,0,0,0,0,0,0,0,0,0};			// 无压缩的TGA文件头
	GLubyte		TGAcompare[12];						// 保存读入的文件头信息
	GLubyte		header[6];						// 保存最有用的图像信息,宽,高,位深
	GLuint		bytesPerPixel;						// 记录每个颜色所占用的字节数
	GLuint		imageSize;						// 记录文件大小
	GLuint		temp;							// 临时变量
	GLuint		type=GL_RGBA;						// 设置默认的格式为GL_RGBA,即32位图像

下面这个函数读取TGA文件,并记录文件信息。TGA文件格式如下所示:

Tga图像格式
无颜色表 rgb 图像

偏移 长度 描述 32位常用图像文件各个字节的值
0 1 指出图像信息字段的长度,其取值范围是 0 到 255 ,当它为 0 时表示没有图像的信息字段。 0
1 1 是否使用颜色表,0 表示没有颜色表,1 表示颜色表存在 0
2 1 该字段总为 2。图像类型码,tga一共有6种格式,2表示无颜色表 rgb 图像 2
3 5 颜色表规格,总为0。 0
4 0
5 0
6 0
7 0
8       10           图像规格说明 开始
8 2 图像 x 坐标起始位置,一般为0 0
9
10 2 图像 y 坐标起始位置,一般为0 0
11
12 2 图像宽度,以像素为单位 256
13
14 2 图像高度,以像素为单位 256
15
16 1 图像每像素存储占用位(bit)数 32
17 1

图像描述符字节
bits 3-0 - 每像素对应的属性位的位数,对于 TGA 24,该值为 0
bit 4 - 保留,必须为 0
bit 5 - 屏幕起始位置标志,0 = 原点在左下角,1 = 原点在左上角
一般这个字节设为0x00即可

00100000(2)
18 可变 图像数据域
这里存储了(宽度)x(高度)个像素,每个像素中的 rgb 色值该色值包含整数个字节
...

如果一切顺利,读取文件后关闭文件。

	FILE *file = fopen(filename, "rb");						// 打开一个TGA文件

	if(	file==NULL ||							// 文件存在么?
		fread(TGAcompare,1,sizeof(TGAcompare),file)!=sizeof(TGAcompare) ||	// 是否包含12个字节的文件头?
		memcmp(TGAheader,TGAcompare,sizeof(TGAheader))!=0		||	// 是否是我们需要的格式?
		fread(header,1,sizeof(header),file)!=sizeof(header))			// 如果是读取下面六个图像信息
	{
		if (file == NULL)							// 文件不存在返回错误
			return false;							
		else
		{
			fclose(file);						// 关闭文件返回错误
			return false;							
		}
	}
下面的代码记录文件的宽度和高度,并判断文件是否为24位/32位TGA文件。
	texture->width  = header[1] * 256 + header[0];					// 记录文件高度
	texture->height = header[3] * 256 + header[2];					// 记录文件宽度

 	if(	texture->width	<=0	||						// 宽度是否小于0
		texture->height	<=0	||						// 高度是否小于0
		(header[4]!=24 && header[4]!=32))						// TGA文件是24/32位?
	{
		fclose(file);								// 如果失败关闭文件,返回错误
		return false;								
	}
下面的代码记录文件的位深和加载它需要的内存大小
	texture->bpp	= header[4];							// 记录文件的位深
	bytesPerPixel	= texture->bpp/8;							// 记录每个象素所占的字节数
	imageSize	= texture->width*texture->height*bytesPerPixel;				// 计算TGA文件加载所需要的内存大小
下面的代码为图像数据分配内存并载入它
	texture->imageData=(GLubyte *)malloc(imageSize);				// 分配内存去保存TGA数据

	if(	texture->imageData==NULL ||						// 系统是否分配了足够的内存?
		fread(texture->imageData, 1, imageSize, file)!=imageSize)		// 是否成功读入内存?
	{
		if(texture->imageData!=NULL)					// 是否有数据被加载
			free(texture->imageData);					// 如果是,则释放载入的数据

		fclose(file);							// 关闭文件
		return false;							// 返回错误
	}
TGA文件中,颜色的存储顺序为BGR,而OpenGL中颜色的顺序为RGB,所以我们需要交换每个象素中R和B的值。如果一切顺利,TGA文件中的图像数据将按照OpenGL的要求存储在内存中了。
	for(GLuint i=0; i<int(imageSize); i+=bytesPerPixel)				// 循环所有的像素
	{									// 交换R和B的值
		temp=texture->imageData[i];						
		texture->imageData[i] = texture->imageData[i + 2];		
		texture->imageData[i + 2] = temp;				
	}

	fclose (file);								// 关闭文件
下面的代码创建一个纹理,并设置过滤方式为线性
	// 创建纹理
	glGenTextures(1, &texture;[0].texID);						// 创建纹理,并记录纹理ID

	glBindTexture(GL_TEXTURE_2D, texture[0].texID);				// 绑定纹理
	glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);		// 设置过滤器为线性过滤
	glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);		
判断图像的位数是否为24,如果是则设置类型为GL_RGB
	if (texture[0].bpp==24)								// 是否为24位图像?
	{
		type=GL_RGB;								// 如果是设置类型为GL_RGB
	}
下面的代码在OpenGL中创建一个纹理
	glTexImage2D(GL_TEXTURE_2D, 0, type, texture[0].width, texture[0].height, 0, type, GL_UNSIGNED_BYTE, texture[0].imageData);

	return true;									// 纹理绑定完成,成功返回
}
下面的代码是从图像创建字体的典型的方法,这些代码将包含在后面的课程中,以显示文字。
只有一个不同的地方,纹理0用来保存字符图像。
GLvoid BuildFont(GLvoid)								// 创建字体显示列表
{
	base=glGenLists(256);							// 创建256个显示列表
	glBindTexture(GL_TEXTURE_2D, textures[0].texID);				// 绑定纹理
	for (int loop1=0; loop1<256; loop1++)					// 循环创建256个显示列表
	{
		float cx=float(loop1%16)/16.0f;					// 当前字符的X位置
		float cy=float(loop1/16)/16.0f;					// 当前字符的Y位置

		glNewList(base+loop1,GL_COMPILE);					// 开始创建显示列表
			glBegin(GL_QUADS);						// 创建一个四边形用来包含字符图像
				glTexCoord2f(cx,1.0f-cy-0.0625f);			// 左下方纹理坐标
				glVertex2d(0,16);					// 左下方坐标
				glTexCoord2f(cx+0.0625f,1.0f-cy-0.0625f);		// 右下方纹理坐标
				glVertex2i(16,16);					// 右下方坐标
				glTexCoord2f(cx+0.0625f,1.0f-cy-0.001f);		// 右上方纹理坐标
				glVertex2i(16,0);					// 右上方坐标
				glTexCoord2f(cx,1.0f-cy-0.001f);			// 左上方纹理坐标
				glVertex2i(0,0);					// 左上方坐标
			glEnd();							// 四边形创建完毕
			glTranslated(14,0,0);					// 向右移动14个单位
		glEndList();							// 结束创建显示列表
	}									
}
下面的函数用来删除显示字符的显示列表
GLvoid KillFont(GLvoid)
{
	glDeleteLists(base,256);							// 从内存中删除256个显示列表
}
glPrint函数只有一点变化,我们在Y轴方向把字符拉长一倍
GLvoid glPrint(GLint x, GLint y, int set, const char *fmt, ...)
{
	char	text[1024];							// 保存我们的字符
	va_list	ap;								// 指向第一个参数

	if (fmt == NULL)								// 如果要显示的字符为空则返回
		return;									

	va_start(ap, fmt);								// 开始分析参数,并把结果写入到text中
	    vsprintf(text, fmt, ap);							
	va_end(ap);									

	if (set>1)								// 如果字符集大于1则使用第二个字符集
	{
		set=1;									
	}

	glEnable(GL_TEXTURE_2D);							// 使用纹理映射
	glLoadIdentity();								// 重置视口矩阵
	glTranslated(x,y,0);							// 平移到(x,y,0)处
	glListBase(base-32+(128*set));						// 选择字符集

	glScalef(1.0f,2.0f,1.0f);							// 沿Y轴放大一倍

	glCallLists(strlen(text),GL_UNSIGNED_BYTE, text);				// 把字符写入到屏幕
	glDisable(GL_TEXTURE_2D);							// 禁止纹理映射 
}
窗口改变大小的函数使用正投影,把视口范围设置为(0,0)-(640,480)
GLvoid ReSizeGLScene(GLsizei width, GLsizei height)
{
	swidth=width;									// 设置剪切矩形为窗口大小
	sheight=height;									
	if (height==0)									// 防止高度为0时,被0除
	{
		height=1;								
	}
	glViewport(0,0,width,height);							// 设置窗口可见区
	glMatrixMode(GL_PROJECTION);							
	glLoadIdentity();								
	glOrtho(0.0f,640,480,0.0f,-1.0f,1.0f);						// 设置视口大小为640x480
	glMatrixMode(GL_MODELVIEW);							
	glLoadIdentity();						
}
初始化操作非常简单,我们载入字体纹理,并创建字符显示列表,如果顺利,则成功返回。
int InitGL(GLvoid)				
{
	if (!LoadTGA(&textures;[0],"Data/Font.TGA"))						// 载入字体纹理
	{
		return false;								// 载入失败则返回
	}

	BuildFont();									// 创建字体

	glShadeModel(GL_SMOOTH);								// 使用平滑着色
	glClearColor(0.0f, 0.0f, 0.0f, 0.5f);						// 设置黑色背景
	glClearDepth(1.0f);								// 设置深度缓存中的值为1
	glBindTexture(GL_TEXTURE_2D, textures[0].texID);					// 绑定字体纹理

	return TRUE;									// 成功返回
}
绘制代码几乎是全新的:),token为一个指向字符串的指针,它将保存OpenGL扩展的全部字符串,cnt纪录扩展的个数。
接下来清楚背景,并显示OpenGL的销售商,实现它的公司和当前的版本。
int DrawGLScene(GLvoid)			
{
	char	*token;									// 保存扩展字符串
	int	cnt=0;									// 纪录扩展字符串的个数

	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);					// 清楚背景和深度缓存

	glColor3f(1.0f,0.5f,0.5f);								// 设置为红色
	glPrint(50,16,1,"Renderer");							
	glPrint(80,48,1,"Vendor");						
	glPrint(66,80,1,"Version");						
下面的代码显示OpenGL实现方面的相关信息,完成之后我们用蓝色在屏幕的下方写上“NeHe Productions”,当然你可以使用任何你想使用的字符,比如"DancingWind Translate"。
	glColor3f(1.0f,0.7f,0.4f);							// 设置为橘黄色
	glPrint(200,16,1,(char *)glGetString(GL_RENDERER));				// 显示OpenGL的实现组织
	glPrint(200,48,1,(char *)glGetString(GL_VENDOR));				// 显示销售商
	glPrint(200,80,1,(char *)glGetString(GL_VERSION));				// 显示当前版本

	glColor3f(0.5f,0.5f,1.0f);							// 设置为蓝色
	glPrint(192,432,1,"NeHe Productions");					// 在屏幕的底端写上NeHe Productions字符串
现在我们绘制显示扩展名的白色线框方块,并用一个更大的白色线框方块把所有的内容包围起来。
	glLoadIdentity();								// 重置模型变换矩阵
	glColor3f(1.0f,1.0f,1.0f);							// 设置为白色
	glBegin(GL_LINE_STRIP);					
		glVertex2d(639,417);							
		glVertex2d(  0,417);							
		glVertex2d(  0,480);							
		glVertex2d(639,480);						
		glVertex2d(639,128);					
	glEnd();								
	glBegin(GL_LINE_STRIP);					
		glVertex2d(  0,128);						
		glVertex2d(639,128);							
		glVertex2d(639,  1);						
		glVertex2d(  0,  1);							
		glVertex2d(  0,417);						
	glEnd();									
glScissor函数用来设置剪裁区域,如果启用了GL_SCISSOR_TEST,绘制的内容只能在剪裁区域中显示。
下面的代码设置窗口的中部为剪裁区域,并获得扩展名字符串。
	glScissor(1	,int(0.135416f*sheight),swidth-2,int(0.597916f*sheight));	// 定义剪裁区域
	glEnable(GL_SCISSOR_TEST);							// 使用剪裁测试

	char* text=(char*)malloc(strlen((char *)glGetString(GL_EXTENSIONS))+1);		// 为保存OpenGL扩展的字符串分配内存空间
	strcpy (text,(char *)glGetString(GL_EXTENSIONS));				// 返回OpenGL扩展字符串
下面我们创建一个循环,循环显示每个扩展名,并纪录扩展名的个数
	token=strtok(text," ");								// 按空格分割text字符串,并把分割后的字符串保存在token中
	while(token!=NULL)									// 如果token不为NULL
	{
		cnt++;									// 增加计数器
		if (cnt>maxtokens)								// 纪录最大的扩展名数量
		{
			maxtokens=cnt;							
		}
现我们已经获得第一个扩展名,下一步我们把它显示在屏幕上。

我们已经显示了三行文本,它们在Y轴上占用了3*32=96个像素的宽度,所以我们显示的第一个行文本的位置是(0,96),一次类推第i行文本的位置是(0,96+(cnt*32)),但我们需要考虑当前滚动过的位置,默认为向上滚动,所以我们得到显示第i行文本的位置为(0,96+(cnt*32)=scroll)。

当然它们不会都显示出来,记得我们使用了剪裁,只显示(0,96)-(0,96+32*9)之间的文本,其它的都被剪裁了。

更具我们上面的讲解,显示的第一个行如下:
1 GL_ARB_multitexture

		glColor3f(0.5f,1.0f,0.5f);						// 设置颜色为绿色
		glPrint(0,96+(cnt*32)-scroll,0,"%i",cnt);				// 绘制第几个扩展名
		glColor3f(1.0f,1.0f,0.5f);						// 设置颜色为黄色
		glPrint(50,96+(cnt*32)-scroll,0,token);				// 输出第i个扩展名
当我们显示完所有的扩展名,我们需要检查一下是否已经分析完了所有的字符串。我们使用strtok(NULL," ")函数代替strtok(text," ")函数,把第一个参数设置为NULL会检查当前指针位置到字符串末尾是否包含" "字符,如果包含返回其位置,否则返回NULL。

我们举例说明上面的过程,例如字符串"GL_ARB_multitexture GL_EXT_abgr GL_EXT_bgra",它是以空格分割字符串的,第一次调用strtok("text"," ")返回text的首位置,并在空格" "的位置加入一个NULL。以后每次调用,删除NULL,返回空格位置的下一个位置,接着搜索下一个空格的位置,并在空格的位置加入一个NULL。直道返回NULL。

返回NULL时循环停止,表示已经显示完所有的扩展名。

		token=strtok(NULL," ");							// 查找下一个扩展名
	}
下面的代码让OpenGL返回到默认的渲染状态,并释放分配的内存资源
	glDisable(GL_SCISSOR_TEST);								// 禁用剪裁测试

	free (text);									// 释放分配的内存
下面的代码让OpenGL完成所有的任务,并返回TRUE
	glFlush();									// 执行所有的渲染命令
	return TRUE;									// 成功返回
}
KillGLWindow函数基本没有变化,唯一改变的是需要删除我们创建的字体
	KillFont();									// 删除字体
CreateGLWindow(), 和 WndProc() 函数保持不变

在WinMain()函数中我们需要加入新的按键控制

下面的代码检查向上的箭头是否被按下,如果scroll大于0,我们把它减少2

				if (keys[VK_UP] && (scroll>0))				// 向上的箭头是否被按下?
				{
					scroll-=2;					// 如果是,减少scroll的值
				}
如果向下的箭头被按住,并且scroll小于32*(maxtoken-9),则增加scroll的值,32是每一个字符的高度,9是可以显示的行数。
				if (keys[VK_DOWN] && (scroll<32*(maxtokens-9)))		// 向下的箭头是否被按住
				{
					scroll+=2;					// 如果是,增加scroll的值
				}
我希望你觉得这个教程有趣,学完了这个教程你应该知道如何获得你的显卡的发售商的名称,实现OpenGL的组织和你的显卡所使用的OpenGL的版本。进一步,你应该知道你的显卡支持的扩展的名称,并熟练的使用剪切矩形和加载TGA图像。

如果你发现任何问题,请让我知道。我想做最好的教程,你的反馈对我很重要。

版权与使用声明:
我是个对学习和生活充满激情的普通男孩,在网络上我以DancingWind为昵称,我的联系方式是[email protected],如果你有任何问题,都可以联系我。

引子
网络是一个共享的资源,但我在自己的学习生涯中浪费大量的时间去搜索可用的资料,在现实生活中花费了大量的金钱和时间在书店中寻找资料,于是我给自己起了个昵称DancingWind,其意义是想风一样从各个知识的站点中吸取成长的养料。在飘荡了多年之后,我决定把自己收集的资料整理为一个统一的资源库。

版权声明
所有DancingWind发表的内容,大多都来自共享的资源,所以我没有资格把它们据为己有,或声称自己为这些资源作出了一点贡献。故任何人都可以复制,修改,重新发表,甚至以自己的名义发表,我都不会追究,但你在做以上事情的时候必须保证内容的完整性,给后来的人一个完整的教程。最后,任何人不能以这些资料的任何部分,谋取任何形式的报酬。

发展计划
在国外,很多资料都是很多人花费几年的时间慢慢积累起来的。如果任何人有兴趣与别人共享你的知识,我很欢迎你与我联系,但你必须同意我上面的声明。

感谢
感谢我的母亲一直以来对我的支持和在生活上的照顾。
感谢我深爱的女友田芹,一直以来默默的在精神上和生活中对我的支持,她甚至把买衣服的钱都用来给我买书了,她真的是我见过的最好的女孩,希望我能带给她幸福。

源码 RAR格式

< 第23课 第25课 >