LearnOpenGL学习笔记—入门03:Hello Triangle

  • 0 前言
  • 1 图形渲染管线
  • 2 顶点输入
  • 3 VAO,VBO
    • 3.1 VAO建立
    • 3.2 VBO建立
  • 4 shader
  • 5 绘制三角形
  • 6 绘制四边形(数组绘制/背面剔除)
  • 7 用EBO绘制四边形/OpenGL的状态机概念

0 前言

本节笔记对应的内容 你好,三角形
在入门01中我们配置好了环境
在入门02中我们可以检测输入并出现一个有颜色的窗口
本节我们将可以实现在窗口中画出一个三角形

1 图形渲染管线

在OpenGL中,事物都是处在3D空间中的,但是我们的屏幕和窗口却是2D像素数组(XY坐标),这导致OpenGL的大部分工作都是关于把3D坐标转变为适应我们屏幕的2D像素。
3D坐标转为2D坐标的处理过程是由OpenGL的图形渲染管线管理的(Graphics Pipeline,译为管线,实际上指的是一堆原始图形数据途经一个输送管道,期间经过各种变化处理最终出现在屏幕的过程)。
图形渲染管线可以被划分为两个主要部分:

  • 第一部分把3D坐标转换为2D坐标
  • 第二部分是把2D坐标转变为实际的有颜色的像素。

简单来说,图形渲染管线接受一组3D坐标,然后把它们转变为屏幕上的有色2D像素输出。

  • 注:2D坐标和像素也是不同的,2D坐标精确表示一个点在2D空间中的位置,而2D像素是这个点的近似值,2D像素受到屏幕/窗口分辨率的限制。

图形渲染管线可以被划分为几个阶段,每个阶段将会把前一个阶段的输出作为输入。
所有这些阶段都是高度专门化的(它们都有一个特定的函数),并且这些函数很容易并行执行。由于它们具有并行执行的特性,所以当今大多数显卡都有成千上万的小处理核心,它们在GPU上为每一个(渲染管线)阶段运行各自的小程序,从而在图形渲染管线中快速处理数据。
这些小程序叫做着色器(Shader)。
OpenGL着色器是用OpenGL着色器语言(OpenGL Shading Language, GLSL)写成的,在下一节中会进行具体展开。

下面是一个图形渲染管线的每个阶段的抽象展示。
蓝色部分代表的是我们可以注入自定义的着色器的部分。

  • 顶点数据(以三角形为例)
    首先,我们以数组的形式传递3个3D坐标作为图形渲染管线的输入,用来表示一个三角形,这个数组叫做顶点数据(Vertex Data)。顶点数据是一系列顶点的集合。
    一个顶点(Vertex)是一个3D坐标的数据的集合。
    而顶点数据是用顶点属性(Vertex Attribute)表示的,它可以包含任何我们想用的数据。
    简单起见,我们假定每个顶点只由一个3D位置和一些颜色值组成的。
  • 图形渲染管线的第一个部分是顶点着色器(Vertex Shader),它把一个单独的顶点作为输入。
    顶点着色器主要的目的是把3D坐标转为另一种3D坐标(比如坐标系统变换等等),同时在顶点着色器中,我们可以对顶点属性进行一些基本处理。
  • 图元装配(Primitive Assembly)阶段将顶点着色器输出的所有顶点作为输入(如果是GL_TRIANGLES,那么就是一个三角形),OpenGL把所有的点装配成指定图元的形状,本节例子中是一个三角形。
    注:为了让OpenGL知道我们的坐标和颜色值构成的到底是什么,我们需要去指定这些数据所表示的渲染类型。
    因此我们会做出提示,告诉它我们是希望把这些数据渲染成一系列的点/一系列的三角形/一个长长的线……做出的这些提示叫做图元(Primitive),任何一个绘制指令的调用都将把图元传递给OpenGL。
    这是其中的几个图元:GL_POINTS、GL_TRIANGLES、GL_LINE_STRIP
  • 图元装配阶段的输出会传递给几何着色器(Geometry Shader)。几何着色器把图元形式的一系列顶点的集合作为输入。
    它可以通过产生新顶点构造出新的(或是其它的)图元来生成其他形状。例子中,它生成了另一个三角形。
  • 几何着色器的输出会被传入光栅化阶段(Rasterization Stage),这里它会把图元映射为最终屏幕上相应的像素,生成供片段着色器(Fragment Shader)使用的片段(Fragment)——OpenGL中的一个片段是指OpenGL渲染一个像素所需的所有数据。
    在片段着色器运行之前会执行裁切(Clipping)。裁切会丢弃超出你的视图以外的所有像素,用来提升执行效率。
  • 片段着色器主要是计算一个像素的最终颜色,这也是所有OpenGL高级效果产生的地方。
    通常,片段着色器包含3D场景的数据(比如光照、阴影、光的颜色等等),这些数据可以被用来计算最终像素的颜色。
  • 最后一个阶段,我们叫做Alpha测试和混合(Blending)阶段。
    这个阶段检测片段的对应的深度(和模板(Stencil))值,用来判断这个像素是其它物体的前面还是后面,决定是否应该丢弃。
    这个阶段也会检查alpha值(alpha值定义了一个物体的透明度)并对物体进行混合(Blend)。

可以看到,图形渲染管线非常复杂,它包含很多可配置的部分。然而,对于大多数场合,我们只需要配置顶点和片段着色器就行了。几何着色器是可选的,通常使用它默认的着色器就行了。
在现代OpenGL中,我们必须定义至少一个顶点着色器和一个片段着色器(因为GPU中没有默认的顶点/片段着色器)。
出于这个原因,刚开始学习现代OpenGL的时候可能会有些困难,因为在我们能够渲染自己的第一个三角形之前已经需要了解一大堆知识了。

2 顶点输入

开始绘制图形之前,我们要给OpenGL输入一些顶点数据。
OpenGL是一个3D图形库,所以我们在OpenGL中指定的所有坐标都是3D坐标(x、y和z)。
OpenGL不是简单地把所有的3D坐标变换为屏幕上的2D像素;OpenGL仅当3D坐标在3个轴(x、y和z)上都为-1.0到1.0的范围内时才处理它。
只有在所谓的标准化设备坐标(Normalized Device Coordinates)范围内的坐标才会最终呈现在屏幕上(在这个范围以外的坐标都不会显示)。

  • 标准化设备坐标是一个x、y和z值在-1.0到1.0的一小段空间。
    任何落在范围外的坐标都会被丢弃/裁剪,不会显示在屏幕上。
    下面是一个定义的在标准化设备坐标中的三角形(忽略z轴):

    关于详细的坐标相关内容,会在之后章节讲解坐标系统时进行描述。

由于是要渲染一个三角形,所以一共要指定三个顶点,每个顶点都有一个3D位置。
我们将它们以标准化设备坐标的形式定义为一个float数组。
由于OpenGL是在3D空间中工作的,而我们渲染的像是一个2D三角形,我们将它顶点的z坐标设置为0.0。这样子的话三角形每一点的深度都是一样的,从而使它看上去像是2D的。

float vertices[] = {-0.5f, -0.5f, 0.0f,0.5f, -0.5f, 0.0f,0.0f,  0.5f, 0.0f
};

3 VAO,VBO

下面我们来讲述一下顶点缓冲对象(Vertex Buffer Objects, VBO)以及顶点数组对象(Vertex Array Object,VAO)这两个概念。
根据图形渲染管线的步骤,我们一开始会拥有一些顶点数组,其实这个说法有些含糊,因为没有建模软件会直接生成这种数组,它们传输出的文件往往是其他类型的文件,比如一个obj档案。
如下图这个折叠的三角形以及它的输出档案。


这些数据包括顶点,法向量,材质,面的定义等等等,没法直接是一个数组,所以为了让它变成一个数组,我们可能会需要做蛮多事情。
所以接下来我们需要对图形渲染管线再做一个说明:

  • obj档案在最左边,经过序列化的处理,转变成一堆vertex的数组

  • 数据跨越CPU和GPU的接口进到GPU,这时候我们要把它存起来,用来存储这些数组的东西就是顶点缓冲对象(Vertex Buffer Objects, VBO)

  • 为了在VBO中认出这堆数据,各自分别是什么,所以我们就需要到类似索引用途的东西,便是顶点数组对象(Vertex Array Object,VAO)。
    在VAO的0号,1号,2号……槽位分别指到VBO里的数据。一般一个模型就会用到一个VAO缓存它。
    如果还有其他模型,比如右上的“哆啦A梦”,“蛋蛋老师”都会各自有一个VAO。

  • 比如一开始有个“兔子”数据(图左),VAO通过认出VBO中哪几个数值是顶点,法向,uv等等,重现出了一个“兔子”(图VAO上方虚线那个)。
    有了VAO之后,我们就可以单独的把vertex的数据放进vertex data[]进入到渲染管线。

  • 接下来会先学习怎么用OpenGL函数新增一个VAO,然后绑到目前所用的位置上面,接下来造一个VBO绑上这个VAO。
    VAO其实还可以绑EBO(索引缓冲对象Element Buffer Object)(下面用到会再进行描述)
    VAO和VBO之间的通道是ARRAY_BUFFER,VAO和EBO之间的通道是ELEMENT_ARRAY_BUFFER。

3.1 VAO建立

之前我们已经有了坐标的数组,将其放进程序,在入门02中写过的glViewport(0, 0, 800, 600);这行代码后面,我们进行VAO的建立

 unsigned int VAO;glGenVertexArrays(1, &VAO);

注意到Array后面有个s,其实这条生成的方法可以产生多个VAO,而这次我们只用1个所以这样写。未来在呼叫完毕后,这个方法就会返还一个VAO的ID,填充到这个int里面。如果一次产生很多个VAO可能会这样写:

unsigned int VAO[10];
glGenVertexArrays(10, VAO);

这两行之后我们就有一个VAO,可以它并没有塞进目前渲染管线需要的数据位置上,所以接下来

glBindVertexArray(VAO);

3.2 VBO建立

然后我们要造一个VBO,绑在ARRAY_BUFFER的位置上

unsigned int VBO;
glGenBuffers(1, &VBO);
glBindBuffer(GL_ARRAY_BUFFER,VBO);

到目前我们就把VBO从图中的红线绑上了VAO,下方的EBO的用法会在之后再描述。

下面我们要把这个三角形的顶点放进VBO里面,即让它进到GPU的buffer里面去,我们会需要用到这个:

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

glBufferData是一个专门用来把用户定义的数据复制到当前绑定缓冲的函数。

  • 它的第一个参数是目标缓冲的类型:顶点缓冲对象的话是当前绑定到GL_ARRAY_BUFFER的目标上。
  • 第二个参数指定传输数据的大小(以字节为单位);用一个简单的sizeof计算出顶点数据大小就行。
  • 第三个参数是我们希望发送的实际数据。
  • 第四个参数指定了我们希望显卡如何管理给定的数据。它有三种形式:
    GL_STATIC_DRAW :数据不会或几乎不会改变。
    GL_DYNAMIC_DRAW:数据会被改变很多。
    GL_STREAM_DRAW :数据每次绘制时都会改变。

4 shader

这一节的学习不用太深入学习shader,其原理和相关概念会在之后的章节进行具体学习,所以我们就直接套用前言中提到的这节教材的两组shader内容复制进代码。

const char* vertexShaderSource =
"#version 330 core                                    \n"
"layout(location = 0) in vec3 aPos;                      \n"
"void main(){                                         \n"
"  gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);}  \n";const char* fragmentShaderSource =
"#version 330 core                                    \n"
"out vec4 FragColor;                                  \n"
"void main(){                                         \n"
"  FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);}        \n";

我们已经写了一个shader源码(储存在字符串中),但是为了能够让OpenGL使用它,我们必须在运行时动态编译它的源码。
于是在建立VBO的代码后面我们加入以下代码:

unsigned int vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);unsigned int fragmentShader;
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);
  • 我们把需要创建的着色器类型以参数形式提供给glCreateShader。如GL_VERTEX_SHADER,GL_FRAGMENT_SHADER。
  • 下一步我们用glShaderSource这个函数把着色器源码附加到着色器对象上,然后用glCompileShader编译它。
  • glShaderSource:
    第一个参数是要编译的着色器对象;
    第二参数指定了传递的源码字符串数量,这里我们每一项只有一个字符串;
    第三个参数是顶点着色器真正的源码;
    第四个参数我们先设置为NULL。

接下来要把这两个shader组装成一个program才能拿出来用

unsigned int shaderProgram;
shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
  • 着色器程序对象(Shader Program Object)是多个着色器合并之后,最终链接完成的版本。
    如果要使用刚才编译的着色器,我们必须把它们链接(Link)为一个着色器程序对象,然后在渲染对象的时候激活这个着色器程序。
    已激活着色器程序的着色器,将在我们发送渲染调用的时候被使用。
  • glCreateProgram函数创建一个程序,并返回新创建程序对象的ID引用。
  • glAttachShader可以把之前编译的着色器附加到程序对象上
  • glLinkProgram来链接它们

5 绘制三角形

接下来我们可以画这个三角形了,根据教材上典型的使用方式,每尝试画一个VAO我们就要bind到目前的槽位上去,然后呼叫目前要用的program。

我们先链接顶点属性

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

关于这两行内容,实际上就是描述示意图中如何把VBO的数据放入VAO上的0,1,2……槽位上。
这些槽位,叫做顶点属性(vertex attribute)。

  • 顶点着色器允许我们指定任何以顶点属性为形式的输入。
  • 这使其具有很强的灵活性的同时,它还的确意味着我们必须手动指定输入数据的哪一个部分对应顶点着色器的哪一个顶点属性。
  • 所以,我们必须在渲染前指定OpenGL该如何解释顶点数据(不然它也不认识这么一大串数组谁是谁)。

比如我们的顶点缓冲数据会被解析为下面这样子:

  • 位置数据被储存为32位(4字节)浮点值。
  • 每个顶点包含3个这样的值。
  • 在这3个值之间没有空隙(或其他值)。这几个值在数组中紧密排列(Tightly Packed)。(未来会在里面塞uv什么的资料进去)
  • 数据中第一个值在缓冲开始的位置。

glVertexAttribPointer函数的参数非常多。

  • 第一个参数指定我们要配置的顶点属性位置在哪里。
    我们在顶点着色器中使用layout(location = 0)定义了position顶点属性的位置值(Location)是0。
    也就是说shader会从0号栏位拿position的信息,因此我们这里指定了0号的位置(也就是说这两个要对到啦!)。
    ( 这一部分内容在之后shader章节的笔记会进行具体说明 )

  • 第二个参数指定顶点属性的大小。顶点属性是一个vec3,它由3个值组成,所以大小是3。

  • 第三个参数指定数据的类型,这里是GL_FLOAT(GLSL中vec*都是由浮点数值组成的)。

  • 第四个参数定义我们是否希望数据被标准化(Normalize)。
    如果我们设置为GL_TRUE,所有数据都会被映射到0(对于有符号型signed数据是-1)到1之间。我们不需要就把它设置为GL_FALSE。

  • 第五个参数叫做步长(Stride),它告诉我们在连续的顶点属性组之间的间隔。
    由于下个组位置数据在3个float之后,我们把步长设置为3*sizeof(float)。
    要注意的是由于我们知道这个数组是紧密排列的(在两个顶点属性之间没有空隙)我们也可以设置为0来让OpenGL决定具体步长是多少(只有当数值是紧密排列时才可用)。
    一旦我们有更多的顶点属性(比如uv值啊什么的),我们就必须更小心地定义每个顶点属性之间的间隔,我们在后面的学习中会看到更多的例子

  • 最后一个参数的类型是void*,所以需要我们进行这个奇怪的强制类型转换。
    它表示位置数据在缓冲中起始位置的偏移量(Offset)。由于位置position数据在数组的开头,所以这里是0。
    如果不只是position的信息,还有其他信息在的话,可能会需要偏移量。(之后的学习会用到)

简而言之,顶点属性的这段代码(再放送)

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

它告诉OpenGL

  • 程序需要选中0号栏位开始放入,把我们送进去的这些数值,每三个当成一份资料,它们都是float,不需要正规化,每隔3*float个长度去挖下一个,不需要偏移量,然后再激活对外开放0号栏位。

最后一步我们在渲染循环中放入

glBindVertexArray(VAO);
glUseProgram(shaderProgram);
glDrawArrays(GL_TRIANGLES, 0, 3);
  • glDrawArrays函数第一个参数是我们打算绘制的OpenGL图元的类型,是一个三角形,这里传递GL_TRIANGLES给它。
  • 第二个参数指定了顶点数组的起始索引,我们这里填0。最后一个参数指定我们打算绘制多少个顶点,这里是3(我们只从我们的数据中渲染一个三角形,它只有3个顶点长)。

终于 费了很大功夫我们画完了,点选运行得到了结果

此时我们的完整代码会是这样:

#include <iostream>#define GLEW_STATIC
#include <GL/glew.h>
#include <GLFW/glfw3.h>
float vertices[] = {-0.5f, -0.5f, 0.0f,0.5f, -0.5f, 0.0f,0.0f,  0.5f, 0.0f
};
const char* vertexShaderSource =
"#version 330 core                                    \n"
"layout(location = 0) in vec3 aPos;                      \n"
"void main(){                                         \n"
"  gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);}  \n";const char* fragmentShaderSource =
"#version 330 core                                    \n"
"out vec4 FragColor;                                  \n"
"void main(){                                         \n"
"  FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);}        \n";void processInput(GLFWwindow* window){if (glfwGetKey(window, GLFW_KEY_ESCAPE )== GLFW_PRESS){glfwSetWindowShouldClose(window, true);}
}int main() {glfwInit();glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR,3);glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);//Open GLFW WindowGLFWwindow* window = glfwCreateWindow(800,600,"My OpenGL Game",NULL,NULL);if(window == NULL){printf("Open window failed.");glfwTerminate();return - 1;}glfwMakeContextCurrent(window);//Init GLEWglewExperimental = true;if (glewInit() != GLEW_OK) {printf("Init GLEW failed.");glfwTerminate();return -1;}glViewport(0, 0, 800, 600);unsigned int VAO;glGenVertexArrays(1, &VAO);glBindVertexArray(VAO);unsigned int VBO;glGenBuffers(1, &VBO);glBindBuffer(GL_ARRAY_BUFFER,VBO);glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);unsigned int vertexShader;vertexShader = glCreateShader(GL_VERTEX_SHADER);glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);glCompileShader(vertexShader);unsigned int fragmentShader;fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);glCompileShader(fragmentShader);unsigned int shaderProgram;shaderProgram = glCreateProgram();glAttachShader(shaderProgram, vertexShader);glAttachShader(shaderProgram, fragmentShader);glLinkProgram(shaderProgram);glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);glEnableVertexAttribArray(0);while (!glfwWindowShouldClose(window)) {processInput(window);glClearColor(0.2f, 0.3f, 0.3f, 1.0f);glClear(GL_COLOR_BUFFER_BIT);glUseProgram(shaderProgram);glBindVertexArray(VAO);glDrawArrays(GL_TRIANGLES, 0, 3);glfwSwapBuffers(window);glfwPollEvents();}glfwTerminate();return 0;}

6 绘制四边形(数组绘制/背面剔除)

我们尝试绘制四边形,所以把顶点数组稍加改动

float vertices[] = {-0.5f, -0.5f, 0.0f,0.5f, -0.5f, 0.0f,0.0f,  0.5f, 0.0f,0.5f, -0.5f, 0.0f,0.0f,  0.5f, 0.0f,0.8f,  0.8f, 0.0f
};

同时改一下DrawArrays

glDrawArrays(GL_TRIANGLES, 0, 6);

得到如下的四边形

这时候会有疑惑,为什么需要6个点来画四边形呢?
因为OpenGL画的是三角面,看起来是四边形,其实是画了两次,一次要三点,所以会重复画两个顶点。
这样很没有效率,所以就出现了EBO的绘制方法。(铺垫下面的第七节)

另外的,OpenGL的三角形绘制是逆时针顺序,在上面的数组中,我们第一组是逆时针,第二组却变成了顺时针(虽然是输入手滑了但是又可以增加记录一个知识点)。
看着绘制出来的是一个四边形,但其实这个四边形是第一个三角形的正面,与第二个三角形的反面的组合产物。
OpenGL会好心的把正反面都画出来,所以我们看不出差异,我们可以试一试开启背面剔除。
glViewport(0, 0, 800, 600);这一行下面,我们输入这些代码

glEnable(GL_CULL_FACE);
glCullFace(GL_BACK);

这样的话背面就被剔除掉了

如果是剔除正面的话,改成glCullFace(GL_FRONT);,结果如下

所以可见数组里顺序出错了。
因为之后用不到,所以就把面剔除的这两行注释了吧。

7 用EBO绘制四边形/OpenGL的状态机概念

如果给坐标标上序号

float vertices[] = {-0.5f, -0.5f, 0.0f,  //00.5f, -0.5f, 0.0f,  //10.0f,  0.5f, 0.0f,    //20.5f, -0.5f, 0.0f,0.0f,  0.5f, 0.0f,0.8f,  0.8f, 0.0f   //3
};

对于一个矩形,怎么看我们都只是需要四点而已,但是多出了两点的空间,所以未来我们画的时候,我们想第一个三角形就只是用0,1,2的顺序,第二个三角形就只是用2,1,3的顺序,用索引值来选点绘制。
这个方法就是所谓的索引绘制(Indexed Drawing),我们只需要存储它的索引值来绘图。而存储索引,就是所谓的索引缓冲对象(Element Buffer Object,EBO,也叫Index Buffer Object,IBO)。
所以接下来我们把前面的位置数据改成

float vertices[] = {-0.5f, -0.5f, 0.0f,  //00.5f, -0.5f, 0.0f,  //10.0f,  0.5f, 0.0f,    //2//0.5f, -0.5f, 0.0f,//0.0f,  0.5f, 0.0f,0.8f,  0.8f, 0.0f   //3
};
//0 1 2   2 1 3
unsigned int indices[] = {0,1,2,2,1,3
};

改动到这里我们停一下,回来讲讲OpenGL来便于理解。

  • OpenGL是一个状态机,任何一个时间只会有一个状态位于运行中,即图中的虚线框,运行中的这个状态叫做Context
  • 这个Context在运行的时候,只会认识当下操作的那个VAO(图中的“兔子”),所以在某个时刻运行时,可能会有很多模型在外面等(右上角的“哆啦A梦”和“蛋蛋老师”)。
  • 当下的VAO如何去存取外界的VBO呢?尽管会送进来很多VBO,但是OpenGL这个状态机同时间只能操作一个VBO,操作的地方就是ARRAY_BUFFER。
  • 于是我们就可以理解bind这个操作了,首先选一个VAO进来当前状态,选的这个动作,就叫做glBindVertexArray(VAO);
  • VBO也到bind进来,并且需要通过ARRAY_BUFFER这个槽位进行操作,所以就是glBindBuffer(GL_ARRAY_BUFFER,VBO);
  • 没有被bind到的东西是不会被Context认到的。
  • 连接一个VBO之后,读取顶点属性到VAO的栏位上,之后可能另外的资料是存在另外的VBO上,那么我们就可以把上一VBO放掉,再bind到下一个读取,读完解除掉,再bind下一个,以此类推。
  • 为了要达到有效地绘制,我们会用到EBO,而EBO操作的槽位,叫做Element Buffer,而且进到Context以后,它会固定到某个栏位上,不属于之前操作VBO读取时用到0-15个栏位。EBO进来,填充它所有的indices。
  • 当然,Context中还有很多功能,比如之前的背面剔除等等,通过glEnable可能打开很多功能。

好,现在回到代码,改完位置和索引之后,我们要创造EBO,在宣告VBO的代码下面,输入以下代码,可以根据上面概念的讲解进行理解:

 unsigned int EBO;glGenBuffers(1, &EBO);glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

在渲染循环bindVAO的下面,输入以下代码

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
  • glDrawElements第一个参数指定了我们绘制的模式,这个和glDrawArrays的一样。
  • 第二个参数是我们打算绘制顶点的个数,这里填6,也就是说我们一共需要绘制6个顶点。
  • 第三个参数是索引的类型,这里是GL_UNSIGNED_INT。
  • 最后一个参数里我们可以指定EBO中的偏移量(或者传递一个索引数组,但是这是当我们不在使用索引缓冲对象的时候),我们会在这里填写0。

关于VAO,VBO,EBO的关系,可以再结合教材上的图片理解

于是就实现啦!

我们也可以用线框模式来看看绘制的效果,可以把线框模式的代码加在背面剔除那块地方。

  • 线框模式(Wireframe Mode)
    要想用线框模式绘制三角形,可以通过glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)函数配置OpenGL如何绘制图元。
  • 第一个参数表示我们打算将其应用到所有的三角形的正面和背面,第二个参数告诉我们用线来绘制。
  • 之后的绘制调用会一直以线框模式绘制三角形,直到我们用glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)将其设置回默认模式。

可以看到线框模式的效果

完整的代码应该是这样(注释掉了线框和背面剔除)

#include <iostream>#define GLEW_STATIC
#include <GL/glew.h>
#include <GLFW/glfw3.h>
float vertices[] = {-0.5f, -0.5f, 0.0f,  //00.5f, -0.5f, 0.0f,  //10.0f,  0.5f, 0.0f,   //2//0.5f, -0.5f, 0.0f,//0.0f,  0.5f, 0.0f,0.8f,  0.8f, 0.0f   //3
};
//0 1 2   2 1 3
unsigned int indices[] = {0,1,2,2,1,3
};const char* vertexShaderSource =
"#version 330 core                                    \n"
"layout(location = 0) in vec3 aPos;                      \n"
"void main(){                                         \n"
"  gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);}  \n";const char* fragmentShaderSource =
"#version 330 core                                    \n"
"out vec4 FragColor;                                  \n"
"void main(){                                         \n"
"  FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);}        \n";void processInput(GLFWwindow* window){if (glfwGetKey(window, GLFW_KEY_ESCAPE )== GLFW_PRESS){glfwSetWindowShouldClose(window, true);}
}int main() {glfwInit();glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR,3);glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);//Open GLFW WindowGLFWwindow* window = glfwCreateWindow(800,600,"My OpenGL Game",NULL,NULL);if(window == NULL){printf("Open window failed.");glfwTerminate();return - 1;}glfwMakeContextCurrent(window);//Init GLEWglewExperimental = true;if (glewInit() != GLEW_OK) {printf("Init GLEW failed.");glfwTerminate();return -1;}glViewport(0, 0, 800, 600);//glEnable(GL_CULL_FACE);//glCullFace(GL_BACK);//glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);unsigned int VAO;glGenVertexArrays(1, &VAO);glBindVertexArray(VAO);unsigned int VBO;glGenBuffers(1, &VBO);glBindBuffer(GL_ARRAY_BUFFER,VBO);glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);unsigned int EBO;glGenBuffers(1, &EBO);glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);unsigned int vertexShader;vertexShader = glCreateShader(GL_VERTEX_SHADER);glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);glCompileShader(vertexShader);unsigned int fragmentShader;fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);glCompileShader(fragmentShader);unsigned int shaderProgram;shaderProgram = glCreateProgram();glAttachShader(shaderProgram, vertexShader);glAttachShader(shaderProgram, fragmentShader);glLinkProgram(shaderProgram);glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);glEnableVertexAttribArray(0);while (!glfwWindowShouldClose(window)) {processInput(window);glClearColor(0.2f, 0.3f, 0.3f, 1.0f);glClear(GL_COLOR_BUFFER_BIT);glUseProgram(shaderProgram);glBindVertexArray(VAO);glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);//glDrawArrays(GL_TRIANGLES, 0, 6);glfwSwapBuffers(window);glfwPollEvents();}glfwTerminate();return 0;}

LearnOpenGL学习笔记—入门03:Hello Triangle相关推荐

  1. C# 学习笔记入门篇(上)

    文章目录 C# 学习笔记入门篇 〇.写在前面 Hello World! 这篇学习笔记适合什么人 这篇学习笔记到底想记什么 附加说明 一.命名空间 "进入"命名空间 嵌套的命名空间. ...

  2. MongoDB学习笔记(入门)

    MongoDB学习笔记(入门) 一.文档的注意事项: 1.  键值对是有序的,如:{ "name" : "stephen", "genda" ...

  3. Vue学习笔记入门篇——数据及DOM

    本文为转载,原文:Vue学习笔记入门篇--数据及DOM 数据 data 类型 Object | Function 详细 Vue 实例的数据对象.Vue 将会递归将 data 的属性转换为 getter ...

  4. LearnOpenGL学习笔记—PBR:IBL

    LearnOpenGL学习笔记-PBR:IBL 0 引入 1 渲染方程的求解 2 hdr文件转成cubemap 3 预计算漫反射积分 4 预计算镜面反射积分 4.1 预滤波HDR环境贴图 4.1.1 ...

  5. LearnOpenGL学习笔记—高级光照 09:SSAO

    LearnOpenGL学习笔记-高级光照 09:SSAO 1 原理引入 2 样本缓冲 3 法向半球 4 随机核心转动 5 SSAO着色器 6 环境遮蔽模糊 7 应用环境遮蔽 8 动手试试 8.0 个人 ...

  6. R语言学习笔记——入门篇:第一章-R语言介绍

    R语言 R语言学习笔记--入门篇:第一章-R语言介绍 文章目录 R语言 一.R语言简介 1.1.R语言的应用方向 1.2.R语言的特点 二.R软件的安装 2.1.Windows/Mac 2.2.Lin ...

  7. R语言学习笔记——入门篇:第三章-图形初阶

    R语言 R语言学习笔记--入门篇:第三章-图形初阶 文章目录 R语言 一.使用图形 1.1.基础绘图函数:plot( ) 1.2.图形控制函数:dev( ) 补充--直方图函数:hist( ) 补充- ...

  8. JS学习笔记——入门基础知识总结

    JS入门基础知识总结1 前言 基础背景知识 一.产生历史: 二.特点: 三.应用方向: 四.Javascript组成: JavaScript书写使用方式 一.行内式(了解即可,项目中不使用,日常练习尽 ...

  9. Verilog学习笔记——入门

    Verilog学习笔记 01 基本逻辑门代码设计与仿真 Veriog基本逻辑门代码结构--以一位反相器为例 ModelSim仿真基本流程 02 组合逻辑代码设计与仿真--多路选择器 二选一逻辑--as ...

最新文章

  1. 《Node.js区块链开发》——1.6 参考
  2. 怎么实现动态设置静态文件存储目录?
  3. 动态内存的分配用法和构造动态一维数组
  4. 进阶之路(基础篇) - 020 放弃Arduino IDE,拥抱Sublime Text 3
  5. JAVA黑马刘意学习笔记
  6. Word文档格式的解码分析
  7. 74ls175四人抢答器电路图_用数字电路实现四人抢答器
  8. Java连接db2数据库(常用数据库连接五)
  9. Modern UI for WPF的使用
  10. 伦敦银短线交易_MOM指标
  11. 最好用的9个php开发工具推荐
  12. 鼠标清除计算机密码,装机大师PE怎么清除修改电脑密码
  13. Android高级界面设计
  14. 头像截图上传两种方式(SWFUpload、一个简单易用的flash插件)
  15. Gavin Wood的故事:神级黄皮书、出走以太坊、乱世成名与三代区块链
  16. 6岁的招聘界“ChatGPT”|企业家俱乐部“创业者下午茶”第八期——AI得贤招聘官创始人方小雷
  17. Android中的su命令使用
  18. python 通过 Snap7 与 PLC 实现数据通信
  19. PID优化系列之给定值斜坡函数(PLC代码+Simulink仿真测试)
  20. RS485设备在智能家居里的应用

热门文章

  1. 安霸Ambarella CV系列芯片
  2. 认识 Arduino 开发板
  3. 微商相册服务器维护,微商相册
  4. 天津大学计算机学院考研复试名单,天津大学计算机学院09考研复试第一批名单...
  5. 一小时学会使用SpringBoot整合阿里云SMS短信服务
  6. 《数学之美》——吴军#读书笔记
  7. Springboot定时任务【多线程处理】
  8. dbus-1 not met问题
  9. jersey tomcat MySQL_基于jersey和Apache Tomcat构建Restful Web服务(一)
  10. 【项目实战-MATLAB】:基于机器学习的虹膜识别系统设计