Saturday, October 23, 2010

OpenGL 的工作流程

上图为 OpenGL Programming Guide 中的图片,特此说明。

我们需要理解 OpenGL 的工作机制。OpenGL本身仅仅支持最原始点、线段和平面的绘制,因此多数情况下我们都是准备好 vertex 数据,这些或者组织成为 display list 提交给显卡的 evaluator,这个 evaluator 将点信息转换成为需要描述的面的信息(如法向量等,这个功能在创建复杂的几何形体如曲面等时会经常用到);之后通过设定的 per-vertex 操作,如进行几何变换,计算纹理位置,光源、材料等等;然后是 assemble,获得显示到窗口的投影坐标。这之后就需要将这些计算出来的数据进行 rasterization(像素化,栅格化),同时将纹理信息加入到这些面上,进一步就可以做 fragment 操作(如根据深度去掉被遮挡的、又或者进行 blending 等,一般每种操作对应某种 buffer),最后将结果放到某个 buffer 中(可能是用于显示的,也可能是用于 swap 的)。而一般外部的位图数据,一般会首先经过处理(如放缩、平移、映射等)形成纹理。

编写 OpenGL 的程序要理解清楚,我们所绘制的对象是一个所谓的 OpenGL context,这个 context 里面包括了所有打开的 OpenGL 使用的 buffer 等对象,我们做图时往往需要指定一个 context,做图的过程就是将这个 context 的状态不停的改变,比如我们希望改变渲染的对象,我们会在 GL_RENDER 下切换到矩阵的 GL_MODELVIEW 里面,而如果只是改变投影我们是在 GL_PROJECTION 里面;如果我们需要选择所画的某些对象,我们会切换到 GL_SELECT 这种模式,并通过 GL_PROJECTION 设定我们观看的区域,然后切换回 GL_MODELVIEW 重新绘制,退回 GL_RENDER 的时候就会获得一个被选择的对象列表。所以我们需要搞清楚有些什么样的状态,对应状态下可以进行一些什么操作。

推荐的文档有两个,一个是 OpenGL 红皮书(红宝书?)即 OpenGL Programming Guide,现在已经到第 7 版了,非常经典的书,现在也开始涉及到 OpenGL 3.0 的东西,最新的 4.1 尚未涉及;4.1 相关的主要是 NVIDIA 在推动;这本书网上有电子版,中文版只能说翻译质量一般;另一个自然就是官方的 spec,网上可以免费下载(见这里)。

另外,我们需要理解清楚下面几个概念。
  • OpenGL 与 WGLAGLGLX 的关系:OpenGL 本质上和窗口系统无关,因此如果需要在某些窗体环境中使用 OpenGL 需要这些 GUI 提供某种 OpenGL 扩展,WGL 是 Windows 的扩展,AGL(另外还有 CGL、Cocoa 的 NSOpenGL 类)是苹果的扩展,而 GLX 是 X11 扩展(并且允许 network-transparent 的 OpenGL 渲染)。
  • OpenGL 与 GLU 和 GLUT 之间的关系:前面说了 OpenGL 本身仅仅提供了非常原始的操作;GLU 另外提供了层次纹理(mipmapping)、简单的矩阵操作(更加方便)、polygon tessellation(将曲面分解成为多边形)、二次曲面以及 NURBS 的支持;GLUT 提供的是接口统一跨平台的 GUI 接口。
后面我们会学习红皮书和 GLU 方面的东西。

Friday, October 22, 2010

OpenGL Utility Toolkit 使用指南

嗯,这个东西非常轻量化,如果希望写一些 OpenGL 的测试程序非常合适。OpenGL Utility Toolkit 简称 GLUT,虽然并不是开源软件,但是多数平台下面都有,并且接口一致简洁。这里有它的主页,上面有文档和源代码。

初始化

void glutInit( int argc, char* argv[] ) ;
用于初始化 glut 程序,会处理一些相关的选项。

void glutInitWindowSize( int width, int height ) ;
void glutInitWindowPosition( int x, int y ) ;
初始化窗口的大小和位置。

void glutInitDisplayMode( unsigned int mode ) ;
支持 GLUT_ 系模式,主要是设置颜色支持,打不打开各种 buffer;

事件处理

void glutMainLoop( void ) ;
事件循环。

窗口管理

int glutCreateWindow( char* name ) ;
创建窗口,返回窗口编号;

int glutCreateSubWindow( int win,
    int x, int y, int width, int height ) ;
在给定窗口内,创建子窗口。

void glutSetWindow( int win ) ;
int glutGetWindow( void ) ;
选择或者获得当前(用于做图的)窗口;

void glutDestroyWindow( int win ) ;
释放窗口;

void glutPostRedisplay( void ) ;
要求重绘当前窗口;

void glutSwapBuffer( void ) ;
如果当前窗口使用了 double buffering 就进行 buffer 交换;

void glutPositionWindow( int x, int y ) ;
重新放置当前窗口;

void glutReshapeWindow( int width, int height ) ;
改变当前窗口大小;

void glutFullScreen( void ) ;
将当前窗口切换到全屏;

void glutFullScreen( void ) ;
切换到全屏。

void glutPushWindow( void ) ;
void glutPushWindow( void ) ;
改变 window 堆积的顺序。

void glutShowWindow( void ) ;
void glutHideWindow( void ) ;
void glutIconifyWindow( void ) ;
显示、隐藏与最小化窗口。

void glutSetWindowTitle( char* name ) ;
void glutSetIconTitle( char* name ) ;
设定窗口和图标的标题。

void glutSetCursor( int cursor ) ;
设定鼠标形状;

overlay 管理

所谓的 overlay 指在 OpenGL 窗口上覆盖的另一个 OpenGL 绘制区域。

void glutEstablishOverlay( void ) ;
在当前窗口上创建 overlay;

void glutUseOverlay( GLenum ) ;
设定使用原始窗口(GL_NORMAL)还是 overlay(GL_OVERLAY);

void glutRemoveOverlay( void ) ;
移除所创建的 overlay;

void glutPostOverlayRedisplay( void ) ;
重绘 overlay。

void glutShowOverlay( void ) ;
void glutHideOverlay( void ) ;
现实或者隐藏 overlay;

菜单管理

int glutCreateMenu( void (*func)(int val) ) ;
创建菜单,使用的 callback 函数在菜单被点击后会调用,传入的是所点的菜单项。返回菜单 id;

void glutSetMenu( int menu ) ;
int glutGetMenu(void) ;
设定 menu 以及获得使用的 menu 编号;

void glutDestroyMenu( void ) ;
析构 menu 对象。

void glutAddMenuEntry( char* name, int value ) ;
void glutAddSubMenu( char* name, int value ) ;
添加(子)菜单项。

void glutChangeToMenuEntry( int entry, char* name, int index ) ;
void glutChangeToSubMenuEntry( int entry, char* name, int index ) ;
设定(子)菜单的新的索引值(传递给 callback 函数用的);

void glutRemoveMenuItem( int entry ) ;
删掉菜单项。

void glutAttachMenu( int button ) ;
void glutDetachMenu( int button ) ;
将按钮关联到菜单。

注册 callback 函数

void glutDisplayFunc( void (*func)(void) ) ;
用于绘制窗口的 callback 函数。一般会把所有的画图部分放在这个函数里面,通过几个参数控制最后的行为。

void glutOverlayDisplayFunc( void (*func)(void) ) ;
设定 overlay 的绘图过程。

void glutReshapeFunc( void (*func)( int width, int height) ) ;
设定窗口大小改变后的行为。

void glutKeyboardFunc( void (*func)(unsigned char key, int x, int y) ) ;
设定响应键盘的 callback 函数。

void glutMouseFunc( void (*func)(int button, int state, int x, int y) ) ;
设定响应鼠标的函数。

void glutMotionFunc( void (*func)(int x, int y) ) ;
void glutPassiveMotionFunc( void (*func)(int x, int y) ) ;
设定响应鼠标按下后行为与一般移动行为的 callback 函数。

void glutEntryFunc( void (*func)( int state) ) ;
鼠标进入、退出窗口时触发;

void glutSpecialFunc( void (*func)(int key, int x, int y) ) ;
响应特殊按键;

void glutSpaceballMotionFunc( void (*func)(int x, int y, int z) ) ;
void glutSpaceballRotateFunc( void (*func)(int x, int y, int z) ) ;
void glutSpaceballButtonFunc( void (*func)(int button, int state ) ) ;
滚动球的移动、旋转和点击行为;

void glutButtonBoxFunc( void (*func)(int button, int state) ) ;
点击按钮的行为;

void glutDialsFunc( void (*func)(int dial, int value) ) ;
设置 dial 和 button 的行为。

void glutTabletMotionFunc( void (*func)( int x, int y) ) ;
void glutTabletButtonFunc( void (*func)( int button, int state, int x, int y) ) ;
设置响应平板操作的函数;

void glutMenuStatusFunc( void (*func)(int status, int x, int y) ) ;
void glutMenuStateFunc( void (*func)(int status) ) ;
设置菜单的行为;

void glutIdleFunc( void (*func)(void) ) ;
如果无事可做就会被调用的 callback 函数;

void glutTimerFunc( unsigned int msecs, void (*func)(int value), int value ) ;
设定计时器;

色彩索引管理

通过 glutGet( GLUT_WINDOW_COLORMAP_SIZE ) 可以获得可使用的 pallete 大小,之后可以用
void glutSetColor( int cell, GLfloat r, GLfloat g, GLfloat b ) ;
GLfloat glutGetColor( int cell, int component ) ;
设定或者获得每个调色板的颜色;

void glutCopyColormap( int win ) ;
可以复制指定窗口的 colormap;

状态获得

可以用
void glutGet( GLenum state ) ;
获得需要的状态;

void glutLayerGet( GLenum info ) ;
获得当前 layer 的信息;

void glutDeviceGet( GLenum info ) ;
获得设备相关信息;

void glutGetModifiers( void ) ;
获得 shift、ctrl 和 alt 的状态;

void glutExtensionSupported( char* extentsion ) ;
返回某些 OpenGL 扩展是否被支持;

字体支持

void glutBitmapCharacter( void* font, int chararacter ) ;
字体中某个字符的 OpenGL 渲染;

int glutBitmapWidth( GLUTbitmapFont font, int chararacter ) ;
返回字体宽度;

void glutStrokeCharacter( void* font, int chararacter ) ;
使用轮廓字体;

int glutBitmapWidth( GLUTstrokeFont font, int chararacter ) ;
返回轮廓字体宽度;

几何体的渲染

这也是我需要好生研究一下 src code 的部分。

void glutSolidSphere( GLdouble radius, GLint slices, GLint stacks ) ;
void glutWireSphere( GLdouble radius, GLint slices, GLint stacks ) ;
画球;

void glutSolidCube( GLdouble size ) ;
void glutWireCube( GLdouble size ) ;
画正方体;

void glutSolidCone( GLdouble base, GLdouble height, GLint slices, GLint stacks ) ;
void glutWireCone( GLdouble base, GLdouble height, GLint slices, GLint stacks ) ;
锥体,base 设定的是底面圆的半径;

void glutSolidTorus( GLdouble innerRadius, GLdouble outerRadius, GLint nsides, GLint rings ) ;
void glutWireTorus( GLdouble innerRadius, GLdouble outerRadius, GLint nsides, GLint rings ) ;
圆环;

void glutSolidDodecahedron( void ) ;
void glutWireDodecahedron( void ) ;
正 12 面体,每个面是五边形;

void glutSolidOctahedron( void ) ;
void glutWireOctahedron( void ) ;
正八面体,每个面是三角形;

void glutSolidTetrahedron( void ) ;
void glutWireTetrahedron( void ) ;
正四面体,每个面是三角形;

void glutSolidIcosahedron( void ) ;
void glutWireIcosahedron( void ) ;
二十面体;

void glutSolidTeapot( GLdouble size ) ;
void glutWireTeapot( GLdouble size ) ;
茶壶;

其他内容

其实 GLUT 的实现有很多免费的版本,如 freeglutOpenGLUT。而 OpenGL 本身也有一个开源的实现,即 mesa,想看源代码,可能更多的应该是 GLU,毕竟 GLUT 提供的主要是窗口操作方面的东西。写轻量级的 OpenGL 程序比较合适。

Saturday, October 16, 2010

depth buffer 与 blending buffer

OpenGL 通过 depth buffer 维护最后显示在屏幕上的像素,深度上较大的像素会被较小的覆盖;blending buffer 主要是为了渲染半透明的效果,主要是为了将不同层次的能够融合在一起。两个 buffer 作用的顺序是先 depth 后 blend。

这导致了一个问题,如果我们打开了两个 buffer,那么深度测试后被遮挡的物体将被剔除,进入 blending buffer 的时候不存在 blending 的问题了。因此 blending 失效。

解决的方案是,打开 depth buffer,先画不需要 blending 的物体,然后自己计算这些半透明物体的深度,并按照深度从深到浅画出。

相关的讨论见 NeHe 的 tutorial 以及 OpenGL 官方的 FAQ

billboard 的 OpenGL 实现

原文见这里

基本想法是两个:

要不我把当前的 MODELVIEW 矩阵拿出来,把旋转去掉,这只需要把那个矩阵的 3x3 部分变成单位阵就行了,因为把矩阵理解成为对当前点的变换可以发现我们仅仅需要平移到当前点,然后画需要的 billboard 就好了。

要不我把当前的 MODELVIEW 矩阵拿出来,3x3 那部分里面每行相当于是在 x、y、z 上的投影,因此只需要取前两行作为方向计算该处物体的坐标就行了。

一般 billboard 都是矩形,因此计算量不算很大,后者需要用 CPU 计算所有的 billboard 坐标,如果 billboard 较少比较适合;前者实际上需要将矩阵从显存放到内存,然后写回去,如果 billboard 计算量太小则不太适合。

Monday, October 11, 2010

Qt 的 OpenGL 支持

首先需要知道 OpenGL 是一个渲染 3D 图像的接口,其定义与语言几乎无关。我们这里直接在 C/C++ 里面使用对应的 OpenGL 语言。说到头,OpenGL 只是渲染一个图像,因此很多做图的原语被反复的使用。我们知道的 Mesa 本质上是实现了一个 CPU 版本(以及少量显卡硬件支持)的 OpenGL。为什么需要 OpenGL 呢?如果每个显卡都有自己的一套渲染方案,但提供给用户的 API 不同,则每个图形应用程序都需要针对特定的显卡进行编写。而如果显卡提供了一致的标准,编写程序的时候则会轻松很多。但是这要求显卡提供商实现两部分内容。一部分是驱动程序,被 OS 隐藏起来,用于直接与硬件打交道;另一部分是用来向 OS 提交请求的用户 API 接口。这样显卡提供商一方面需要尽可能的通过自己显卡的硬件环境优化 OpenGL 的渲染实现,也需要通过 marshalling 减少从用户态转换到内核态的次数(避免每次 OpenGL 调用都 involve 一次转换)。这样将一些 rendering 的工作(特别是大量 3D 渲染)从 CPU 转移到 GPU 之后就能加速应用程序。

第二,Qt 对 OpenGL 的支持在于它提供了一个用于显示 OpenGL 渲染结果的 widget,即 QGLWidget,如果我们需要显示 OpenGL 的渲染结果,我们就应该继承 QGLWidget,然后实现它三个方法:initializeGL() 用于初始化 OpenGL,通常设置一些前期的背景色、光照等等;paintGL() 用于渲染整个场景;resizeGL() 用于在 widget 大小变化的时候产生合理的 view。

第三,如果需要设置动画效果,本质上是在 paintGL() 过程中加入某些变化的参数,这样每次调用 paintGL() 的时候产生不同的渲染结果。实现动画需要调用 QTimer 之类的定时器,而这些对象并不能访问前面的这些方法(因为是 protected 的),因此一般调用含有 updateGL() 方法的 slot,将 QTimer 的某些 signal 与之 connect 即可。

第四,如果需要加入鼠标和键盘的操作,应该设置 mousePressEvent()、mouseMoveEvent() 等事件,将需要的方法绑定到对应的事件中。

总而言之,OpenGL 的使用并不神秘,只是我们需要进一步理解其中的一些基本概念。下面是一个简单的 OpenGL 程序,可以看到前面第二点里面所说的三个要素,我们在后面的分析中会逐步加深。下面是继承 QGLWidget 类的声明
#ifndef GLWIDGET_H
#define GLWIDGET_H

#include <QGLWidget>
#include <QVector>
#include <gl.h>

class GLWidget : public QGLWidget {
 public:
  GLWidget( QWidget* = NULL, const QGLWidget* = NULL,
            Qt::WindowFlags = Qt::Widget ) ;

 protected:
  void initializeGL();
  void paintGL();
  void resizeGL(int width, int height);

 private:
  QVector<GLuint> stars ;
} ;

#endif
下面是相关实现,
#include "glwidget.h"

GLWidget::GLWidget( QWidget* parent, const QGLWidget* shared,
                    Qt::WindowFlags flags)
  : QGLWidget( parent, shared, flags ), stars(1) {
}

void
GLWidget::initializeGL() {
  QGLWidget::initializeGL() ;
  glClearColor(0.0, 0.0, 0.0, 0.0);
  glClearDepth( 1.0 ) ;
}

void
GLWidget::paintGL() {
  glClearColor( 0.0, 0.0, 0.0, 0.0 ) ;
  glClear (GL_COLOR_BUFFER_BIT);

  glLoadIdentity() ;
  glColor3f (1.0, 1.0, 1.0);
  glOrtho(0.0, 1.0, 0.0, 1.0, -1.0, 1.0);
  glBegin(GL_POLYGON);
    glVertex3f( 0.25, 0.25, .0 ) ; 
    glVertex3f( 0.75, 0.25, .0 ) ; 
    glVertex3f( 0.75, 0.75, .0 ) ; 
    glVertex3f( 0.25, 0.75, .0 ) ;
  glEnd();
  glFlush();
}

void
GLWidget::resizeGL(int w, int h ) {
  glViewport( 0, 0, w, h ) ;
  glMatrixMode( GL_PROJECTION ) ;
  glLoadIdentity() ;
}
我想后面我们应该会更关注 OpenGL 本身,而不仅仅在 Qt 上。