OpenGL 多视图与截屏

时间:2022-12-30 20:53:15

最近看红宝书学习 OpenGL 一段时间了,写了简单的 demo 程序温习一下知识。 
主要是 使用 glScissor 多视图显示画面和使用 glReadPixels 给画面截屏,使用显示列表(display list)加上一些简单的光照。
程序运行后,按字母 P 键截屏,图片存放在当前目录,按字母 Q 键在单视图与多视图之间切换。
效果图如下,代码已上传到 github 上,地址

OpenGL 多视图与截屏

下面是创建显示列表的函数。

GLuint displayList(void)
{
static GLfloat gold_ambient[] = { 0.24725f, 0.1995f, 0.0745f, 1.0f };
static GLfloat gold_diffuse[] = { 0.75164f, 0.60648f, 0.22648f, 1.0f };
static GLfloat gold_specular[] = { 0.628281f, 0.555802f, 0.366065f, 1.0f };
static GLfloat gold_shininess = 41.2f; static GLfloat silver_ambient[] = { 0.05f, 0.05f, 0.05f, 1.0f };
static GLfloat silver_diffuse[] = { 0.4f, 0.4f, 0.4f, 1.0f };
static GLfloat silver_specular[] = { 0.7f, 0.7f, 0.7f, 1.0f };
static GLfloat silver_shininess = 12.0f; GLuint list = glGenLists();
glNewList(list, GL_COMPILE); glMaterialfv(GL_FRONT, GL_AMBIENT, gold_ambient);
glMaterialfv(GL_FRONT, GL_DIFFUSE, gold_diffuse);
glMaterialfv(GL_FRONT, GL_SPECULAR, gold_specular);
glMaterialf(GL_FRONT, GL_SHININESS, gold_shininess); glMaterialfv(GL_BACK, GL_AMBIENT, silver_ambient);
glMaterialfv(GL_BACK, GL_DIFFUSE, silver_diffuse);
glMaterialfv(GL_BACK, GL_SPECULAR, silver_specular);
glMaterialf(GL_BACK, GL_SHININESS, silver_shininess); glutWireTorus(0.3/*innerRadius*/, 0.5/*outerRadius*/, /*nsides*/, /*rings*/);
glEndList(); return list;
}

关于截图的代码有点多,请下载文件查看,不在这里粘贴了,这里简单说一下步骤。

1. 分配足够的内存 GLubyte *image=(GLubyte*)malloc(width*height*3*sizeof(GLubyte));

2. 调用 void glReadBuffer(GLenum mode); 函数声明颜色缓冲区,单缓冲默认用 GL_FRONT,双缓冲默认用 GL_BACK

3. 调用 void glReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, GLvoid * data); 函数从 frame buffer 取数据,一般为 glReadPixels(0, 0, height, width, GL_RGB, GL_UNSIGNED_BYTE ,image); 读取缓冲区RGB数据存入分配的 image 中,也可以根据需求调节参数配置。需要注意的一点是内存对齐问题,由于 bmp 文件数据按 4 字节(DWORD)对齐,设置 glPixelStorei(GL_PACK_ALIGNMENT,4);,虽然默认的也是 4 字节对齐,还是显示写一下比较好。jpg 文件没有如此要求,所以设置 glPixelStorei(GL_PACK_ALIGNMENT,1);

4. 数据在手,剩下的就是调用 png, jpg, tiff 等图像库的 API 进行读写了。

之前接触过 DirectX,由于刚刚接触 OpenGL,把自己遇到一些问题总结一下。

Direct3D 和 OpenGL 是在 GPU 上渲染 3D 图形的两大技术。拿 DirectX 跟 OpenGL 比是错误的,DirectX 包括许多其他的功能(DirectInput,DirectSound 等),末尾的X是未知数,也是一系列功能的统称。
Direct3D 局限于 Windows 平台, OpenGL 跨平台;
Direct3D 使用 左手坐标系, OpenGL 使用右手坐标系。
Direct3D 使用 row_major      矩阵,变换矩阵之间左乘, 向量看成行向量;
OpenGL  使用 column_major 矩阵,变换矩阵之间右乘, 向量看成列向量。
Direct3D 函数多使用弧度制, OpenGL 函数多使用角度制。

1.  glColor* 函数使用
        OpenGL 偏向于 C 语言,很多函数通过添加后缀定义了不同的版本,以便使用不同类型的参数,有数量{3,4},类型{b,s,i,f,d,ub,us,ui},表示该函数接受3/4个8位整数/16位整数/32位整数/32位浮点数/64位浮点数/8位无符号整数/16位无符号整数/32位无符号整数类型的参数,有的最后还有一个字母v,表示该函数所接受的参数是一个指向向量或数组的指针,而不是一系列的单独参数。
       
当时自己很讨厌这些后缀,心想着如果用 C++ 的函数重载,也不用那么麻烦啊,直到我的膝盖中了一箭。
        在用 Directx3D 时,经常用宏 D3DCOLOR_RGBA 来指定一种颜色,使用起来也很简单。

// from d3d9types.h

// maps unsigned 8 bits/channel to D3DCOLOR
#define D3DCOLOR_ARGB(a,r,g,b) \
((D3DCOLOR)((((a)&0xff)<<)|(((r)&0xff)<<)|(((g)&0xff)<<)|((b)&0xff)))
#define D3DCOLOR_RGBA(r,g,b,a) D3DCOLOR_ARGB(a,r,g,b)
#define D3DCOLOR_XRGB(r,g,b) D3DCOLOR_ARGB(0xff,r,g,b)

有一天,我用上 OpenGL 的函数 glColor3i(255, 255, 255); 发现显示的是黑色,但是换用 glColor3f(1.0, 1.0, 1.0); 就是白色,不明白错误所在。稍微查看了文档 man glColor,明白了错误的所在。如果使用整数的话,应该使用 glColor3ub(255, 255, 255); 当前颜色值都是以浮点数存储的,浮点值直接映射;无符号整形 [0,255] 线性映射到 [0.0,1.0],有符号整形 [-128,127] 线性映射到 [-1.0,1.0]。所以 glColor3i(255, 255, 255); 等价于 glColor3f(255.0/INT_MAX, 255.0/INT_MAX, 255.0/INT_MAX); 值只会在更新当前颜色的时候 clamp 到 [0.0,1.0] 区间。不过在插值或者写入到颜色缓冲区前也会 clamp,多查看文档学习的来源。所以,有时候看别人的代码觉得似乎很简单,但是轮到自己亲自动手写的时候不一定会那么写,导致犯错。

2. glGet* 函数使用
       OpenGL 是一个状态机,打开某些状态调用函数 void glEnable(GLenum capability);,关闭某些状态调用函数 void glDisable(GLenum capability); 查询用 glGet* 族函数,我想知道我这台机器有关 OpenGL 的版本信息。

void glInfo()
{
printf("\n");
printf("Vendor : %s\n", glGetString(GL_VENDOR));
printf("Renderer : %s\n", glGetString(GL_RENDERER));
printf("Version : %s\n", glGetString(GL_VERSION));
printf("GLSL version : %s\n", glGetString(GL_SHADING_LANGUAGE_VERSION));
printf("Extensions : %s\n", glGetString(GL_EXTENSIONS));
printf("\n");
}

我插入上面 glInfo 函数的位置太靠前,函数没有返回任何结果。经过多次试验发现,glGet* 函数在 glutCreateWindow 函数后调用,否则输出为空。OpenGL 提供了功能强大且又非常基本的渲染函数,但是 OpenGL 程序必须使用窗口系统的底层机制。int glutCreateWindow(char* string); 创建了一个支持 OpenGL 渲染环境的窗口,函数返回唯一的标识符。而且,在调用 glutMainLoop(); 函数前窗口是不会显示的。如果在 glutCreateWindow 之前没有调用 glutInit(&argc, argv);会给出错误提示 freeglut  ERROR:  Function <glutCreateWindow> called without first calling 'glutInit'.
需要注意的是,在程序对性能有所要求的情况下尽量少调用 glGet*/glIs* 查询状态,但调试的时候使用没什么。因为 OpenGL 是一个状态机,设置状态来得快,但查询某些状态会很耗时,想想多个流水线在跑,CPU 等待 GPU 忙玩一批渲染命令后停下来,找到并返回正确的而且是当前最新的状态给你。

3. GLenum 错误
        有一次笔误,将 glEnable(GL_DEPTH_TEST) 写成 glEnable(GL_DEPTH),没有出现预期的效果。OpenGL 的头文件里,很多都是宏定义成的数值,写错了话,编译不会出错,运行也不会有错,只能在调式的时候希望能快点找到错误。由于自己粗心,glMatrixMode 函数名字里面也有 matrix,导致自己写程序时,顺手就将 glMatrixMode(GL_MODELVIEW); 写成了 glMatrixMode(GL_MODELVIEW_MATRIX); 很长一段时间,程序没有输出,我不知道错在哪里。也然后调用了 glGetDoublev 打印当前矩阵栈信息

/*
* @param mode Specifies which matrix stack is the target for subsequent matrix
* operations. Three values are accepted: GL_MODELVIEW_MATRIX,
* GL_PROJECTION_MATRIX, and GL_TEXTURE_MATRIX. The initial value is
* GL_MODELVIEW_MATRIX.
*/
void printMatrix(GLenum mode=GL_MODELVIEW_MATRIX)
{
const int ORDER=;
GLdouble M[ORDER*ORDER]={};
glGetDoublev(mode, (GLdouble*)&M); for(int i=; i<ORDER; ++i)
printf("%10.6lf %10.6lf %10.6lf %10.6lf\n",
M[i], M[i+ORDER], M[i+ORDER*], M[i+ORDER*]);
}

发现打印的都是0,程序也没有崩溃。(当然,上面是已经改正后的函数。)后来利用 glGetError 函数才看到打印出错的信息
OpenGL error 1280: invalid enumerant,枚举值有问题,然后一点点排除,又对比别人的代码,才找出问题所在。

void printIfError()
{
GLenum gl_error=glGetError();
if(gl_error!=GL_NO_ERROR)
printf("OpenGL error %d: %s\n", gl_error, gluErrorString(gl_error));
}

还需要注意的是记住当前的矩阵栈模式,是 GL_MODELVIEW,还是 GL_PROJECTION,还是 GL_TEXTURE?以免在错误的矩阵栈上面做了变换,较好的做法是每次一大堆矩阵变换前调用一次 glMatrixMode,最后的最后再调用一次 glMatrixMode(GL_MODELVIEW); 回到默认的模式 GL_MODELVIEW。

还有很多其他的陷阱(pitfall),下面列出一些很好的网址链接。

http://www.opengl.org/wiki/Common_Mistakes

http://www.opengl.org/archives/resources/features/KilgardTechniques/oglpitfall/