原文地址:http://ogldev.atspace.co.uk/www/tutorial02/tutorial02.html
通常在写OpenGL程序时候,我们都需要glew库,该库包装了OpenGL的各种扩展,便于我们使用。 我们可以在main函数中调用glew初始化函数,之后就可以查询OpenGL各种扩展能否使用了,对于能够使用的函数,可以动态的加载。
在这篇教程中,我们首先了解一下顶点缓冲对象(VBO,vertex buffer object)的用法。在计算机图形学的3D世界中,三维物体对象都是由一系列顶点组成的,比如武士模型、城堡模型等等(注意下面两副图),这些顶点相连接,组成mesh(三角形)。
顾名思义,顶点缓冲就是保存顶点的内存对象,通常VBO是GPU显存中的一块区域,使用VBO可以加速顶点的读取速度。
在本篇教程和下一篇教程中,我们仅使用OpenGL中固定管线(相对固定管线的是可编程管线)的编程方法分别来画一个点和一个三角形。我们并没有对所渲染的物体(就是一个点或一个三角形)进行平移、缩放、旋转等操作,实际上我们只是在归一化的裁剪空间中来安排我们的物体,可以把归一化的裁剪空间看成一个正方体,坐标原点在正方形的中心,x, y,z坐标的范围都是[-1.0,1.0],我们把物体映射到屏幕空间(可以看作立方体中的物体先投影到z=-1的面上,然后该面的四边形映射到屏幕空间),比如屏幕空间是1024*768,则x坐标-1,映射到0,+1映射到1023,x和y等于0的点,则被映射到屏幕中心,最终根据draw函数中指定的体元语义,通过光栅化操作,把物体在屏幕空间画出来。
下面我们看下程序的部分源代码实现:
#include <GL/glew.h>
首先我们要包含glew头文件,注意我们要把这个头文件放在freee_glut.h的前面,否则的话,程序编译有可能出错。
#include "math_3d.h"
在这篇教程中,我们画一个顶点,顶点结构在math_3d.h中定义。
GLenum res = glewInit();
if (res != GLEW_OK)
{
fprintf(stderr, "Error: '%s'\n", glewGetErrorString(res));
return 1;
}
上面是glew初始化的代码,如果初始化失败,则会输出错误信息,注意:glew初始化代码必须在glut初始化之后。
Vector3f Vertices[1];
Vertices[0] = Vector3f(0.0f, 0.0f, 0.0f);
我们创建了一个顶点数组,该数组只有一个元素,其顶点坐标X,Y,Z都为0,后面我们将会看到这个点在屏幕中心位置显示。
GLuint VBO;
我们定义一个GLuint类型的全局变量VBO来表示顶点缓冲,通常情况下,OpenGL对象都是用一个GLuint类型的变量表示。
glGenBuffers(1, &VBO);
在OpenGL中,都是通过glGen*类型的函数产生各种各样的对象,这类函数有2个参数,第一个参数指定你要创建对象的数量,第二个参数是一个GLuint类型数组的地址,该地址中存放着driver分配给你的各种对象句柄。driver会保证将来再调用该函数时不会产生相同的对象句柄,除非你调用函数glDeleteBuffers显示删除对象句柄。
glBindBuffer(GL_ARRAY_BUFFER, VBO);
在OpenGL中,通常把一个对象绑定到一个target name,比如把VBO对象绑定到GL_ARRAY_BUFFER(表示缓冲是顶点数组,另一个常用的target name是GL_ELEMENT_ARRAY_BUFFER,表示索引数组),然后在这个target name上执行命令,这些命令将会一直影响绑定的对象,除非我们把该target name绑定到一个新的对象,此时,在target name上执行命令,将会影响新绑定的对象。
glBufferData(GL_ARRAY_BUFFER, sizeof(Vertices), Vertices, GL_STATIC_DRAW);
绑定对象后,我们开始准备顶点缓冲数据,上面的函数中,我们用Vertices中的数据填充顶点缓冲,其中GL_STATIC_DRAW表示,这些顶点数据渲染过程中不会改变,相对应的是GL_DYNAMIC_DRAW,driver会根据这些状态对程序进行一定的优化。
glEnableVertexAttribArray(0);
在后面的shader程序中,我们将看到更详细的顶点属性介绍,在本教程的例子中,我们没有使用shader,但是我们在顶点缓冲中装入了顶点位置,位置属性就作为顶点属性的index 0(注意:由于没有shader,我们此时用的是固定管线渲染),所以必须打开它,否则不能使用,因为所有的顶点属性在使用前都必须打开。
glBindBuffer(GL_ARRAY_BUFFER, VBO);
再一次绑定顶点缓冲,当然也可以不绑定,因为我们只有一个缓冲,前面已经绑定过了,但是在大型3D程序中,可能有很多顶点缓冲,就需要在渲染前,根据实际需要,切换绑定不同的顶点缓冲对象。
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);
这个函数告诉管线,怎么解释顶点缓冲中的数据。第一个参数指定属性的索引,第二个参数是属性的数量(3,表示,x,y,z),第三个参数是属性的数据类型,第四个参数是属性是否是归一化的,第五个参数是stride,表示两个连续属性实例之间的字节数目,如果值为0,那么顶点属性会被理解为:它们是紧密排列在一起的。最后一个参数offset,就是该属性从第几个字节开始,如果有2个属性,那么对于第一个属性,该值为0,没有偏移,对于第二个属性为12,因为第一个属性有12byte,偏移12byte后的位置即为第二个属性。
OpenGL函数。 void glVertexAttribPointer( GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride,const GLvoid * pointer); 参数: index 指定要修改的顶点属性的索引值 size 指定每个顶点属性的组件数量。必须为1、2、3或者4。初始值为4。(如position是由3个(x,y,z)组成,而颜色是4个(r,g,b,a)) type 指定数组中每个组件的数据类型。可用的符号常量有GL_BYTE, GL_UNSIGNED_BYTE, GL_SHORT,GL_UNSIGNED_SHORT, GL_FIXED, 和 GL_FLOAT,初始值为GL_FLOAT。 normalized 指定当被访问时,固定点数据值是否应该被归一化(GL_TRUE)或者直接转换为固定点值(GL_FALSE)。 stride 指定连续顶点属性之间的偏移量。如果为0,那么顶点属性会被理解为:它们是紧密排列在一起的。初始值为0。 pointer 指定第一个组件在数组的第一个顶点属性中的偏移量。该数组与GL_ARRAY_BUFFER绑定,储存于缓冲区中。初始值为0;
glDrawArrays(GL_POINTS, 0, 1);
最后,我们调用draw函数,该函数是GPU开始工作的起始点,GPU将绑定draw函数的参数以及状态等数据,然后通过driver传递到GPU。OpenGL有几种形式的draw函数,通常它们可以划分为ordered draws和indexed draws。ordered draw函数就是把指定的顶点按语义顺序画一遍,比如你指定GL_TRIANGLES ,则0-2顶点为第一个三角形,3-5顶点为第二个三角形等等。而Index draws要通过索引缓冲来索引顶点数据(实际上,在硬件层次,没有无索引的draw实现,如果没有指定索引缓冲,硬件会按照顶点的顺序,生成一个和顶点顺序对应的索引),这样可以重复利用顶点数据,比如我们可以用四个点来表示一个四边形(2个三角形组成),其中2个点是2个三角形共享的,索引缓冲中的数据为顶点在顶点缓冲中的位置。
我们调用DrawArrays函数画一个点,第一个参数指定体元语义,画的是点,第二个参数是第一个顶点的索引位置,第三个参数是要渲染的顶点的数量。
glDisableVertexAttribArray(0);
不使用顶点属性的时候,记得要关闭它,以免引起未知的一些错误。
程序执行后,界面如下,在窗口中心,有一个小的几乎看不见的白点。
我们也可以加入下面的代码,设置点的大小以及设置点为圆点。
//下面四行代码,设置点的大小,以及启动alpha blend以及多采样,这样可以画圆形的点
glEnable( GL_POINT_SMOOTH );
glEnable( GL_BLEND );
glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );
glPointSize(8.0f);