浏览量:1,762

像素相关的操作

本篇文章主要包括四个部分,介绍与像素有关的一些操作,第1节,介绍光栅化在图形管线中所处的位置,像素图的概念和像素操作;第2节,介绍颜色混合在OpenGL中的应用,即像素操作在OpenGL当中的应用;第3节,主要介绍光栅位置和位图绘制,简单的介绍了glRasterPos*()和glBitmap()两个函数使用;第4节,主要图像管线,OpenGL当中像素是怎么处理,纹理图像如何在硬件端处理等,还介绍了其中主要的三个函数glDrawPixels()、glReadPixels()、glCopyPixels()用法。

示例代码的下载地址:https://github.com/twinklingstar20/twinklingstar_cn_demo_operation_about_pixels/

一.      像素操作

光栅化就是把几何数据和像素数据转换为片段的过程,每个片段对应于帧缓冲区中的一个像素,每个片段包括颜色、深度、纹理坐标信息。如图1所示,很多步骤和测试会在“per fragment operations”中进行,之后片段会将像素值写进帧缓冲区(frame buffer)中。

2013-11-9 16-46-25

图1 图形管线中的光栅阶段

一张图片在内存中是以像素图(pixmap)的形式保存,即一个矩形的数值数组,像素图随意的保存在内存中,能从一个地方复制到另一个地方,当它被复制到帧缓冲区后,会显示在显示器上。在一个像素图上,每个像素占用固定的位数(bit),固定的位数就是该像素的颜色深度(color depth)。如果颜色深度为1,即产生两种颜色的像素图,就称之为位图(bitmap);如果每个像素用一个字节表示,表示从0到255的灰度级,0表示黑色,255表示白色,称为灰度图像(gray-scale pixmap);通过LUT索引号来存储颜色,每个像素用颜色表(color lookup table,LUT)的一个索引来保存,通常颜色表有256个表项,所以索引用一个字节表示就够了;RGB图像,R、G、B三个颜色分量各占一个字节,这种图像可以称之为真彩图;RGBA图像,除了R、G、B三个颜色分量外,还包括一个alpha分量或者称为alpha通道(alpha channel)。

在一些环境下,我们希望将不同的像素图组合起来,生成一个新的像素图,这样的像素操作是非常有用的。用一个数学公式表示这个过程,如公式所示。其中⊕符号并不明确指定两个像素的操作类型,所以它可以是取平均值操作、加操作、减操作等等。

2013-11-9 16-46-35

假设像素图C是通过像素图S和D计算得到,其中D表示目标像素图,S表示源像素图,如果C与D相同,则S和D像素操作后,还要将结果写回D中。这样将D中的像素读取到内存当中,通过与S中的像素操作并修改它的值,最后将结果又写回D中的整个操作称为读-修改-写周期,OpenGL对这种操作提供了一个很高效的方法。

回过来叙述下前面提到的RGBA图像,RGBA图像在图像混合上就起到了重要的作用,前面说的alpha值的取值范围是0到255,0表示完全透明,255表示完全不透明,alpha也通过作为一个0到1范围内的缩放因子,即alpha/255。因此在加入alpha通道后,两个图像的混合计算方法如下所示:

D[i][j].R = a•S[i][j].R + (1-a)D[i][j].R;

D[i][j].G = a•S[i][j].G + (1-a)D[i][j].G;

D[i][j].B = a•S[i][j].B + (1-a)D[i][j].B;

  其中a=S[i][j].A/255

二.      颜色混合在OpenGL中的应用

下面介绍像素混合在OpenGL中的应用,采用的函数主要有:

void glBlendFunc( GLenum sfactor, GLenum dfactor);
功能:规定了像素操作算法,只在RGBA颜色模式下可用。
1. sfactor:规定了源图像的混合因子,有9个可选的参数
2. dfactor:规定了目标图像的混合因子,有8个可选的参数

默认情况下,颜色混合是被禁用的,可以使用参数GL_BLEND将它激活。源图像指的是要画的图元,图元范围外的图像不进行混合操作,例如,要画一个正方形,只有正方形内的像素进行混合操作,正方形外的图像不进行。目标图像是指已存在帧缓存中的像素组成的图像。

该函数定义了像素操作算法,接下来具体介绍该函数像素操作算法的公式、参数等。原像素用(Rs,Gs,Bs,As)表示,目标像素用(Rd,Gd,Bd,Ad)表示,如果用整数值表示像素值,则像素值的取值范围是(0,0,0,0)到(kR,kG,kB,kA):

kR=2^mR-1

kG=2^mG-1

kB=2^mB-1

kA=2^mA-1

(mR,mG,mB,mA)分别是R、G、B、A四个颜色分量的位平面数,即占用的位数,用(sR,sG,sB,sA)和(dR,dG,dB,dA)分别表示源图像和目标图像的缩放因子。则RGBA的计算公式可以表示为:

R(d)=min(kR,RssR + RddR)

G(d)=min(kG,GssG + GddG)

B(d)=min(kB,BssB + BddB)

A(d)=min(kA,AssA + AddA)

下面举个简单的例子说明该混合方法的使用,如下面的代码片段所示,首先激活GL_BLEND,使用函数glBlendFunc()指定源图像的混合因子和目标图像的混合因子,接着就可以在函数myDisplay()中进行绘制了。下面的代码片段指定源图像的混合因子是源图像的alpha通道,而目标图像的混合因子是(1-原图像的alpha)。附录文件BlendDemo提供了完整的演示代码。

void init()
{
	glClearColor(0.0f,0.0f,0.0f,0.0f);
	glEnable(GL_BLEND);
	glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
}
void myDisplay(void)
{
	glClear(GL_COLOR_BUFFER_BIT);
	glBegin(GL_QUADS);
		glColor4f(1.0f,0,0,1.0f);
		glVertex3f(-1.0f,-1.0f,-6.0f);
		glVertex3f(1.0f,-1.0f,-6.0f);
		glVertex3f(1.0f,1.0f,-6.0f);
		glVertex3f(-1.0f,1.0f,-6.0f);

		glColor4f(0,1.0f,0,0.0f);
		glVertex3f(-1.0f + 1.5f,-1.0f,-6.0f);
		glVertex3f(1.0f + 1.5f,-1.0f,-6.0f);
		glVertex3f(1.0f + 1.5f,1.0f,-6.0f);
		glVertex3f(-1.0f + 1.5f,1.0f,-6.0f);

		glColor4f(0,0,1.0f,0.5f);
		glVertex3f(-1.0f - 1.5f,-1.0f,-6.0f);
		glVertex3f(1.0f - 1.5f,-1.0f,-6.0f);
		glVertex3f(1.0f - 1.5f,1.0f,-6.0f);
		glVertex3f(-1.0f - 1.5f,1.0f,-6.0f);
	glEnd();
	glFlush();
}

这里简单解释下程序的流程,当绘制第1个四边形时,alpha值是1,源因子是1,目标因子是0,只显示源图像,即显示的是中间一个红色的正方形;当绘制第2个四边形时,源因子是0,目标因子是1,不显示源图像,即在图中未显示该正方形,如果显示出来它应该在右侧的位置;当绘制最后一个四边形时,源因子是0.5,目标因子是0.5,发生混合,根据上面的公式进行计算,与中间红色的四边形的一部分发生混合,形成紫色区域,其余部分与黑色的背景混合,得到的结果还是蓝色。在附录中上传了相关的代码。

2013-11-9 16-50-17

图2 颜色混合示例

    (引用了【2】中的原话)进行三维场景的混合时必须注意深度缓冲,深度缓冲记录了每一个像素距离观察者有多近。在启用深度缓冲测试的情况下,如果将要绘制的像素比原来的像素更近,则像素将被绘制,否则,像素就会被忽略掉,不进行绘制。这在绘制不透明的物体时非常有用——不管是先绘制近的物体再绘制远的物体,还是先绘制远的物体再绘制近的物体,或者干脆以混乱的顺序进行绘制,最后的显示结果总是近的物体遮住远的物体。但是在你需要实现半透明效果时,发现一切都不是那么美好了。如果你绘制了一个近距离的半透明物体,则它在深度缓冲区内保留了一些信息,使得远处的物体将无法再被绘制出来。虽然半透明的物体仍然半透明,但透过它看到的却不是正确的内容了。

要解决以上问题,需要在绘制半透明物体时将深度缓冲区设置为只读,这样一来,虽然半透明物体被绘制上去了,深度缓冲区还保持在原来的状态。如果再有一个物体出现在半透明物体之后,在不透明物体之前,则它也可以被绘制(因为此时深度缓冲区中记录的是那个不透明物体的深度)。以后再要绘制不透明物体时,只需要再将深度缓冲区设置为可读可写的形式即可。

NeHe教程第08课介绍了混合在OpenGL中的应用,它使用三维混合时直接将深度缓冲区禁用,即调用glDisable(GL_DEPTH_TEST)。这样做并不正确,如果先绘制一个不透明的物体,再在其背后绘制半透明物体,本来后面的半透明物体将不会被显示(被不透明的物体遮住了),但如果禁用深度缓冲,则它仍然将会显示,并进行混合。

下面的代码片段介绍了在3D场景,存在深度测试时颜色混合的方法:(1)开启颜色混合和深度测试,(2)指定源图像和目标图像的混合因子,(3)在绘制前清理深度缓冲和颜色缓冲区,(4)要绘制半透明图元前,将深度缓冲区设置为只读,绘制完成后恢复。附录文件3DBlendDemo提供了完整的演示代码。

void init()
{
	glClearColor(0.0f,0.0f,0.0f,0.0f);
	glEnable(GL_BLEND);
	glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
	glEnable(GL_DEPTH_TEST);
	glDepthFunc(GL_LEQUAL);	
}
void myDisplay(void)
{
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	glLoadIdentity();

	glDepthMask(GL_FALSE);
	glBegin(GL_QUADS);
		glColor4f(1.0f,0,0,0.4f);
		glVertex3f(-1.0f,-1.0f,-6.0f);
		glVertex3f(1.0f,-1.0f,-6.0f);
		glVertex3f(1.0f,1.0f,-6.0f);
		glVertex3f(-1.0f,1.0f,-6.0f);
	glEnd();
	glDepthMask(GL_TRUE);

	glBegin(GL_QUADS);
		glColor4f(0,1.0f,0,1.0f);
		glVertex3f(-1.0f,-1.0f,-5.0f);
		glVertex3f(1.0f,-1.0f,-5.0f);
		glVertex3f(1.0f,1.0f,-5.0f);
		glVertex3f(-1.0f,1.0f,-5.0f);
	glEnd();

	glDepthMask(GL_FALSE);
	glBegin(GL_QUADS);
	glColor4f(0,0,1.0f,0.5f);
	glVertex3f(-1.0f,-1.0f,-4.0f);
	glVertex3f(1.0f,-1.0f,-4.0f);
	glVertex3f(1.0f,1.0f,-4.0f);
	glVertex3f(-1.0f,1.0f,-4.0f);
	glEnd();
	glDepthMask(GL_TRUE);

	glFlush();
}

演示结果如下所示,总共画了3个正方形,中间一个是完全不透明的,所以将最靠后的一个正方形挡住了,最前面的一个正方形是半透明的,与中间一个正方形发生颜色混合。

2013-11-9 16-50-47

图3 包含深度检测的颜色混合

    此外,颜色混合中还有几个其它方法,例如glBlendFuncSeparate(),glBlendEquation(),glBlendEquationSeparate(),这里不详细介绍了。

三.      光栅位置和位图绘制

void glRasterPos2*(TYPE x,TYPE y);
void glRasterPos3*(TYPE x,TYPE y,TYPE z);
void glRasterPos4*(TYPE x,TYPE y,TYPE z,TYPE w);
void glRasterPos{2,3,4}*v(TYPE *v);

GL维护一个窗口坐标的3D位置信息,这个3D位置信息就称为光栅位置,通常用于指定象素位置和位图写操作,可以精确到亚像素。当前的光栅位置包括窗口坐标(x,y,z),一个裁剪值(w),一个眼睛坐标距离,一个有效位和相关的颜色数据、纹理信息。glRasterPos4规定了对象坐标x,y,z和w;glRasterPos3()规定了对象坐标x,y,z,w被设置为1;glRasterPos2规定了对象坐标x,y,而z和w 分别被设置为0,1。glRasterPos规定的对角坐标就像glVertex命令一样,能够进行模型视图变换、投影变换并被传递到裁剪阶段,如果该顶点非被剔除,则它会被投影并缩放到窗口坐标,GL_CURRENT_RASTER_POSITION_VALID标记会被设置有效,否则该位会被清除并且当前的光栅位置、相关联的颜色、纹理信息都处于未定义状态。简单来说,当前的光栅位置就是下一个位图要画的位置。

void glWindowPos3*(TYPE x,TYPE y,TYPE z);
void glWindowPos{2,3}*v(TYPE* v);

glWindowPos*()函数规定了窗口坐标系统下当前的光栅位置,坐标(x,y)不经过模型视图变换等。

void glBitmap(GLsizei width, GLsizei height, GLfloat xorig, GLfloat yorig, GLfloat xmove, GLfloat ymove, const GLubyte * bitmap);
width,height:规定了位图图像的宽和高
xorig,yorig:规定了位图图像原点的位置,原点指的是位图的左下角
xmove,ymove:在位图图像绘制完成后,x,y与当前光栅位置的偏移
bitmap:位图图像的地址

使用下面的代码片段,可以绘制出图4所示的字体,附录文件RasterBitmap提供了完整的演示代码。

GLubyte rasters[24] = {
	0xc0,0x00,0xc0,0x00,0xc0,0x00,0xc0,0x00,0xc0,0x00,
	0xff,0x00,0xff,0x00,0xc0,0x00,0xc0,0x00,0xc0,0x00,
	0xff,0xc0,0xff,0xc0 };
	glClear(GL_COLOR_BUFFER_BIT);
	glColor3f(1.0f,1.0f,1.0f);
	glRasterPos2i(20,20);
	glBitmap(15,12,0.0,0.0,11.0,0.0,rasters);

2013-11-9 16-51-00

图4. “F”像素字体

四.      图像管线

4.1 几个重要的函数说明

void glReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, GLvoid* data);
功能:从帧缓冲区中读取一个矩形块的像素,并把它保存进处理器内存中。
1. x,y:规定要复制的矩形区域左下角的坐标
2. width,height:规定要复制的矩形区域的宽和高,必需是非负的
3. format:规定了像素数据的格式
4. type:规定了像素数据的数据类型
5. data:返回的像素数据
void glDrawPixels(GLsizei width, GLsizei height, GLenum format, GLenum type, const GLvoid * data);
功能:把处理器内存中的一个矩形块像素写进帧缓冲glRasterPos*()指定的光栅位置。
1. width,height:规定写进帧缓存中像素矩阵块的宽和高
2. format: 规定了像素数据的格式
3. type: 规定了像素数据的数据类型
4. data:返回的像素数据
void glCopyPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum type);
功能:把一个矩形块像素由帧缓冲区中一个区域复制到它的另一个区域,数据不经过处理器内存。
1. x,y:规定了要复制的矩形区域左下角的窗口坐标
2. width,height:规定了要复制的矩形区域的宽和高
3. type:规定了要复制的是颜色值,深度值还是模板值,传入的参数只能是GL_COLOR,GL_DEPTH,GL_STENCIL

注意:帧缓冲区是服务器端的,或者说在硬件端。

4.2 图像处理管线

2013-11-9 16-51-14

图5. 图像管线(Imaging Pipeline)

4.2.1  像素的包装和拆分(pack and unpack

像素的包装和拆分涉及到像素数据写入到处理器内存或者从处理器内存中读入的方式。存储在内存中的图像包括1至4个数据块,该数据块称为元素(element),数据可能只包含颜色索引,亮度(是r,g,g颜色值的加权和),或者它可能包含每个像素的r,g,b,alpha分量,像素数据可能的分配或者格式决定了每个像素存储元素数量和顺序。一些元素(比如颜色索引或者模板索引)可能只是一些整数,其它可能是浮点数(比如r,g,b,alph值,深度值)。在帧缓存区存储的浮点数占的位数可能比全浮点数(full floating-point number)要求的位数少,具体占用的位数依赖于硬件具体的实现。元素在内存当中以各种数据形式存储,从8位的字节到32位的整数或者浮点数的形式,OpenGL明确定义了每种格式的每个分量转换成的可能的数据类型。举个简单的例子,如果函数glReadPixels()中的format变量是GL_RGB,type变量是GL_UNSIGNED_BYTE,这时候就表示从帧缓冲区中以R,G,B的顺序读取颜色,每个颜色分量各用一个无符号的字节存储。

4.2.2  控制像素存储模式(pixel-storage mode

OpenGL支持的所有像素存储模式由glPixelStore*()函数控制,该函数设置像素存储模式,能影响到的操作包括glDrawPixels(),glReadPixels(),glPolygonStipple(),glBitmap(),glTexImage*(),glTexSubImage*(),glConvolutionFilter*(),glSeparableFilter2D,glColorTable(),glColorSubTable(),glHistogram(),glMinmax()。例如glPixelStorei(GL_PACK_ALIGNMENT,4)就规定了4字节对齐的存储格式。这些函数具体的解释参见相关文档。

4.2.3  像素转换操作(pixel-transfer operation)

当图像数据从内存转换到帧缓冲区或者从帧缓冲区转换到内存中时,OpenGL都会进行多种操作。例如,颜色分量范围的变化:正常情况下红色分量的范围是0.0到1.0,但是在显卡中需要把它转换到别的范围,或者在不同显卡中,红色分量以不同的取值范围存储。在像素变换阶段,我们甚至能创建映射进行颜色索引或者颜色分量的任意转化,这些转化称为像素变换操作。主要由glPixelTransfer*()和glPixelMap*()两条指令完成。

4.2.4   光栅化阶段(rasterization)

在像素存储模式和像素转换操作完成后,可以对图像进行放大,减小或者是反转变化,这些变化由函数glPixelZoom()进行操作。

4.2.5  几个函数的流程

当调用glDrawPixels()时,在处理器内存中的数据根据像素存储模式、数据格式等信息先被拆分开,然后进行像素转化操作,处理得到像素传给光栅处理。在光栅过程中,会根据当前的状态进行放大或者缩小操作,最后进行片段处理,将得到的结果传进帧缓冲区中。

当调用glReadPixels()函数时,从帧缓冲区中读取数据,然后进行像素转换操作,最后将得到的像素进行包装(pack)存入帧缓冲区中,相当于glDrawPixels()的逆过程。

2013-11-9 16-51-28

图6. glCopyPixels()像素路径

glCopyPixels()进行glReadPixels()函数中所有的像素转化操作,得到的结果再按照函数glDrawPixels()接下来进行的操作一样,但是转化不会进行两次,也不会经过处理器内存,如图6所示。

位图的渲染过程比图像的图像更加的简单,既没有像素转换操作,也没有像素数组的放大缩小操作,如下图所示。

2013-11-9 16-51-47

图7. glBitmap()函数的像素路径

总结下上述几个方法的数据流简化图,如图8所示:

2013-11-9 16-51-58

图8. 像素数据流的简化图

像素存储模式和像素转换操作被应用到纹理处理上,它们会被写入纹理内存中,对于OpenGL中的函数glTexImage*(),glTexSubImage*()和glGetTexImage()的像素路径如下图所示。

2013-11-9 16-52-08

图9. glTexImage*(),glTexSubImage*()和glGetTexImage()的像素路径

当像素数据由帧缓冲区复制到纹理内存中的处理路径如下所示:

2013-11-9 16-52-18

图10. glCopyTexImage*()和glCopyTexSubImage*()的像素路径

4.2.6  例子说明

下面举个很简单的例子来说明glDrawPixels()和glReadPixels()两个方法的使用,首先设置成双缓冲区,RGB颜色模式显示。

glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB);

在显示函数里面,先绘制一个正方形;然后从颜色缓冲区中,以RGB的格式,每个分量用UNSIGNED BYTE数据类型读取出数据;对后100行的数据进行修改,并通过glDrawPixels()传回到颜色缓冲区中;最后,使用glutSwapBuffers(),将缓冲区中的内容显示出来。附录文件PixelDemo提供了完整的演示代码。

void myDisplay(void)
{
	glClear(GL_COLOR_BUFFER_BIT);
	glLoadIdentity();
	glBegin(GL_QUADS);
		glColor3f(0,1.0f,0);
		glVertex2i(10,10);
		glVertex2i(10,200);
		glVertex2i(200,200);
		glVertex2i(200,10);
	glEnd();

	unsigned char* ptrBuf = new unsigned char[g_inWidth*g_inHeight*3];
	glReadPixels(0,0,g_inWidth,g_inHeight,GL_RGB,GL_UNSIGNED_BYTE,ptrBuf);
	int i , j;
	int heightEnd = g_inHeight-100>0 ? g_inHeight - 100 : 0;
	for( i=g_inHeight-1 ; i>=heightEnd ; i-- )
	{
		if( i%2==0 )
			continue;
		for( j=0 ; j<g_inWidth ; j++ )
		{
			int tmpIndex = (i*g_inWidth + j)*3;
			ptrBuf[ tmpIndex ] = 255;
			ptrBuf[ tmpIndex + 1] = 255;
			ptrBuf[ tmpIndex + 2] = 255;
		}
	}

	glDrawPixels(g_inWidth,g_inHeight,GL_RGB,GL_UNSIGNED_BYTE,ptrBuf);

	delete []ptrBuf;
	glutSwapBuffers();
}

2013-11-9 16-52-29

图11 glDrawPixels()和glReadPixels()演示结果

五.      参考

【1】  http://msdn.microsoft.com/en-us/library/windows/desktop/dd318368(v=vs.85).aspx

【2】  http://blog.csdn.net/sjzcandy/article/details/5775633

【3】  Computer Graphics Using OpenGL, Second Edition

【4】  OpenGL programming guide, the official guide to learning OpenGL, Version 2.1, sixth edition

spacer

Leave a reply