OpenGL常用命令备忘录(Part B)

3.glPixelStore

像glPixelStorei(GL_PACK_ALIGNMENT, 1)这样的调用,通常会用于像素传输(PACK/UNPACK)的场合。尤其是导入纹理(glTexImage2D)的时候:

glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glTexImage2D(,,,, &pixelData);
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);

很明显地,它是在改变某个状态量,然后再Restore回来。——为什么是状态?你难道8知道OpenGL就是以状态机不?——什么状态?其实名字已经很直白了,glPixelStore这组函数要改变的是像素的存储格式。

涉及到像素在CPU和GPU上的传输,那就有个存储格式的概念。在本地内存中端像素集合是什么格式?传输到GPU时又是什么格式?格式会是一样么?在glTexImage2D这个函数中,包含两个关于颜色格式的参数,一个是纹理(GPU端,也可以说server端)的,一个是像素数据(程序内存上,也就是client端)的,两者是不一定一样的,哪怕一样也无法代表GPU会像内存那样去存储。或者想象一下,从一张硬盘上的图片提取到内存的像素数据,上传给GPU成为一张纹理,这个“纹理”还会是原来的那种RGBARGBA的一个序列完事么?显然不是的。作为一张纹理,有其纹理ID、WRAP模式、插值模式,指定maipmap时还会有一串各个Level下的map,等等。就纹理的数据来说,本质纹理是边长要满足2的n次方(power of two)的数据集合,这样首先大小上就有可能不一样,另外排列方式也未必就是RGBA的形式。在OpenGL的“解释”中,纹理就是一个“可以被采样的复杂的数据集合”,无论外面世界千变万化,GPU只认纹理作为自己“图像数据结构”,这体现着“规范化”这条世界纽带的伟大之处。

姑且把GPU里面的像素存储格式看做一个未知数,把该存储空间内那批像素看做一堆X。不要深究一堆X究竟是什么样子的,嘛,反正就想象成一堆软绵绵的,或者模糊不清的,打满马赛克的,(哔——)的一样的东西就可以了。与此相比,内存中的像素数据实在太规则规范了!可能源文件各种图片格式,什么bmp、jpg、png甚至dds,但只要你按该格式的算法结构来提取(类似[Bmp文件的结构与基本操作(逐像素印屏版)] ),总可以提取出一列整齐的RGBARGBA(或者RGBRGB什么的,反正很整齐就行了管他呢)的数据堆出来,是可以在程序中实测的东西。

涉及到像素在CPU和GPU上的传输,那就有个传输方向的概念。那就是大家耳濡目染的PACK和UNPACK。嘛,装载和卸载也可以,打包和解压也可以,随你怎么译了。结合上述存储格式的概念:

  • PACK —— 把像素从一堆X的状态转变到规则的状态(把一堆泥土装载进一个花盆,把散散的货物装上货柜,或者把一堆各样的文件打包成一个rar压缩包,等等);
  • UNPACK —— 把像素从规则的状态转变到一堆X的状态(把花盆里的泥倒出来,把货柜中的货物卸载到盐田港,或者解压压缩包,等等)。

我认为这两个概念还是很容易混淆的,所以形象化一点总好点嘛。从本地内存向GPU的传输(UNPACK),包括各种glTexImage、glDrawPixel;从GPU到本地内存的传输(PACK),包括glGetTexImage、glReadPixel等。也正因如此,PBO也有PACK和UNPACK模式的区别。

好像说了好多不相关的事情。嘛,适当也当做延伸。回头来真正看一下glPixelStore吧。它的第一个参数,譬如ALIGNMENT、ROW_LENGTH、IMAGE_HEIGHT等等,都有PACK和UNPACK的两种版本,所以对应的也是上述关于PACH和UNPACK的两类函数。所以对于glTexImage2D,才使用GL_UNPACK_ALIGNMENT的版本。但要说明的是,无论是哪种传输方式,它都是针对本地内存端(client端)上的像素数据的。在上述例子中,它起着补充glTexImage2D中关于传输起点——本地像素集合的格式,的作用。

一般来说,这些本地的数据集合,只要知道其起始位置、大小(width*height)和颜色格式(譬如GL_RGBA等等)、值格式(GL_UNSIGNED_CHAR、GL_FLOAT等等),就能准确地传输。而这些都是需要向glTexImage2D函数(或者上述的其他传输型函数)提供的。但是,这里头也一些细节,其实是需要glPixelStore这个函数来进行设置的。

3.1 GL_UNPACK_ALIGNMENT / GL_PACK_ALIGNMENT

通常,提取一张图像的时候,我们怎么知道一行的数据量呢?这个一行的数据量应该是:width * sizeof(Pixel) ,应对最一般RGBA、各通道各占一个字节的像素结构,width * sizeof(Pixel) = width * 4 * sizeof(byte),是4的整数倍。但是也有时候,我们的像素数据中一行的数据量不一定是4的整数倍(譬如一张RGB的图像、宽度150、各通道各占一个字节的像素结构,一行的数据量就是450个字节)。

另一方面,跟编译器一样,GPU传输时也喜欢4字节对齐,也即是说喜欢对像素数据按4字节存取。所以它更偏向于喜欢每一行的数据量是4的整数倍(按上述,这恰好是比较常见的)。所以为了更高的存取效率,OpenGL默认让像素数据按4字节4字节的方式传输向GPU——但是问题在于,对于行非4字节对齐的像素数据,第一行的最后一次存取的4字节将部分包括第一行的数据部分包括第二行的数据,当然致命的不是在这里,而是在最后一行:存取将很可能会越界。为了防止这样的情况,一是硬性把像素数据延展成4字节对齐的(就像BMP文件的存储方式一样,[Bmp文件的结构与基本操作(逐像素印屏版)] );二是选择绝对会造成4字节对齐的颜色格式或值格式(GL_RGBA啦,或者GL_INT、GL_FLOAT之类);三是以牺牲一些存取效率为代价,去更改OpenGL的字节对齐方式——这就是glPixelStore结合GL_UNPACK_ALIGNMENT / GL_PACK_ALIGNMENT。

glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glTexImage2D(,,,, &pixelData);
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);

再次看回这段代码,这时候就明白了:让字节对齐从默认的4字节对齐改成1字节对齐(选择1的话,无论图片本身是怎样都是绝对不会出问题的,嘛,以效率的牺牲为代价),UNPACK像素数据,再把字节对齐方式设置回默认的4字节对齐。至于哪种方式更适合,就看你依据硬件环境限制、麻烦程度等,去选择了。

3.2 GL_UNPACK_ROW_LENGTH/ GL_PACK_ROW_LENGTH 和 
GL_UNPACK_SKIP_ROWS / GL_PACK_SKIP_ROWS 、 GL_UNPACK_SKIP_PIXELS/GL_PACK_SKIP_PIXELS

有的时候,我们把一些小图片拼凑进一张大图片内,这样使用大图片生成的纹理,一来可以使多个原本使用不同的图片作为纹理的同质物件如今能够在同一个Batch内,节省了一些状态切换的开销,二来也容易综合地降低了显存中纹理的总大小。但是,也有些时候,我们需要从原本一张大的图片中,截取图片当中的某一部分作为纹理。要能够做到这样,可以通过预先对图片进行裁剪或者在获得像素数据后,把其中需要的那一部分另外存储到一个Buffer内再交给glTexImage2D之类的函数。而上述这些参数下glPixelStore的使用将帮助我们更好地完成这个目的:

//原图中需要单独提取出来制成纹理的区域
RECT subRect = {{100, 80}, {500, 400}}; //origin.x, origin.y, size.width, size.height

//假设原图的宽度为BaseWidth, 高度为BaseHeight

glPixelStorei(GL_UNPACK_ROW_LENGTH,  BaseWidth);    //指定像素数据中原图的宽度
glPixelStorei(GL_UNPACK_SKIP_ROWS,      subRect. origin.y.); //指定纹理起点偏离原点的高度值
glPixelStorei(GL_UNPACK_SKIP_PIXELS,     subRect. origin.x);  //指定纹理起点偏离原点的宽度值

glTexImage2D(..., subRect.size.width, ubRect.size.height,.. &pixelData);  //使用区域的宽高

glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
glPixelStorei(GL_UNPACK_SKIP_ROWS, 0);
glPixelStorei(GL_UNPACK_SKIP_PIXELS, 0);

这段代码本身,即使没有注释也很清楚了。注意的是GL_UNPACK_ROW_LENGTH的必要性,因为为了确认区域起点的Offset,就需要把线性数据pixelData上标记起点的“游标”从0移动到OffsetToData = subRect. origin.y * BaseWidth + subRect. origin.x的位置。有了区域纹理原点的在原图数据的位置,以及区域的尺寸,glTexImage2D就可以确定区域纹理生成所需要的信息了。通过glPixelStore的使用,避免了新建Buffer和自己处理图像数据的开销和麻烦了。

说到这里,到底为什么要这样做来提取区域纹理呢?尤其是原图若其他部分都是程序所需要的,那是不是就可以直接通过纹理坐标去切割更好呢?我想到的是一种情况(也可以说我是因为这种情况才注意到glPixelStore的这种用法):如果这块区域纹理需要作重复铺设(wrap mode选择GL_REPEAT)呢?这时候纹理坐标的方法就没用了,因为REPEAT所依据的也是纹理坐标(使用纹理坐标的小数部分进行采样)。这时候就需要上述做法了。(事实上3DSMAX等软件纹理导入的类似区域纹理平铺的功能就能如此实现。)

4.glScissor

我想这个函数也应该很常见才对。裁剪测试啊,当年跟Alpha测试、Depth测试、Stencil测试可以并列哦,而今更是不掉时髦值啊。因为我实在很难想象在Shader里能容易地实现它的功能:裁剪。当然这只是矩形裁剪,但是对于discard掉渲染中不需要的像素真是颇简单粗暴。我使用它最多的是一些二维图片缩略图栏——有时候我们只需要把这些缩略图的显示限制在一个区域里,但又要支持滑动。

glEnable(GL_SCISSOR_TEST);
glScissor(GLint(m_rtThumbRegion.x), GLint(m_rtThumbRegion.y), GLint(m_rtThumbRegion.width), GLint(m_rtThumbRegion.height));
//.....  Render
glDisable(GL_SCISSOR_TEST);

其中,除了启用GL_SCISSOR_TEST外,只要给glScissor指出需要保留显示的区域就可以了。在此区域外的像素依然会被渲染(不会怎么省流水线操作,所以也别指望它附带什么提高效率之类的功能),在下图中,其实左右两侧还是继续渲染其他的图片(或者说,其实这个缩略图栏横跨整个屏幕),但是就在fragment shader之后,它们会被检测到不在该区域内而被discard掉罢。

原文地址:http://www.zwqxin.com/archives/opengl/opengl-api-memorandum-2.html

OpenGL常用命令备忘录(Part A)

1. glCullFace 和 glFrontFace

这两个都是opengl的初级命令,但是其实我都是最近才算得上“用”,以前的话,是因为有一次做billboard广告牌的时候,不剔除面片其中一面的话,很难看出效果成功了没,于是用了一次;最近的话,包括shadow volume中Z-FAIL和Z-PASS都需要正反面分别渲染,shadowmap中在pass 1里剔除正面,在光源视觉下渲染到纹理也用到了,一来减少渲染的”费用”,二来可以提高深度测试的精度(橙书里说的)。以前曾经在哪里看到篇文章说是其实剔除正或反面都提高不了多少效率,首先作者肯定做过测试来了,但是这样算不算武断我也不会说,问过一些搞游戏开发的人,答到“当然能提高效率啦,只不过不可能提高一倍而已”,事实上提高一点点都是很好的说。谁对谁错我也懒得分辨了,因为至少目前我还很少用,而且根据以前尝试的经验决定还是“能不用就不用”,因为正反面判断实在太烦了,有很多时候你用到的是别人弄的模型,而不那么专业的美工会在建模的时候搞乱面片绕序。

所以这组命令除了用在某些特殊场合(实现某些技术所必须),还有就是提高渲染效率的可能性,对某些效果正确性的辅助判断。其中glFrontFace用于指定正面的绕序,默认是“逆时针为正”,即传入参数GL_CCW,可以改成GL_CW顺时针。glCullFace 传入GL_FRONT来剔除正面,或者GL_BACK来剔除背面,之前先得glEnable(GL_CULL_FACE)。

通常情况是这样的:当你想剔除背面的时候,你只需要调用glEnable(GL_CULL_FACE)就可以了,OPENGL状态机会自动按照默认值进行CULL_FACE,默认是glFrontFace(GL_CCW),glCullFace(GL_BACK),当然如果你不想按照默认的来,你就得设置一下了。最后在不再需要剔除的时候调用glDisable(GL_CULL_FACE)。我建议的是“每次开始用,无论是不是默认的你最好都设置一下”,不然你迟些很容易被状态机(保持上一次设置的状态)搞到呕血——除非真的是很简单的demo制作,譬如之前shadow volume中我只是glEnable(GL_CULL_FACE),然后直接glFrontFace(GL_CCW)——它启用剔面功能,指定了“顶点逆时针绕序为正面”,之后的渲染它就会自动剔除那些反面;之后再调用glFrontFace(GL_CW),重新指定为“顶点顺时针绕序为正面”,再渲染一次,注意这时候它同样也是剔除背面(你没改过glCullFace),但是剔除的面就恰恰与前面的相反了。最后在disable前记得先改回glFrontFace(GL_CCW)!否则下次一enable就会按“顺时针绕序为正”了(与常理不一)。(望天!我忘记当时有没设置回来了~)

2.glShadeModel

在opebgl流水线里头,有一个步骤是栅格化(Rasterization),它在顶点组合的几何信息处理后执行,目的是“插值”,vertex shader的varying变量就是在这里被栅格化(/插值),然后再传入fragment shader作象素级别的处理。两顶点之间的象素怎么处理呢?就是靠这两顶点的信息的线性插值。譬如最简单的,颜色,一条线段两顶点A和B的颜色分别为红色和绿色,但是最后“显示颜色”的不可能是顶点,只有象素,因此这两点之间的象素就得按照它们临近的顶点的颜色而获得,越靠近点A的那些象素越红,越靠近点B的那些象素越绿,中间因此呈现渐变效果(一般来说,线段中点有一半红一半绿而成为黄色…)。对于填充的三角形,矩形也是一样的道理,只不过是平面内的线性插值罢了。

说了那么多,其实glShadeModel作用相当于打开/关闭这种功能(栅格化),传入参数GL_FLAT,流水线还是要经过这步骤但相应顶点不被处理,故顶点间的象素的颜色只会参考其中一个点的信息。譬如线段AB上的象素点全是红的或全是绿的——是哪种通常不要紧,因为无论是哪种,出来的结果都会好难看,所以渲染最初(初始化阶段)都会把参数设置成GL_SMOOTH,即启用栅格化功能。当然插值的计算量就上来了…..在渲染不注重效果而只注重速度的时候,譬如我做shadow map的PASS1作场景深度图的离线渲染时,非象素深度的信息根本对我无用,而且象素深度不是插值来的,故关闭栅格化计算,直接glShadeModel(GL_FLAT)再渲染就可以了,之后记得调回glShadeModel(GL_SMOOTH)。

好了,PART 1到此。

原文地址:http://www.zwqxin.com/archives/opengl/opengl-api-memorandum-2.html

Const,Const函数,Const变量,函数后面的Const 的区别

看到const 关键字,C++程序员首先想到的可能是const 常量。这可不是良好的条件反射。如果只知道用const 定义常量,那么相当于把火药仅用于制作鞭炮。const 更大的魅力是它可以修饰函数的参数、返回值,甚至函数的定义体。

const 是constant 的缩写,“恒定不变”的意思。被const 修饰的东西都受到强制保护,可以预防意外的变动,能提高程序的健壮性。所以很多C++程序设计书籍建议:“Use const whenever you need”。

1.用const 修饰函数的参数

如果参数作输出用,不论它是什么数据类型,也不论它采用“指针传递”还是“引用传递”,都不能加const 修饰,否则该参数将失去输出功能。const 只能修饰输入参数:

如果输入参数采用“指针传递”,那么加const 修饰可以防止意外地改动该指针,起到保护作用。

例如StringCopy 函数:

void StringCopy(char *strDestination, const char *strSource);

其中strSource 是输入参数,strDestination 是输出参数。给strSource 加上const修饰后,如果函数体内的语句试图改动strSource 的内容,编译器将指出错误。

如果输入参数采用“值传递”,由于函数将自动产生临时变量用于复制该参数,该输入参数本来就无需保护,所以不要加const 修饰。

例如不要将函数void Func1(int x) 写成void Func1(const int x)。同理不要将函数void Func2(A a) 写成void Func2(const A a)。其中A 为用户自定义的数据类型。

对于非内部数据类型的参数而言,象void Func(A a) 这样声明的函数注定效率比较底。因为函数体内将产生A 类型的临时对象用于复制参数a,而临时对象的构造、复制、析构过程都将消耗时间。

为了提高效率,可以将函数声明改为void Func(A &a),因为“引用传递”仅借用一下参数的别名而已,不需要产生临时对象。但是函数void Func(A &a) 存在一个缺点:

“引用传递”有可能改变参数a,这是我们不期望的。解决这个问题很容易,加const修饰即可,因此函数最终成为void Func(const A &a)。

以此类推,是否应将void Func(int x) 改写为void Func(const int &x),以便提高效率?完全没有必要,因为内部数据类型的参数不存在构造、析构的过程,而复制也非常快,“值传递”和“引用传递”的效率几乎相当。

问题是如此的缠绵,我只好将“const &”修饰输入参数的用法总结一下。

对于非内部数据类型的输入参数,应该将“值传递”的方式改为“const 引用传递”,目的是提高效率。例如将void Func(A a) 改为void Func(const A &a)。

对于内部数据类型的输入参数,不要将“值传递”的方式改为“const 引用传递”。否则既达不到提高效率的目的,又降低了函数的可理解性。例如void Func(int x) 不应该改为void Func(const int &x)。

2 用const 修饰函数的返回值
如果给以“指针传递”方式的函数返回值加const 修饰,那么函数返回值(即指针)的内容不能被修改,该返回值只能被赋给加const 修饰的同类型指针。例如函数
const char * GetString(void);
如下语句将出现编译错误:
char *str = GetString();
正确的用法是
const char *str = GetString();
如果函数返回值采用“值传递方式”,由于函数会把返回值复制到外部临时的存储单元中,加const 修饰没有任何价值。
例如不要把函数int GetInt(void) 写成const int GetInt(void)。
同理不要把函数A GetA(void) 写成const A GetA(void),其中A 为用户自定义的数据类型。
如果返回值不是内部数据类型,将函数A GetA(void) 改写为const A & GetA(void)的确能提高效率。但此时千万千万要小心,一定要搞清楚函数究竟是想返回一个对象的“拷贝”还是仅返回“别名”就可以了,否则程序会出错。
函数返回值采用“引用传递”的场合并不多,这种方式一般只出现在类的赋值函数中,目的是为了实现链式表达。

例如:
class A
{
A & operate = (const A &other); // 赋值函数
};
A a, b, c; // a, b, c 为A 的对象

a = b = c; // 正常的链式赋值
(a = b) = c; // 不正常的链式赋值,但合法
如果将赋值函数的返回值加const 修饰,那么该返回值的内容不允许被改动。上例中,语句 a = b = c 仍然正确,但是语句 (a = b) = c 则是非法的。
3 const 成员函数
任何不会修改数据成员的函数都应该声明为const 类型。如果在编写const 成员函数时,不慎修改了数据成员,或者调用了其它非const 成员函数,编译器将指出错误,这无疑会提高程序的健壮性。以下程序中,类stack 的成员函数GetCount 仅用于计数,从逻辑上讲GetCount 应当为const 函数。编译器将指出GetCount 函数中的错误。
class Stack
{
public:
void Push(int elem);
int Pop(void);
int GetCount(void) const; // const 成员函数
private:
int m_num;
int m_data[100];
};
int Stack::GetCount(void) const
{
++ m_num; // 编译错误,企图修改数据成员m_num
Pop(); // 编译错误,企图调用非const 函数
return m_num;
}
const 成员函数的声明看起来怪怪的:const 关键字只能放在函数声明的尾部,大概是因为其它地方都已经被占用了。
关于Const函数的几点规则:

a. const对象只能访问const成员函数,而非const对象可以访问任意的成员函数,包括const成员函数.
b. const对象的成员是不可修改的,然而const对象通过指针维护的对象却是可以修改的.
c. const成员函数不可以修改对象的数据,不管对象是否具有const性质.它在编译时,以是否修改成员数据为依据,进行检查.
d. 然而加上mutable修饰符的数据成员,对于任何情况下通过任何手段都可修改,自然此时的const成员函数是可以修改它的

文章来源:http://www.cnblogs.com/Fancyboy2004/archive/2008/12/23/1360810.html