为GLUT应用编写TGA图像加载程序

GLUT是学习OpenGL编程时一个很好的助手。但它缺乏相应的图像加载功能。这使得我们在学习与研究诸如纹理贴图等内容时,不免显得有些尴尬 —— 要么坚持使用GLUT而不得不忍受在内存中手工生成简单图像的窘境,要么放弃使用GLUT而转向使用Cocoa或MFC等庞大类库。如果选择了后者,看似解决了这个问题,但我们学习研究的方向恐怕会不知不觉地从OpenGL转向Cocoa或MFC了。并且,我们的OpenGL应用也将不得不绑定于特定平台,最终丧失了借助GLUT跨越平台的优越性。

OpenGL是使用C语言来制定的编程接口。利用C语言简单、灵活、强大的语言特性,我们可以很轻松地编写图像加载程序。

难点在于图像文件的格式可能很复杂。但TGA类型的图像文件则属例外。它的格式比较简单,但足以胜任类似纹理贴图等任务。

下面是一个名为c1.tga的图像文件。

为GLUT应用编写TGA图像加载程序_第1张图片

下面是其格式在Xcode中的十六进制显示。

为GLUT应用编写TGA图像加载程序_第2张图片

当然,对于人类来讲,直接看懂这些内容是勉为其难的。所以上图只作感性认识。遇到不懂的地方就去查字典,而网络时代最好的字典就是维基百科了(抱歉,这里仅指英文版的,中文的维基自愿者太少了)。http://en.wikipedia.org/wiki/Truevision_TGA较为详细地介绍了TGA的格式。根据该内容,我们很容易就能识破TGA的天机。

TGA的结构分为必备部分与可选部分。本文只讨论必备部分。必备部分又包括头部与实际内容部分。先看头部部分。

表格1
字段编号 长度 字段名 简介
1 1字节 Id长度 ID字段的长度
2 1字节 颜色表类型 是否包含颜色表
3 1字节 图像类型 压缩及颜色类型
4 5字节 颜色表规范 颜色表的细节
5 10字节 图像规范 图像大小及格式

其中,颜色表规范的5字节又分为下表中的3个子字段。

表格2
长度 字段名 简介
2字节 第一个条目的索引值 在颜色表中的偏移位置
2字节 颜色表长度 条目数量
1字节 颜色表条目的大小 每像素多少位

图像规范的10字节又分为下表中的6个子字段。

表格3
长度 字段名 简介
2字节 X-origin 左下角(原点)的绝对坐标值
2字节 Y-origin 左下角(原点)的绝对坐标值
2字节 图像宽度 以像素为单位
2字节 图像高度 以像素为单位
1字节 像素深度 每像素多少位
1字节 图像描述符 3-0位代表alpha通道深度,5-4位代表方向

结合上述3表,比照TGA内容的十六进制的表示,您能找出规律吗?

十六进制表中,每两个数字为1字节。字段编号1的长度为1字节,因此,十六进制表中的第一个“00”就是Id长度。第二个“00”就是字段编号2的颜色表类型,接下来的“02”即是图像类型,表示它是无压缩的真彩图像。后面的5个“00”,对应于表格2的内容,其值全为0,说明这个TGA图像没有颜色表。接着的4个“00”,表示图像的原点坐标。而“46”“00”,合并后成为“0046”,根据表3,是图像宽度,十进制为70。后面的“002E”,是图像高度,十进制为46。这两组数字表示TGA图像的大小是70 X 46。“18”是像素深度,十进制为24,表示图像是24位真彩。接下来的“00”表示该图像没有alpha通道。

表格1、2、3解释了TGA图像的头部部分。接下来又该是什么了?我们来看第4个表格。

表格4
字段编号 长度 字段名 简介
6 来自于Id长度字段 图像的ID 可选字段,包括一些标识信息
7 来自于颜色表规范字段 颜色表数据 包含颜色表数据的查询表
8 来自于图像规范字段 图像数据 根据图像描述符所存储的图像内容

字段编号为6、7的字段都是可选的。上面的c1.tga文件就没有这方面的信息。因此,没有这两个字段的存储空间。字段编号8的内容,就是我们最关心的,即图像本身的内容。因此,在十六进制表中,从第2行的“44 58 4C”开始,一直到结束,全部都是图像的内容。当然,我们的肉眼看不出这些数字到底画了什么,但计算机自有识别它们的解码方法。

以上4个表格是实现本文功能的基础。或许,作为程序员,看懂这些表格实属不易。那么,我们来看看用代码如何重现这些表格吧。

//
//  tga.h
//
//  Created by Sarkuya on 12-4-27.
//  Copyright (c) 2012年 Sarkuya. All rights reserved.
//

#ifndef _tga_h
#define _tga_h

#ifndef __glut_h__
#if defined(__APPLE__) || defined(MACOSX)
#   include <GLUT/GLUT.h>
#else
#   if defined(_WIN32)
#       include <GL/freeglut.h>
#   endif
#endif
#endif          /* __glut_h__ */

typedef struct {
    GLushort firstEntryIndex;
    GLushort length;
    GLubyte entrySize;
} TGA_COLOR_MAP_SPEC;

typedef struct {
    GLushort xOrigin;
    GLushort yOrigin;
    GLushort imageWidth;
    GLushort imageHeight;
    GLubyte pixelDepth;
    GLubyte imageDescriptor;
} TGA_IMAGE_SPEC;

typedef struct {
    GLubyte idLength;
    GLubyte colorMapType;
    GLubyte imageType;
    TGA_COLOR_MAP_SPEC colorMapSpec;
    TGA_IMAGE_SPEC imageSpec;
} TGA_HEADER;

typedef struct {
    TGA_HEADER header;
    GLubyte* imageData;
} TGA_IMAGE;

void loadTGA(const char* fileName, TGA_IMAGE* tgaImage);
void releaseTGA(TGA_IMAGE* tgaImage);

#endif          /* _tga_h */

TGA_HEADER对应于表格1。idLength, colorMapType, imageType的长度均为1字节,故用GLubyte的数据类型。colorMapSpec及imageSpec均有子字段,故根据这些子字段的长度及名称,另外定义了TGA_COLOR_MAP_SPEC及TGA_IMAGE_SPEC的结构,分别对应于表格2和表格3。4个表格加起来,代表了整个TGA图像的全部内容,所以用TGA_IMAGE来表示。

在这个tga.h的头文件中,仅声明了两个方法。loadTGA用以加载图像,releaseTGA用以释放图像所占空间的资源。

关于tga.h的头文件,还有两点需要说明。首先,由于GLUT在其头文件中自动包括了OpenGL应用所需的主要文件,因此,只要将GLUT.h包含进来,就可以减轻我们的编程负荷。freeglut由于与目前的Mac OS X存在一些不兼容的地方,因此,在Mac OS X中则使用系统自带的GLUT,而Windows则可以较方便地安装使用freeglut。

其次,我们在tga.h中使用了OpenGL特有的GLubyte, GLushort等数据类型,是经过慎重选择的。从上面各个表格可以看出,图像文件的格式信息,都是通过字节,甚至是位的方式来存储的。因此,我们需要以字节作为基本单位与之打交道。但由于C语言的int, short, long等数据类型在各平台的实现中字长可以不一致,因此我们不能断定一个int到底包括了多少个字节。而OpenGL的GLubyte, GLushort等数据类型则完全以字长为标准来定义,因此非常符合本文中的场合。在计算机中,由于char是最小的存储单位,我们可以通过“多少个char”这种方式来逐步检索图像内容。下面您将看到如何做到这一点。

给定了数据结构,我们可以编写算法了。

#include "tga.h"

#include <stdio.h>
#include <stdlib.h>
#include <limits.h>

void loadTGA(const char* fileName, TGA_IMAGE* tgaImage)
{
    FILE* fp = NULL;
    
    fp = fopen(fileName, "r");
    
    if (fp == NULL) {
        fprintf(stderr, "Error opening file!\n");
        exit(EXIT_FAILURE);
    }
    
    fscanf(fp, "%c%c%c%2c%2c%c%2c%2c%2c%2c%c%c",
           &(tgaImage->header.idLength),
           &(tgaImage->header.colorMapType),
           &(tgaImage->header.imageType),
           &(tgaImage->header.colorMapSpec.firstEntryIndex),
           &(tgaImage->header.colorMapSpec.length),
           &(tgaImage->header.colorMapSpec.entrySize),
           &(tgaImage->header.imageSpec.xOrigin),
           &(tgaImage->header.imageSpec.yOrigin),
           &(tgaImage->header.imageSpec.imageWidth),
           &(tgaImage->header.imageSpec.imageHeight),
           &(tgaImage->header.imageSpec.pixelDepth),
           &(tgaImage->header.imageSpec.imageDescriptor)
           );
    
    //assume we get the color map of 0, image type of 2
    if (tgaImage->header.colorMapType != 0 && tgaImage->header.imageType != 2) {
        fprintf(stderr, "Unimplemented TGA type Found!");
        exit(EXIT_FAILURE);
    }
    
    int imageWidth = tgaImage->header.imageSpec.imageWidth;
    int imageHeight = tgaImage->header.imageSpec.imageHeight;
    int totalPixels =  imageWidth * imageHeight;
    int bytesPerPixels = tgaImage->header.imageSpec.pixelDepth / CHAR_BIT;
    int totalBytes = totalPixels * bytesPerPixels;
    
    int imageSize = totalPixels * tgaImage->header.imageSpec.pixelDepth;
    tgaImage->imageData = (GLubyte*) malloc(imageSize);
    
    char formatStr[20];
    sprintf(formatStr, "%%%dc", totalBytes);
    
    fscanf(fp, formatStr, tgaImage->imageData);
    
    fclose(fp);
}

void releaseTGA(TGA_IMAGE* tgaImage)
{
    printf("releasing tga...\n");
    
    if (tgaImage->imageData != NULL) {
        free(tgaImage->imageData);
        tgaImage->imageData = NULL;
    }
}

在loadTGA函数中,我们使用fscanf函数将TGA图像的头部部分的信息一次性地扫进tgaImage的header结构。此功能还可以通过getc(), fgetc(), fgets()等函数来实现,但显而易见,桂冠非fscanf()莫属。在fscanf()函数的格式字符串中,通过“%c”, “%2c”这种方式,严格依照4表的字节数依序进行扫描并赋与相应的变量。程序是以字节(即char, 或unsigned char)为基本单位而扫描的,那么在程序中是否需要将这些字节转换为其他数据类型?不需要。计算机内存中实际上只是连续或间断地存储“0”、“1”两种数字,只要它知道从哪个地址开始、偏移多少位的特定空间存储了何种数据类型的数据就足够了。它并不关心这个空间段到底是由多少个char,或是多少个int组成。连取2个char(共16位)的数据赋值于一个GLushort的变量,我们回头既可以取出char的数据,又可取出GLushort的数据。这是C语言中数据转换的自由。当然,我们甚至可以取出long的数据,但long的数据可能将随后的不明内存空间的内容也捎带出来,因此这种作法非常危险。

由于未有更多时间加以完善,loadTGA函数目前只能处理没有颜色表、且图像类型是非压缩的真彩TGA图像。如果遇到例外,则打印未实现的消息后强行退出程序。有兴趣的读者可以自己试着完善。

在存储图像时,我们先算出图像共有多少个像素,求得其占用内存的总空间并分配内存空间,再求出这些像素共多少个字节,然后以字节为单位,依次存入tgaImage的imageData中。

releaseTGA()负责将imageData所占用的空间释放。

现在,我们可以在实战中检测其功效了。下面的代码是一段功能完整的代码,使用上面的代码加载一个TGA图像(图像来源于OpenGL Super Bible的一个随盘文件)来作为纹理贴图。

//
//  TGADemo.c
//  TGALoader
//
//  Created by Sarkuya on 12-4-27.
//  Copyright (c) 2012年 Sarkuya. All rights reserved.
//

#include "tga.h"
#include <stdio.h>
#include <stdlib.h>

int winWidth = 1024;
int winHeight = 1024;

TGA_IMAGE tgaImage;

struct {
    GLenum code;
    char* msg;
} g_gl_error_table[] = {
    GL_NO_ERROR, "GL_NO_ERROR",
    GL_INVALID_ENUM, "GL_INVALID_ENUM",
    GL_INVALID_VALUE, "GL_INVALID_VALUE",
    GL_INVALID_OPERATION, "GL_INVALID_OPERATION",
    GL_STACK_OVERFLOW, "GL_STACK_OVERFLOW",
    GL_STACK_UNDERFLOW, "GL_STACK_UNDERFLOW",
    GL_OUT_OF_MEMORY, "GL_OUT_OF_MEMORY"
};

void checkGLErrors(void)
{
    GLenum result = glGetError();
    
    if (result != GL_NO_ERROR) {
        fprintf(stderr, "Error occurs! The error code is: %d\n", result);
        int errorcodecounts = sizeof(g_gl_error_table) / sizeof(g_gl_error_table[0]);
        for (int i = 0; i < errorcodecounts; i++) {
            if (result == g_gl_error_table[i].code) {
                fprintf(stderr, "And the error enum is: %s\n", g_gl_error_table[i].msg);
            }
        }
    }
}

void init(void)
{    
    glShadeModel(GL_SMOOTH);
    glClearColor (0.0, 0.0, 0.0, 0.0);
    glEnable(GL_DEPTH_TEST);
    
    char* pathName = "";
    char* fileName = "brick.tga";
    
#if defined(__APPLE__) || defined(MACOSX)
    pathName = "/Volumes/MAC Data/Programming TestField/XcodeProjects/C/TGADemo/TGADemo/";
#else
#	if defined(_WIN32)
    pathName = "";
#	endif
#endif
    
    char pathFileName[255];
    sprintf(pathFileName, "%s%s", pathName, fileName);
    
    loadTGA(pathFileName, &tgaImage);
    
    GLsizei imgWidth, imgHeight;
    imgWidth = tgaImage.header.imageSpec.imageWidth;
    imgHeight = tgaImage.header.imageSpec.imageHeight;
    
    glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
    
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, imgWidth, 
                 imgHeight, 0, GL_RGB, GL_UNSIGNED_BYTE, 
                 tgaImage.imageData);
    
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    
    glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_DECAL);
    glEnable(GL_TEXTURE_2D);
}

void display(void)
{
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    
    gluLookAt( 2.0, 0.0, 4.0,
    	      -0.5, 0.0, 0.0,
               0.0, 1.0, 0.0);
    
    glBegin(GL_QUADS);
    glTexCoord2f(0.0f, 0.0f); glVertex3f(-2.0f, -1.0f, 0.0f);
    glTexCoord2f(0.0f, 1.0f); glVertex3f(-2.0f,  1.0f, 0.0f);
    glTexCoord2f(1.0f, 1.0f); glVertex3f( 0.0f,  1.0f, 0.0f);
    glTexCoord2f(1.0f, 0.0f); glVertex3f( 0.0f, -1.0f, 0.0f);
    
    glTexCoord2f(0.0f, 0.0f); glVertex3f(0.0f, -1.0f,  0.0f);
    glTexCoord2f(0.0f, 1.0f); glVertex3f(0.0f,  1.0f,  0.0f);
    glTexCoord2f(1.0f, 1.0f); glVertex3f(0.0f,  1.0f, -2.0f);
    glTexCoord2f(1.0f, 0.0f); glVertex3f(0.0f, -1.0f, -2.0f);
    glEnd();
    
    glutSwapBuffers();
    
    checkGLErrors();
}

void reshape(int width, int height)
{
    glViewport(0, 0, (GLsizei) width, (GLsizei) height);
    
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    gluPerspective(35.0, (GLdouble) width / (GLdouble) height, 1.0, 30.0);
    
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
}

void keyboard(unsigned char key, int x, int y)
{
    switch (key) {
        case 27:
            exit(EXIT_SUCCESS);
            break;
            
        default:
            break;
    }
}

int main(int argc, char** argv)
{
    glutInit(&argc, argv);
    glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH);
    
    glutInitWindowPosition((glutGet(GLUT_SCREEN_WIDTH) - winWidth) / 2,
                           (glutGet(GLUT_SCREEN_HEIGHT) - winHeight) / 2);
    glutInitWindowSize(winWidth, winHeight);
    glutCreateWindow("TGA DEMO");
    
    glutReshapeFunc (reshape);
    glutDisplayFunc(display);
    glutKeyboardFunc(keyboard);
    
    init();
    
    glutMainLoop();
    
    releaseTGA(&tgaImage);
    
    return EXIT_SUCCESS;
}

两点需要注意。一是Xcode项目要读取特定文件,需要对此文件使用绝对路径引用,而Visual Studio则可以直接放在与源文件同一路径下。代码反映了这种情况。二是由于GLUT的lgutMainLoop()函数一旦进入就不能再返回,因此程序中的releaseTGA()函数实际上是无法被调用的。freeglut可以解决这个问题。

程序运行后渲染的效果如下图。

为GLUT应用编写TGA图像加载程序_第3张图片

您看,即便是GLUT,也可以拥有绚丽多彩的真实世界了。

[本文调试环境:Xcode V4.3, Mac OS X Lion.]

你可能感兴趣的:(image,struct,cocoa,xcode,header,keyboard)