OpenGL Rendering Pipeline

这里是分割线

渲染管线

固定管线和可编程管线

core-profile immediate mode,教程都针对core-profile进行。这些对应的是3.3及其以后的版本,相比固定管线来说效率更高。固定管线学习起来比较容易封装的好,但是渲染效率低。core-profile的话学习起来稍微困难点,需要真的理解相关机制和原理才可以。
现在许多高版本的opengl都支持很多新的特定,这些实现必须要更新显卡相关的驱动。但是一般情况下核心的函数基本都是能用的。
扩展是OpenGL的一大特色,提供使用思路:

1
2
3
4
5
6
7
8
if(GL_ARB_extension_name)
{
// Do cool new and modern stuff supported by hardware
}
else
{
// Extension not supported: do it the old way
}

状态机

context说白了就是状态机的内容。在OpenGL我们通过设定选项,操控buffers来改变状态机,然后使用当下的context进行渲染。

对象

一个对象就是opengl里面状态的子集的集合。比如我们可以有一个对象表示绘制窗口的设置。然后我们可以设置它的大小,支持多少颜色等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct object_name {
float option1;
int option2;
char[] name;
};
// The State of OpenGL
struct OpenGL_Context {
object* object_Window_Target;
};
// create object
unsigned int objectId = 0;
glGenObject(1, &objectId);
// bind object to context
glBindObject(GL_WINDOW_TARGET, objectId);
// set options of object currently bound to GL_WINDOW_TARGET
glSetObjectOption(GL_WINDOW_TARGET, GL_OPTION_WINDOW_WIDTH, 800);
glSetObjectOption(GL_WINDOW_TARGET, GL_OPTION_WINDOW_HEIGHT, 600);
// set context target back to default
glBindObject(GL_WINDOW_TARGET, 0);
//以上不是连续代码,而且只是一个过程的抽象

There are objects for example that act as container objects for 3D model data (a house or a character) and whenever we want to draw one of them, we bind the object containing the model data that we want to draw (we first created and set options for these objects). Having several objects allows us to specify many models and whenever we want to draw a specific model, we simply bind the corresponding object before drawing without setting all their options again.

hellowindow的一些流程解释

创建窗口-定义context,然后处理输入输出。

固定管线

具体的代码在上一篇文章中已经给出了,都是基于glut的最基本的实现,比较简单,属于固定管线编程。基于固定管线我上手了一些例子,当时学习的时候进行过一些简单的绘制,比如立方体啊什么的。但是因为最后项目需要要用到shader,所以那块的进度也就基本到那个进度。当时给同事做了一个个基于硬件交互来进行动态绘制的demo。可惜并没有保存视频,大致就是基于视觉追踪进行用交互笔抓去一个立方体的实现,基本实现了整个AR的流程(识别,矩阵变换,现实渲染,虚拟内容渲染和shader融合等),以后会把当初的思路通过专题记录下来。

可编程管线

在这没什么好讲的,因为只是绘制一个窗口的话没用到具体内容,说下大致的流程:(使用glfw)
1 glfw初始化和相关配置,注意平台相关;
2 glfw创建窗口,包括创建窗口重绘回调;
3 glad加载所有的opengl函数指针;
4 渲染循环 input处理-渲染-交换缓冲区和poll IO events;
5 glfw结束,清除glfw资源;
相关完整代码:

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <iostream>
void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void processInput(GLFWwindow *window);
// settings
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;
int main()
{
// glfw: initialize and configure
// ------------------------------
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
#ifdef __APPLE__
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // uncomment this statement to fix compilation on OS X
#endif
// glfw window creation
// --------------------
GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
if (window == NULL)
{
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
// glad: load all OpenGL function pointers
// ---------------------------------------
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
// render loop
// -----------
while (!glfwWindowShouldClose(window))
{
// input
// -----
processInput(window);
// render
// ------
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.)
// -------------------------------------------------------------------------------
glfwSwapBuffers(window);
glfwPollEvents();
}
// glfw: terminate, clearing all previously allocated GLFW resources.
// ------------------------------------------------------------------
glfwTerminate();
return 0;
}
// process all input: query GLFW whether relevant keys are pressed/released this frame and react accordingly
// ---------------------------------------------------------------------------------------------------------
void processInput(GLFWwindow *window)
{
if(glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true);
}
// glfw: whenever the window size changed (by OS or user resize) this callback function executes
// ---------------------------------------------------------------------------------------------
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
// make sure the viewport matches the new window dimensions; note that width and
// height will be significantly larger than specified on retina displays.
glViewport(0, 0, width, height);
}

绘制一个三角形

你的绘制永远是3D内容,但是opengl需要将3d空间变换为2D空间,这部分是由OpenGL的Graphic Pipeline完成的。这个管线可以分成两个大的阶段:第一部分将3d坐标转换成2d坐标,第二部分是将2d坐标转换成实际的颜色像素。该部分我们着重讨论这个流程。这个流程是高度specialized的(有一个专门的函数)而且很容易能够并行处理。相应,显卡里有巨多的小单元来处理这个过程,在管线的每一步都在它上面跑一个小程序,这就是shader。下图是可编程管线的流程,蓝色部分是可以自定义的。
见图opengl2-001
Markdown
顶点数据:顶点shader-形状组装-几何shader-光栅化-片元shader-测试和渲染
作为输入,我们提供了一组可以组成三角形的3d坐标,叫做Vertex Data。这个vertexdata是一些顶点的集合。一个顶点就是每一个3d坐标的数据集合。这个数据集合可能被包含了任何可能的数据,这些东西用vertex attributes表示。
你告诉opengl你渲染的是什么类型的数据集,我们需要规定primitives(就是预定义好的渲染类型),包括GL_POINTS, GL_TRIANGLES and GL_LINE_STRIP等。
1 vertex shader
进行3d坐标转换,目的是我们可以基于顶点属性做一些基本的处理。
齐次坐标变换等。
2 primitive assembly
将所有顶点作为输入,基于给定的形状组织所有的顶点,比如一个三角形
3 几何shader
他会基于给定的形状增加一些新的顶点,比如我们要生成一个三角形,知道三个点,几何shader会根据预置类型-三角形,自动填补三角形三条边上的点。
4 光栅化
将上一步的结果转化成相应的屏幕上的像素,分割成可以被frgament shader处理的片元。同时进行裁剪,它会把所有位于视野外/屏幕外的片元裁剪掉,提高性能。
片元就是opengl来进行渲染要一个单独像素所需要的所有数据。
5 片元shader最主要的目的是计算像素的最终颜色,这是所有opengl高级特性发生的阶段。包含可以进行这些处理的数据:光照、阴影、光的颜色等
6 深度测试和渲染
所有的像素点颜色计算完成后,进行遮挡、半透明等的处理。
我们核心要关注的就是顶点shader和片元shader。

顶点输入

opengl最后渲染的的是一个在单位正方体内的齐次(normalized device ccordinate)坐标,xyz都从-1到1。
现在我们要渲染一个三角形,直接定义到NDC坐标:
float vertices[] = {
-0.5f, -0.5f, 0.0f,
​ 0.5f, -0.5f, 0.0f,
​ 0.0f, 0.5f, 0.0f
};
图002Markdown

顶点数据在被vertex shader处理后,肯定是在NDC空间内的坐标。其他的不被渲染。
接着,这些齐次坐标系内的坐标,会通过你配置的glViewport转换到屏幕坐标screen-space coordinate。这些屏幕坐标转换成片元就是片元shader的输入数据。

顶点数据准备好以后,我们把他们提供给vertex shader,通过在GPU上创建显存区域,在这部分区域存储vertex data。具体通过vertex buffet object(VBO)实现。他能够存储大量的顶点数据,所以我们要尽可能地将这些数据一次发送给GPU,剩下的GPU处理起来就很快了。VBO是一个对象,我们在OpenGL里都有一个唯一的ID指向这个buffer对象:
unsigned int VBO;
glGenBuffers(1, &VBO);
OpenGL有很多不同种类的buffer对象,顶点的buffer对象叫GL_ARRAY_BUFFER,OpenGL容许我们一次绑定到不同的buffers,只要他们有不同的buffer类型。我们使用glBindBuffer将新创建的buffer绑定给GL_ARRAY_BUFFER对象:

第一个参数是要生成的缓冲对象的数量,第二个是要输入用来存储缓冲对象名称的数组。该函数会在buffers里返回n个缓冲对象的名称。

个人理解如下,可以声明一个GLuint变量,然后使用glGenBuffers后,它就会把缓冲对象保存在vbo里,当然也可以声明一个数组类型,那么创建的3个缓冲对象的名称会依次保存在数组里。

GLuint vbo;
glGenBuffers(1,&vbo);
GLuint vbo[3];
glGenBuffers(3,vbo);

注意:这里我用的是VBO做的示范,解释一下,glGenBuffers()函数仅仅是生成一个缓冲对象的名称,这个缓冲对象并不具备任何意义,它仅仅是个缓冲对象,还不是一个顶点数组缓冲,它类似于C语言中的一个指针变量,我们可以分配内存对象并且用它的名称来引用这个内存对象。OpenGL有很多缓冲对象类型,那么这个缓冲对象到底是什么类型,就要用到下面的glBindBuffer()函数了。

glBindBuffer(GL_ARRAY_BUFFER, VBO);
绑定以后,针对GL_ARRAY_BUFFER对象的任何调用都被用来配置当前的VBO。使用glBufferData将定义好的顶点数据传给buffer的memory:
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

glBufferData解释说明:
glBufferData is a function specifically targeted to copy user-defined data into the currently bound buffer. Its first argument is the type of the buffer we want to copy data into: the vertex buffer object currently bound to the GL_ARRAY_BUFFER target. The second argument specifies the size of the data (in bytes) we want to pass to the buffer; a simple sizeof of the vertex data suffices. The third parameter is the actual data we want to send.
The fourth parameter specifies how we want the graphics card to manage the given data. This can take 3 forms:
GL_STATIC_DRAW: the data will most likely not change at all or very rarely.
GL_DYNAMIC_DRAW: the data is likely to change a lot.
GL_STREAM_DRAW: the data will change every time it is drawn.

The position data of the triangle does not change and stays the same for every render call so its usage type should best be GL_STATIC_DRAW. If, for instance, one would have a buffer with data that is likely to change frequently, a usage type of GL_DYNAMIC_DRAW or GL_STREAM_DRAW ensures the graphics card will place the data in memory that allows for faster writes.

我们已经通过VBO将定点数据存储到显卡的内存中,下一步就是创建vertex shader 和fragment shader来处理这些数据。

vertex shader

现代OpenGL中,我们至少需要一个vertex shader和一个fragment shader。这里简单讨论和配置两个shader 的实现。

1
2
3
4
#version 330 core
layout (location = 0) in vec3 aPos;
void main()
{gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);}

使用GLSL语言,通过in关键字声明输入的顶点数据。我们现在只关注位置数据,所以我们是需要一个顶点属性。GLSL有一个向量类型包含1-4个浮点数据。同时,通过layout (location = 0)我们也设置了输入数据的位置。注意xyzw中w是用来处理perspective division的。
参考阅读:https://stackoverflow.com/questions/17269686/why-do-we-need-perspective-division
为了设置vertex shader的输出我们必须将位置数据赋值给预置的gl_position蓝色参数,在后台是一个vec4类型。在main函数的最后,gl_position都是vertex shader 的输出,不管我们怎么设置的。因为我们的输入是三维的向量所以转化成四维。
当前的shader只是一个特别简单的,没有做任何处理,也没有做到NDC的坐标变化-通常来讲这一步是必须的。

编译shader

1create a shader object:
unsigned int vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER);
2attach the shader source code to the shader object and compile
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);

片元shader

这一步计算最终像素的颜色,这里只输出特定的颜色。
计算机系统中颜色是四维的向量,rgba,每一个范围在0.0和1.0之间。

1
2
3
4
#version 330 core
out vec4 FragColor;
void main()
{ FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);}

fragment仅仅需要一个输出参数,是一个定义最终颜色的四维的向量,我们需要自己构建。利用关键字out,我们定义了FragColor。编译类似:

1
2
3
4
unsigned int fragmentShader;
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);

最后一步,将shader对象链接进shader program绿色用来渲染。

sahder program

将编译好的shaderlink到shder program对象,然后在渲染前激活这个shader。

  1. 创建shaderprogram 对象by glcreateprogram:
    unsigned int shaderProgram;
    shaderProgram = glCreateProgram();
  2. 用gllinkprogram连接:
    glAttachShader(shaderProgram, vertexShader);
    glAttachShader(shaderProgram, fragmentShader);
    glLinkProgram(shaderProgram);
  3. 连接最终得到的是一个program对象,使用:
    glUseProgram(shaderProgram);
  4. 删除shader 对象,连接后shader program就不用了。
    glDeleteShader(vertexShader);
    glDeleteShader(fragmentShader);
    综上,我们将输入数据给了GPU,也通过两个shader告诉GPU应该如何处理这些数据。但是opengl依然不知道如何如何解释顶点中的数据、如何链接顶点数据和vertexshader中的属性。我们告诉他。

    连接顶点属性

    vertex容许我们以vertex attributes的形式规定任何的输入,比较灵活,也需要更多的人工处理。我们需要在渲染前告诉opengl如何解释这些数据。
    我们的vertex buffer data 结构现在这样:

  5. The position data is stored as 32-bit (4 byte) floating point values.

  6. Each position is composed of 3 of those values.
  7. There is no space (or other values) between each set of 3 values. The values are tightly packed in the array.
  8. The first value in the data is at the beginning of the buffer.
    图003
    Markdown
    所以我们用以下函数告诉OpenGL该怎么理解:
    1
    2
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);

1 定义了我们匹配哪个vertex 属性,跟location = 0对应
2 规定了vertex 属性的长度,我们现在是vec3,所以是3
3 数据类型
4 规定了我们是不是需要将数据齐次化
5 下一个数据出现的距离间隔,默认0的话就是连续的数据空间(stride)
6 where psition data begins in the buffer(offset)

总结来说就是以下两点:
1 每一个vertex 属性从VBO管理的显存中拿取它的数据;
2 从那个VBO中拿数据呢(可能有很多个VBO),取决于当前绑定给GL_ARRAY_BUFFER的VBO,这个过程发生在调用glVertexAttribPointer的时候。

然后,通过glEnableVertexAttribArray,我们使能vertex attributes,将vertex attributes location(也就是0)当作它的参数。因为默认vertex attributes是关闭的。接下来整个绘制流程就可以这样展开:

1
2
3
4
5
6
7
8
9
10
// 0. copy our vertices array in a buffer for OpenGL to use
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 1. then set the vertex attributes pointers
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// 2. use our shader program when we want to render an object
glUseProgram(shaderProgram);
// 3. now draw the object
someOpenGLFunctionThatDrawsOurTriangle();

最后考虑这个问题,如果有5个vertex attributes,100多个不同的物体,绑定buffer object和为每个物体配置verex attributes将很繁杂。

Vertex Array Object

wiki:https://www.khronos.org/opengl/wiki/Vertex_Specification#Vertex_Array_Object
主要目的是解决在不同的vertex data和attribute config切换时,只需要切换绑定的VAO即可。根据wiki的解释这个VAO里存放了提供顶点数据时所需要的状态数据。
准确来说,VAO对象存储了以下的信息:
1 glEnableVertexAttribArray或者glDisableVertexAttribArray的调用
2 通过 glVertexAttribPointer的vertex attributes 配置
3 通过glVertexAttribPointer的调用,找到与vertex attributes关联的VBO
图004
Markdown
创建和绑定VAO:
unsigned int VAO;
glGenVertexArrays(1, &VAO);
要使用VAO你必须使用glBindVertexArray绑定VAO。从这点上来讲,我们应该绑定或者配置相应的VBOS和属性pointers ,然后再将VAO解绑以便后续需要。一旦我们需要画一个图形,我们只需要将绑定VAOwith首选的设置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ..:: Initialization code (done once (unless your object frequently changes)) :: ..
// 1. bind Vertex Array Object
glBindVertexArray(VAO);
// 2. copy our vertices array in a buffer for OpenGL to use
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 3. then set our vertex attributes pointers
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// ..:: Drawing code (in render loop) :: ..
// 4. draw the object
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
someOpenGLFunctionThatDrawsOurTriangle();

以上就是所有的流程。一个VAO保存我们vertex atttributes配置数据和用哪个VBO。通常你需要绘制不同的对象时,首先生成/配置所有的VAOs(thus VBO和attributes pointers)并且保存以便后面需要。我们需要绘制我们的对象时,我们拿取相应的VAO,绑定它,然后再解绑。

绘制三角形

调用glDrawArrays绘制,使用:当前激活的shader,之前定义的vertex attributes 配置以及VBO的顶点数据(通过VAO间接绑定的)。

1
2
3
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);

完整代码

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <iostream>

void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void processInput(GLFWwindow *window);
int main()
{
// glfw window creation
// --------------------
GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
glfwMakeContextCurrent(window);
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
// glad: load all OpenGL function pointers
// ---------------------------------------
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
// build and compile our shader program
// ------------------------------------
// vertex shader
int vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);
// fragment shader
int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);
// link shaders
int shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
// set up vertex data (and buffer(s)) and configure vertex attributes
// ------------------------------------------------------------------
float vertices[] = {
-0.5f, -0.5f, 0.0f, // left
0.5f, -0.5f, 0.0f, // right
0.0f, 0.5f, 0.0f // top
};
unsigned int VBO, VAO;
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
// bind the Vertex Array Object first, then bind and set vertex buffer(s), and then configure vertex attributes(s).
glBindVertexArray(VAO);

glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);

// note that this is allowed, the call to glVertexAttribPointer registered VBO as the vertex attribute's bound vertex buffer object so afterwards we can safely unbind
glBindBuffer(GL_ARRAY_BUFFER, 0);

// You can unbind the VAO afterwards so other VAO calls won't accidentally modify this VAO, but this rarely happens. Modifying other
// VAOs requires a call to glBindVertexArray anyways so we generally don't unbind VAOs (nor VBOs) when it's not directly necessary.
glBindVertexArray(0);

// uncomment this call to draw in wireframe polygons.
//glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);

// render loop
// -----------
while (!glfwWindowShouldClose(window))
{
// input
// -----
processInput(window);
// render
// ------
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// draw our first triangle
glUseProgram(shaderProgram);
glBindVertexArray(VAO); // seeing as we only have a single VAO there's no need to bind it every time, but we'll do so to keep things a bit more organized
glDrawArrays(GL_TRIANGLES, 0, 3);
// glBindVertexArray(0); // no need to unbind it every time

// glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.)
// -------------------------------------------------------------------------------
glfwSwapBuffers(window);
glfwPollEvents();
}
// optional: de-allocate all resources once they've outlived their purpose:
// ------------------------------------------------------------------------
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
// glfw: terminate, clearing all previously allocated GLFW resources.
// ------------------------------------------------------------------
glfwTerminate();
return 0;
}

EBO

比如我们要画一个矩形,需要两个三角形,定一堆点,却发现两个点重复的。我们用EBO来处理画点的顺序(这里定四个点的顺序就好0123)。EBO也是个buffer,只不过多存了索引信息。这就是indexed drawing
图005
Markdown
代码流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ..:: Initialization code :: ..
// 1. bind Vertex Array Object
glBindVertexArray(VAO);
// 2. copy our vertices array in a vertex buffer for OpenGL to use
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 3. copy our index array in a element buffer for OpenGL to use
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
// 4. then set the vertex attributes pointers
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// ..:: Drawing code (in render loop) :: ..
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0)
glBindVertexArray(0);

注意:
当目标是GL_ELEMENT_ARRAY_BUFFER的时候,VAO存储glBindBuffer调用。
这也意味着,在解绑你的VAO之前,不要解绑EBO对象。
全部代码如下:

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <iostream>
void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void processInput(GLFWwindow *window);
// settings
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;
const char *vertexShaderSource = "#version 330 core\n"
"layout (location = 0) in vec3 aPos;\n"
"void main()\n"
"{\n"
" gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n"
"}\0";
const char *fragmentShaderSource = "#version 330 core\n"
"out vec4 FragColor;\n"
"void main()\n"
"{\n"
" FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n"
"}\n\0";

int main()
{
// glfw: initialize and configure
// ------------------------------
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

#ifdef __APPLE__
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // uncomment this statement to fix compilation on OS X
#endif
// glfw window creation
// --------------------
GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
if (window == NULL)
{
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
// glad: load all OpenGL function pointers
// ---------------------------------------
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
// build and compile our shader program
// ------------------------------------
// vertex shader
int vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);
// check for shader compile errors
int success;
char infoLog[512];
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
if (!success)
{
glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
}
// fragment shader
int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);
// check for shader compile errors
glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
if (!success)
{
glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl;
}
// link shaders
int shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
// check for linking errors
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
if (!success) {
glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
}
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
// set up vertex data (and buffer(s)) and configure vertex attributes
// ------------------------------------------------------------------
float vertices[] = {
0.5f, 0.5f, 0.0f, // top right
0.5f, -0.5f, 0.0f, // bottom right
-0.5f, -0.5f, 0.0f, // bottom left
-0.5f, 0.5f, 0.0f // top left
};
unsigned int indices[] = { // note that we start from 0!
0, 1, 3, // first Triangle
1, 2, 3 // second Triangle
};
unsigned int VBO, VAO, EBO;
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glGenBuffers(1, &EBO);
// bind the Vertex Array Object first, then bind and set vertex buffer(s), and then configure vertex attributes(s).
glBindVertexArray(VAO);

glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);

// note that this is allowed, the call to glVertexAttribPointer registered VBO as the vertex attribute's bound vertex buffer object so afterwards we can safely unbind
glBindBuffer(GL_ARRAY_BUFFER, 0);
// remember: do NOT unbind the EBO while a VAO is active as the bound element buffer object IS stored in the VAO; keep the EBO bound.
//glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);

// You can unbind the VAO afterwards so other VAO calls won't accidentally modify this VAO, but this rarely happens. Modifying other
// VAOs requires a call to glBindVertexArray anyways so we generally don't unbind VAOs (nor VBOs) when it's not directly necessary.
glBindVertexArray(0);
// uncomment this call to draw in wireframe polygons.
//glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);

// render loop
// -----------
while (!glfwWindowShouldClose(window))
{
// input
// -----
processInput(window);
// render
// ------
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// draw our first triangle
glUseProgram(shaderProgram);
glBindVertexArray(VAO); // seeing as we only have a single VAO there's no need to bind it every time, but we'll do so to keep things a bit more organized
//glDrawArrays(GL_TRIANGLES, 0, 6);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
// glBindVertexArray(0); // no need to unbind it every time

// glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.)
// -------------------------------------------------------------------------------
glfwSwapBuffers(window);
glfwPollEvents();
}
// optional: de-allocate all resources once they've outlived their purpose:
// ------------------------------------------------------------------------
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
glDeleteBuffers(1, &EBO);
// glfw: terminate, clearing all previously allocated GLFW resources.
// ------------------------------------------------------------------
glfwTerminate();
return 0;
}

// process all input: query GLFW whether relevant keys are pressed/released this frame and react accordingly
// ---------------------------------------------------------------------------------------------------------
void processInput(GLFWwindow *window)
{
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true);
}

// glfw: whenever the window size changed (by OS or user resize) this callback function executes
// ---------------------------------------------------------------------------------------------
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
// make sure the viewport matches the new window dimensions; note that width and
// height will be significantly larger than specified on retina displays.
glViewport(0, 0, width, height);
}

结果见图006
Markdown

总结

我这里根据教程将整个过程用自己的语言过额一遍,英语好直接去原post阅读,不好的话也有系列的翻译文章,但是翻译的实在是。。。个人体会,所以自己将整个流程记录了下来。关于shader更详细的文章可以自己再去阅读,比如现在shader都是写在源码里,你可以i定义自己的类去读取shader文本,灵活使用,现在这样硬怼实在是太麻烦了。
还有,看来矩阵变换和坐标系要单独写一篇文章了。
其实这里还有另外一个想法,写作和学习过程中,一帮国内的博客实在是没眼看,说来说去不是架子太空,专业字眼太多,就是西拼东凑,各有各的逻辑和理解。说实在的,官网的资料一堆既清晰又明白,以后这种学习性质的博客就不会再出了,可能还是更多地分享一些成形的想法、代码和项目。
话又说回来,公司的能在这写么。。。