OpenGL超级宝典学习笔记——像素图


像素包装

位图和像素图很少会被紧密包装到内存中。在许多硬件平台上,考虑到性能的原因位图和像素图的每一行的数据会从特殊的字节对齐地址开始。绝大多数编译器会自动把变量和缓冲区放置在当前计算机架构优化的对齐地址上。OpenGL默认是4字节对齐的。在之前的例子中,篝火图的数据是紧密包装在一起的,但这不会引起什么问题,因为篝火图刚好是按照4字节对齐的,其宽是32位即4字节。如果位图是34位宽的话,为了按照4字节对齐我们需要为每一行的数据多添加额外的30位(凑齐64位)无用的存储空间。虽然这会浪费存储空间,但会提升CPU从内存中抓取数据的效率。

你可以用下面两个函数改变位图和像素图的存储方式和检索方式。

void glPixelStorei(GLenum pname, GLint param);

void glPixelStoref(GLenum pname, GLfloat param);

例如你的位图是紧密的包装在一起,你可以调用:

glPixelStorei(GL_UNPACK_ALIGMENT, 1);

GL_UNPACK_ALIGMENT告诉OpenGL如何从数据缓冲区中解包图像数据。同样地,你可以通过GL_PACK_ALIGMENT告诉OpenGL从颜色缓冲区读取的数据如何打包然后放到用户指定的内存空间中去。所有像素存储可用的模式如下表:

image

参考:http://www.opengl.org/sdk/docs/man/xhtml/glPixelStore.xml

像素图

像素图在内存中的布局类似于位图,然而每个像素在内存中不仅仅只用一位来表示。存储像素的位中包含了强度(亮度)和颜色成分。在当前的光栅位置绘制像素图:

void glDrawPixels(GLsizei width, GLsizei height, GLenum format, GLenum type, const void *pixels);

头两个参数指定了像素图的宽和高,第三个参数指定图像数据的格式,第四个参数表示数据类型,最后一个参数是指向数据的指针。不像glBitmap函数,此函数不会改变光栅的位置。数据元素的颜色布局通过format参数指定。format的常量如下表:

image

GL_STENCIL_INDEX和GL_DEPTH_COMPONENT用于直接从模板缓冲区和深度缓冲区中读和写。type参数用于指定*pixels指针存储的数据类型。它告诉OpenGL在缓冲区中的颜色成分是用什么样的数据类型存储的。其常量表格如下:

常量 描述
GL_UNSIGNED_BYTE 每种颜色成分用 8位无符号整数表示
GL_BYTE 8位有符号整数
GL_BITMAP 单个位,无颜色数据,与glBitmap相同
GL_UNSIGNED_SHORT 无符号16位整数
GL_SHORT 有符号16位整数
GL_UNSIGNED_INT 无符号32位整数
GL_INT 有符号32位整数
GL_FLOAT 单精度浮点数
GL_UNSIGNED_BYTE_3_3_2 经过包装的RGB值
GL_UNSIGNED_BYTE_2_3_3_REV 经过包装的RGB值,逆序
GL_UNSIGNED_SHORT_5_6_5 经过包装的RGB值
GL_UNSIGNED_SHORT_5_6_5_REV 经过包装的RGB值,逆序
GL_UNSIGNED_SHORT_4_4_4_4 经过包装的RGBA值
GL_UNSIGNED_SHORT_4_4_4_4_REV 经过包装的RGBA值,逆序
GL_UNSIGNED_SHORT_5_5_5_1 经过包装的RGBA值
GL_UNSIGNED_SHORT_1_5_5_5_REV 经过包装的RGBA值,逆序
GL_UNSIGNED_INT_8_8_8_8 经过包装的RGBA值
GL_UNSIGNED_INT_8_8_8_8_REV 经过包装的RGBA值,逆序
GL_UNSIGNED_INT_10_10_10_2 经过包装的RGBA值
GL_UNSIGNED_INT_2_10_10_10_REV 经过包装的RGBA值,逆序

包装的像素格式

对于一些更小的经过包装的像素格式,在显示设备上能够节省存储空间和有更快处理速度。这种经过包装的像素格式仍然可以在一些PC硬件中找到,并且在将来也可能发挥作用。

经过包装的像素格式把颜色数据尽可能地压缩的更小。例如:GL_UNSIGNED_BYTE_3_3_2格式用3位存储第一个成分,3位存储第二个成分,2位存储第三个成分。颜色成分的顺序依靠于glDrawPixels函数中设置的format参数(如有GL_RGB, GL_BGR等)。颜色成分的顺序是从最高位到最低位。GL_UNSIGNED_BYTE_2_3_3_REV反转了这个顺序,即把最后的那个颜色成分用头两位表示。

image

像素图示例

此程序中,从fire.tga文件中加载图像数据,然后通过glDrawPixels把图像数据渲染到颜色缓冲区中。此程序与BITMAP没有什么大的区别。不同的仅仅是使用glTools库中的函数gltLoadTGA来加载后缀为.tga的文件,然后把glBitmap换成glDrawPixels。示例如下:

void RenderScene()
{

glClear(GL_COLOR_BUFFER_BIT);
glColor3f(0.0f, 0.0f, 0.0f);

GLint iWidth = 0;
GLint iHeight = 0;
GLint iComponents = 0;
GLenum eFormat;
void *pImage = gltLoadTGA("fire.tga", &iWidth, &iHeight, &iComponents, &eFormat);

if (pImage)
{
glDrawPixels(iWidth, iHeight, eFormat, GL_UNSIGNED_BYTE, pImage);
free(pImage);
pImage = NULL;
}


glutSwapBuffers();
}

image

void *pImage = gltLoadTGA("fire.tga", &iWidth, &iHeight, &iComponents, &eFormat);

gltLoadTGA函数加载.tga后缀的图像文件,第一个参数是文件路径,第二第三个分别是图像文件的宽和高,第四个参数是图像包含多少个成分,最后一个参数是图像数据的格式。如果函数执行成功,就返回一个图像数据的指针(使用malloc分配的内存空间),如果失败就返回为NULL. 函数的实现如下:

GLbyte *gltLoadTGA(const char *szFileName, GLint *iWidth, GLint *iHeight, GLint *iComponents, GLenum *eFormat)
{
FILE *pFile; // File pointer
TGAHEADER tgaHeader; // TGA file header
unsigned long lImageSize; // Size in bytes of image
short sDepth; // Pixel depth;
GLbyte *pBits = NULL; // Pointer to bits

// Default/Failed values
*iWidth = 0;
*iHeight = 0;
*eFormat = GL_BGR_EXT;
*iComponents = GL_RGB8;

// Attempt to open the fil
pFile = fopen(szFileName, "rb");
if(pFile == NULL)
return NULL;

// Read in header (binary)
fread(&tgaHeader, 18/* sizeof(TGAHEADER)*/, 1, pFile);

// Do byte swap for big vs little endian
#ifdef __APPLE__
LITTLE_ENDIAN_WORD(&tgaHeader.colorMapStart);
LITTLE_ENDIAN_WORD(&tgaHeader.colorMapLength);
LITTLE_ENDIAN_WORD(&tgaHeader.xstart);
LITTLE_ENDIAN_WORD(&tgaHeader.ystart);
LITTLE_ENDIAN_WORD(&tgaHeader.width);
LITTLE_ENDIAN_WORD(&tgaHeader.height);
#endif


// Get width, height, and depth of texture
*iWidth = tgaHeader.width;
*iHeight = tgaHeader.height;
sDepth = tgaHeader.bits / 8;

// Put some validity checks here. Very simply, I only understand
// or care about 8, 24, or 32 bit targa's.
if(tgaHeader.bits != 8 && tgaHeader.bits != 24 && tgaHeader.bits != 32)
return NULL;

// Calculate size of image buffer
lImageSize = tgaHeader.width * tgaHeader.height * sDepth;

// Allocate memory and check for success
pBits = (GLbyte*)malloc(lImageSize * sizeof(GLbyte));
if(pBits == NULL)
return NULL;

// Read in the bits
// Check for read error. This should catch RLE or other
// weird formats that I don't want to recognize
if(fread(pBits, lImageSize, 1, pFile) != 1)
{
free(pBits);
return NULL;
}

// Set OpenGL format expected
switch(sDepth)
{
case 3: // Most likely case
*eFormat = GL_BGR_EXT;
*iComponents = GL_RGB8;
break;
case 4:
*eFormat = GL_BGRA_EXT;
*iComponents = GL_RGBA8;
break;
case 1:
*eFormat = GL_LUMINANCE;
*iComponents = GL_LUMINANCE8;
break;
};


// Done with File
fclose(pFile);

// Return pointer to image data
return pBits;
}



你可以注意到iComponents不是被赋值为1,3,或者4而是用GL_LUMINANCE8, GL_RGB8, GL_RGBA8. 当OpenGL操作数据时,会把这些特殊的常量作为维护完整图像的精度的内部请求。例如:出于性能的原因,一些OpenGL的实现会在内部把24位色的图像数据降低为16位色的。这种操作在纹理加载尤为常见。因为在许多实现上,显示输出分辨率只有16位,但实际加载的图像具有更高的分辨率。这些常量是对OpenGL实现所作的请求,要求按照完整的每个通道8位的颜色深度进行存储与使用。

移动像素

OpenGL可以从颜色缓冲区中读取像素数据,或者拷贝到另一个缓冲区中。读取缓冲区的函数:

void glReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, const void *pixels);

x,y指定窗口的坐标(从左下角开始),width,height指定读取的矩形范围的宽高,fromat指定像素的格式,type指定数据的类型。如果颜色缓冲区中存储的数据不是你想要的类型,OpenGL将会自动帮你转换。最后一个参数是存储图像数据的指针。指针指向的内存空间要可以容纳转换后的图像数据,否则会有内存越界的错误。

从颜色缓冲区中拷贝像素到另一个缓冲区中也是很容易的,而且在此操作中不需要分配临时的内存空间。首先,使用glRasterPos或glWindowsPos函数设置希望图像数据被拷贝的目标角落(左下角)的光栅位置。然后调用下面的函数进行拷贝。

void glCopyPixels(GLint x, GLint y, GLsizei height, GLsizei width, GLenum type);

x,y指定了要复制的矩形的左下角,width,height指定了宽高(以像素为单位),type为GL_COLOR 拷贝颜色数据,GL_DEPTH拷贝深度缓冲区中的数据,GL_STENCIL拷贝模板缓冲区的数据。

默认情况下,在双缓冲渲染环境中像素操作是在后缓冲区中进行,单缓冲则是前缓冲区中进行。可以使用下面两个函数改变像素操作的源或目标:

void glDrawBuffer(GLenum mode);

void glReadBuffer(GLenum mode);

glDrawBuffer决定了glDrawPixels和glCopyPixels往哪个缓冲区渲染。可以选的参数有:GL_NONE, GL_FRONT, GL_BACK, GL_FRONT_AND_BACK, GL_FRONT_LEFT,GL_FRONT_RIGHT。

glReadBuffer接受的参数也是一样的,决定了glReadPixels和glCopyPixels操作的目标缓冲区。

保存像素

我们可以从前颜色缓冲区中读取数据保存为图像文件。下面是gltools库中把图像数据保存为tga文件的程序:

GLint gltWriteTGA(const char *szFileName)
{
FILE *pFile; // 文件指针
TGAHEADER tgaHeader; // tga文件头
unsigned long lImageSize; // 图像的大小
GLbyte *pBits = NULL; // 图像数据
GLint iViewport[4]; // 视口
GLenum lastBuffer; // 保存当前读取缓冲区的设置

// 取得当前视口大小
glGetIntegerv(GL_VIEWPORT, iViewport);

// 获得图像大小,因为tga的图像数据是紧密包装的,所以用视口的宽高乘以3个字节
lImageSize = iViewport[2] * 3 * iViewport[3];

// 分配内存用于存储读取出来的图像数据
pBits = (GLbyte *)malloc(lImageSize);
if(pBits == NULL)
return 0;

// 设置为逐个像素的方式读取
glPixelStorei(GL_PACK_ALIGNMENT, 1);
glPixelStorei(GL_PACK_ROW_LENGTH, 0);
glPixelStorei(GL_PACK_SKIP_ROWS, 0);
glPixelStorei(GL_PACK_SKIP_PIXELS, 0);

// 保存当前的设置,后面再恢复它
glGetIntegerv(GL_READ_BUFFER, (GLint *)&lastBuffer);
glReadBuffer(GL_FRONT);
glReadPixels(0, 0, iViewport[2], iViewport[3], GL_BGR_EXT, GL_UNSIGNED_BYTE, pBits);
glReadBuffer(lastBuffer);

// 初始化tag文件头的格式
tgaHeader.identsize = 0;
tgaHeader.colorMapType = 0;
tgaHeader.imageType = 2;
tgaHeader.colorMapStart = 0;
tgaHeader.colorMapLength = 0;
tgaHeader.colorMapBits = 0;
tgaHeader.xstart = 0;
tgaHeader.ystart = 0;
tgaHeader.width = iViewport[2];
tgaHeader.height = iViewport[3];
tgaHeader.bits = 24;
tgaHeader.descriptor = 0;

// 苹果操作需要,进行大小端的转换
#ifdef __APPLE__
LITTLE_ENDIAN_WORD(&tgaHeader.colorMapStart);
LITTLE_ENDIAN_WORD(&tgaHeader.colorMapLength);
LITTLE_ENDIAN_WORD(&tgaHeader.xstart);
LITTLE_ENDIAN_WORD(&tgaHeader.ystart);
LITTLE_ENDIAN_WORD(&tgaHeader.width);
LITTLE_ENDIAN_WORD(&tgaHeader.height);
#endif

// 打开文件
pFile = fopen(szFileName, "wb");
if(pFile == NULL)
{
free(pBits);
return 0;
}

// 写文件头
fwrite(&tgaHeader, sizeof(TGAHEADER), 1, pFile);

// 写图像数据
fwrite(pBits, lImageSize, 1, pFile);

// 释放临时分配的内存空间
free(pBits);
fclose(pFile);

return 1;
}

OpenGL超级宝典 第4版 中文版PDF+英文版+源代码 见 

OpenGL编程指南(原书第7版)中文扫描版PDF 下载

OpenGL 渲染篇

Ubuntu 13.04 安装 OpenGL

OpenGL三维球体数据生成与绘制【附源码】

Ubuntu下OpenGL编程基础解析

如何在Ubuntu使用eclipse for c++配置OpenGL  

更多《OpenGL超级宝典学习笔记》相关知识 见 

相关内容