业精于勤,荒于嬉
全部博文(763)
分类: C/C++
2012-06-27 17:07:43
几乎所有OpenGL应用目的都是在屏幕窗口内绘制彩色图形,所以颜色在OpenGL编程中占有很重要的地位。这里的颜色与绘画中的颜色概念不一样,它属于RGB颜色空间,只在监视器屏幕上显示。另外,屏幕窗口坐标是以象素为单位,因此组成图形的每个象素都有自己 的颜色,而这种颜色值是通过对一系列OpenGL函数命令的处理最终计算出来的。本章将讲述计算机颜色的概念以及OpenGL的颜色模式、颜色定义和两种模式应用场合等内容,若掌握好颜色的应用,你就能走进缤纷绚丽的色彩世界,从中享受无穷的乐趣。
9.1、计算机颜色
9.1.1 颜色生成原理
计算机颜色不同于绘画或印刷中的颜色,显示于计算机屏幕上每一个点的颜色都是由监视器内部的电子枪激发的三束不同颜色的光(红、绿、蓝)混合而成,因此,计算机颜色通 常用R(Red)、G(Green)、B(Blue)三个值来表示,这三个值又称为颜色分量。颜色生成原理 示意图见图9-1所示。
图9-1 计算机颜色生成原理
9.1.2 RGB色立体(RGB Color Cube)
所有监视器屏幕的颜色都属于RGB颜色空间,如果用一个立方体形象地表示RGB颜色组成关系,那么就称这个立方体为RGB色立体,如图9-2所示。
图9-2 RGB色立体
在图中,R、G、B三值的范围都是从0.0到1.0。如果某颜色分量越大,则表示对应的颜色分量越亮,也就是它在此点所贡献的颜色成分越多;反之,则越暗或越少。当R、G、B三个值都为0.0时,此点颜色为黑色(Black);当三者都为1.0时,此点颜色为白色(White);当三个颜色分量值相等时,表示三者贡献一样,因此呈现灰色(Grey),在图中表现为从黑色顶点到白色顶点的那条对角线;当R=1.0、G=1.0、B=0.0时,此点颜色为黄色(Yellow);同理,R=1.0、G=0.0、B=1.0时为洋红色,也叫品色(Magenta);R=0.0、G=1.0、B=1.0时为青色(Cyan)。
9.2、颜色模式
OpenGL颜色模式一共有两个:RGB(RGBA)模式和颜色表模式。在RGB模式下,所有的颜色定义全用R、G、B三个值来表示,有时也加上Alpha值(与透明度有关),即RGBA模式。在颜色表模式下,每一个象素的颜色是用颜色表中的某个颜色索引值表示,而这个索引值指向了相应的R、G、B值。这样的一个表成为颜色映射(Color Map)。
9.2.1 RGBA模式(RGBA Mode)
在RGBA模式下,可以用glColor*()来定义当前颜色。其函数形式为:
void glColor3{b s i f d ub us ui}(TYPE r,TYPE g,TYPE b);
void glColor4{b s i f d ub us ui}(TYPE r,TYPE g,TYPE b,TYPE a);
void glColor3{b s i f d ub us ui}v(TYPE *v);
void glColor4{b s i f d ub us ui}v(TYPE *v);
设置当前R、G、B和A值。这个函数有3和4两种方式,在前一种方式下,a值缺省为1.0,后一种Alpha值由用户自己设定,范围从0.0到1.0。同样,它也可用指针传递参数。另外,函数的第二个后缀的不同使用,其相应的参数值及范围不同,见下表9-1所示。虽然这些参数值不同,但实际上OpenGL已自动将它们映射在0.0到1.0或-1.0或范围之内。因此,灵活使用这些后缀,会给你编程带来很大的方便。
后缀 数据类型 最小值 最小值映射 最大值 最大值映射
b 1字节整型数 -128 -1.0 127 1.0
s 2字节整型数 -32,768 -1.0 32,767 1.0
i 4字节整型数 -2,147,483,648 -1.0 2,147,483,647 1.0
ub 1字节无符号整型数 0 0.0 255 1.0
us 2字节无符号整型数 0 0.0 65,535 1.0
ui 4字节无符号整型数 0 0.0 4,294,967,295 1.0
表9-1 整型颜色值到浮点数的转换
9.2.2 颜色表模式(Color_Index Mode)
在颜色表方式下,可以调用glIndex*()函数从颜色表中选取当前颜色。其函数形式为:
void glIndex{sifd}(TYPE c);
void glIndex{sifd}v(TYPE *c);
设置当前颜色索引值,即调色板号。若值大于颜色位面数时则取模。
9.2.3 两种模式应用场合
在大多情况下,采用RGBA模式比颜色表模式的要多,尤其许多效果处理,如阴影、光照、雾、反走样、混合等,采用RGBA模式效果会更好些;另外,纹理映射只能在RGBA模式下进行。下面提供几种运用颜色表模式的情况(仅供参考):
1)若原来应用程序采用的是颜色表模式则转到OpenGL上来时最好仍保持这种模式,便于移植。
2)若所用颜色不在缺省提供的颜色许可范围之内,则采用颜色表模式。
3)在其它许多特殊处理,如颜色动画,采用这种模式会出现奇异的效果。
9.3、颜色应用举例
颜色是一个极具吸引力的应用,在前面几章中已经逐步介绍了RGBA模式的应用方式,这里就不再多述。下面着重说一下颜色表模式的应用方法,请看例程:
例9-1 颜色表应用例程(cindex.c)
#include "glos.h"
#include
#include
void myinit(void);
void InitPalette(void);
void DrawColorFans(void);
void CALLBACK myReshape(GLsizei w,GLsizei h);
void CALLBACK display(void);
void myinit(void)
{
glClearColor(0.0,0.0,0.0,0.0);
glClear(GL_COLOR_BUFFER_BIT);
glShadeModel(GL_FLAT);
}
void InitPalette(void)
{
GLint j;
static GLfloat rgb[][3]={
{1.0,0.0,0.0},{1.0,0.0,0.5},{1.0,0.0,1.0},{0.0,0.0,1.0},
{0.0,1.0,1.0},{0.0,1.0,0.0},{1.0,1.0,0.0},{1.0,0.5,0.0}};
for(j=0;j<8;j++)
auxSetOneColor(j+1,rgb[j][0],rgb[j][1],rgb[j][2]);
}
void CALLBACK myReshape(GLsizei w,GLsizei h)
{
glViewport(0,0,w,h);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
if(w<=h)
glOrtho(-12.0,12.0,-12.0*(GLfloat)h/(GLfloat)w, 12.0*(GLfloat)h/(GLfloat)w,-30.0,30.0);
else
glOrtho(-12.0*(GLfloat)h/(GLfloat)w, 12.0*(GLfloat)h/(GLfloat)w,-12.0,12.0,-30.0,30.0);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
}
void CALLBACK display(void)
{
InitPalette();
DrawColorFans();
glFlush();
}
void DrawColorFans(void)
{
GLint n;
GLfloat pp[8][2]={
{7.0,-7.0},{0.0,-10.0},{-7.0,-7.0},{-10.0,0.0},
{-7.0,7.0}, {0.0,10.0},{7.0,7.0},{10.0,0.0}};
/* draw some filled_fan_triangles */
glBegin(GL_TRIANGLE_FAN);
glVertex2f(0.0,0.0);
glVertex2f(10.0,0.0);
for(n=0;n<8;n++)
{
glIndexi(n+1);
glVertex2fv(pp[n]);
}
glEnd();
}
void main(void)
{
auxInitDisplayMode(AUX_SINGLE|AUX_INDEX);
auxInitPosition(0,0,500,500);
auxInitWindow("Color Index");
myinit();
auxReshapeFunc(myReshape);
auxMainLoop(display);
}
这个程序运行结果是在屏幕上显示八个连成扇形的不同颜色的三角形,每个三角形的颜色定义采用颜色表模式。其中,调用了辅助库函数auxSetOneColor()来装载颜色映射表,即调色板。因为将某个颜色装载到颜色查找表(color lookup table)中的过程必须依赖窗口系统,而OpenGL函数与窗口系统无关,所以这里就调用辅助库的函数来完成这个过程,然后才调用OpenGL自己的函数glIndex()设置当前的颜色号。
在OpenGL窗口中, 左下角的像素为(0, 0). 一般而言, 像素(x, y)占据的矩形区域左下角为(x, y), 右上角为(x+1, y+1).
10.1 缓存及其用途
1) 颜色缓存, 左前,右前,左后,右后和任意数量的辅助颜色缓存;
2) 深度缓存
3) 模板缓存
4) 累积缓存
注意:X窗口系统,RGBA模式至少保证1个颜色缓冲区,模板缓冲区,深度缓冲区,累计缓冲区
颜色索引模式至少保证1个眼色缓冲区,深度缓冲区,模板缓冲区.
可以使用glXGetConfig{}函数查询.
用glGetIntegerv()查询每个像素占据的缓存空间的参数
GL_RED_BITS, GL_GREEN_BITS, GL_BLUE_BITS, GL_ALPHA_BITS --- 颜色缓存中R, G, B, A分量的位数
GL_INDEX_BITS --- 颜色缓存中每个颜色索引的位数
GL_DEPTH_BITS --- 深度缓存中每个像素的位数
GL_STENCIL_BITS --- 模板缓存中每个像素的位数
GL_ACCUM_RED_BITS, GL_ACCUM_GREEN_BITS, GL_ACCUM_BLUE_BITS, GL_ACCUM_ALPHA_BITS --- 累积缓存中R, G, B, A分量的位数.
1) 颜色缓存存储了颜色索引或RGB颜色数据, 还可能存储了alpha值.
2) 支持立体观察(stereoscopic viewing)的OpenGL实现有左颜色缓存和右颜色缓存. 它们分别用于左立体图像和右立体图像.
3) 如不支持立体观察, 则只使用左颜色缓存.
4) 双颜色缓存有前缓存和后缓存, 单缓存系统只有前缓存.
5) 支持不可显示的辅助颜色缓存
6) 函数glGetBooleanv()查询是否支持立体观察和双缓存: GL_STEREO和GL_DOUBLE_BUFFER.
函数glGetIntegerv()查询多少个辅助缓存可用: GL_AUX_BUFFERES.
深度缓存 --- 存储了每个像素的深度值. 通常是离视点的距离, 因此深度值大的像素会被深度值小的像素覆盖.
用途之一: 绘图范围限制在屏幕的特定区域内.
累积缓存也存储RGBA颜色数据, 将一系列的图像合成一幅图像.
可以对图像进行超量采样, 然后对样本进行平均, 并将结果写入颜色缓存中, 从而实现场景反走样. 不能将数据直接写入累积缓存.
累加操作总是针对一个矩形块的, 通常是将数据移入或移出颜色缓存.
void glClearColor(GLclampf red, GLclampf green, GLclampf blue, GLclampf alpha);
void glClearIndex(GLfloat index);
void glClearDepth(GLclampd depth);
void glClearStencil(GLint s);
void glClearAccum(GLfloat red, GLfloat green, GLfloat blue, GLfloat alpha);
功能: 指定用于清除颜色缓存(RGBA模式或颜色索引模式), 深度缓存, 模板缓存和累积缓存的值.
类型为GLclampf和GLclampd的参数值被截取到[0.0, 1.0]. 深度缓存的默认清除值为1.0, 其他缓存为0.0.
设置清除值后, 便可以调用函数glClear()来清除缓存.
void glClear(GLbitfield mask);
功能: 清除指定的缓存.
mask:GL_COLOR_BUFFER_BIT, GL_DEPTH_BUFFER_BIT, GL_STENCIL_BUFFER_BIT, GL_ACCUM_BUFFER_BIT的逻辑或(OR).
清除颜色缓存时, 如果启用了像素所有权测试, 裁剪测试和抖动操作, 它们都会在清除操作中执行.
屏蔽操作(glColorMask()和glIndexMask())也会生效.alpha测试, 模版测试, 深度测试并不会影响glClear()函数的操作.
指定要写入的缓存 glDrawBuffer();
指定要读取的缓存 glReadBuffer();
使用双缓存, 通常只绘制后缓存, 并在绘制完成后交换缓存. 你可能想将双缓存窗口视为单缓存窗口: 通过调用函数glDrawBuffer()使得可以同时绘制前缓存和后缓存.
void glDrawBuffer(GLenum mode);
功能: 指定要写入或消除的颜色缓存以及禁用之前被启用的颜色缓存. 可以一次性启用多个缓存.
GL_FRONT: 单缓存的默认值
GL_FRONT_RIGHT:
GL_NONE:
GL_FRONT_LEFT:
GL_FRONT_AND_BACK:
GL_RIGHT:
GL_AUXi: i表示第几个辅助缓存.
GL_LEFT:
GL_BACK_RIGHT:
GL_BACK: 双缓存的默认值
GL_BACK_LEFT:
注意: 启用多个缓存用于写操作时, 只要其中一个缓存存在, 就不会发生错误. 如果指定的缓存都不存在, 就发生错误.
void glReadBuffer(GLenum mode);
功能: 选择接下来的函数调用glReadPixels(), glCopyPixels(), glCopyTexImage*(), glCopyTexSubImage*() 和 glCopyConvolutionFilter*()将读取的缓存.
并启用以前被函数glReadBuffer()启用的缓存.
参数mode取值:
GL_FRONT: 单缓存默认
GL_FRONT_RIGHT:
GL_BACK_RIGHT:
GL_FRONT_LEFT:
GL_LEFT:
GL_AUX:
GL_BACK_LEFT:
GL_BACK: 双缓存默认
GL_RIGHT:
注意: 启用缓存用于读取操作时, 指定的缓存必须存在, 否则将发生错误.
void glIndexMask(GLuint mask);
void glColorMask(GLboolean red, GLboolean green, GLboolean blue, GLboolean alpha);
void glDepthMask(GLboolean flag);
void glStencilMask(GLuint mask);
功能: 设置掩码, 用于控制写入到指定缓存中的数据,
glIndexMask: 只用于颜色索引模式中, 掩码中1对应的数据位被写入颜色索引缓存中. 0对应的位不写入.
glColorMask: 只影响RGBA模式下的写入, red, green, blue, alpha决定是否写入相应的分量, GL_TRUE时写入.
glDepthMask(): 如果参数flag的值为GL_TRUE, 则启用深度缓存用于写入, 否则禁用深度缓存.
glStencilMask(): 参数mask的含义与函数glIndexMask()中相同.
所有GLboolean参数的默认值是GL_TRUE, 两个GLuint参数的默认值都是1.
禁用深度缓存: 如果背景很复杂, 则在背景绘制之后, 禁用深度缓存. 绘制新增的物体, 只要不相互重叠.. 下一帧时, 只需恢复树木图像, 无需恢复深度缓存中的值.
这种方法很有效.
模板缓存的屏蔽操作让你能够使用一个多位模板缓存来存储多个模板(每位一个)
函数glStencilMask()指定的掩码用于控制哪些模板位面可写, 与函数glStencileFunc()的第三个参数指定的掩码无关, 后者指定模板函数将考虑哪些位面.
测试顺序:
1. 裁剪测试
2. alpha测试
3. 模版测试
4. 深度测试
5. 混合
6. 抖动
7. 逻辑操作
void glScissor(GLint x, GLint y, GLsizei width, GLsizei height);
设置裁剪矩形的位置和大小. 需要启用GL_SCISSOR_TEST.
需要启用GL_ALPHA_TEST
void glAlphaFunc(GLenum func, GLclampf ref);
设置用于alpha测试的参考值和比较函数.
alpha测试可用于透明算法和贴花.
void glStencilFunc(GLenum func, GLint ref, GLuint mask);
void glStencilFuncSeparate(GLenum face, GLenum func, GLint ref, GLuint mask);
设置模板测试所使用的比较函数(func),参考值(ref),掩码(mask)
void glStencilOp(GLenum fail, GLenum zfail, GLenum zpass);
void glStencilOpSeparate(GLenum face, GLenum fail, GLenum zfail, GLenum zpass);
指定当一个片段通过或未通过模板测试时, 模板缓冲区中的数据如何进行修改.
下图设置模板为中间的方块
模板查询glGetInteger()可使用的参数:
GL_STENCIL_FUNC
GL_STENCIL_REF
GL_STENCIL_VALUE_MASK
GL_STENCIL_FAIL
GL_STENCIL_PASS_DEPTH_FAIL
GL_STENCIL_PASS_DEPTH_PASS
10.2.4 深度测试
void glDepthFunc(GLenum func);
为深度测试设置比较函数.
遮挡测试允许我们判断一组几何图形在进行深度测试之后是否可见.
步骤:
1. 为每个所需的遮挡查询生成一个查询ID
2. 调用glBeginQuery(), 表示开始一个遮挡查询
3. 渲染需要进行遮挡查询的几何图形
4. 调用glEndQuery(), 表示已经完成了遮挡查询
5. 提取通过遮挡查询的样本数量.
生成查询对象
void glGenQueries(GLsizei n, GLuint* ids);
对遮挡查询对象进行初始化
void glBeginQuery(GLenum target, GLuint id);
void glEndQuery(GLenum target);
target必须为GL_SAMPLES_PASSED.
判断遮挡查询的结果
void glGetQueryObjectiv(GLenum id, GLenum pname, GLint *params);
void glGetQueryObjectuiv(GLenum id, GLenum pname, GLuint *params);
清除遮挡查询对象
void glDeleteQueries(GLsizei n, const GLuint *ids);
void glLogicOp(GLenum opcode);
选择需要执行的逻辑操作.
void glAccum(GLenum op, GLfloat value);
控制累积缓冲区
op参数:
GL_ACCUM--用glReadBuffer()所读取的当前缓冲区中读取每个像素.把R,G,B,A值乘以value.而后结果累加到累积缓冲区.
GL_LOAD--同上,只是用结果替换累积缓冲区中的值.
GL_RETURN--累积缓冲区中的值乘以value, 结果放在颜色缓冲区中.
GL_ADD和AL_MULT--累积缓冲区中的值与value相加或者相乘,结果写回累积缓冲区. 另GL_MULT的结果截取[-1.0. 1.0].GL_ADD的结果不截取.
首先清除累积缓冲区, 并启用前缓冲区用于读取和写入.
然后循环执行几次(例如n次)代码, 对图像进行微移和绘制.
对数据进行累积的方法:
glAccum(GL_ACCUM, 1.0/n); // 绘制到不会显示的颜色缓冲区中, 避免显示中间图像.
并最终调用
glAccum((GL_RETURN, 1.0); // 绘制到可显示的颜色缓冲区中(或即将交换的后缓冲区).
可提供一用户接口, 来显示每次图像累积之后所获得的改善, 如图像足够满意, 可随时终止累积.
本例主要是逐步在窗口累积各颜色分量.
一共累积八次,且其中每次都用j8数组的数据微移场景, 使用glFrustum函数可以是的我们场景不必对称.
正交投影的偏移只需要用glTranslatef()移动一个像素内的偏移即可.
下图为没有反锯齿没有用累积缓存的图像
下图为使用了累积缓存反锯齿的图像
累积缓存我分步骤显示,看看效果
10.3.2 运动模糊
按照相同的方式设置累积缓冲区, 但不是对图像进行空间上的微移, 而是进行时间上的微移.
glAccum(GL_MULT, decayFactor);
这样随着场景绘制到累积缓冲区中,整个场景越来越模糊. 其中decayFactor是个0.0到1.0之间的数字, 其值越小, 运动速度越快.
然后使用
glAccum(GL_RETURN, 1.0);
转移到眼色缓冲区中.
距离聚焦平面越远,物体就越模糊.
accPerspective函数
第五个和第六个参数表示在x和y方向上微移, 实现场景抗锯齿
第九个参数设定聚焦平面.
模糊程度有第七个和第八个参数决定, 由这两个参数的乘积决定.
多个光源所产生的柔和阴影-- 可以多次渲染场景,每次只打开一个光源, 然后将渲染结果累积起来.
样本的微移值
在前面的章节中,已经讲述了几何数据(点、线、多边形)绘制的有关方法,但OpenGL还有另外两种重要的数据类:一是位图,二是图像。这两种数据都是以象素矩阵形式存储,即用一个矩形数组来表示某一位图或图像。二者不同之处是位图包含每个象素的一位信息,而图像数据一般包含每个象素的多位信息(如,红、绿、蓝和Alpha值);还有位图类似于掩码,可用于遮掩别的图像,而图像数据则简单地覆盖先前已经存在的数据或者与之混合。下面将详述这些内容。
11.1、位图
11.1.1 位图(Bitmap)与字符(Font)
位图是以元素值为0或1的矩阵形式存储的,通常用于对窗口中相应区域的绘图屏蔽。比如说,当前颜色设置为红色,则在矩阵元素值为1的地方象素用红色来取代,反之,在为0的地方,对应的象素不受影响。位图普遍用于字符显示,请看下面例子:
例11-1 位图字符例程(font.c)
以上程序运行结果是显示三个相同的字符F。OpenGL函数库只提供了最底层操作,即用glRasterPos*()和glBitmap()在屏幕上定位和画一个位图,图11-1显示了F的位图和相应的位图数据。
在图中,字符大小为12*8的方阵,每一行数据用8位16进制表示。注意:位图数据总是按块存储,每块的位数总是8的倍数,但实际位图的宽并不一定使8的倍数。组成位图的位从位图的左下角开始画:首先画最底下的一行,然后是这行的上一行,依此类推。这个程序中的几个重要函数的解释将在下面几个小节,其中函数glPixelstorei()描述了位图数据在计算机内存中存储的方式。
11.1.2 当前光栅位置
当前光栅位置函数就是:
设置当前所画位图或图像的原点。其中参数x、y、z、w给出了光栅位置坐标。在变换到屏幕坐标时(即用模型变换和透视变换),光栅位置坐标与glVertex*()提供的坐标同样对待。也就是说,变换后要么确定一个有效点,要么认为位于视口以外的点的当前光栅位置无效。
在上一例中,颜色设置的位置与当前光栅位置函数调用的位置有关,glColor*()必须放 在glRasterPos*()前,则紧跟其后的位图就都继承当前的颜色,例前两个紫色的F;若要改变当前位图颜色,则需重新调用glColor*()和glRasterPos*(),如第三个黄色字符F的显示。
11.1.3 位图显示
当设置了光栅位置后,就可以调用glBitmap()函数来显示位图数据了。这个函数形式为:
显示由bitmap指定的位图,bitmap是一个指向位图的指针。位图的原点放在最近定义的当前光栅位置上。若当前光栅位置是无效的,则不显示此位图或其一部分,而且当前光栅位置仍然无效。参数width和height一象素为单位说明位图的宽行高。宽度不一定是8的倍数。参数xbo和ybo定义位图的原点(正值时,原点向上移动;负值时,原点向下移动)。参数xbi和ybi之处在位图光栅化后光栅位置的增量。在上一例中:
第一个字符F与第二个字符F的间距是由glBitmap()的两个增量参数决定的,即第二个字符F在第一个字符F的基础上分别向X正轴和Y负轴移动20个象素单位。
11.2 图像
一般来说,OpenGL图像(image)操作包括象素读写、象素拷贝和图像缩放,下面分别来介绍。
11.2.1 象素读写
OpenGL提供了最基本的象素读和写函数,它们分别是:
读取象素数据:
函数参数(x, y)定义图像区域左下角点的坐标,width和height分别是图像的高度和宽度,*pixel是一个指针,指向存储图像数据的数组。参数format指出所读象素数据元素的格式(索引值或R、G、B、A值,如表11-1所示),而参数type指出每个元素的数据类型(见表11-2)。
写入象素数据:
函数参数format和type与glReadPixels()有相同的意义,pixel指向的数组包含所要画的象素数据。注意,调用这个函数前必须先设置当前光栅位置,若当前光栅位置无效,则给出该函数时不画任何图形,并且当前光栅位置仍然保持无效。
名称 | 象素数据类型 |
GL_INDEX | 单个颜色索引 |
GL_RGB | 先是红色分量,再是绿色分量,然后是蓝色分量 |
GL_RED | 单个红色分量 |
GL_GREEN | 单个绿色分量 |
GL_BLUE | 单个蓝色分量 |
GL_ALPHA | 单个Alpha值 |
GL_LUMINANCE_ALPHA | 先是亮度分量,然后是Alpha值 |
GL_STENCIL_INDEX | 单个的模板索引 |
GL_DEPTH_COMPONENT | 单个深度分量 |
表11-1 函数glReadPixels()及glDrawPixels()的象素格式 |
名称 | 数据类型 |
GL_UNSIGNED_BYTE | 无符号的8位整数 |
GL_BYTE | 8位整数 |
GL_BITMAP | 无符号的8位整数数组中的单个数位 |
GL_UNSIGNED_SHORT | 无符号的16位整数 |
GL_SHORT | 16位整数 |
GL_UNSIGNED_INT | 无符号的32位整数 |
GL_INT | 32位整数 |
GL_FLOAT | 单精度浮点数 |
表11-2 函数glReadPixels()及glDrawPixels()的象素数据类型 |
图像的每个元素按表11-2给出的数据类型存储。若元素表示连续的值,如红、绿、蓝或亮度分量,每个值都按比例放缩使之适合于可用的位数。例如,红色分量是0.0到1.0之 间的浮点值。若它需要放到无符号单字节整数中,也仅有8位精度保存下来,其他无符号整数类型同理。对于有符号的数据类型还要少一位,例如颜色索引存到有符号的8位整数中,它的第一位被0xfe屏蔽掉了(即这个掩码包含7个1)。若类型是GL_FLOAT,索引值简单地转化成单精度浮点值,例如索引17转化成17.0,同理。
11.2.2 象素拷贝
象素拷贝函数是:
这个函数使用起来有点类似于先调用glReadPixels()函数后再调用glDrawPixels()一样,但它不需要将数据写到内存中去,因它只将数据写到framebuffer里。函数功能就是拷贝framebuffer中左下角点在(x, y)尺寸为width、height的矩形区域象素数据。数据拷贝到一个新的位置,其左下角点在当前光栅的位置,参数type可以是GL_COLOR、GL_STENCIL、GL_DEPTH。在拷贝过程中,参数type要按如下方式转换成format:
1)若type为GL_DEPTH或GL_STENCIL,那么format应分别是GL_DEPTH_COMPONENT或GL_STENCIL_INDEX;
2)若type为GL_COLOR,format则用GL_RGB或GL_COLOR_INDEX,这要依赖于图形系统是处于RGBA方式还是处于颜色表方式。
11.2.3 图像缩放
一般情况下,图像的一个象素写到屏幕上时也是一个象素,但是有时也需要将图像放大或缩小,OpenGL提供了这个函数:
设置象素写操作沿X和Y方向的放大或缩小因子。缺省情况下,zoomx、zoomy都是1.0。如果它们都是2.0,则每个图像象素被画到4个屏幕象素上面。注意:小数形式的缩放因子和负数因子都是可以的。
11.2.4 图像例程
下面举出一个图像应用的例子:
以上程序运行的结果是在屏幕正上方显示一个最初的五彩三角形,然后在下半部显示一串拷贝的三角形。当然,读者自己可以再加上图像放大缩小等,试试看,会发生怎样的情形?
今天我们先简单介绍Windows中常用的BMP文件格式,然后讲OpenGL的像素操作。虽然看起来内容可能有点多,但实际只有少量几个知识点,如果读者对诸如“显示BMP图象”等内容比较感兴趣的话,可能不知不觉就看完了。
像素操作可以很复杂,这里仅涉及了简单的部分,让大家对OpenGL像素操作有初步的印象。
学过多媒体技术的朋友可能知道,计算机保存图象的方法通常有两种:一是“矢量图”,一是“像素图”。矢量图保存了图象中每一几何物体的位置、形状、大小等信息,在显示图象时,根据这些信息计算得到完整的图象。“像素图”是将完整的图象纵横分为若干的行、列,这些行列使得图象被分割为很细小的分块,每一分块称为像素,保存每一像素的颜色也就保存了整个图象。
这两种方法各有优缺点。“矢量图”在图象进行放大、缩小时很方便,不会失真,但如果图象很复杂,那么就需要用非常多的几何体,数据量和运算量都很庞大。“像素图”无论图象多么复杂,数据量和运算量都不会增加,但在进行放大、缩小等操作时,会产生失真的情况。
前面我们曾介绍了如何使用OpenGL来绘制几何体,我们通过重复的绘制许多几何体,可以绘制出一幅矢量图。那么,应该如何绘制像素图呢?这就是我们今天要学习的内容了。
1、BMP文件格式简单介绍
BMP文件是一种像素文件,它保存了一幅图象中所有的像素。这种文件格式可以保存单色位图、16色或256色索引模式像素图、24位真彩色图象,每种模式种单一像素的大小分别为1/8字节,1/2字节,1字节和3字节。目前最常见的是256色BMP和24位色BMP。这种文件格式还定义了像素保存的几种方法,包括不压缩、RLE压缩等。常见的BMP文件大多是不压缩的。
这里为了简单起见,我们仅讨论24位色、不使用压缩的BMP。(如果你使用Windows自带的画图程序,很容易绘制出一个符合以上要求的BMP)
Windows所使用的BMP文件,在开始处有一个文件头,大小为54字节。保存了包括文件格式标识、颜色数、图象大小、压缩方式等信息,因为我们仅讨论24位色不压缩的BMP,所以文件头中的信息基本不需要注意,只有“大小”这一项对我们比较有用。图象的宽度和高度都是一个32位整数,在文件中的地址分别为0x0012和0x0016,于是我们可以使用以下代码来读取图象的大小信息:
GLint width, height; // 使用OpenGL的GLint类型,它是32位的。
// 而C语言本身的int则不一定是32位的。
FILE* pFile;
// 在这里进行“打开文件”的操作
fseek(pFile, 0x0012, SEEK_SET); // 移动到0x0012位置
fread(&width, sizeof(width), 1, pFile); // 读取宽度
fseek(pFile, 0x0016, SEEK_SET); // 移动到0x0016位置
// 由于上一句执行后本就应该在0x0016位置
// 所以这一句可省略
fread(&height, sizeof(height), 1, pFile); // 读取高度
54个字节以后,如果是16色或256色BMP,则还有一个颜色表,但24位色BMP没有这个,我们这里不考虑。接下来就是实际的像素数据了。24位色的BMP文件中,每三个字节表示一个像素的颜色。
注意,OpenGL通常使用RGB来表示颜色,但BMP文件则采用BGR,就是说,顺序被反过来了。
另外需要注意的地方是:像素的数据量并不一定完全等于图象的高度乘以宽度乘以每一像素的字节数,而是可能略大于这个值。原因是BMP文件采用了一种“对齐”的机制,每一行像素数据的长度若不是4的倍数,则填充一些数据使它是4的倍数。这样一来,一个17*15的24位BMP大小就应该是834字节(每行17个像素,有51字节,补充为52字节,乘以15得到像素数据总长度780,再加上文件开始的54字节,得到834字节)。分配内存时,一定要小心,不能直接使用“图象的高度乘以宽度乘以每一像素的字节数”来计算分配空间的长度,否则有可能导致分配的内存空间长度不足,造成越界访问,带来各种严重后果。
一个很简单的计算数据长度的方法如下:
int LineLength, TotalLength;
LineLength = ImageWidth * BytesPerPixel; // 每行数据长度大致为图象宽度乘以
// 每像素的字节数
while( LineLength % 4 != 0 ) // 修正LineLength使其为4的倍数
++LineLenth;
TotalLength = LineLength * ImageHeight; // 数据总长 = 每行长度 * 图象高度
这并不是效率最高的方法,但由于这个修正本身运算量并不大,使用频率也不高,我们就不需要再考虑更快的方法了。
2、简单的OpenGL像素操作
OpenGL提供了简洁的函数来操作像素:
glReadPixels:读取一些像素。当前可以简单理解为“把已经绘制好的像素(它可能已经被保存到显卡的显存中)读取到内存”。
glDrawPixels:绘制一些像素。当前可以简单理解为“把内存中一些数据作为像素数据,进行绘制”。
glCopyPixels:复制一些像素。当前可以简单理解为“把已经绘制好的像素从一个位置复制到另一个位置”。虽然从功能上看,好象等价于先读取像素再绘制像素,但实际上它不需要把已经绘制的像素(它可能已经被保存到显卡的显存中)转换为内存数据,然后再由内存数据进行重新的绘制,所以要比先读取后绘制快很多。
这三个函数可以完成简单的像素读取、绘制和复制任务,但实际上也可以完成更复杂的任务。当前,我们仅讨论一些简单的应用。由于这几个函数的参数数目比较多,下面我们分别介绍。
3、glReadPixels的用法和举例
3.1 函数的参数说明
该函数总共有七个参数。前四个参数可以得到一个矩形,该矩形所包括的像素都会被读取出来。(第一、二个参数表示了矩形的左下角横、纵坐标,坐标以窗口最左下角为零,最右上角为最大值;第三、四个参数表示了矩形的宽度和高度)
第五个参数表示读取的内容,例如:GL_RGB就会依次读取像素的红、绿、蓝三种数据,GL_RGBA则会依次读取像素的红、绿、蓝、alpha四种数据,GL_RED则只读取像素的红色数据(类似的还有GL_GREEN,GL_BLUE,以及GL_ALPHA)。如果采用的不是RGBA颜色模式,而是采用颜色索引模式,则也可以使用GL_COLOR_INDEX来读取像素的颜色索引。目前仅需要知道这些,但实际上还可以读取其它内容,例如深度缓冲区的深度数据等。
第六个参数表示读取的内容保存到内存时所使用的格式,例如:GL_UNSIGNED_BYTE会把各种数据保存为GLubyte,GL_FLOAT会把各种数据保存为GLfloat等。
第七个参数表示一个指针,像素数据被读取后,将被保存到这个指针所表示的地址。注意,需要保证该地址有足够的可以使用的空间,以容纳读取的像素数据。例如一幅大小为256*256的图象,如果读取其RGB数据,且每一数据被保存为GLubyte,总大小就是:256*256*3 = 196608字节,即192千字节。如果是读取RGBA数据,则总大小就是256*256*4 = 262144字节,即256千字节。
注意:glReadPixels实际上是从缓冲区中读取数据,如果使用了双缓冲区,则默认是从正在显示的缓冲(即前缓冲)中读取,而绘制工作是默认绘制到后缓冲区的。因此,如果需要读取已经绘制好的像素,往往需要先交换前后缓冲。
再看前面提到的BMP文件中两个需要注意的地方:
3.2 解决OpenGL常用的RGB像素数据与BMP文件的BGR像素数据顺序不一致问题
可以使用一些代码交换每个像素的第一字节和第三字节,使得RGB的数据变成BGR的数据。当然也可以使用另外的方式解决问题:新版本的OpenGL除了可以使用GL_RGB读取像素的红、绿、蓝数据外,也可以使用GL_BGR按照相反的顺序依次读取像素的蓝、绿、红数据,这样就与BMP文件格式相吻合了。即使你的gl/gl.h头文件中没有定义这个GL_BGR,也没有关系,可以尝试使用GL_BGR_EXT。虽然有的OpenGL实现(尤其是旧版本的实现)并不能使用GL_BGR_EXT,但我所知道的Windows环境下各种OpenGL实现都对GL_BGR提供了支持,毕竟Windows中各种表示颜色的数据几乎都是使用BGR的顺序,而非RGB的顺序。这可能与IBM-PC的硬件设计有关。
3.3 消除BMP文件中“对齐”带来的影响
实际上OpenGL也支持使用了这种“对齐”方式的像素数据。只要通过glPixelStore修改“像素保存时对齐的方式”就可以了。像这样:
int alignment = 4;
glPixelStorei(GL_UNPACK_ALIGNMENT, alignment);
第一个参数表示“设置像素的对齐值”,第二个参数表示实际设置为多少。这里像素可以单字节对齐(实际上就是不使用对齐)、双字节对齐(如果长度为奇数,则再补一个字节)、四字节对齐(如果长度不是四的倍数,则补为四的倍数)、八字节对齐。分别对应alignment的值为1, 2, 4, 8。实际上,默认的值是4,正好与BMP文件的对齐方式相吻合。
glPixelStorei也可以用于设置其它各种参数。但我们这里并不需要深入讨论了。
现在,我们已经可以把屏幕上的像素读取到内存了,如果需要的话,我们还可以将内存中的数据保存到文件。正确的对照BMP文件格式,我们的程序就可以把屏幕中的图象保存为BMP文件,达到屏幕截图的效果。
我们并没有详细介绍BMP文件开头的54个字节的所有内容,不过这无伤大雅。从一个正确的BMP文件中读取前54个字节,修改其中的宽度和高度信息,就可以得到新的文件头了。假设我们先建立一个1*1大小的24位色BMP,文件名为dummy.bmp,又假设新的BMP文件名称为grab.bmp。则可以编写如下代码:
FILE* pOriginFile = fopen("dummy.bmp", "rb);
FILE* pGrabFile = fopen("grab.bmp", "wb");
char BMP_Header[54];
GLint width, height;
// 读取dummy.bmp中的头54个字节到数组
fread(BMP_Header, sizeof(BMP_Header), 1, pOriginFile);
// 把数组内容写入到新的BMP文件
fwrite(BMP_Header, sizeof(BMP_Header), 1, pGrabFile);
// 修改其中的大小信息
fseek(pGrabFile, 0x0012, SEEK_SET);
fwrite(&width, sizeof(width), 1, pGrabFile);
fwrite(&height, sizeof(height), 1, pGrabFile);
// 移动到文件末尾,开始写入像素数据
fseek(pGrabFile, 0, SEEK_END);
fclose(pOriginFile);
fclose(pGrabFile);
我们给出完整的代码,演示如何把整个窗口的图象抓取出来并保存为BMP文件。
把这段代码复制到以前任何课程的样例程序中,在绘制函数的最后调用grab函数,即可把图象内容保存为BMP文件了。(在我写这个教程的时候,不少地方都用这样的代码进行截图工作,这段代码一旦写好,运行起来是很方便的。)
4、glDrawPixels的用法和举例
glDrawPixels函数与glReadPixels函数相比,参数内容大致相同。它的第一、二、三、四个参数分别对应于glReadPixels函数的第三、四、五、六个参数,依次表示图象宽度、图象高度、像素数据内容、像素数据在内存中的格式。两个函数的最后一个参数也是对应的,glReadPixels中表示像素读取后存放在内存中的位置,glDrawPixels则表示用于绘制的像素数据在内存中的位置。
注意到glDrawPixels函数比glReadPixels函数少了两个参数,这两个参数在glReadPixels中分别是表示图象的起始位置。在glDrawPixels中,不必显式的指定绘制的位置,这是因为绘制的位置是由另一个函数glRasterPos*来指定的。glRasterPos*函数的参数与glVertex*类似,通过指定一个二维/三维/四维坐标,OpenGL将自动计算出该坐标对应的屏幕位置,并把该位置作为绘制像素的起始位置。
很自然的,我们可以从BMP文件中读取像素数据,并使用glDrawPixels绘制到屏幕上。我们选择Windows XP默认的桌面背景Bliss.bmp作为绘制的内容(如果你使用的是Windows XP系统,很可能可以在硬盘中搜索到这个文件。当然你也可以使用其它BMP文件来代替,只要它是24位的BMP文件。注意需要修改代码开始部分的FileName的定义),先把该文件复制一份放到正确的位置,我们在程序开始时,就读取该文件,从而获得图象的大小后,根据该大小来创建合适的OpenGL窗口,并绘制像素。
绘制像素本来是很简单的过程,但是这个程序在骨架上与前面的各种示例程序稍有不同,所以我还是打算给出一份完整的代码。
这里仅仅是一个简单的显示24位BMP图象的程序,如果读者对BMP文件格式比较熟悉,也可以写出适用于各种BMP图象的显示程序,在像素处理时,它们所使用的方法是类似的。
OpenGL在绘制像素之前,可以对像素进行若干处理。最常用的可能就是对整个像素图象进行放大/缩小。使用glPixelZoom来设置放大/缩小的系数,该函数有两个参数,分别是水平方向系数和垂直方向系数。例如设置glPixelZoom(0.5f, 0.8f);则表示水平方向变为原来的50%大小,而垂直方向变为原来的80%大小。我们甚至可以使用负的系数,使得整个图象进行水平方向或垂直方向的翻转(默认像素从左绘制到右,但翻转后将从右绘制到左。默认像素从下绘制到上,但翻转后将从上绘制到下。因此,glRasterPos*函数设置的“开始位置”不一定就是矩形的左下角)。
5、glCopyPixels的用法和举例
从效果上看,glCopyPixels进行像素复制的操作,等价于把像素读取到内存,再从内存绘制到另一个区域,因此可以通过glReadPixels和glDrawPixels组合来实现复制像素的功能。然而我们知道,像素数据通常数据量很大,例如一幅1024*768的图象,如果使用24位BGR方式表示,则需要至少1024*768*3字节,即2.25兆字节。这么多的数据要进行一次读操作和一次写操作,并且因为在glReadPixels和glDrawPixels中设置的数据格式不同,很可能涉及到数据格式的转换。这对CPU无疑是一个不小的负担。使用glCopyPixels直接从像素数据复制出新的像素数据,避免了多余的数据的格式转换,并且也可能减少一些数据复制操作(因为数据可能直接由显卡负责复制,不需要经过主内存),因此效率比较高。
glCopyPixels函数也通过glRasterPos*系列函数来设置绘制的位置,因为不需要涉及到主内存,所以不需要指定数据在内存中的格式,也不需要使用任何指针。
glCopyPixels函数有五个参数,第一、二个参数表示复制像素来源的矩形的左下角坐标,第三、四个参数表示复制像素来源的举行的宽度和高度,第五个参数通常使用GL_COLOR,表示复制像素的颜色,但也可以是GL_DEPTH或GL_STENCIL,分别表示复制深度缓冲数据或模板缓冲数据。
值得一提的是,glDrawPixels和glReadPixels中设置的各种操作,例如glPixelZoom等,在glCopyPixels函数中同样有效。
下面看一个简单的例子,绘制一个三角形后,复制像素,并同时进行水平和垂直方向的翻转,然后缩小为原来的一半,并绘制。绘制完毕后,调用前面的grab函数,将屏幕中所有内容保存为grab.bmp。其中WindowWidth和WindowHeight是表示窗口宽度和高度的常量。