前言
声明,此 NeHe OpenGL教程系列文章由51博客yarin翻译(2010-08-19),本博客为转载并稍加整理与修改。对NeHe的OpenGL管线教程的编写,以及yarn的翻译整理表示感谢。
NeHe OpenGL第二十四课:扩展
扩展,剪裁和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 |
图像描述符字节 |
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的值
}
原文及其个版本源代码下载: