学习目的

为了搭配Games101课程实践,以及了解进一步底层的游戏制作,再以及重新熟悉c++(忘得差不多了);

学习自

[OpenGL - LearnOpenGL CN (learnopengl-cn.github.io)](https://learnopengl-cn.github.io/01 Getting started/01 OpenGL/)

Opengl编程指南第八版

配置环境

网上教程挺多的,我偷懒全是在Visual Studio Nuget程序包安装的

另外在这里科普一下glad、glew、glfw、Freeglut的区别:

glew(The OpenGL Extension Wrangler Library)是对底层OpenGL接口的封装,可以让你的代码跨平台。

glad与glew作用相同,可以看作它的升级版。

Freeglut(OpenGL Utility Toolkit)主要用于创建OpenGL上下文、接收一些鼠标键盘事件等等。

glfw(Graphics Library Framework)是Freeglut升级版,作用基本一样。

本人用的是Glad加Glfw的组合。

引用自

https://www.zhihu.com/question/264132001/answer/729626917

freeglut与GLFW介绍及其不同 (qq.com)

是什么

[简介](https://learnopengl-cn.github.io/01 Getting started/01 OpenGL/)

这里截取一些关键信息:

  • Opengl的渲染管线

    Opengl首先接收用户提供的集合数据(顶点和几何图元),然后将它输入到一系列着色器阶段中进行处理,包括:顶点着色,细分着色,图元装配,几何着色,然后他将被送入光栅化单元。光栅化单元负责对所有剪切区域内的图元生成片元数据,然后对每一个生成的片元都执行一个片元着色器。

    顶点着色器和片元着色器是必需的,细分和几何着色器可选。

    着色器语言为GlSL(OpenGL Shading Language).

    opengl渲染管线

  • 状态机

    OpenGL自身是一个巨大的状态机:一系列的变量描述OpenGL此刻应当如何运行。通过改变上下文变量来改变Opengl状态,状态机同一时间只允许一个状态存在,opengl也是如此,它只允许一个当前对象。

    例如通过使用状态设置函数,改变状态变量等

  • Context上下文

    看文档时经常看到这样一个概念,可以理解为状态机的一个状态。(所以为什么要翻译成这样)

  • 对象

    虽然opengl是用c语言写的,但为了更加高效和兼容到高级语言,引用了对象的概念;

  • 基本工作流

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 创建对象
    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);
    • 函数

      例如glGenObject() 开头“gl”是其命名规则,代表Opengl库函数,GenObject 即 generate object 分配对象的作用, 函数作用顾名思义即可知道是啥意思了。

    • 数据处理

      Opengl需要将所有的数据都保存到缓存对象(buffer object)中,缓存对象就是Opengl维护的以一块内存区域,它位于显卡中,而不是内存条中,因为访问显存比访问内存快得多,分配对象后要记得绑定对象到上下文目标位置,再通过目标位置来设置对象,解绑后 对象的设置将会保留在内存中。

      总结就是三步走 第一,创建对象 ,第二,绑定对象到上下文,第三 设置对象。

      比如说上面的代码,但这次创建了、并设置了两个对象,在解绑状态下, 分别绑定这两个状态就会呈现其该两状态原本的设置。

  • 支持的数据类型

    Opengl数据类型

创建窗口

Glfw官方配置窗口指南:GLFW: Window Guide

直接面向过程创建

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
#include <glad/glad.h>
#include <GLFW/glfw3.h>

#include <iostream>
using std::cout;
using std::endl;
int main()
{
//初始化glfw
glfwInit();
#pragma region 配置Glfw
//配置主版本
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
//配置副版本
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
//配置Glfw的渲染模式为核心模式
glfwWindowHint(GLFW_OPENGL_PROFILE,GLFW_OPENGL_CORE_PROFILE);
#pragma endregion
GLFWwindow* window = glfwCreateWindow(800, 600, "Winodw", NULL, NULL);
if (window == nullptr)
{
cout << "创建失败" << endl;
//终止Glfw
glfwTerminate();
return 0;

}
//在指定窗口展示上下文
glfwMakeContextCurrent(window);

}

运行看到窗口被成功创建。

以及持续打开窗口:

注意点

  • glViewport(int,int,int,int)

    Opengl使用该函数进行Opengl内部坐标到屏幕2D坐标的映射(opengl坐标范围为-1~1)

    也就是说,每当窗口大小改变时,就需要重新映射一次,因此得在主循环在持续调用,Glfw库函数有glfwSetFramebufferSizeCallback(GLFWwindow*, GlfwFramebufferSizefun)用来注册视口坐标映射的回调函数。

    主循环里调用glfwPollEvents()来响应所有的回调函数(内部用了事件队列来保存所有事件)

  • glfwSwapBuffers(window)

    该函数用来交换前缓存和后缓存。

    要知道 屏幕图像的显示是按照从左到右,由上而下逐像素地绘制而成的,如果这样以一种撕裂感展现给用户,简直视觉灾难,所以使用双缓冲模式,前缓冲保存着最终输出的图像,它会在屏幕上显示;而所有的的渲染指令都会在缓冲上绘制,当渲染指令执行完毕后,我们交换前缓冲和后缓冲,这样整个图像就立即呈显出来了。

  • gladLoadGLLoader((GLADloadproc)glfwGetProcAddress) 获取当前上下文从而初始化Glad,如果当前opengl还没启动上下文则会报错,所以在下面代码里要确保在glfwMakeContextCurrent(window)后调用

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
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <iostream>
// Settings
using std::cout;
using std::endl;
using std::cin;
const int WIDTH = 800;
const int HEIGHT = 600;
GLFWwindow* window;

//Function Declaration
void FrameBufferSizeCallback(GLFWwindow*, int, int);

#pragma region 使用opengl库函数保持窗口打开
int main()
{
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
window = glfwCreateWindow(800, 600, "Winodw", NULL, NULL);
if (window == nullptr)
{
cout << "创建失败" << endl;
glfwTerminate();
return 0;
}
glfwMakeContextCurrent(window);

//注册回调函数,窗口大小改变,视口映射也要改变
glfwSetFramebufferSizeCallback(window, FrameBufferSizeCallback);

###################
//初始化Glad
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
cout << "Glad初始化失败" << std::endl;
return 0;
}
glViewport(0, 0, WIDTH, HEIGHT);
####################

while (!glfwWindowShouldClose(window))
{
glfwSwapBuffers(window);
glfwPollEvents();
}
glfwTerminate();
return 0;
}
#pragma endregion

#pragma region 函数定义
void FrameBufferSizeCallback(GLFWwindow* window, int width, int height)
{
glViewport(0,0,width,height);
}
#pragma endregion


创建三角形

流程

创建三角形,就必须要传入坐标数据,Opengl是3D图形库,所有坐标都是3D,(x,y)为平面坐标,z为深度值,并且要符合标准化设备坐标范围(-1~1)。

而传入一系列的顶点数据,则需要分配缓存对象并保存到缓存对象中,并且由当前绑定的顶点数组对象管理。

(缓存对象是什么?上文基本工作流中提到过)

所以假如有下面一系列三角形顶点数据

1
2
3
4
5
6
float vertices[] = {
// positions
0.5f, -0.5f, 0.0f,
-0.5f, -0.5f, 0.0f,
0.5f, 0.5f, 0.0f,
};
  1. 第一步 分配缓存对象

    1
    2
    3
    4
    5
    6
    7
    8
    //声明对象名称,分配缓存对象
    unsigned int buffer[n];
    glGenBuffers(1, &buffer);

    #############
    void glGenBuffers(Glsizei n, Gluint *buffers)

    返回n个当前未使用的缓存对象名称,并保存到buffers中
  2. 第二步,绑定对象到上下文目标位置,我们要的是顶点缓冲对象,Opengl将其缓冲类型定义为GL_ARRAY_BUFFER

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    glBindBuffer(GL_ARRAY_BUFFER, VBO);

    ##############
    void glBindBuffer(Glenum target, GLuint buffer)

    targer 指定当前激活的缓存对象类型,其包括(要用再看):
    1.GL_ARRAY BUFFER 顶点属性数据
    2.GL-ELEMENT_ARRAY_BUFFER 索引数据类型
    3.GL_PIXEL_UNPACK_BUFFER Opengl的像素数据
    4.GL_PIXEL_PACK_BUFFER 从OpenGL中获取的像素数据
    5.GL_COPY_READ_BUFFER 缓存之间复制的数据
    6.GL_COPY_WRITE_BUFFER 缓存之间复制的数据
    7.GL_TEXTURE_BUFFER 纹理数据
    8.GL_TRANSFORM_FEEDBACK_BUFFER transform feedback着色器获得的结果
    9.GL_UNIFORM_BUFFER 一致变量
    buffer 缓存对象名称
    该函数完成了三项工作
    1) 如果是第一次绑定buffer,且他是一个非零的无符号整型,那么将创建一个与该名称相对应的新缓存对象
    2) 如果绑定到一个已经创建的缓存对象,那么它将成为当前被激活的缓存对象
    3) 如果绑定的buffer值为0,则OpenGL将不再对当前target应用任何缓存对象。
  3. 第三步就是要给当前绑定的缓冲对象传入数据了,对于顶点数据,我们可以调用glBufferData函数把顶点数据复制到对象的内存中

    1
    2
    3
    4
    5
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

    ################################
    void glBufferData(GLenum target,GLsizeiptr size,const GLvoid* data,GLenum usage)
    在OpenGL内存中分配size个存储单元(单位通常为Byte),用于存储数据或者索引。如果当前绑定的对象已经存在了关联的数据,那么会首先删除这些数据

    至此对VBO的操作以及结束

  4. 设置顶点属性

    这一步是为了告诉Opengl如何解析VBO管理的数据。

    image-20221101221331879

    具体如何设置得看我们的顶点数据结构,例如

    我们传入的顶点数据只包含位置信息,且

    1
    2
    3
    4
    5
    6
    float vertices[] = {
    // positions
    0.5f, -0.5f, 0.0f,
    -0.5f, -0.5f, 0.0f,
    0.5f, 0.5f, 0.0f,
    };

    下图其在VBO中的结构图

    image-20221101230718828

    参数一location取决于着色器的标识

    参数二我们这里一次读入三个float,大小为3,

    参数三类型为float,所以传入GL_FLOAT

    参数四:从起始读入数据开始,加上起始读入数据还需读入三个float数据,就是下一组读入数据;所以传入3个float字节大小

    参数五:即起始读入数据离数组开头的距离,这里为0,所以输入(void*)0

  5. VAO

    同下

    ……

VBO 的理解

这两步刚看的时候究极究极究极懵懂,以至于不弄得清楚的话我感觉我自己将不可能学会

我们将顶点缓存对象(VBO)理解为一个指针变量,它是位于OpenGL维护的内存区域中(显存)的一块内存,管理着一组顶点数据。

最主要的作用:

CPU传送数据给GPU比较耗费时间,通过VBO可以尽可能的一次性把需要的顶点数据全部传给GPU,这样顶点着色器几乎能立即访问到顶点,有助于加快顶点着色器效率。

看书以及各种博客后对内部结构的个人理解

**在调用glGenBuffers(Glsizei n, Gluint *buffers)时,就相当于我们声明了OpenGL所维护的内存区域内的一个指针,我们在之前声明的unsigned int buffer只不过是该指针的标识。**这个代称目的是为了我们可以把数据从主存传到显存而存在的,因为我们无法直接操作显存里的对象。

而当我们第一次绑定它时,即调用glBindBuffer(Glenum target, GLuint buffer)时,OpenGL内部会分配该缓存对象所需的内存并且将它作为当前对象,所有后续的操作都会作用与这个被绑定的对象,也就是说,buffer对象被设为绑定类型的默认对象,以后我们对对象类型target目标位置上的操作都会用来配置默认对象。(我们实际上操作的是缓冲类型,Opengl内部通过我们对缓冲类型的配置来配置位于该缓冲类型位置下的缓冲对象);

编程指南里提到:总体上说,在两种情况下我们需要绑定一个对象:创建对象并初始化它所对应的数据时;以及每次我们准备使用这个对象,但它不是当前绑定的对象时。


VAO的理解

对顶点数据的分配工作已经结束,然后就蹦出来了个VAO

VAO作用:

  1. 不同于VBO管理数据,VAO旨在解析数据

    一堆数据也许包含着位置,颜色等不同的关键信息,通过VAO将帮助Opengl解释数据。

  2. 复用顶点属性,节省工作量

    当我们在初始化并绑定顶点数组对象后进行顶点属性设置时,他会保存该顶点属性配置,其包括

    • glEnable(Disable)VertexAttribArray顶点属性启用与否
    • 通过glVertexAttribPointer设置的顶点属性配置。
    • 通过glVertexAttribPointer调用与顶点属性关联的顶点缓冲对象。

    所以当我们绘制图形时,就不需要每次都重复的链接顶点属性,如果当前绑定的顶点数组对象得顶点属性是符合绘制图形时,我们就直接绘制即可,否则绑定对应的VAO,然后再绘制,绘制后根据下一个绘制图形的顶点属性来选择受否解绑当前VAO。这和绑定VBO道理时相通的

EBO

易理解 看这个就好[你好,三角形 - LearnOpenGL CN (learnopengl-cn.github.io)](https://learnopengl-cn.github.io/01 Getting started/04 Hello Triangle/)

完整代码

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
//井号内是对着色器的简单使用
#####################################################################################
const char* vertexShaderSource = "#version 330 core\n"
"layout (location = 0) in vec3 aPos;\n"
"out vec4 vertexColor;\n"
"void main()\n"
"{\n"
" gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n"
" vertexColor = vec4(0.5, 0.0, 0.0, 1.0);\n"
"}\0";
unsigned int vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);

const char* fragmentShaderSource = "#version 330 core\n"
"out vec4 FragColor;\n"
"uniform vec4 ourColor;\n"
"void main()\n"
"{\n"
" FragColor = ourColor;\n"
"}\0";
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);
#####################################################################################
float vertices[] = {
// positions
0.5f, -0.5f, 0.0f,
-0.5f, -0.5f, 0.0f,
0.5f, 0.5f, 0.0f,
-0.5f, 0.5f, 0.0f
};
#pragma region 元素缓存/索引缓冲对象

unsigned int indices[] = {
0,1,2,
1,2,3
};
unsigned int EBO;
glGenBuffers(1, &EBO);

#pragma endregion

////配置顶点属性
unsigned int VBO, VAO;
glGenVertexArrays(1, &VAO);
glBindVertexArray(VAO);
glGenBuffers(1, &VBO);
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);

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



// render loop
// -----------


while (!glfwWindowShouldClose(window))
{
// 输入
processInput(window);

// 渲染
// 清除颜色缓冲
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);

//glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
// render the triangle
//ourShader.use();
glUseProgram(shaderProgram);

float timeValue = glfwGetTime();
float redValue = cos(timeValue) / 2.0f+0.5f;
float greenValue = sin(timeValue) / 2.0f+0.5f ;
//float blueValue = sin(timeValue) / 2.0f + 0.5f;
int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");
glUniform4f(vertexColorLocation, redValue, greenValue, 0, 1.0f);

glBindVertexArray(VAO);
//glDrawArrays(GL_TRIANGLES, 0, 3);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
glBindVertexArray(0);

// 交换缓冲并查询IO事件
glfwSwapBuffers(window);
glfwPollEvents();
}

glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);

glfwTerminate();
return 0;
}

void processInput(GLFWwindow* window)
{
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);
}

着色器的初步使用

初步瞅瞅GLSL

语法就不干学了,多用多写再多出错学得更好

简单看一段顶点着色器代码,语言为Glsl

1
2
3
4
5
6
7
8
9
10
11
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;

out vec3 ourColor;

void main()
{
gl_Position = vec4(aPos, 1.0);
ourColor = aColor; // 将ourColor设置为我们从顶点数据那里得到的输入颜色
}

版本,

声明输入变量,

声明输出变量,

主函数

{

处理输入变量,将结果输出

}

  • (location = 0)为设置顶点属性时glVertexAttribPointer 参数一
  • 着色器之间若需要数据的传递,则着色器输入变量的类型和变量名要与上一个着色器的对应的输出变量相同,这样就能识别。
  • 给着色器输入数据也可以通过全局变量来获得
    • 首先int glGetUniformLocation(shaderProgram, "ourColor")通过传入渲染程序和全局变量字段名参数来获取全局变量位置,
    • 再通过glUniform(int pos) 传入全局变量位置,来设置全局变量值

数据类型

Glsl包含:

GLSL数据类型

写入渲染程序

流程如下:

着色器编译顺序

  • 创建着色器对象

    1
    2
    GLuint glCreateShader(Glenum type)
    分配一个着色器对象,成功则返回一个非零值
  • 将着色器源代码编译为对象

    1
    2
    3
    4
    5
    6
    void ShaderSource(Gluint,Glsizei,const GLchar**,const GLint*)
    将着色器源代码关联到指定着色器对象上
    参数分别为 着色器对象,代码的字符串数组行数,代码字符串数组,
    -------
    void glCoompileShader(GLuint shader);
    编译着色器对象
  • 验证着色器编译是否成功

    1
    2
    void glGetShaderiv(GLuint, GL_COMPILE_STATUS, bool*);
    void glGetShaderInfoLog(GLuint,GLsizei,GLsizei*,char*)

至此着色器对象的准备工作完成,接下来需要将多个着色器对象链接为一个着色器程序

  • 创建程序

    1
    GLuint glCreateProgram()
  • 关联着色器对象

    1
    2
    3
    glAttachShader(program, shader_int);
    移除
    glDetachShader...
  • 生成完整着色器程序

    1
    glLinkProgram(program);
  • 获取日志

    1
    2
    3
    4
    5
    6
    7
    // 打印连接错误(如果有的话)
    glGetProgramiv(ID, GL_LINK_STATUS, &success);
    if(!success)
    {
    glGetProgramInfoLog(ID, 512, NULL, infoLog);
    std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
    }
  • 绑定着色器程序

    1
    void glUseProgram(Gluint program);
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

// 声明着色器标识(顶点,片段)
unsigned int vertex, fragment;
int success;
char infoLog[512];

// 创建顶点着色器
vertex = glCreateShader(GL_VERTEX_SHADER);
//
glShaderSource(vertex, 1, &vShaderCode, NULL);
glCompileShader(vertex);
// 打印编译错误(如果有的话)
glGetShaderiv(vertex, GL_COMPILE_STATUS, &success);
if(!success)
{
glGetShaderInfoLog(vertex, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
};

// 片段着色器也类似
[...]

// 着色器程序
ID = glCreateProgram();
glAttachShader(ID, vertex);
glAttachShader(ID, fragment);
glLinkProgram(ID);
// 打印连接错误(如果有的话)
glGetProgramiv(ID, GL_LINK_STATUS, &success);
if(!success)
{
glGetProgramInfoLog(ID, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
}

// 删除着色器,它们已经链接到我们的程序中了,已经不再需要了
glDeleteShader(vertex);
glDeleteShader(fragment);