在绘制之前的创建项目等准备工作,这里就不阐述了,假设我们已经打好了基础,开始编写代码。
我们的这个示例程序仅仅是在蓝色的背景上绘制一个红色的正方形,乍看起来没有什么挑战性,但它实践了所有必要的步骤。
1.包含头文件
在开始编写任何C++(或者是C)程序之前,都要先将用到的函数和类定义的头文件包含进来。
GLTools.h头文件中包含了大部分GLTools中类似C语言的独立函数,每个GLTools的C++类则有自己的头文件。
GLShaderManager.h移入了GLTools着色器管理器(Shader Manager)类。有了着色器,我们才能在OpenGL中进行着色。着色器管理器不仅允许我们创建并管理着色器,还提供一组“存储着色器(Stock Shader)”,它们能够进行一些初步和基本的渲染操作。
2. 启动GLUT
控制台模式的C语言和C++程序总是从“main”函数开始处理。
1 int main(int argc,char *argv[]) 2 { 3 //设置当前工作目录,针对MAC OS X 4 /* 5 `GLTools`函数`glSetWorkingDrectory`用来设置当前工作目录。实际上在Windows中是不必要的,因为工作目录默认就是与程序可执行执行程序相同的目录。但是在Mac OS X中,这个程序将当前工作文件夹改为应用程序捆绑包中的`/Resource`文件夹。`GLUT`的优先设定自动进行了这个中设置,但是这样中方法更加安全。 6 */ 7 gltSetWorkingDirectory(argv[0]); 8 //初始化GLUT库,这个函数只是传说命令参数并且初始化glut库 9 glutInit(&argc, argv); 10 11 /* 12 初始化双缓冲窗口,其中标志GLUT_DOUBLE、GLUT_RGBA、GLUT_DEPTH、GLUT_STENCIL分别指 13 双缓冲窗口、RGBA颜色模式、深度测试、模板缓冲区 14 15 --GLUT_DOUBLE`:双缓存窗口是指绘图命令实际上是离屏缓存区执行的,然后迅速转换成窗口视图 16 --GLUT_DEPTH`:标志将一个深度缓存区分配为显示的一部分,因此我们能够执行深度测试; 17 --GLUT_STENCIL`:确保我们也会有一个可用的模板缓存区。 18 */ 19 glutInitDisplayMode(GLUT_DOUBLE|GLUT_RGBA|GLUT_DEPTH|GLUT_STENCIL); 20 21 //GLUT窗口大小、窗口标题 22 glutInitWindowSize(500, 500); 23 glutCreateWindow("Triangle"); 24 25 /* 26 GLUT 内部运行一个本地消息循环,拦截适当的消息。然后调用我们不同时间注册的回调函数。我们一共注册2个回调函数: 27 1)为窗口改变大小而设置的一个回调函数 28 2)包含OpenGL 渲染的回调函数 29 */ 30 //注册重塑函数 31 glutReshapeFunc(changeSize); 32 //注册显示函数 33 glutDisplayFunc(RenderScene); 34 //注册特殊函数 35 glutSpecialFunc(SpecialKeys); 36 37 /* 38 初始化一个GLEW库,确保OpenGL API对程序完全可用。 39 在试图做任何渲染之前,要检查确定驱动程序的初始化过程中没有任何问题 40 */ 41 GLenum status = glewInit(); 42 if (GLEW_OK != status) { 43 printf("GLEW Error:%s ",glewGetErrorString(status)); 44 return 1; 45 } 46 47 //设置我们的渲染环境,进行预加载纹理,建立几何图形、渲染器等工作。 48 setupRC(); 49 //开始主消息循环 50 glutMainLoop(); 51 return 0; 52 }
glutMainLoop函数被调用之后,在主窗口被关闭之前都不会返回,并且一个应用程序中只需调用一次。这个函数负责处理所有操作系统特定的消息、按键动作等,直到我们关闭程序为止。
3. 定义视口
由于在不同环境下窗口的大小变化的检测和处理方式不同,GLUT库专门提供了glutReshapeFunc函数,这个函数注册了一个回调,让GLUT库在窗口改变时调用,我们传递到glutReshapeFunc函数的原形就是void changeSize(int w,int h);
1 void changeSize(int w,int h) 2 { 3 /* 4 x,y 参数代表窗口中视图的左下角坐标,而宽度、高度是像素为表示,通常x,y 都是为0 5 */ 6 glViewport(0, 0, w, h); 7 8 }
changeSize函数在窗口大小改变时接受新的宽和高,在OpenGL函数glViewport的帮助下,修改从目的坐标系到屏幕坐标系上的映射。
glViewport函数定义如下:
void glViewport(GLint x, GLint y, GLsizei width, GLsizei height);
其中x参数和y参数代表窗口中视口的左下角图标,宽度和高度参数用像素表示,通常x和y都为0,视口以实际屏幕坐标定义了窗口中的区域,OpenGL可以在这个区域中进行绘图的裁剪区域被映射到新的视口。如果指定了一个比窗口坐标更小的视口,渲染区域就会缩小,如下图所示:
4. 完成设置
在开始main函数中的GLUT主循环之前,我们先调用SetupRC函数,为程序做一些一次性的设置。
1 1 void setupRC() 2 2 { 3 3 //设置清屏颜色(背景颜色) 4 4 glClearColor(0.0f, 0.0f, 1.0f, 1); 5 5 6 6 //没有着色器,在OpenGL 核心框架中是无法进行任何渲染的。初始化一个渲染管理器。 7 7 shaderManager.InitializeStockShaders(); 8 8 9 9 //设置为GL_TRIANGLE_FAN ,4个顶点 10 10 triangleBatch.Begin(GL_TRIANGLE_FAN, 4); 11 11 triangleBatch.CopyVertexData3f(vVerts); 12 12 triangleBatch.End(); 13 13 14 14 }
源文件的开头部分,我们声明一个GLBatch类:GLBatch triangleBatch;
GLBatch是GLTools库中的一个简单容器类,可以作为7种图元简单批次的容器使用,它的使用也很简单:
首先对批次进行初始化,告诉这个类它代表哪种图元,包括顶点数,以及(可选)一组或两组纹理坐标。
void Begin(GLenum primitive, GLuint nVerts, GLuint nTextureUnits = 0);
然后复制由3分量(x,y,z)顶点组成的数组
void CopyVertexData3f(GLfloat *vVerts)
这里我们还可以选择复制表面法线、颜色或者纹理坐标
void CopyNormalDataf(M3DVector3f *vNorms); void CopyColorData4f(M3DVector4f *vColors); void CopyTexCoordData2f(M3DVector2f *vTexCoords, GLuint uiTextureLayer);
完成上述工作后,调用End来表明已经完成了数据复制工作,一旦调用End函数,就不能再增加新的属性了。
6. 开始渲染
1 void RenderScene(void) 2 { 3 //1.清除一个或者一组特定的缓存区 4 /* 5 缓冲区是一块存在图像信息的储存空间,红色、绿色、蓝色和alpha分量通常一起分量通常一起作为颜色缓存区或像素缓存区引用。 6 OpenGL 中不止一种缓冲区(颜色缓存区、深度缓存区和模板缓存区) 7 清除缓存区对数值进行预置 8 参数:指定将要清除的缓存的 9 GL_COLOR_BUFFER_BIT :指示当前激活的用来进行颜色写入缓冲区 10 GL_DEPTH_BUFFER_BIT :指示深度缓存区 11 GL_STENCIL_BUFFER_BIT:指示模板缓冲区 12 */ 13 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT|GL_STENCIL_BUFFER_BIT); 14 15 //2.设置一组浮点数来表示红色 16 GLfloat vRed[] = {1.0,0.0,0.0,1.0f}; 17 //传递到存储着色器,即GLT_SHADER_IDENTITY着色器,这个着色器只是使用指定颜色以默认笛卡尔坐标第在屏幕上渲染几何图形 18 shaderManager.UseStockShader(GLT_SHADER_IDENTITY,vRed); 19 20 //提交着色器 21 triangleBatch.Draw(); 22 23 //在开始的设置openGL 窗口的时候,我们指定要一个双缓冲区的渲染环境。这就意味着将在后台缓冲区进行渲染,渲染结束后交换给前台。这种方式可以防止观察者看到可能伴随着动画帧与动画帧之间的闪烁的渲染过程。缓冲区交换平台将以平台特定的方式进行。 24 //将后台缓冲区进行渲染,然后结束后交换给前台 25 glutSwapBuffers(); 26 27 }
就这样,我们已经用OpenGL渲染出了这个正方形。
7. 加点儿互动
GLUT还提供了另一个回调函数glutSpecialFunc,它注册了一个能够在按一个特殊按键(上下左右箭头键,page up/down等)时被调用的函数。
我们在主函数中加入下面的代码行,来注册SpecialKeys回调函数。
glutSpecialFunc(SpecialKeys);
它在按键时接受一个相应的按键编码,以及在使用鼠标时光标的x和y坐标位置(像素形式)。
在这个示例程序中,我们将顶点存储在一个全局数组中
1 //blockSize 边长 2 GLfloat blockSize = 0.1f; 3 4 //正方形的4个点坐标 5 GLfloat vVerts[] = { 6 -blockSize,-blockSize,0.0f, 7 blockSize,-blockSize,0.0f, 8 blockSize,blockSize,0.0f, 9 -blockSize,blockSize,0.0f 10 };
我们只需要复制新的顶点数据,就可以更新正方形的位置。
1 void SpecialKeys(int key, int x, int y){ 2 3 GLfloat stepSize = 0.025f; 4 5 GLfloat blockX = vVerts[0]; 6 GLfloat blockY = vVerts[10]; 7 8 printf("v[0] = %f ",blockX); 9 printf("v[10] = %f ",blockY); 10 11 12 if (key == GLUT_KEY_UP) { 13 14 blockY += stepSize; 15 } 16 17 if (key == GLUT_KEY_DOWN) { 18 19 blockY -= stepSize; 20 } 21 22 if (key == GLUT_KEY_LEFT) { 23 blockX -= stepSize; 24 } 25 26 if (key == GLUT_KEY_RIGHT) { 27 blockX += stepSize; 28 } 29 30 //触碰到边界(4个边界)的处理 31 32 //当正方形移动超过最左边的时候 33 if (blockX < -1.0f) { 34 blockX = -1.0f; 35 } 36 37 //当正方形移动到最右边时 38 //1.0 - blockSize * 2 = 总边长 - 正方形的边长 = 最左边点的位置 39 if (blockX > (1.0 - blockSize * 2)) { 40 blockX = 1.0f - blockSize * 2; 41 } 42 43 //当正方形移动到最下面时 44 //-1.0 - blockSize * 2 = Y(负轴边界) - 正方形边长 = 最下面点的位置 45 if (blockY < -1.0f + blockSize * 2 ) { 46 47 blockY = -1.0f + blockSize * 2; 48 } 49 50 //当正方形移动到最上面时 51 if (blockY > 1.0f) { 52 53 blockY = 1.0f; 54 55 } 56 57 printf("blockX = %f ",blockX); 58 printf("blockY = %f ",blockY); 59 60 // Recalculate vertex positions 61 vVerts[0] = blockX; 62 vVerts[1] = blockY - blockSize*2; 63 printf("(%f,%f) ",vVerts[0],vVerts[1]); 64 65 vVerts[3] = blockX + blockSize*2; 66 vVerts[4] = blockY - blockSize*2; 67 printf("(%f,%f) ",vVerts[3],vVerts[4]); 68 69 vVerts[6] = blockX + blockSize*2; 70 vVerts[7] = blockY; 71 printf("(%f,%f) ",vVerts[6],vVerts[7]); 72 73 vVerts[9] = blockX; 74 vVerts[10] = blockY; 75 printf("(%f,%f) ",vVerts[9],vVerts[10]); 76 77 triangleBatch.CopyVertexData3f(vVerts); 78 79 glutPostRedisplay(); 80 }
默认情况下,在窗口创建、改变大小或者需要重绘时,GLUT通过调用RenderScene函数来更新窗口,我们可以手动调用glutPostRedisplay来告诉GLUT发生了某些改变,要对场景进行渲染了。