NeHe OpenGL第三十三课:TGA文件

加载压缩和未压缩的TGA文件:

在这一课里,你将学会如何加载压缩和为压缩的TGA文件,由于它使用RLE压缩,所以非常的简单,你能很快地熟悉它的。
 
我见过很多人在游戏开发论坛或其它地方询问关于TGA读取的问题。接下来的程序及注释将会向你展示如何读取未压缩的TGA文件和RLE压缩的文件。这个详细的教程适合于OpenGL,但是我计划改进它使其在将来更具普遍性。

我们将从两个头文件开始。第一个文件控制纹理结构,在第二个里,结构和变量将为程序读取所用。

就像每个头文件那样,我们需要一些包含保护措施以防止文件被重复包含。

在文件的顶部加入这样几行程序:

#ifndef __TEXTURE_H__    // 看看此头文件是否已经被包含
 #define __TEXTURE_H__    // 如果没有,定义它

然后滚动到程序底部并添加:

#endif      // __TEXTURE_H__ 结束包含保护

这三行程序防止此文件被重复包含。文件中剩下的代码将处于这头两行和这最后一行之间。

在这个头文件中,我们将要加入完成每件工作所需的标准头文件。在#define __TGA_H__后添加如下几行:

#pragma comment(lib, "OpenGL32.lib")  // 链接 Opengl32.lib
 #include <windows.h>   // 标准Windows头文件
 #include <stdio.h>    // 标准文件I/O头文件
 #include <gl\gl.h>    // 标准OpenGL头文件

第一个头文件是标准Windows头文件,第二个是为我们稍后的文件I/O所准备的,第三个是OpenGL32.lib所需的标准OpenGL头文件。

我们将需要一块空间存储图像数据以及OpenGL生成纹理所需的类型。我们将要用到以下结构:

typedef struct
 {
  GLubyte* p_w_picpathData;   // 控制整个图像的颜色值
  GLuint  bpp;    // 控制单位像素的bit数
  GLuint width;    // 整个图像的宽度
  GLuint height;    // 整个图像的高度
  GLuint texID;    // 使用glBindTexture所需的纹理ID.
  GLuint type;     // 描述存储在*ImageData中的数据(GL_RGB Or GL_RGBA)
 } Texture;

现在说说其它的,更长的头文件。同样我们需要一些包含保护措施,这和上述最后一个是一样的。

接下来,看看另外两个结构,它们将在处理TGA文件的过程中使用。

typedef struct
 {
  GLubyte Header[12];   // 文件头决定文件类型
 } TGAHeader;

typedef struct
 {
  GLubyte header[6];    // 控制前6个字节
  GLuint bytesPerPixel;   // 每像素的字节数 (3 或 4)
  GLuint p_w_picpathSize;    // 控制存储图像所需的内存空间
  GLuint type;    // 图像类型 GL_RGB 或 GL_RGBA
  GLuint Height;    // 图像的高度
  GLuint Width;    // 图像宽度
  GLuint Bpp;    // 每像素的比特数 (24 或 32)
 } TGA;

现在我们声明那两个结构的一些实例,那样我们可以在程序中使用它们。

TGAHeader tgaheader;    // 用来存储我们的文件头
 TGA tga;      // 用来存储文件信息

我们需要定义一对文件头,那样我们能够告诉程序什么类型的文件头处于有效的图像上。如果是未压缩的TGA图像,前12字节将会是0 0 2 0 0 0 0 0 0 0 0 0,如果是RLE压缩的,则是0 0 10 0 0 0 0 0 0 0 0 0。这两个值允许我们检查正在读取的文件是否有效。

// 未压缩的TGA头
 GLubyte uTGAcompare[12] = {0,0, 2,0,0,0,0,0,0,0,0,0};
 // 压缩的TGA头
 GLubyte cTGAcompare[12] = {0,0,10,0,0,0,0,0,0,0,0,0};

最后,我们声明两个函数用于读取过程。

// 读取一个未压缩的文件
 bool LoadUncompressedTGA(Texture *, char *, FILE *);
 // 读取一个压缩的文件
 bool LoadCompressedTGA(Texture *, char *, FILE *);

现在,回到cpp文件,和程序中真正首当其冲部分,我将会省去一些错误消息处理代码并且使教程更短、更具可读性。你可以参看教程包含的文件(在文章的尾部有链接)。

马上,我们就可以在文件开头包含我们刚刚建立的头文件。

#include "tga.h"    // 包含我们刚刚建立的头文件

不我们不需要包含其它任何文件了,因为我们已经在自己刚刚完成的头文件中包含他们了。

接下来,我们要做的事情是看看第一个函数,名为LoadTGA(…)。

// 读取一个TGA文件!
 bool LoadTGA(Texture * texture, char * filename)
 {

它有两个参数。前者是一个指向纹理结构的指针,你必须在你的代码中声明它(见包含的例子)。后者是一个字符串,它告诉计算机在哪里去找你的纹理文件。

函数的前两行声明了一个文件指针,然后打开由“filename”参数指定的文件,它由函数的第二个指针传递进去。

FILE * fTGA;     // 声明文件指针
 fTGA = fopen(filename, "rb");   // 以读模式打开文件

接下来的几行检查指定的文件是否已经正确地打开。

if(fTGA == NULL)    // 如果此处有错误
 {
  ...Error code...
  return false;    // 返回 False
 }

下一步,我们尝试读取文件的首12个字节的内容并且将它们存储在我们的TGAHeader结构中,这样,我们得以检查文件类型。如果fread失败,则关闭文件,显示一个错误,并且函数返回false。

if(fread(&tgaheader, sizeof(TGAHeader), 1, fTGA) == 0)
 {
  ...Error code here...
  return false;    //  如果失败则返回 False
 }

接着,通过我们用辛苦编的程序刚读取的头,我们继续尝试确定文件类型。这可以告诉我们它是压缩的、未压缩甚至是错误的文件类型。为了达到这个目的,我们将会使用memcmp(…)函数。

// 如果文件头附合未压缩的文件头格式
 if(memcmp(uTGAcompare, &tgaheader, sizeof(tgaheader)) == 0)
 {
  // 读取未压缩的TGA文件
  LoadUncompressedTGA(texture, filename, fTGA);
 }
 // 如果文件头附合压缩的文件头格式
 else if(memcmp(cTGAcompare, &tgaheader, sizeof(tgaheader)) == 0)
 {
  // 读取压缩的TGA格式
  LoadCompressedTGA(texture, filename, fTGA);
 }
 else      // 如果任一个都不符合
 {
  ...Error code here...
  return false;    // 返回 False
 }

我们将要开始读取一个未压缩格式文件的章节。

下面开始我们要做的第一件事,像往常一样,是函数头。

//读取未压缩的TGA文件
 bool LoadUncompressedTGA(Texture * texture, char * filename, FILE * fTGA)
 {

这个函数有3个参数。头两个和LoadTGA中的一样,仅仅是简单的传递。第三个是来自前一个函数中的文件指针,因此我们没有丢失我们的空间。

接下来我们试着再从文件中读取6个字节的内容,并且存储在tga.header中。如果他失败了,我们运行一些错误处理代码,并且返回false。

// 尝试继续读取6个字节的内容
 if(fread(tga.header, sizeof(tga.header), 1, fTGA) == 0)
 {
  ...Error code here...
  return false;    // 返回 False
 }

现在我们有了计算图像的高度、宽度和BPP的全部信息。我们在纹理和本地结构中都将存储它。

texture->width  = tga.header[1] * 256 + tga.header[0]; // 计算高度
 texture->height = tga.header[3] * 256 + tga.header[2]; // 计算宽度
 texture->bpp = tga.header[4];   // 计算BPP
 tga.Width = texture->width;    // 拷贝Width到本地结构中去
 tga.Height = texture->height;   // 拷贝Height到本地结构中去
 tga.Bpp = texture->bpp;    // 拷贝Bpp到本地结构中去

现在,我们需要确认高度和宽度至少为1个像素,并且bpp是24或32。如果这些值中的任何一个超出了它们的界限,我们将再一次显示一个错误,关闭文件,并且离开此函数。

// 确认所有的信息都是有效的
 if((texture->width <= 0) || (texture->height <= 0) || ((texture->bpp != 24) && (texture->bpp !=32)))
 {
  ...Error code here...
  return false;    // 返回 False
 }

接下来我们设置图像的类型。24 bit图像是GL_RGB,32 bit 图像是GL_RGBA

if(texture->bpp == 24)    // 是24 bit图像吗?
 {
  texture->type = GL_RGB;   //如果是,设置类型为GL_RGB
 }
 else      // 如果不是24bit,则必是32bit
 {
  texture->type = GL_RGBA;  //这样设置类型为GL_RGBA
 }

现在我们计算每像素的字节数和总共的图像数据。

tga.bytesPerPixel = (tga.Bpp / 8);  // 计算BPP
 // 计算存储图像所需的内存
 tga.p_w_picpathSize = (tga.bytesPerPixel * tga.Width * tga.Height);

我们需要一些空间去存储整个图像数据,因此我们将要使用malloc分配正确的内存数量

然后我们确认内存已经分配,并且它不是NULL。如果出现了错误,则运行错误处理代码。

// 分配内存
 texture->p_w_picpathData = (GLubyte *)malloc(tga.p_w_picpathSize);
 if(texture->p_w_picpathData == NULL)   // 确认已经分配成功
 {
  ...Error code here...
  return false;    // 确认已经分配成功
 }

这里我们尝试读取所有的图像数据。如果不能,我们将再次触发错误处理代码。

// 尝试读取所有图像数据
 if(fread(texture->p_w_picpathData, 1, tga.p_w_picpathSize, fTGA) != tga.p_w_picpathSize)
 {
  ...Error code here...
  return false;    // 如果不能,返回false
 }

TGA文件用逆OpenGL需求顺序的方式存储图像,因此我们必须将格式从BGR到RGB。为了达到这一点,我们交换每个像素的第一个和第三个字节的内容。

Steve Thomas补充:我已经编写了能稍微更快速读取TGA文件的代码。它涉及到仅用3个二进制操作将BGR转换到RGB的方法。

然后我们关闭文件,并且成功退出函数。

//  开始循环
 for(GLuint cswap = 0; cswap < (int)tga.p_w_picpathSize; cswap += tga.bytesPerPixel)
 {
  // 第一字节 XOR第三字节XOR 第一字节 XOR 第三字节
  texture->p_w_picpathData[cswap] ^= texture->p_w_picpathData[cswap+2] ^=
  texture->p_w_picpathData[cswap] ^= texture->p_w_picpathData[cswap+2];
 }

fclose(fTGA);     // 关闭文件
 return true;     // 返回成功
}

以上是读取未压缩型TGA文件的方法。读取RLE压缩型文件的步骤稍微难一点。我们像平时一样读取文件头并且收集高度/宽度/色彩深度,这和读取未压缩版本是一致的。 
  
 bool LoadCompressedTGA(Texture * texture, char * filename, FILE * fTGA)
 {
  if(fread(tga.header, sizeof(tga.header), 1, fTGA) == 0)
  {
   ...Error code here...
  }
  texture->width  = tga.header[1] * 256 + tga.header[0];
  texture->height = tga.header[3] * 256 + tga.header[2];
  texture->bpp = tga.header[4];
  tga.Width = texture->width;
  tga.Height = texture->height;
  tga.Bpp = texture->bpp;
  if((texture->width <= 0) || (texture->height <= 0) || ((texture->bpp != 24) && (texture->bpp !=32)))
  {
   ...Error code here...
  }        }
  tga.bytesPerPixel = (tga.Bpp / 8);
  tga.p_w_picpathSize  = (tga.bytesPerPixel * tga.Width * tga.Height);

现在我们需要分配存储图像所需的空间,这是为我们解压缩之后准备的,我们将使用malloc。如果内存分配失败,运行错误处理代码,并且返回false。 
  
 // 分配存储图像所需的内存空间
 texture->p_w_picpathData = (GLubyte *)malloc(tga.p_w_picpathSize);
 if(texture->p_w_picpathData == NULL)   // 如果不能分配内存
 {
  ...Error code here...
  return false;    // 返回 False
 }

下一步我们需要决定组成图像的像素数。我们将它存储在变量“pixelcount”中。

我们也需要存储当前所处的像素,以及我们正在写入的图像数据的字节,这样避免溢出写入过多的旧数据。

我们将要分配足够的内存来存储一个像素。

GLuint pixelcount = tga.Height * tga.Width; // 图像中的像素数
 GLuint currentpixel = 0;  // 当前正在读取的像素
 GLuint currentbyte = 0;   // 当前正在向图像中写入的像素
// 一个像素的存储空间
 GLubyte * colorbuffer = (GLubyte *)malloc(tga.bytesPerPixel);

接下来我们将要进行一个大循环。

让我们将它分解为更多可管理的块。

首先我们声明一个变量来存储“块”头。块头指示接下来的段是RLE还是RAW,它的长度是多少。如果一字节头小于等于127,则它是一个RAW头。头的值是颜色数,是负数,在我们处理其它头字节之前,我们先读取它并且拷贝到内存中。这样我们将我们得到的值加1,然后读取大量像素并且将它们拷贝到ImageData中,就像我们处理未压缩型图像一样。如果头大于127,那么它是下一个像素值随后将要重复的次数。要获取实际重复的数量,我们将它减去127以除去1bit的的头标示符。然后我们读取下一个像素并且依照上述次数连续拷贝它到内存中。

do      // 开始循环
 {
 GLubyte chunkheader = 0;    // 存储Id块值的变量
 if(fread(&chunkheader, sizeof(GLubyte), 1, fTGA) == 0) // 尝试读取块的头
 {
  ...Error code...
  return false;    // If It Fails, Return False
 }

接下来我们将要看看它是否是RAW头。如果是,我们需要将此变量的值加1以获取紧随头之后的像素总数。

if(chunkheader < 128)    // 如果是RAW块
 {
  chunkheader++;    // 变量值加1以获取RAW像素的总数

我们开启另一个循环读取所有的颜色信息。它将会循环块头中指定的次数,并且每次循环读取和存储一个像素。

首先,我们读取并检验像素数据。单个像素的数据将被存储在colorbuffer变量中。然后我们将检查它是否为RAW头。如果是,我们需要添加一个到变量之中以获取头之后的像素总数。

// 开始像素读取循环
 for(short counter = 0; counter < chunkheader; counter++)
 {
  // 尝试读取一个像素
  if(fread(colorbuffer, 1, tga.bytesPerPixel, fTGA) != tga.bytesPerPixel)
  {
   ...Error code...
   return false;   // 如果失败,返回false
  }

我们循环中的下一步将要获取存储在colorbuffer中的颜色值并且将其写入稍后将要使用的p_w_picpathData变量中。在这个过程中,数据格式将会由BGR翻转为RGB或由BGRA转换为RGBA,具体情况取决于每像素的比特数。当我们完成任务后我们增加当前的字节和当前的像素计数器。 
  
 texture->p_w_picpathData[currentbyte] = colorbuffer[2];  // 写“R”字节
 texture->p_w_picpathData[currentbyte + 1 ] = colorbuffer[1]; //写“G”字节
 texture->p_w_picpathData[currentbyte + 2 ] = colorbuffer[0]; // 写“B”字节
 if(tga.bytesPerPixel == 4)     // 如果是32位图像...
 {
  texture->p_w_picpathData[currentbyte + 3] = colorbuffer[3]; // 写“A”字节
 }
 // 依据每像素的字节数增加字节计数器
 currentbyte += tga.bytesPerPixel;
 currentpixel++;     // 像素计数器加1
  
下一段处理描述RLE段的“块”头。首先我们将chunkheader减去127来得到获取下一个颜色重复的次数。 
  
 else      // 如果是RLE头
 {
  chunkheader -= 127;   //  减去127获得ID Bit的Rid

然后我们尝试读取下一个颜色值。

// 读取下一个像素
 if(fread(colorbuffer, 1, tga.bytesPerPixel, fTGA) != tga.bytesPerPixel)
 {
  ...Error code...
  return false;    // 如果失败,返回false
 }

接下来,我们开始循环拷贝我们多次读到内存中的像素,这由RLE头中的值规定。

然后,我们将颜色值拷贝到图像数据中,预处理R和B的值交换。

随后,我们增加当前的字节数、当前像素,这样我们再次写入值时可以处在正确的位置。

// 开始循环
 for(short counter = 0; counter < chunkheader; counter++)
 {
  // 拷贝“R”字节
  texture->p_w_picpathData[currentbyte] = colorbuffer[2];
  // 拷贝“G”字节
  texture->p_w_picpathData[currentbyte + 1 ] = colorbuffer[1];
  // 拷贝“B”字节
  texture->p_w_picpathData[currentbyte + 2 ] = colorbuffer[0];
  if(tga.bytesPerPixel == 4)  // 如果是32位图像
  {
   // 拷贝“A”字节
   texture->p_w_picpathData[currentbyte + 3] = colorbuffer[3];
  }
  currentbyte += tga.bytesPerPixel; // 增加字节计数器
  currentpixel++;   // 增加字节计数器
  
只要仍剩有像素要读取,我们将会继续主循环。

最后,我们关闭文件并返回成功。

while(currentpixel < pixelcount); // 是否有更多的像素要读取?开始循环直到最后
  fclose(fTGA);   // 关闭文件
  return true;   // 返回成功
 }
原文及其个版本源代码下载:

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

转载于:https://blog.51cto.com/yarin/381885

NeHe OpenGL第三十三课:TGA文件相关推荐

  1. NeHe OpenGL教程 第二十三课:球面映射

    转自[翻译]NeHe OpenGL 教程 前言 声明,此 NeHe OpenGL教程系列文章由51博客yarin翻译(2010-08-19),本博客为转载并稍加整理与修改.对NeHe的OpenGL管线 ...

  2. NeHe OpenGL第三十二课:拾取游戏

    NeHe OpenGL第三十二课:拾取游戏 拾取, Alpha混合, Alpha测试, 排序: 这又是一个小游戏,交给的东西会很多,慢慢体会吧   欢迎来到32课. 这课大概是在我所写作已来最大的一课 ...

  3. NeHe OpenGL第三十五课:播放AVI

    NeHe OpenGL第三十五课:播放AVI 在OpenGL中播放AVI: 在OpenGL中如何播放AVI呢?利用Windows的API把每一帧作为纹理绑定到OpenGL中,虽然很慢,但它的效果不错. ...

  4. NeHe OpenGL第三十九课:物理模拟

    NeHe OpenGL第三十九课:物理模拟 物理模拟简介: 还记得高中的物理吧,直线运动,自由落体运动,弹簧.在这一课里,我们将创造这一切.   物理模拟介绍 如果你很熟悉物理规律,并且想实现它,这篇 ...

  5. NeHe OpenGL第二十八课:贝塞尔曲面

    NeHe OpenGL第二十八课:贝塞尔曲面 贝塞尔曲面: 这是一课关于数学运算的,没有别的内容了.来,有信心就看看它吧. 贝塞尔曲面 作者: David Nikdel ( ogapo@ithink. ...

  6. NeHe OpenGL第二十四课:扩展

    NeHe OpenGL第二十四课:扩展 扩展,剪裁和TGA图像文件的加载: 在这一课里,你将学会如何读取你显卡支持的OpenGL的扩展,并在你指定的剪裁区域把它显示出来.   这个教程有一些难度,但它 ...

  7. NeHe OpenGL第十九课:粒子系统

    NeHe OpenGL第十九课:粒子系统 粒子系统: 你是否希望创建爆炸,喷泉,流星之类的效果.这一课将告诉你如何创建一个简单的例子系统,并用它来创建一种喷射的效果. 欢迎来到第十九课.你已经学习了很 ...

  8. NeHe OpenGL第二十五课:变形

    NeHe OpenGL第二十五课:变形 变形和从文件中加载3D物体: 在这一课中,你将学会如何从文件加载3D模型,并且平滑的从一个模型变换为另一个模型.   欢迎来到这激动人心的一课,在这一课里,我们 ...

  9. NeHe OpenGL第二十九课:Blt函数

    NeHe OpenGL第二十九课:Blt函数 Blitter 函数: 类似于DirectDraw的blit函数,过时的技术,我们有实现了它.它非常的简单,就是把一块纹理贴到另一块纹理上. 这篇文章是有 ...

最新文章

  1. 订单倒计时取消,nodejs 辅助实现倒计时任务
  2. 《相约星期六》男嘉宾才华横溢,现场用女嘉宾名字作诗一首
  3. HDU1114 Piggy-Bank 【全然背包】
  4. 小众编程语言同样值得你关注
  5. Mybatis一级缓存和二级缓存 Redis缓存
  6. 线程间到底共享了哪些进程资源?
  7. python语言公式求圆周率_通过Python实现圆周率的计算(公式方法和蒙特卡罗方法)...
  8. 玩转SpringBoot2.x之缓存对象
  9. 小时候有哪些丑事,让你终身难忘?
  10. 501.二叉搜索树中的众数
  11. k近邻算法_K近邻算法(一)
  12. Hadoop集群配置
  13. [Ansible系列②]Ansible使用说明
  14. 知乎上线诺贝尔奖主题圆桌 让科普更加多元有趣
  15. nginx防火墙设置
  16. linux中无线管理员密码,无线网管理员密码
  17. 通过ssh工具,使PC远程连接机载电脑
  18. 有关于配环境为什么这么糟心的一点事
  19. 21天学通C语言-学习笔记(3)
  20. 2021年T电梯修理免费试题及T电梯修理作业模拟考试

热门文章

  1. linux下使用NetBeans调试libevent库
  2. DllMain中不当操作导致死锁问题的分析--加载卸载DLL与DllMain死锁的关系
  3. 【Qt】QAudioDeviceInfo获取不到音频设备
  4. 海思3536:osdrv编译过程中报错及解决方法
  5. 可疑文件_【国家标准】印刷文件鉴定技术规范点阵式打印文件的同机鉴定
  6. 魔兽世界服务器位面 稳定,因抗议《魔兽世界》位面技术在RP服务器中被取消
  7. 启动子级时出错_减速机安装与使用时需注意的八个要点,细节很重要!
  8. 计算机专业看能力还是学校,【计算机专业论文】学校计算机专业学生实践能力的培养(共3502字)...
  9. Java项目:在线水果商城系统(java+JSP+Spring+SpringMVC +MyBatis+html+mysql)
  10. Java项目:仿小米商城系统(前后端分离+java+vue+Springboot+ssm+mysql+maven+redis)