OpenGL入门学习

今天心血来潮突然想学习一下图形相关的内容,我听说过的这方面的东西就只有OpenGL和OpenCV了。恰好Android上的VR涉及到了OpenGL的一些内容,今天就开始记录我的OpenGL学习内容。
1.什么是OpenGL
OpenGL是一套开源的、跨平台的绘图API。在应用上,OpenGL常用于CAD、虚拟实境、科学可视化程序和电子游戏开发。它可以绘制2D及3D图形。需要注意的是OpenGL通常要搭配窗口库,如QT,因为OpenGL本身不处理窗口的创建和鼠标键盘输入,因此需要我们自己动手来处理这些问题。
2.OpenGL的版本变迁
OpenGL的第一个版本:
OpenGL 1.0
发布时间: 1992年1月
以下截取自某博客:
OpenGL 1.0~OpenGL 1.5是经典的固定管线时代;OpenGL 2.0~OpenGL 2.1是固定管线和可编程管线并存的时代;OpenGL 3.0~OpenGL 4.x开始是可编程管线崛起的时代。在出现可编程管线的那个时代。
就我现在接触到的的资料来看,现代使用的OpenGL版本至少在3.0以上。
OpenGL各个版本很混乱,在各个版本中会出现严重的断层。
本次我学习使用的OpenGL为3.2。
注:Android中使用的OpenGL为OpenGL ES,这是一套专门为嵌入式设备准备的API,它是OpenGL的子集。
官网传送门
它的使用和一般的OpenGL使用是有比较大的区别的。这些内容留置我以后整理。
3.环境配置
由于Mac的Xcode内置了OpenGL的库,我没有特意去配置开发环境,从其他地方找来的example可以直接编译并运行。
4.学习之前要先了解Shader是什么。
Wiki:

着色器(英语:shader)应用于计算机图形学领域,指一组供计算机图形资源在执行渲染任务时使用的指令,用于计算图像的颜色或明暗。但进来,它也能用于处理一些特殊效果,或者视频后处理。通俗地说,着色器告诉电脑如何用特有的一种方法去绘制物体。
程序员将着色器应用于图形处理器(GPU)的可编程流水线,来实现三维应用程序。这样的图形处理器有别于传统的固定流水线处理器,为GPU编程带来更高的灵活性和适应性。以前固有的流水线只能进行一些几何变换和像素灰度计算。现在可编程流水线还能处理所有像素、顶点、纹理的位置、色调、饱和度、明度、对比度并实时地绘制图像。着色器还能产生如模糊、高光、有体积光源、失焦、卡通渲染、色调分离、畸变、凹凸贴图、边缘检测、运动检测等效果

从程序开发者的角度来看:
Shader是使用GLSL(OpenGL Shading Language)或HLSL(High Level Shader Language)编写的程序,它运行于GPU上,专注于对图像相关的数据计算的处理。Shader是一个通用的概念,它不受限于某个框架。

编写OpenGL程序时需要同时编写OpenGL主程序和Shader程序。早期的OpenGL使用时可以选择有没有Shader,但是现在更高版本的OpenGL已经是必须使用Shader了。

在OpenGL中Shader分为以下几种(包括但不限于)。
(1)Fragment Shader
Fragment Shader用于计算每个需要绘制的像素点的颜色。
(2)Geometry Shader
Geometry Shader用于处理点、线、面的几何坐标变换。
(3)Vertex Shader
Vertex Shader用于计算将目标点坐标变换为实际屏幕上显示的二维坐标。
下面就正式开始学习OpenGL吧!

4.Hello Triangle!
从一个最简单的绘制三角形的example开始,初步了解使用OpenGL的流程。

1
2
3
4
// initialise GLFW
glfwSetErrorCallback(OnError);
if(!glfwInit())
throw std::runtime_error("glfwInit failed");

初始化GLFW。GLFW是什么呢?开始提到OpenGL的使用必须搭配窗口图形库,GLFW是一个轻量级的跨平台的为OpenGL设计的窗口库。
在使用OpenGL前,首先要创建一个窗口。这里初始化GLFW。

1
2
3
4
5
6
7
8
9
// open a window with GLFW
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2);
glfwWindowHint(GLFW_RESIZABLE, GL_FALSE);
gWindow = glfwCreateWindow((int)SCREEN_SIZE.x, (int)SCREEN_SIZE.y, "OpenGL Tutorial", NULL, NULL);
if(!gWindow)
throw std::runtime_error("glfwCreateWindow failed. Can your hardware handle OpenGL 3.2?");

在这里使用GLFW打开一个窗口。

1
2
// GLFW settings
glfwMakeContextCurrent(gWindow);

为当前窗口设置一个OpenGL上下文,类似于Android中的Context。

1
2
3
4
// initialise GLEW
glewExperimental = GL_TRUE; //stops glew crashing on OSX :-/
if(glewInit() != GLEW_OK)
throw std::runtime_error("glewInit failed");

这里初始化GLEW。什么是GLEW?在现代OpenGL版本里,不能直接使用#include< … >这样的静态链接的方式调用OpenGL的API,它的API在运行时动态调用。GLEW就是我们动态调用OpenGL的入口。

1
2
3
4
5
6
7
// loads the vertex shader and fragment shader, and links them to make the global gProgram
static void LoadShaders() {
std::vector<tdogl::Shader> shaders;
shaders.push_back(tdogl::Shader::shaderFromFile(ResourcePath("vertex-shader.txt"), GL_VERTEX_SHADER));
shaders.push_back(tdogl::Shader::shaderFromFile(ResourcePath("fragment-shader.txt"), GL_FRAGMENT_SHADER));
gProgram = new tdogl::Program(shaders);
}

自定义一个LoadShaders方法来初始化shader。
在之前提过的,shader以GLSL书写,这里shader的具体代码存在一个txt文件里,通过这个函数来加载shader文件里的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// loads a triangle into the VAO global
static void LoadTriangle() {
// make and bind the VAO
glGenVertexArrays(1, &gVAO);
glBindVertexArray(gVAO);

// make and bind the VBO
glGenBuffers(1, &gVBO);
glBindBuffer(GL_ARRAY_BUFFER, gVBO);

// Put the three triangle verticies into the VBO
GLfloat vertexData[] = {
// X Y Z
0.0f, 0.8f, 0.0f,
-0.8f,-0.8f, 0.0f,
0.8f,-0.8f, 0.0f,
};
glBufferData(GL_ARRAY_BUFFER, sizeof(vertexData), vertexData, GL_STATIC_DRAW);

// connect the xyz to the "vert" attribute of the vertex shader
glEnableVertexAttribArray(gProgram->attrib("vert"));
glVertexAttribPointer(gProgram->attrib("vert"), 3, GL_FLOAT, GL_FALSE, 0, NULL);

// unbind the VBO and VAO
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
}

这里是真正绘制三角形的代码。
一点点看。
// make and bind the VBO glGenBuffers(1, &gVBO); glBindBuffer(GL_ARRAY_BUFFER, gVBO);
// make and bind the VAO glGenVertexArrays(1, &gVAO); glBindVertexArray(gVAO);
这里创建一个VBO对象和VAO对象,并把它和OpenGL绑定
VBO和VAO是什么呢?
VAO(Vertex Array Object)和VBO(Vertex Buffer Object)是C++程序和Shader程序传递数据使用的对象。
VBO存储要传递给Shader的数据,但是这里的数据没有类型。VAO对VBO和Shader进行连接,它描述了VBO存储的数据的类型,以及该传递个Shader的数据。

// Put the three triangle verticies into the VBO GLfloat vertexData[] = { // X Y Z 0.0f, 0.8f, 0.0f, -0.8f,-0.8f, 0.0f, 0.8f,-0.8f, 0.0f, }; glBufferData(GL_ARRAY_BUFFER, sizeof(vertexData),vertexData, GL_STATIC_DRAW);
这里向VBO添加数据,因为要绘制三角形,所以给VBO添加三个点的坐标值。
// connect the xyz to the "vert" attribute of the vertex shader glEnableVertexAttribArray(gProgram->attrib("vert")); glVertexAttribPointer(gProgram->attrib("vert"), 3, GL_FLOAT, GL_FALSE, 0, NULL);
调用VAO配置数据。这里设置变量的类型为vert。

// unbind the VBO and VAO glBindBuffer(GL_ARRAY_BUFFER, 0); glBindVertexArray(0);
最后对VBO和VAO解除绑定,因为以后不再需要它们了,这样也可以避免对数据误操作。

1
2
3
4
5
6
7
8
// run while the window is open
while(!glfwWindowShouldClose(gWindow)){
// process pending events
glfwPollEvents();

// draw one frame
Render();
}

数据设定完后调用Render函数开始渲染绘图。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// draws a single frame
static void Render() {
// clear everything
glClearColor(0, 0, 0, 1); // black
glClear(GL_COLOR_BUFFER_BIT);

// bind the program (the shaders)
glUseProgram(gProgram->object());

// bind the VAO (the triangle)
glBindVertexArray(gVAO);

// draw the VAO
glDrawArrays(GL_TRIANGLES, 0, 3);

// unbind the VAO
glBindVertexArray(0);

// unbind the program
glUseProgram(0);

// swap the display buffers (displays what was just drawn)
glfwSwapBuffers(gWindow);
}

glClearColor(0, 0, 0, 1); // black glClear(GL_COLOR_BUFFER_BIT);
清空屏幕,让窗口背景变成黑色。
``
// bind the program (the shaders)
glUseProgram(gProgram->object());

// bind the VAO (the triangle)
glBindVertexArray(gVAO);

``
通知OpenGL使用VAO和shader。