使用 glew
glew 全称是 OpengGL Extension Wrangler Library,它能够帮忙解决 OpengGL 不断扩展的问题。初始化 glew 之后,它将查询系统上所有可用的扩展功能并自动加载它们,然后提供一个头文件作为接口,我们直接通过头文件就可以使用这些扩展功能。
glew 的下载地址
http://glew.sourceforge.net/
下载之后进行解压,得到的目录结构
|-glew-2.1.0
|-bin
|-lib
|-include
|-doc
接下来开始配置环境,配置的方式有两种,第一种方式跟配置 glut 一样,系统级的配置,配置之后所有的 opengl 项目都不需要配置
- 第一步,把 include 目录下的头文件放在 %VISUAL_STUDIO%\VC\include\gl 目录下
-
第二步,把 lib 目录下的 lib 文件放在 %VISUAL_STUDIO%\VC\lib 目录下,然后在项目属性的
链接器
–>
输入
添加相应库的引用 - 第三步,把 bin 目录下的 dll 文件放在 system32 目录下,同样的 64 位系统要放在 sysWOW64 目录下
这种方式虽然能够一劳永逸,但因为 win32 和 x64 的库文件名一样,所以无法做到兼容,即无法同时把 32 位的库文件和 64 位的库拷到系统路径;另外过多地把库文件拷到系统路径也不好,因此推荐使用第二种方式,即给每个创建的项目配置环境
-
把头文件放在 $PROJECT_ROOT%\include\GL 目录下,然后在项目属性的
C/C++
–>
附加包含目录
中添加
.\include
-
把静态库 lib 文件放在
$PROJECT_ROOT%\lib
目录下,然后在项目属性的
链接器
–>
常规
–>
附加库目录
中添加
.\lib
,在
链接器
–>
输入
中添加相应库的引用 - 把动态库 dll 文件放在可执行程序 exe 同级目录下
如果一个项目想同时编译 32 位和 64 位,则可以分别把 32 位的 lib 文件和 64 位的 lib 文件放在 .\lib\win32 和 .\lib\x64 目录下,然后分别修改附加库目录,再把相应的 dll 文件拷到编译后的 32 位程序和 64 位程序目录下。
GL context
在开始绘制图形之前,我们必须先了解 GL context 和 GL objects 这两个重要概念,参考文档
https://www.khronos.org/opengl/wiki/Main_Page
上一篇文章讲到 OpenGL 渲染是基于状态(state)的。OpenGL context 是一个重要的概念的,只有创建了 context,OpenGL 才存在,context 一旦被销毁了,OpenGL 就不存在了。context 存储了一个 OpenGL 实例的所有状态,类似于一个程序开辟的所有内存空间。context 可以看作进程在操作系统中的一个执行过程,一个进程可以创建多个 context,每一个 context 代表一个可视面,就像一个应用程序的一个界面一样。
简单来讲,context 保存了一个 OpenGL 实例的所有状态,在使用 OpenGL 之前必须先创建一个 context。
int main(int argc, char **argv)
{
char *GL_version=(char *)glGetString(GL_VERSION);
char *GL_vendor=(char *)glGetString(GL_VENDOR);
char *GL_renderer=(char *)glGetString(GL_RENDERER);
return 0;
}
这里是想获取一些系统信息,但得到的结果却全是空,这里因为此时还没有创建 context,OpenGL 相当于不存在。
int main(int argc, char** argv)
{
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA);
glutInitWindowSize(400, 400);
glutInitWindowPosition(100, 100);
glutCreateWindow("Create Dot");
//获取 OpenGL 版本号
char *GL_version = (char *)glGetString(GL_VERSION);
//获取本机提供 GL 支持的公司
char *GL_vendor = (char *)glGetString(GL_VENDOR);
//获取渲染器的名称
char *GL_renderer = (char *)glGetString(GL_RENDERER);
//获取着色器的版本号
char* GL_shader_version = (char*)glGetString(GL_SHADING_LANGUAGE_VERSION);
//获取本机硬件支持的最大顶点属性数
GLint max_vertex_attrib;
glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &max_vertex_attrib);
return 0;
}
这样就可以正常获取信息了,因为
glutCreateWindow
创建一个窗口的同时就相当于已经创建了一个 context。
GL objects
再重申一遍,OpenGL 基于状态来渲染,可以说 OpenGL 被定义成“状态机”,所有的 API 都是修改状态、查询状态或者使用状态来渲染。GL Object 是一些状态的集合,这点看起来跟 GL Context 有点像,也可以这样类比,但要知道 object 和 context 的 state 是相互独立的,context 有一套状态,每个 object 也会有自己的一些状态。只有把 object 绑定到 context 上,它的状态都会映射到 context 上,绑定之后修改 context 的状态,object 也会受影响;相反基于 context 状态的函数也可以使用 object 的状态。
对象可以分成两大类,
regular objects
和
container objects
。
regular object 包括
Buffer Objects,Query Objects,Renderbuffer Objects,Sampler Objects,Texture Objects
。
container objects 包括
Vertex Array Objects,Framebuffer Objects,Program Pipeline Objects,Transform Feedback Objects
。
对象创建、销毁和绑定
使用 glGen* 函数给对象生成一个名字,就是创建对象了。
void glGen*(GLsizei n, GLuint *objects);
这个函数可以同时创建多个对象,只需要给定一个对象数组的指针;对象名是 Glsizei 类型,也就是一个 32 位无符号整型,这个整数并不是一个指针,只是对象的一个引用,用于标识这个对象。另外,整数 0 这个名字用于特殊对象,所以我们给对象起名时只能从 1 开始。
void glDelete*(GLsizei n, const GLuint *objects);
这个函数用于销毁对象,同样如果传进来的 object 是一个数组的话,可以批量销毁多个对象。关于对象销毁有几点要注意的
- 如果对象已经绑定到 context ,则对象销毁后会自动解绑;但如果对象附加到另一个对象上,则这我种附加关系不会解除
- 对象被 delete 之后并不会立即删除,它的名字还可以使用,但请不要用
void glBind*(GLenum target, GLuint object);
这个函数用于把对象绑定到 context。target 指明了对象的类型,有些对象可以绑定为多个类型,比如一个 buffer obejct 可以绑定为 array buffer,index buffer,pixel buffer,transform buffer 或者其它。
VAO && VBO
我们知道了 OpenGL 是一个状态机,使用 object 来保存数据和状态,钴绑定到 context,将 object 的状态和 context 的状态关联起来,然后使用 context 的状态进行渲染。接下来就着手准备绘制一个点,这里需要用到两个对象 VBO 和 VAO。
VAO 即 Vertex Array Object,它保存了所有顶点数据的状态,并没有保存顶点数据,而是保存了顶点数据的格式和所需 buffer 对象的引用。每一个状态属性都是可以开启和关闭的,使用下面两个函数
void glEnableVertexAttribArray(GLuint index);
void glDisableVertexAttribArray(GLuint index);
VBO,即 Vertex Buffer Object,在缓存区保存的就是顶点数据了。为了让 VAO 能使用 VBO 的数据,我们需要告诉 OpenGL,编号为 index 属性使用当前绑定到 GL_ARRAY_BUFFER 的 VBO
glVertexAttribPointer(index, size, type, normalized, stride, pointer);
index 是第几个属性,像顶点的第 0 个属性就是位置;size 指定构成属性的分量个数,像顶点位置由 x,y,z 三个分量组成,所以 size=3;type 指定属性值的类型,像顶点位置为 GL_FLOAT;normalized 指属性在管线中使用之前是否需要被规范化;stride 指两个相同属性值之间间隔的字节数,只有一个属性时间隔为 0;pointer 指存储数据的偏移值,同样只有一个属性时偏移值为 0。
绘制一个点
#include <gl/glew.h>
#include <gl/glut.h>
#include <math3d.h>
#include <iostream>
using namespace std;
void init();
void renderPerFrame();
int main(int argc, char** argv)
{
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA);
glutInitWindowSize(400, 400);
glutInitWindowPosition(100, 100);
glutCreateWindow("Create Dot");
char *GL_version = (char *)glGetString(GL_VERSION);
char *GL_vendor = (char *)glGetString(GL_VENDOR);
char *GL_renderer = (char *)glGetString(GL_RENDERER);
GLenum res = glewInit();
if (GLEW_OK != res)
{
cout << "glewInit failed: " << glewGetErrorString(res) << endl;
system("pause");
return 1;
}
init();
glutDisplayFunc(renderPerFrame);
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glutMainLoop();
}
void init()
{
//创建 buffer 对象
GLuint VBO;
glGenBuffers(1, &VBO);
//绑定 buffer 对象
glBindBuffer(GL_ARRAY_BUFFER, VBO);
//定义数据
M3DVector3f vertices[1];
vertices[0][0] = 0.0f;
vertices[0][1] = 0.0f;
vertices[0][2] = 0.0f;
//填充 buffer 的值
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
}
void renderPerFrame()
{
glClear(GL_COLOR_BUFFER_BIT);
//开启顶点属性
glEnableVertexAttribArray(0);
//指定属性使用的 buffer
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);
//绘制顶点
glDrawArrays(GL_POINTS, 0, 1);
//关闭顶点属性
glDisableVertexAttribArray(0);
glutSwapBuffers();
}
总结一下这个过程:
首先,创建顶点缓冲对象,即 VBO;然后将顶点缓冲对象绑定到 context;最后给顶点缓冲对象赋顶点数据。
经过上面三步之后,在 context 中就已经有了一个顶点缓冲对象,这个对象保存了顶点数据。接下来就是在主回调函数中取顶点数据进行绘制,这里使用的是固定管线,所以不用 VAO,后面主要使用可编程管线。
绘制的时候是基于状态的,要绘制顶点,必须知道每个状态属性从哪里取数据,即把顶点对象(VAO)和顶点缓冲对象(VBO)关联起来。首先,要开启顶点对象的某个属性;然后指定该属性使用的 buffer 对象;然后开始绘制;最后关闭顶点对象属性。