NeHe OpenGL教程 第二十四课:扩展

时间:2023-12-21 19:26:32

转自【翻译】NeHe OpenGL 教程

前言

声明,此 NeHe OpenGL教程系列文章由51博客yarin翻译(2010-08-19),本博客为转载并稍加整理与修改。对NeHe的OpenGL管线教程的编写,以及yarn的翻译整理表示感谢。

NeHe OpenGL第二十四课:扩展

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

图像描述符字节

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的值

    }

原文及其个版本源代码下载:

http://nehe.gamedev.net/data/lessons/lesson.asp?lesson=24