LearnOpenGL学习笔记(一) - 环境配置与对象绘制

本文最后更新于 2024年7月15日 晚上

为终身之路踏上第一步。

入门

OpenGL可以被看作是一个大的状态机。API中的一些函数会根据当前OpenGL的状态的不同,而产生不同的效果,这些函数被称为状态函数。

OpenGL的工作流:

创建对象->绑定对象到上下文(Bind Gen出来的Object到OpenGL上下文的内置属性)->设置已绑定对象的选项->解绑对象

PS:解绑对象只是让对象和上下文之间断开联系。实际上选项已经改变,与解绑无关。当要获取之前那个对象的信息时,只需要重新绑定那个上下文变量就行了。比如说我们有一些作为3D模型数据(一栋房子或一个人物)的容器对象,在我们想绘制其中任何一个模型的时候,只需绑定一个包含对应模型数据的对象就可以了。拥有数个这样的对象允许我们指定多个模型,在想画其中任何一个的时候,直接将对应的对象绑定上去,便不需要再重复设置选项了。

1
2
3
4
5
6
7
8
9
10
// 创建对象
unsigned int objectId = 0;
glGenObject(1, &objectId);
// 绑定对象至上下文
glBindObject(GL_WINDOW_TARGET, objectId);
// 设置当前绑定到 GL_WINDOW_TARGET 的对象的一些选项
glSetObjectOption(GL_WINDOW_TARGET, GL_OPTION_WINDOW_WIDTH, 800);
glSetObjectOption(GL_WINDOW_TARGET, GL_OPTION_WINDOW_HEIGHT, 600);
// 将上下文对象设回默认
glBindObject(GL_WINDOW_TARGET, 0);

环境配置

GLFW

下载源代码,使用Clion打开源代码根目录作为项目。

点击构建-构建项目,完成后在cmake-build-output/src文件夹下找到glfw3.dll文件。

新建空项目,新建libs和include文件夹,将GLFW源代码中include文件夹的内容拖入新include文件夹,将编译完成的glfw3.dll拖入libs文件夹。同时,也要把glfw3.dll拖入cmake-build-output文件夹下

修改CMakeList.txt内容如下:

1
2
3
4
5
6
7
8
9
10
cmake_minimum_required(VERSION 3.28)
project(LearnOpenGL) # 定义项目名

set(CMAKE_CXX_STANDARD 17)

add_executable(LearnOpenGL main.cpp) # 这步必须放在链接操作之前

INCLUDE_DIRECTORIES(include) # include文件所在路径
link_directories(libs) # libs文件所在路径
target_link_libraries(LearnOpenGL libs/glfw3.dll) # dll文件所在路径

重新加载CMake即可。可新建cpp文件,输入#include <GLFW\glfw3.h>,若未报错则链接成功。

GLAD

打开http://glad.dav1d.de/,按照以下规则配置:

Language:C/C++

Specification:OpenGL

API-gl:3.3 and newer

Opentions - Generate a loader:打勾

点击Generate按钮,下载glad.zip,解压,将include内的文件拖动到工程的include文件里,glad.c则放到工程根目录。使用#include <glad\glad.h>验证是否配置成功。

范例工程

头文件

1
2
#include <glad/glad.h>
#include <GLFW/glfw3.h>

注意:glad头文件必须在GLFW之前include

代码

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
#include <iostream>
#include <glad/glad.h>
#include <GLFW/glfw3.h>
using namespace std;

//处理键盘输入,当用户按下ESC键时,提醒程序要退出
void processInput(GLFWwindow *window)
{
//glfwGetKey函数用于检查一个键是否正在被按下
if(glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true);
}

//回调函数,使视口范围随窗口大小变化。
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
glViewport(0, 0, width, height);
}

int main(){
// -----------glfw初始化与窗口创建-----------
// 初始化glfw组件
glfwInit();
//指定opengl版本为3.3
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
//告诉glfw我们使用的是核心模式
//核心模式意味着我们只能使用OpenGL功能的核心功能子集
glfwWindowHint(GLFW_OPENGL_PROFILE,GLFW_OPENGL_CORE_PROFILE);
//创建glfw窗口
//第一个参数是窗口的宽度,第二个是高度,第三个是窗口的标题,第四个参数指定窗口是否共享资源,第五个参数是共享资源的窗口
//共享资源指的是多个窗口可以共享同一个上下文
GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", NULL, NULL);
//检查窗口是否创建成功
if (window == NULL)
{
cout << "Failed to create GLFW window" << endl;
//销毁glfw
glfwTerminate();
return -1;
}
//通知glfw将我们窗口的上下文设置为当前线程的主上下文
glfwMakeContextCurrent(window);
// -----------GLAD加载所有OpenGL函数指针-----------
if(!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)){
cout << "Failed to initialize GLAD" << endl;
return -1;
}
//先前只是生成了窗口的大小,但窗口大小与实际渲染区域无关。因此,我们需要定义渲染区域的大小,即视口(Viewport)。
//视口定义了窗口中可以渲染的区域,将其设置为窗口的维度
//前两个参数代表渲染区域左下角在GLFW窗口的坐标。第三个和第四个参数是渲染区域的宽度和高度(像素)
//注意,这里并没有传入window,所以glViewport是个状态使用函数。
glViewport(0, 0, 800, 600);
//为了让窗口被改变时,视口也能相应改变,我们需要注册一个窗口大小的回调函数
//glfwSetFramebufferSizeCallback函数接受一个窗口,一个函数指针,当窗口大小改变时调用这个函数
glfwSetFramebufferSizeCallback(window,framebuffer_size_callback);
// -----------渲染循环-----------
//当窗口被要求关闭时,glfwWindowShouldClose函数返回true
while(!glfwWindowShouldClose(window)){
// 清除颜色缓冲
// glClearColor用来设置清空屏幕所用的颜色
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
// 清除颜色缓冲。除此之外还有GL_DEPTH_BUFFER_BIT,GL_STENCIL_BUFFER_BIT,分别用于清除深度缓冲、模板缓冲
// glClearColor是状态设置函数,而glClear是状态使用函数
glClear(GL_COLOR_BUFFER_BIT);
// 调用输入函数,处理键盘输入
processInput(window);
//
// 这里放渲染指令...
//
// 交换缓冲区(双缓冲区的实现, 用于避免图像闪烁)
glfwSwapBuffers(window);
// 处理事件,比如键盘和鼠标事件,随后更新窗口状态并调用回调函数
glfwPollEvents();
}
// -----------清理资源-----------
glfwDestroyWindow(window);
glfwTerminate();
return 0;
}

通常的OpenGL范例程序:

初始化GLFW->设置GLFW信息,如OpenGL版本号->生成窗口->设置线程上下文->使用GLAD获取GL函数->定义渲染视口大小->注册各类回调函数->进入渲染循环->销毁资源

渲染循环内部:

清空缓存->处理输入->进行渲染操作(在后缓冲上绘制)->检查并调用事件,交换缓冲(使后缓冲的内容显示到画面上)

GLFW函数可分为两类:状态设置函数和状态使用函数。前者用于设置状态量,后者用于借助已经设置完毕的状态来改变程序行为。

对象绘制

基础

顶点数组对象:Vertex Array Object,VAO

顶点缓冲对象:Vertex Buffer Object,VBO

元素缓冲对象:Element Buffer Object,EBO 或 索引缓冲对象 Index Buffer Object,IBO

图形渲染管线可以被分为两个部分:

  • 第一部分负责将3D坐标转换为2D坐标
  • 第二部分负责将2D坐标转换为有颜色的,实际屏幕上的像素。

2D坐标与像素不同。像素是2D坐标的近似值,受分辨率影响。

着色器是运行在GPU上的处理程序。每个小核心负责一个着色器的计算。

Vertex Shader、Geometry Shader和Fragment Shader可以由开发者自定义。

img

Vertex Shader把单独的顶点作为输入,将局部坐标系下的顶点坐标转换到标准化设备坐标(NDC),同时对顶点属性(Vertex Attribute)进行基本处理。

经Vertex Shader处理过的坐标必定是NDC,范围为[-1, 1]

Geometry Shader把一个图元(包括点、线、三角形三种类型)的顶点作为输入,根据需要处理这些顶点,也可以生成新的顶点,用于构建新的形状。

这个过程可选。

Shape Assembly将前阶段的所有顶点作为输入,并将其装配为指定图元的形状。

Rasterization把图元映射为屏幕上的像素,生成Fragment, 并剔除View以外的所有像素。

glViewport函数定义了视口信息。视口变换(Viewport Transform)将NDC变换为屏幕空间坐标。

屏幕空间坐标被变换为Fragment,输入到Fragment Shader中。

Fragment Shader用于计算一个像素的最终颜色。该阶段包含3D场景的数据,如光照、阴影等。

Test and Blending阶段,首先检测所有像素的深度值、模板值,用于判断像素是正面还是背面,并据此决定是否丢弃。随后,根据像素的alpha值,进行blend操作。

顶点输入

顶点数据首先被送到顶点着色器。这些数据以顶点缓冲对象(Vertex Buffer Objects, VBO)的状态存储在GPU内存(即显存)中。

使用VBO的优点在于,可以一次发送一大批数据到GPU上,而非一个顶点传送一次。

CPU到GPU的传输速度较慢,所以要尽量减少传输次数,一次发送尽可能多的数据。

通过**glGenBuffers(int count, unsigned int* VBO)**函数生成VBO对象。生成完毕后,变量VBO将存储VBO实例的id。随后,进行绑定操作。

1
2
3
unsigned int VBO;
glGenBuffers(1, &VBO); //生成一个VBO对象,带有缓冲区id
glBindBuffer(GL_ARRAY_BUFFER, VBO); //将该对象绑定到GL上下文的GL_ARRAY_BUFFER目标

任何对象在生成以后都需要与GL上下文中的特定目标进行绑定,才能生效。

每个缓冲区目标都只能同时绑定一个对象。

GL_ARRAY_BUFFER存储的对象通常是:需要在CPU和GPU之间传输的顶点相关数据。

**glBufferData(CONTEXT_TARGET, int data_len, float[] data, DRAW_FORM)**用于向当前绑定的缓冲区存入用户定义数据。

1
2
3
4
5
6
float verticals[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
glBufferData(GL_ARRAY_BUFFER, sizeof(verticals),verticals, GL_STATIC_DRAW);

DRAW_FORM参数用于指定显卡如何管理存入的数据。

  • GL_STATIC_DRAW:存入的数据几乎不会发生变化
  • GL_DYNAMIC_DRAW:存入的数据时常会有发生变化
  • GL_STREAM_DRAW:存入的数据每时每刻都在变化

至此,顶点已完成了输入,此刻的顶点数据以VBO的形式存储在显存中。

顶点着色器

1
2
3
4
5
6
7
8
#version 330 core //定义版本号和PROFILE模式
//定义一个名为aPos的输入变量,其位置索引为0,类型为vec3。
layout (location = 0) in vec3 aPos;
void main()
{
//将输入的顶点位置转换为4维向量(齐次坐标),并赋值给内建变量gl_Position,该变量表示顶点着色器的输出位置。
gl_Position = vec4(aPos, 1.0);
}

location变量用于绑定顶点属性的特定位置索引。一般而言,顶点属性都是用float类型存储的。而存放顶点数据的vertical数组又是一维而非二维的。因此,可能出现前N个数据里,数据[0,N-M]是位置数据,而[N-M+1,N]是颜色数据。通过设置不同的location变量,可以解明顶点数据的具体含义。

gl_Position的值将会成为顶点着色器的输出。

在实际的顶点着色器中,往往还需要经过坐标变换到NDC的过程。

C++源码文件无法直接嵌入GLSL代码。所以需要在运行时动态编译。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//顶点着色器源码
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";
//用于保存着色器对象ID引用的无符号整数
unsigned int vertexShader;
//使用glCreateShader函数创建指定类型的着色器对象
//由于同种类型的着色器只能生成一个,所以可以直接赋值而非传入地址
vertexShader = glCreateShader(GL_VERTEX_SHADER);
//对于glShaderSource,其参数含义分别为:着色器对象、源码字符串数量、源码字符串首地址、包含每个字符串长度的整数数组。该函数是把“源码字符串”绑定到“着色器对象”的操作。
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
//进行编译
glCompileShader(vertexShader);

通过glGetShaderiv函数可以检测编译是否成功。

1
2
3
4
5
6
7
8
9
10
11
12
int  success; //是否成功编译的flag
char infoLog[512]; //错误信息字符数组
//glGetShaderiv用于获取着色器信息
//第二个参数可替换为以下选项:
//GL_SHADER_TYPE:获取着色器类型
//..待补充
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success); //获取编译结果
if(!success)
{ //若未编译成功
glGetShaderInfoLog(vertexShader, 512, NULL, infoLog); //获取编译错误信息,512代表infoLog数组大小
std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl; //打印
}

片元着色器

片元着色器用于计算像素最后的颜色输出。

1
2
3
4
5
6
#version 330 core
out vec4 FragColor; // 片元着色器允许用户自定义输出变量
void main()
{
FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f); //橘黄色
}

随后进行编译。

1
2
3
4
5
6
7
8
9
10
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";
unsigned int fragmentShader;
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);

着色器程序

完成着色器编译后,还需要编写着色器程序。整个渲染管线就像一个链表,着色器是其中的一个个节点,而着色器程序负责把这些节点连接(Link)起来,并负责数据的输入输出。

当链接着色器至一个程序的时候,它会把每个着色器的输出链接到下个着色器的输入。当输出和输入不匹配的时候,你会得到一个连接错误。

1
2
3
4
5
6
7
unsigned int shaderProgram; //句柄
shaderProgram = glCreateProgram();
//注意Attach操作的顺序
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
//完成Attach以后,进行Link操作。
glLinkProgram(shaderProgram);

借助glGetProgramiv函数,可以判断链接是否出错。

1
2
3
4
5
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
if(!success) {
glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
...
}

链接完毕后,使用glUseProgram函数激活着色器程序对象,同时,删除先前定义的着色器,以释放内存。

1
2
3
glUseProgram(shaderProgram);
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);

完成这一系列操作以后,我们完成了以下内容:

  • 发送顶点数据,让其以VBO的状态存储在显存。
  • 编写了Vertex Shader和Fragment Shader,并指示GPU该如何使用这些着色器处理顶点数据。

链接顶点属性

我们输入的verticals数组是一个一维float数组。在这个数组里,每3个元素代表着一个顶点的位置数据。但OpenGL不知道,所以我们要告诉OpenGL,数组的哪些位置代表着哪个顶点的什么属性。

1
2
3
4
5
6
7
8
9
10
11
12
//指定解析顶点数据的方式
//第一个参数指定我们要配置的顶点属性,即着色器中的location。位置信息是一个location,颜色信息就是另一个location
//第二个参数指定顶点数据的大小,也就是每个属性的维度数。位置数据是一个三维向量,所以输入3
//第三个参数指定数据的类型
//第四个参数指定是否归一化,映射到0-1(或-1到1,对于有符号数来说。)
//第五个参数指定步长,即:这个属性第二次出现的地方到整个数组0位置之间有多少字节。对于单一属性数组,可设置为0
//第六个参数表示数据在缓冲区相对于起始位置的偏移量。如:位置信息包含3字节,紧随其后的是颜色信息。那么对于位置信息,这个参数就是(void*)0,对于颜色信息就是(void*)3
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
//设置完成后,启用顶点属性
glEnableVertexAttribArray(0);
//随后启用着色器程序
glUseProgram(shaderProgram);

顶点数组对象

所以,每次绘制一个物体,我们都必须经历以下步骤:

  • 生成VBO对象
  • 绑定VBO对象到GL_ARRAY_BUFFER
  • 将顶点数据(float数组)传入GL_ARRAY_BUFFER
  • 设置顶点属性指针
  • 启用顶点属性
  • 使用着色器程序
  • 绘制物体

非常繁琐。为了减少工作量,我们引入顶点数组对象(Vertex Array Object,VAO)的概念。

一个顶点数组对象会储存以下这些内容:

  • glEnableVertexAttribArray和glDisableVertexAttribArray的调用。
  • 通过glVertexAttribPointer设置的顶点属性配置。
  • 通过glVertexAttribPointer调用与顶点属性关联的顶点缓冲对象。

一般,完整的渲染代码可以表示为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// ..:: 初始化代码(只运行一次 (除非你的物体频繁改变)) :: ..
// 生成VAO、VBO对象....
unsigned int VAO;
glGenVertexArrays(1, &VAO);
unsigned int VBO;
glGenVertexArrays(1, &VBO);
// 1. 绑定VAO
glBindVertexArray(VAO);
// 2. 把顶点数组复制到缓冲中供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 3. 设置顶点属性指针
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// ..:: 绘制代码(渲染循环中) :: ..
// 4. 绘制物体
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
someOpenGLFunctionThatDrawsOurTriangle();

绘制

glDrawArray函数使用当前激活的着色器和VAO(包含VBO信息)来绘制图元(点、线、三角)。

1
2
3
4
5
6
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
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
#include <iostream>
#include <glad/glad.h>
#include <GLFW/glfw3.h>
using namespace std;

//顶点数据
float verticals[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};

//顶点着色器源码
const char* vertex_shader="#version 330 core\n"
"layout(location=0) in vec3 aPos;\n"
"void main()\n"
"{\n"
"gl_Position = vec4(aPos,1.0);"
"}\n";

//片段着色器源码
const char* fragment_shader="#version 330 core\n"
"out vec4 Fragcolor;\n"
"void main()\n"
"{\n"
"Fragcolor = vec4(1.0f, 0.5f, 0.2f, 1.0f);"
"}\n";

//输入处理函数
void process_input(GLFWwindow* window, int width, int height){
glViewport(0,0,width,height);
}

int main(){
//初始化glfw并配置版本和PROFILE模式
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR,3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR,3);
glfwWindowHint(GLFW_OPENGL_PROFILE,GLFW_OPENGL_CORE_PROFILE);
//创建glfw窗口
GLFWwindow* window = glfwCreateWindow(800,600,"LearnOpenGL",NULL,NULL);
//创建错误处理
if(window==NULL){
cout<<"Failed to create GLFW Window!"<<endl;
glfwTerminate();
return -1;
}
//将窗口上下文设置为当前线程上下文
glfwMakeContextCurrent(window);
//加载glad函数
if(!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)){
cout<<"Failed to load proc"<<endl;
return -1;
}
//定义视口大小
glViewport(0,0,800,600);
//配置窗口大小改变的回调函数,使得窗口改变时视口随之改变
glfwSetFramebufferSizeCallback(window,process_input);
//创建、绑定VBO对象
unsigned int VBO;
glGenBuffers(1,&VBO);
glBindBuffer(GL_ARRAY_BUFFER,VBO);
glBufferData(GL_ARRAY_BUFFER,sizeof(verticals),verticals,GL_STATIC_DRAW);
//创建、绑定VAO对象
unsigned int VAO;
glGenVertexArrays(1,&VAO);
glBindVertexArray(VAO);
//配置顶点属性指针,说明顶点数据与属性的对应关系
glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,0,(void*)0);
//启用顶点属性
glEnableVertexAttribArray(0);
//创建顶点着色器
unsigned int vertex = glCreateShader(GL_VERTEX_SHADER);
//链接着色器对象与源代码
glShaderSource(vertex,1,&vertex_shader,NULL);
//编译着色器
glCompileShader(vertex);
int success; //是否成功编译的flag
char infoLog[512]; //错误信息字符数组
glGetShaderiv(vertex, GL_COMPILE_STATUS, &success); //获取编译结果
if(!success)
{ //若未编译成功
glGetShaderInfoLog(vertex, 512, NULL, infoLog); //获取编译错误信息,512代表infoLog数组大小
std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl; //打印
}
//同上
unsigned int fragment = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragment,1,&fragment_shader,NULL);
glCompileShader(fragment);
glGetShaderiv(fragment, GL_COMPILE_STATUS, &success); //获取编译结果
if(!success)
{ //若未编译成功
glGetShaderInfoLog(fragment, 512, NULL, infoLog); //获取编译错误信息,512代表infoLog数组大小
std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl; //打印
}
//创建着色器程序
unsigned int shader_program = glCreateProgram();
//添加着色器“节点”,注意顺序
glAttachShader(shader_program,vertex);
glAttachShader(shader_program,fragment);
//完成链接操作
glLinkProgram(shader_program);
//激活着色器程序
glUseProgram(shader_program);
//删除着色器对象,释放内存
glDeleteShader(vertex);
glDeleteShader(fragment);
//渲染循环
while(!glfwWindowShouldClose(window)){
//设置清空颜色
glClearColor(0.2f,0.3f,0.3f,1.0f);
//清空上一帧
glClear(GL_COLOR_BUFFER_BIT);
//激活着色器程序
glUseProgram(shader_program);
//绑定VAO对象
glBindVertexArray(VAO);
//绘制三个顶点的三角形图元
glDrawArrays(GL_TRIANGLES,0,3);
//交换前后缓冲
glfwSwapBuffers(window);
//处理外部输入
glfwPollEvents();
}
}

元素缓冲对象

当图元存在共用顶点的情况时,传统的绘制方法会把共用的顶点绘制两次,导致额外开销。

为此,元素缓冲对象(Element Buffer Object, EBO)提供了一种方式,用于存储OpenGL用来决定要绘制哪些顶点的索引。

使用EBO时,顶点数据必须是不重复的顶点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
float vertices[] = {
0.5f, 0.5f, 0.0f, // 右上角
0.5f, -0.5f, 0.0f, // 右下角
-0.5f, -0.5f, 0.0f, // 左下角
-0.5f, 0.5f, 0.0f // 左上角
};
unsigned int indices[] = {
// 注意索引从0开始!
// 此例的索引(0,1,2,3)就是顶点数组vertices的下标,
// 这样可以由下标代表顶点组合成矩形
0, 1, 3, // 第一个三角形
1, 2, 3 // 第二个三角形
};
unsigned int EBO;
glGenBuffers(1, &EBO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
//渲染循环内...
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); //注意,使用EBO时需要使用glDrawElements。其中,第四个参数表示EBO中的偏移量。

使用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
#include <iostream>
#include <glad/glad.h>
#include <GLFW/glfw3.h>
using namespace std;

float vertices[] = {
0.5f, 0.5f, 0.0f, // 右上角
0.5f, -0.5f, 0.0f, // 右下角
-0.5f, -0.5f, 0.0f, // 左下角
-0.5f, 0.5f, 0.0f // 左上角
};

unsigned int indices[] = {
0, 1, 3, // 第一个三角形
1, 2, 3 // 第二个三角形
};

//顶点着色器源码
const char* vertex_shader="#version 330 core\n"
"layout(location=0) in vec3 aPos;\n"
"void main()\n"
"{\n"
"gl_Position = vec4(aPos,1.0);"
"}\n";

//片段着色器源码
const char* fragment_shader="#version 330 core\n"
"out vec4 Fragcolor;\n"
"void main()\n"
"{\n"
"Fragcolor = vec4(1.0f, 0.5f, 0.2f, 1.0f);"
"}\n";

void process_input(GLFWwindow* window,int width, int height){
glViewport(0,0,width,height);
}

int main(){
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR,3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR,3);
glfwWindowHint(GLFW_OPENGL_PROFILE,GLFW_OPENGL_CORE_PROFILE);
GLFWwindow* window = glfwCreateWindow(800,600,"LearnOpenGL",NULL,NULL);
if(window==NULL){
cout<<"Failed to Create Window"<<endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
if(!gladLoadGLLoader((GLADloadproc) glfwGetProcAddress)){
cout<<"Failed to Load GLAD proc"<<endl;
glfwTerminate();
return -1;
}
glViewport(0,0,800,600);
glfwSetFramebufferSizeCallback(window,process_input);

//编译着色器
unsigned int vertex = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertex,1,&vertex_shader,NULL);
glCompileShader(vertex);
int success;
char info[512];
glGetShaderiv(vertex,GL_COMPILE_STATUS,&success);
if(!success){
glad_glGetShaderInfoLog(vertex,512,NULL,info);
cout<<"VERTEX SHADER COMPILE ERROR:"<<info<<endl;
}
unsigned int fragment = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragment,1,&fragment_shader,NULL);
glCompileShader(fragment);
glGetShaderiv(fragment,GL_COMPILE_STATUS,&success);
if(!success){
glad_glGetShaderInfoLog(fragment,512,NULL,info);
cout<<"FRAGMENT SHADER COMPILE ERROR:"<<info<<endl;
}
unsigned int shader_program = glCreateProgram();
glAttachShader(shader_program,vertex);
glAttachShader(shader_program,fragment);
glLinkProgram(shader_program);
glGetProgramiv(shader_program, GL_LINK_STATUS, &success);
if (!success) {
glGetProgramInfoLog(shader_program, 512, NULL, info);
std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << info << std::endl;
}
glDeleteShader(vertex);
glDeleteShader(fragment);

//定义VAO、VBO、EBO
unsigned int VBO;
glGenBuffers(1,&VBO);
glBindBuffer(GL_ARRAY_BUFFER,VBO);
glBufferData(GL_ARRAY_BUFFER,sizeof(vertices),vertices,GL_STATIC_DRAW);
unsigned int VAO;
glGenVertexArrays(1,&VAO);
glBindVertexArray(VAO);
glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,0,(void*)0);
glEnableVertexAttribArray(0);
unsigned int EBO;
glGenBuffers(1,&EBO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER,sizeof(indices),indices,GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
while(!glfwWindowShouldClose(window)){
glClearColor(0.5f,0.4f,0.3f,1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glBindVertexArray(VAO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,EBO);
glUseProgram(shader_program);
glDrawElements(GL_TRIANGLES,6,GL_UNSIGNED_INT,0);
glBindVertexArray(0);
glBindBuffer(GL_ARRAY_BUFFER,0);
glfwSwapBuffers(window);
glfwPollEvents();
}
glDeleteBuffers(1,&VBO);
glDeleteProgram(shader_program);
glDeleteVertexArrays(1,&VAO);
glfwTerminate();
return 0;
}

LearnOpenGL学习笔记(一) - 环境配置与对象绘制
http://example.com/2024/07/10/LearnOpenGL学习笔记(一) - 环境配置与对象绘制/
作者
Yoi
发布于
2024年7月10日
许可协议