基于glut的OpenGL框架(一)
——面向对象框架的搭建
我是一名OpenGL的初学者,在看完《OpenGL超级宝典》的前几章后,开始迫不及待地动手验证一下书上程序的代码了。我发现书上的例子程序是用C语言描述的,虽然简单,但却无法在此基础上进行扩展,即使扩展了也仅仅是添加了几个函数并且调用之,没有什么层次性。所以我开始利用C++和已经形成了标准的glut框架编写自己的OpenGL框架。
首先是我的main.cpp文件:
// main.cpp // 11时08分52秒 最后编辑 #include "GLWidget.h" // 宽屏的程序要求纵横比16:9,我们指定高,宽就出来了。 #define _WINDOW_HEIGHT_ 360 #define _WINDOW_WIDTH_ _WINDOW_HEIGHT_ * 16 / 9 static GLWidget* pWidget = 0; void Reshape( int x, int y ) { assert( pWidget != 0 ); pWidget->Reshape( x, y ); } void Render( void ) { glClear( GL_COLOR_BUFFER_BIT ); // 用黑色清屏 glColor3ub( 255, 255, 255 ); glRecti( 0, 0, _WINDOW_WIDTH_, _WINDOW_HEIGHT_ );// 绘制白色的矩形背景 // 执行widget里的绘图函数 assert( pWidget != 0 ); pWidget->Render( ); // 交换缓存 glutSwapBuffers( ); } void Idle( void ) // 空转时候运行的函数 { assert( pWidget != 0 ); // 如果有必要的话,让其更新 glutPostRedisplay( ); } int main( int argc, char** argv ) { // 初始化控件类 pWidget = new GLWidget; glutInit( &argc, argv ); glutInitDisplayMode( GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH ); glutInitWindowSize( _WINDOW_WIDTH_, _WINDOW_HEIGHT_ ); glutCreateWindow( "Simple Object" ); glutDisplayFunc( Render ); // 渲染函数 glutReshapeFunc( Reshape ); // 重改变形状函数 glutIdleFunc( Idle ); // 空转时运行的函数 pWidget->Init( _WINDOW_WIDTH_, _WINDOW_HEIGHT_ ); glutMainLoop( ); return 0; }
我的目标是制作一个宽高比为16:9的程序,暂时不考虑全屏。为什么是16:9呢?因为大多数计算机显示屏都是16:9,宽屏的显示器越来越多,方屏的显示器越来越少,我们学校里面今年来也都换成了宽屏的显示屏,而且我工作的游戏公司在制作游戏时的标准也是按照16:9制作的。适配方屏屏幕时上下空余部分自动变成黑色。所以我们在定义窗口大小时,只需指定高,宽可以让预编译器自动计算出来。这里我默认了640×480的分辨率。
用面向对象的方法编写OpenGL程序自然要用到类。这里我使用了自定义的GLWidget类,稍后我会对这个类进行介绍。这里我创建了一个全局静态指针(静态是因为它仅在main.cpp文件中使用)。
接下来则是一些回调函数的定义了,Reshape()函数作用是对窗口大小更改是我们该怎样应对,我们将在GLWidget::Reshape()里面介绍。Render()函数作用是进行渲染,这里我们用不透明的黑色清除了屏幕,并且使用一个白色的矩形填充了以后需要渲染的区域。随后的绘制就在GLWidget::Render()里面完成了。Idle()函数我认为是一个非常重要的函数了。记得我在编写DirectX相关程序时,自己就要写消息循环体,其中就有如果窗口没有任何消息时,我们该做什么。我和很多书上的做法是一样的,强制让程序渲染。这里也是一样的,用glPostRedisplay()函数告诉OpenGL,下一个循环一定要调用Render()函数进行渲染。
为了防止错误地使用了空指针,我们使用assert()函数来假定不会访问出错。assert.h被包含在GLWidget.h中。
在main.cpp函数中我们用new创建了一个对象。我们有一些步骤可以放在构造函数里面做(比如说验证版本的一致啊,更新啦……扯远了),随后是初始化glut的相关组件并且注册回调函数,在初始化GLWidget控件之后glutMainLoop()表示开始了游戏的循环。
循环之后就是return0;,随后程序结束。但是我想告诉大家,这里有一个隐含的内存泄漏,因为glutMainLoop()函数没有办法跳出。起初我认为是有办法退出glut的循环的,但是在我看了这篇文章:
OpenGL内存泄漏之主循环函数glutMainLoop()
之后,我发现没法跳出循环了!由于各个glut的实现不一样,在试了原作者的方法后,我发现没有找到相关的函数。所以我索性后面不写对pWidget空间的释放了,在需要退出程序的时候,我们这样调用:deletepWidget; exit( 0 );。这样也能不造成内存泄漏。
随后介绍的就是GLWidget类,它维持着我们需要处理OpenGL图形的相关代码。接下来就是GLWidget类的定义:
#ifndef GLWIDGET_H #define GLWIDGET_H #include#include class GLWidget { public: GLWidget( void ); ~GLWidget( void ); void Init( int width, int height ); void Release( void ); void Render( void ); void Reshape( int width, int height ); // 重新改变窗口大小 private: GLdouble m_Width, m_Height; GLdouble m_AspectRatio; }; #endif // GLWIDGET_H
这里定义了构造函数、析构函数、初始化、释放空间、渲染和重新改变大小的函数。私有变量有初始化时候的宽和高,以及保存的宽高比。接下来我们看GLWidget.cpp文件。
// GLWidget.cpp 包含了控件的使用 // 11:14:27 最后编辑 #include "GLWidget.h" GLWidget::GLWidget( void ) { // 构造函数的代码在这里 m_Width = 0.0; m_Height = 0.0; } GLWidget::~GLWidget( void ) { Release( ); } void GLWidget::Init( int width, int height ) { // 保存初始化时窗口的宽和高 m_Width = GLdouble( width ); m_Height = GLdouble( height ); m_AspectRatio = m_Width / m_Height; // 初始化代码 glClearColor( 0.0, 0.0, 0.0, 1.0 ); } void GLWidget::Render( void ) { // 渲染代码 } void GLWidget::Release( void ) { // 释放空间代码 } void GLWidget::Reshape( int width, int height ) { // 改变大小时程序如何应对? GLdouble aspectRatio = GLdouble( width ) / GLdouble( height ); // 设置视口 if ( aspectRatio < m_AspectRatio ) { GLint smallHeight = GLint( GLdouble( width ) / m_AspectRatio ); GLint heightBlank = ( GLint( height ) - smallHeight ) / 2; glViewport( 0, heightBlank, GLint( width ), smallHeight ); } else { GLint smallWidth = GLint( GLdouble( height ) * m_AspectRatio ); GLint widthBlank = ( GLint( width ) - smallWidth ) / 2; glViewport( widthBlank, 0, smallWidth, GLint( height ) ); } glMatrixMode( GL_PROJECTION ); glLoadIdentity( ); // 设置裁剪区域(左右下上近远) glOrtho( 0.0, m_Width, 0.0, m_Height, -10.0, 10.0 ); // 为模型视图载入标准矩阵 glMatrixMode( GL_MODELVIEW ); glLoadIdentity( ); }
构造函数和析构函数就介绍了。Init()函数保存了传入的宽和高,并且规定了清除屏幕时使用的颜色是不透明的黑色(R:0G:0B:0A:255)。这里介绍Reshape()函数。假设用户对窗口改变了形状,这是传入的width和height就是新的窗口宽和高,我们用aspectRatio来保存宽高比,
随后我们就要进行判断了。如果当前的宽高比比我们期望的宽高比小,比如说这种情况:
我们这个时候让中间白色场景的高为smallHeight,两边黑框的高为heightBlank,计算之,并且设置视口(viewport,即我们能绘制的区域)。GlViewport函数的原型为:
glViewport( GLint x, GLint y, GLsizei width, GLsizei height );
第一、二个参数是屏幕的起始坐标,后面的两个参数是从这个坐标开始延伸的宽和高。注意这里遵循屏幕坐标系。所以我们填入了glViewport(0, heightBlank, GLint( width ), smallHeight );。如果前的宽高比比我们期望的宽高比大,如这样:
同理分析,这里就不介绍了。
然后设置投影矩阵为单位矩阵(重置投影矩阵),并且使用glOrtho()函数设置正交投影空间,glOrtho()函数的原型是这样的:
glOrtho( GLdouble left, GLdouble right, GLdouble bottom, GLdouble top, GLdouble near_val, GLdouble far_val );
这里设定了左、右、下、上、近、远六个参数,就像一个长方体一样。如果要显得有三维的感觉,使用gluPerspective()函数设置透视投影空间,它看起来很像金字塔削去了尖头剩下的部分。这里不详细介绍gluPerspective()函数了,由于我们主要处理二维图形,因此near_val和far_val就简单地设为-10.0和10.0了。最后让模型矩阵为单位矩阵。
编译成功,链接,在g++下需要加上-lglut这个选项。如果你能顺利地运行,那么太好了,我的小程序能在你的机子上运行了。运行的时候是不是这个结果啊?下面是我在Ubuntu下的截图:
是一个空白,宽和高分别是640和360。在随后的介绍中我们将会在这里绘制一些简单的图形。相信你一定觉得这个例子很简单!