GLUT是学习OpenGL编程时一个很好的助手。但它缺乏相应的图像加载功能。这使得我们在学习与研究诸如纹理贴图等内容时,不免显得有些尴尬 —— 要么坚持使用GLUT而不得不忍受在内存中手工生成简单图像的窘境,要么放弃使用GLUT而转向使用Cocoa或MFC等庞大类库。如果选择了后者,看似解决了这个问题,但我们学习研究的方向恐怕会不知不觉地从OpenGL转向Cocoa或MFC了。并且,我们的OpenGL应用也将不得不绑定于特定平台,最终丧失了借助GLUT跨越平台的优越性。
OpenGL是使用C语言来制定的编程接口。利用C语言简单、灵活、强大的语言特性,我们可以很轻松地编写图像加载程序。
难点在于图像文件的格式可能很复杂。但TGA类型的图像文件则属例外。它的格式比较简单,但足以胜任类似纹理贴图等任务。
下面是一个名为c1.tga的图像文件。
下面是其格式在Xcode中的十六进制显示。
当然,对于人类来讲,直接看懂这些内容是勉为其难的。所以上图只作感性认识。遇到不懂的地方就去查字典,而网络时代最好的字典就是维基百科了(抱歉,这里仅指英文版的,中文的维基自愿者太少了)。http://en.wikipedia.org/wiki/Truevision_TGA较为详细地介绍了TGA的格式。根据该内容,我们很容易就能识破TGA的天机。
TGA的结构分为必备部分与可选部分。本文只讨论必备部分。必备部分又包括头部与实际内容部分。先看头部部分。
字段编号 | 长度 | 字段名 | 简介 |
---|---|---|---|
1 | 1字节 | Id长度 | ID字段的长度 |
2 | 1字节 | 颜色表类型 | 是否包含颜色表 |
3 | 1字节 | 图像类型 | 压缩及颜色类型 |
4 | 5字节 | 颜色表规范 | 颜色表的细节 |
5 | 10字节 | 图像规范 | 图像大小及格式 |
其中,颜色表规范的5字节又分为下表中的3个子字段。
长度 | 字段名 | 简介 |
---|---|---|
2字节 | 第一个条目的索引值 | 在颜色表中的偏移位置 |
2字节 | 颜色表长度 | 条目数量 |
1字节 | 颜色表条目的大小 | 每像素多少位 |
图像规范的10字节又分为下表中的6个子字段。
长度 | 字段名 | 简介 |
---|---|---|
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个表格。
字段编号 | 长度 | 字段名 | 简介 |
---|---|---|---|
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,也可以拥有绚丽多彩的真实世界了。
[本文调试环境:Xcode V4.3, Mac OS X Lion.]