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

转自【翻译】NeHe OpenGL 教程

前言

声明,此 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

图像描述符字节
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

 
 
posted @ 2016-12-31 16:29  wenglabs  阅读(497)  评论(0编辑  收藏  举报