当前位置: 首页 > news >正文

OpenGL - 如何理解 VAO 与 VBO 之间的关系

系列文章目录

  • LearnOpenGL 笔记 - 入门 01 OpenGL
  • LearnOpenGL 笔记 - 入门 02 创建窗口
  • LearnOpenGL 笔记 - 入门 03 你好,窗口
  • LearnOpenGL 笔记 - 入门 04 你好,三角形

文章目录

  • 系列文章目录
  • 1. 前言
  • 2. 渲染管线的入口 - 顶点着色器
    • 2.1 顶点着色器处理过程
    • 2.2 输入更多数据
  • 3. VBO 顶点缓冲对象
    • 3.1 顶点属性数据的存放方式
    • 3.2 从 VBO 中获取数据
    • 3.3 更进一步
  • 4.VAO 与 VBO 之间的关系
  • 5. 理解代码
  • 6. 总结


1. 前言

在上一章 LearnOpenGL 笔记 - 入门 04 你好,三角形 中引入了很多很多概念,VBO、VAO、EBO、Shader 等等。密集的知识点向你轰炸而来,让这一章的难度陡然上升。说实话,这一章相当的劝退我。我心中有太多的困惑没有得到解答,文章虽然对 VBO、VAO 等做了解释,但其解释没有能让我这个入门者理解。以至于让阅读者相当的挫败。

今天我尝试将本章概念「幼儿园」化,站在入门菜鸟的角度,以伪代码的形式来理解 VAO、VBO 等概念。

2. 渲染管线的入口 - 顶点着色器

我们用 OpenGL 渲染一个三角形也好,渲染一个复杂的模型也好,无非就是输入一些顶点数据,得到一张图片。
在这里插入图片描述
Rendering pipeline 包含了多个阶段(这部分上一章有详细的说明),包括顶点着色器、几何着色器、片段着色器等等。

2.1 顶点着色器处理过程

其中,顶点着色器位于整个 Pipeline 的第一个阶段,所有顶点数据首先发送到顶点着色器中。它接收顶点坐标、颜色、纹理坐标等数据,并对这些数据进行变换,例如旋转、缩放、平移等,最终将处理后的顶点数据传递给后续的渲染步骤。

以渲染一个三角形为例,它的顶点着色器代码非常简单:

const char *vertexShaderSource = R"(#version 330layout (location = 0) in vec3 aPos;void main(){gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);}
)";float vertices[] = {-0.5f, -0.5f, 0.0f,0.5f, -0.5f, 0.0f,0.0f,  0.5f, 0.0f
};

其中 vertices[] 中存放了三个顶点的位置,而观察顶点着色器的代码,却发现它只处理了一个顶点。这是我的第一个困惑:OpenGL 是如何渲染多个顶点的?

实际上,顶点着色器可以在图形处理单元(GPU)上并行运行,这意味着它可以同时处理多个顶点数据。在 GPU 中,存在大量的简单处理单元,可以同时处理顶点数据。

举例,假设现在有 100 个顶点数据,GPU 上有 10 个处理单元,那么顶点着色器处理的过程大概是

  1. 数据分配:100个顶点数据被分配给GPU上的10个处理单元。每个处理单元分到的顶点数据数量可能不同。
  2. 数据处理:每个处理单元都独立地处理分配给它的顶点数据。在Vertex shader中定义的变换(例如旋转、缩放、平移等)被应用到每个顶点数据上。
  3. 结果合并:每个处理单元处理后的结果被合并到一起。最终的结果是100个顶点数据的处理结果。
  4. 传递结果:处理后的顶点数据被传递给后续的渲染步骤,以完成3D图形的渲染。

这是一个简化的过程描述,实际的处理过程可能更加复杂。但是,通过上述过程,100个顶点数据可以高效地处理,从而实现高效的3D图形渲染。

我们使用伪代码来描述上面的过程:

#define NUM_VERTICES 100
#define NUM_UNITS 10vector<vec3> vertex_data(NUM_VERTICES); // 有 100 个顶点数据// 1. 数据分配
vector<vec3> processing_unit_data[NUM_UNITS]; // 有 10 个处理单元,每个单元处理 10 个顶点
const int num_vertices_per_unit = NUM_VERTICES / NUM_UNITS;
for (int i = 0; i < NUM_UNITS; i++) {processing_unit_data[i].assign(vertex_data.begin() + i * num_vertices_per_unit,vertex_data.begin() + (i + 1) * num_vertices_per_unit);
}// 2. 数据处理
for (int i = 0; i < NUM_UNITS; i++) {for (int j = 0; j < processing_unit_data[i].size(); j++) {processing_unit_data[i][j] = vertex_shader(processing_unit_data[i][j]);}
}// 3. 结果合并
vector<vec3> result; // 最终得到 100 个处理后的数据
for (int i = 0; i < NUM_UNITS; i++) {result.insert(result.end(), processing_unit_data[i].begin(), processing_unit_data[i].end());
}// 4. 传递结果
render(result);

在伪代码中的 2. 数据处理 部分,使用了一个 for 循环顺序地在每一个 GPU 处理单元上执行一次 shader。但请注意,在实际 GPU 运算中这部分是并行的,GPU 可以并行地处理非常非常多数据。如下图
在这里插入图片描述

2.2 输入更多数据

在前面绘制三角形时,我们输入了三角形的顶点位置数据。为了绘制更加精美更加复杂的模型,我们要输入的数据可不单单只有顶点位置,还可能有颜色、纹理坐标、法向量坐标等数据。我们通通称这些为顶点属性,名副其实,它确确实实描述顶点的某些属性。

如果将顶点着色器看成是一个函数的话,如果输入只有顶点位置信息,那么可以理解为该函数参数只有一个;当顶点着色器输入更多其他顶点属性时,例如输入了顶点的颜色,那么该函数输入参数有两个:

void vertex_shader(vec3 pos);	// 输入顶点位置数据
void vertex_shader(vec3 pos, vec3 color); // 输入顶点位置数据、顶点颜色数据

多个输入体现在 shader 源码,则以多个 in 变量来表示,例如

const char *vertexShaderSource_one_input = R"(#version 330layout (location = 0) in vec3 aPos; // 顶点位置数据void main(){// ...}
)";const char *vertexShaderSource_two_input = R"(#version 330layout (location = 0) in vec3 aPos; // 顶点位置数据layout (location = 1) in vec3 aColor; // 顶点颜色数据void main(){// ...}
)";

OpenGL 确保至少有 16 个包含 4 分量的顶点属性可用。也就是说我们的 vertex_shader 函数至少可以处理 16 个参数的输入。此时,GPU 执行 shader 时将输入多个数据,如下图:
在这里插入图片描述

3. VBO 顶点缓冲对象

顶点着色器输入的是顶点属性数据,那么这些数据存放在哪里呢?答案是存放在的显存中

你可能会说:“不对啊,你看前面的 vertices[] 变量,它是存放在代码中的,代码中数据应该是存放在内存中的”。

这么说没错,vertices 确实存放在内存中,但我们需要使用 OpenGL API 将存放在内存的数据拷贝到显存中。在显存中,我们需要一个类似 vertices 对象来表示这块显存,而这样的对象就是 VBO。
在这里插入图片描述

3.1 顶点属性数据的存放方式

假设渲染三角形时,除了顶点位置数据外,还有各顶点的颜色信息,那么这两种信息可以怎么摆放呢?
位置和颜色是不同的属性,编程直觉来说,我更倾向使用两个数组来分别存放,例如 3 个 xyz 顶点位置和 3个 rgb 颜色数据:

// xyz
float positions[] = {-0.5f, -0.5f, 0.0f,0.5f, -0.5f, 0.0f,0.0f,  0.5f, 0.0f
};// rgb
float colors[] = {1.0f, 0.0f, 0.0f,0.0f, 1.0f, 0.0f,0.0f, 0.0f, 1.0f,
}

对应的,你将使用 OpenGL API 创建 2 个 vbo,分别将 positionscolors 数据从内存拷贝到显存,代码大致是这样的:

GLuint vbos[2] = {0,0
};
glGenBuffers(2, vbos);// copy positions to first vbo
glBindBuffer(GL_ARRAY_BUFFER, vbos[0]);
glBufferData(GL_ARRAY_BUFFER, sizeof(positions), positions, GL_STATIC_DRAW);// copy colors to second vbo
glBindBuffer(GL_ARRAY_BUFFER, vbos[1]);
glBufferData(GL_ARRAY_BUFFER, sizeof(colors), colors, GL_STATIC_DRAW);

当然,你可以把所有顶点属性数据放在一个数组和一个 vbo 中,例如

float vertices[] = {// 位置              // 颜色0.5f,  -0.5f, 0.0f, 1.0f, 0.0f, 0.0f,  // 右下-0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, // 左下0.0f,  0.5f,  0.0f, 0.0f, 0.0f, 1.0f  // 顶部
};
GLuint vbo{0}
glGenBuffers(1, vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

两种方式有何优劣?

将数据存储在单个 VBO 中:

  • 优点:
    • 简单易用:只需创建一个 VBO 即可存储所有数据。
    • 高效:如果所有数据都是一起使用的,则可以减少 CPU/GPU 之间的数据传输次数。
  • 缺点:
    • 不灵活:如果要修改某些数据,则必须更新整个 VBO。
    • 更新时间长:由于数据量较大,因此更新 VBO 时间可能较长。
    • 占用内存多:由于数据量较大,因此占用的内存可能较多。

将数据存储在多个 VBO 中:

  • 优点:
    • 灵活:可以单独修改每个 VBO 中的数据。
    • 更新时间短:由于每个 VBO 中的数据量较小,因此更新 VBO 时间可能较短。
    • 占用内存少:由于每个 VBO 中的数据量较小,因此占用的内存可能较少。
  • 缺点:
    • 稍微复杂:需要管理多个 VBO,以确保所有数据都被正确渲染。
    • 效率较低:如果所有数据都是一起使用的,则可能增加 CPU/GPU 之间的数据传输次数,导致渲染效率降低。

总体来说,如果所有数据都是一起使用的,则使用单个 VBO 可能更高效。但如果需要灵活地修改数据,则使用多个 VBO 可能更合适。因此,选择使用单个 VBO 或多个 VBO 取决于具体应用的需求。

3.2 从 VBO 中获取数据

VBO 表示了一块显存,里头存放了很多数据。前面提到,顶点着色器的输入来自于显存,其实就是来自与 VBO。

现在思考一个问题:一个 VBO 中可能存放着很多数据,包括位置、颜色等,也有可能在显存中有多个 VBO 分别存放着这些数据。那么 OpenGL 在渲染时,是如何正确地找打这些数据,并将它们喂给 shader 的呢?

在这里插入图片描述
这个问题的答案其实就是 VAO,但在解释这个问题之前,让我们来看看 GPU 为了正确地获取数据,要知道哪些信息。

仍然是绘制三角形,vertex shader 输入顶点信息和颜色信息,其源代码大致是这样的:

const char *kVertexShaderSource = R"(#version 330layout (location = 0) in vec3 aPos;layout (location = 1) in vec3 aColor;out vec3 ourColor;void main(){gl_Position = vec4(aPos, 1.0);ourColor = aColor;}
)";

假设现在顶点数据包括位置和颜色,全部放在一个 VBO 中,那么你可能这么放,先存放全部 xyz,再放全部 rgb,给它一个方便记忆的名字,就叫平面型

x0 y0 z0 x1 y1 z1 x2 y2 z2 r0 g0 b0 r1 g1 b1 r2 g2 b2

也有可能存放第一个点的 xyz 和 rgb,接着第二个点,以此类推,这种也给它取个名字,就叫交织型

x0 y0 z0 r0 g0 b0 x1 y1 z1 r1 g1 b1 x2 y2 z2  r2 g2 b2

这两种存放数据都是合理的,你希望提供一个接口,它足够灵活,可以支持这两种布局。

如果你是 GPU,那么从 VBO 中获取顶点属性的伪代码大致是这样的:

void* vbo = some_address;
const int num_vertex = 3;
const int vertex_pos_index = 0;
const int vertex_rgb_index = 1;for(int i = 0; i < num_vertex; ++i)
{vec3_float xyz = getDataFromVBO(vbo, i, ...);vec3_float rgb = getDataFromVBO(vbo, i, ...);auto result = vertex_shader(xyz, rgb);
}
// ...

其中:

  • vbo 指向一个显存的地址,把它看成是我们熟悉的 C 指针即可
  • vertex_pos_index = 0vertex_rgb_index = 1,对应 shader 源码中的 layout (location = 0) in vec3 aPoslayout (location = 1) in vec3 aColor。表明想要第几个顶点属性
  • 通过 getDataFromVBO 从 vbo 中获取地 i 顶点的位置信息和颜色信息
  • vertex_shader 输入两个参数,分别是顶点位置信息和颜色信息

现在,你要思考如何实现 getDataFromVBO 函数,简单起见假设 vbo 存放的都是 float 类型的数据,返回的都是 vec3_float 数据(看成是 大小为 3 的 std::vector)。为了兼容前面提到的两种数据布局,我们引入 stride 和 offset 参数,getDataFromVBO 实现大概是这样的:

vec3_float getDataFromVBO(VBO vbo, int vertex_index, int stride, int offset)
{const int num_float_in_vec3 = 3;float* begin = (float*)(vbo) + offset;	// 起始位置偏移const int vertex_offset = vertex_index * stride; // 第 i 个顶点属性的获取位置vec3_float result = vec3_float{begin + vertex_offset, begin + vertex_offset + num_float_in_vec3}return result;
}

offset 参数很好理解,即偏移量。下表列举了不同类型获取顶点位置信息(xyz)和颜色信息(rgb)所需的 offset

位置数据颜色数据
平面型09
交织型03
  • 平面型时,第一个顶点位置(x0)偏移量为 0;第一个顶点颜色(r0)偏移量为 9
  • 交织型时,第一个顶点位置(x0)偏移量为 0;第一个顶点颜色(r0)偏移量为 3

stride 参数意为“步长”,指的是为了拿到下一个数据,我需要跨域多少个单位。下表列举了不同类型获取顶点位置信息(xyz)和颜色信息(rgb)所需的 stride

位置数据颜色数据
平面型33
交织型66
  • 平面型时,当前顶点位置到一下个顶点位置需要跨域 3 个单位,例如 x0 到 x1,中间隔了 3 个数据;颜色数据的 stride 同理。
  • 交织型时,当前顶点位置到一下个顶点位置需要跨域 6 个单位,例如 x0 到 x1,中间隔了 6 个数据;颜色数据的 stride 同理。

非常好,有了 strideoffset 参数我们已经能够很好的处理两种不同的排列了。现在,根据我们要获取的是顶点位置还是颜色,设置不同的参数,就可以顺利地从 vbo 中拿到数据了。伪代码更新为:

void* vbo = some_address;
const int num_vertex = 3;
const int vertex_pos_index = 0;
const int vertex_index_0_offset = 0; // 平面型为 0,交织型为 0
const int vertex_index_0_stride = 3; // 平面型为 3,交织型为 6const int vertex_rgb_index = 1;
const int vertex_index_1_offset = 9	 // 平面型为 9,交织型为 3
const int vertex_index_1_stride = 3; // 平面型为 3,交织型为 6for(int i = 0; i < num_vertex; ++i)
{vec3_float xyz = getDataFromVBO(vbo, i, vertex_index_0_stride,vertex_index_0_offset);vec3_float rgb = getDataFromVBO(vbo, i, vertex_index_1_stride,vertex_index_1_offset);auto result = vertex_shader(xyz, rgb);
}

3.3 更进一步

或许你感觉到了,我在前面讲解的其实是 glVertexAttribPointer 函数的参数部分。让我们接着完善,让伪代码更加接近 glVertexAttribPointer

首先,之前的伪代码中,我们默认获取的是一个 vec3。在实际使用场景,不一定所有顶点属性都是 vec3,或许是 vec4 或者 vec2,甚至是单个 float。因此我们将属性的个数抽象为 size 这个参数,得到:

vecn_float getDataFromVBO(VBO vbo, int vertex_index, int size, int stride, int offset)
{float* begin = (float*)(vbo) + offset;	// 起始位置偏移const int vertex_offset = vertex_index * stride; // 第 i 个顶点属性的获取位置vecn_float result = vec3_float{begin + vertex_offset, begin + vertex_offset + size}return result;
}

接着,顶点属性也不一定是 float 类型的,有可能是 intbool 类型。将类型抽象出来作为一个新的参数,type

enum DataType
{GL_BYTE, GL_SHORT, GL_INT,GL_FLOAT,
}
vecn getDataFromVBO(VBO vbo, int vertex_index, int size, DataType type, int stride, int offset)
{type* begin = (type*)(vbo) + offset;	// 起始位置偏移const int vertex_offset = vertex_index * stride; // 第 i 个顶点属性的获取位置vecn result = vecn{begin + vertex_offset, begin + vertex_offset + size}return result;
}

最后,为了更加通用一些,我们将 strideoffset 都以 byte 为单位:

vecn getDataFromVBO(VBO vbo, int vertex_index, int size, DataType type, int stride, int offset)
{void* begin = vbo + offset;	// 起始位置偏移const int vertex_offset = vertex_index * stride; // 第 i 个顶点属性的获取位置const int vertex_size = sizeof(tpye) * size;vecn result = vecn{begin + vertex_offset, begin + vertex_offset + vertex_size}return result;
}

经过上述的调整,从 vbo 获取顶点数据的的伪代码更新为:

void* vbo = some_address;
const int num_vertex = 3;
const int vertex_pos_index = 0;
const int vertex_index_0_size = 3;
const int vertex_index_0_type = GL_FLOAT;
const int vertex_index_0_offset = 0; 
const int vertex_index_0_stride = 3 * sizeof(float);const int vertex_rgb_index = 1;
const int vertex_index_1_size = 3;
const int vertex_index_1_type = GL_FLOAT;
const int vertex_index_1_offset = 9 * sizeof(float)
const int vertex_index_1_stride = 3 * sizeof(float);for(int i = 0; i < num_vertex; ++i)
{vec3_float xyz = getDataFromVBO(vbo, i, vertex_index_1_size,vertex_index_1_type,vertex_index_0_stride,vertex_index_0_offset);vec3_float rgb = getDataFromVBO(vbo, i, vertex_index_1_size,vertex_index_1_type,vertex_index_1_stride,vertex_index_1_offset );auto result = vertex_shader(xyz, rgb);
}

4.VAO 与 VBO 之间的关系

前面三章,我们对从 vbo 中获取顶点属性数据,进而送给 shader 进行渲染的过程进行梳理,发现如果要从显存中顺利拿到数据,需要给定一系列的参数,包括 sizestride 等等,还要指定从哪个 vbo 里拿。

有的时候,我们要渲染的模型很多,如果在使用模型前都进行一遍参数的设置,那这个过程会非常的繁琐。人们就想,能不能用一个对象来存放这些东西,于是就出现了 VAO(Vertex Array Object)。

在 OpenGL 中我们使用 glVertexAttribPointer 来设置顶点属性数组属性和位置,它将顶点属性数组的数据格式和位置存储在当前绑定的 VAO 中,以便在渲染时使用。

如果用伪代码描述 glVertexAttribPointer 做了哪些事情,可能是这样的:

// 定义一个glVertexAttribPointer函数
function glVertexAttribPointer(index, size, type, normalized, stride, offset) {// 获取当前绑定的VAO和VBOvao = glGetVertexArray();vbo = glGetBuffer();// 检查参数的有效性if (index < 0 or index >= MAX_VERTEX_ATTRIBS) {return GL_INVALID_VALUE;}if (size < 1 or size > 4) {return GL_INVALID_VALUE;}if (type not in [GL_BYTE, GL_UNSIGNED_BYTE, GL_SHORT, GL_UNSIGNED_SHORT, GL_INT, GL_UNSIGNED_INT, GL_FLOAT]) {return GL_INVALID_ENUM;}if (stride < 0) {return GL_INVALID_VALUE;}// 将顶点属性数组的数据格式和位置存储在VAO中vao.vertexAttribs[index].enable = true;vao.vertexAttribs[index].size = size;vao.vertexAttribs[index].type = type;vao.vertexAttribs[index].normalized = normalized;vao.vertexAttribs[index].stride = stride;vao.vertexAttribs[index].offset = offset;vao.vertexAttribs[index].buffer = vbo;
}
  • 首先,从 OpenGL Context 中获取当前绑定的 vao 和 vbo
  • vao 中有一个 vertexAttribs 数组,将当前 index 的属性设置到这个数组中

是的,vao 与 vbo 之间的关系就是这么简单:vao 里纪录如何从 vbo 中拿数据的参数。

5. 理解代码

让我们回到代码层面,看看当初那让我不知所云的代码片段,vao 与 vbo 的使用:

	GLuint VBO{0};GLuint VAO{0};glGenVertexArrays(1, &VAO);glGenBuffers(1, &VBO);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);glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void *)(9 * sizeof(float)));glEnableVertexAttribArray(1);

这段代码每个函数我都认识,但函数与函数之间的关系却捋不清。例如 glVertexAttribPointer 其实用到之前绑定的 vao 和 vbo,但函数参数中却没有任何体现,导致这段代码在理解上是“断层”的。主要原因是 OpenGL API 后面隐藏着对 OpengGL Context 属性的修改和访问,这部分是如何实现的,我们是未知的。

现在,为了更好的理解这段代,尝试使用伪代码的形式来说明每个函数都干了啥。

class OpenGLContext
{
public:const int max_num_vao = 256;const int max_num_buffers = 256;std::vector<Buffer> buffers(256);std::vector<VAO> vaos(256);VAO* current_vao;VBO* current_vbo;
}// 全局的 OpenGL Context 对象
OpenGLContext context;
void glGenBuffers(GLsizei n, GLuint * buffers)
{static int count = 0;GLuint* index = new GLuint[n];for(int i = 0; i < n; ++i){index[i] = ++count;}for(int i = 0; i < n; ++i){// create_new_vao 创建一个新的 vao 对象context.buffers[index[i]] = create_new_buffer_ojbect();}buffers = index;
}void glGenVertexArrays(	GLsizei n, GLuint * arrays)
{static int count = 0;GLuint* index = new GLuint[n];for(int i = 0; i < n; ++i){index[i] = ++count;}for(int i = 0; i < n; ++i){// create_new_vao 创建一个新的 vao 对象context.vaos[index[i]] = create_new_vao();}arrays = index;
} void glBindBuffer(GLenum target,GLuint buffer)
{if(target == GL_ARRAY_BUFFER){context.current_vbo = &context.buffers[buffer];}//....
}
void glBufferData(GLenum target,GLsizeiptr size, const void * data, GLenum usage)
{if(target == GL_ARRAY_BUFFER){copy_data_to_vbo(size, data, context.current_vbo);}
}void glVertexAttribPointer(GLuint index,GLint size,GLenum type,GLboolean normalized,GLsizei stride,const void * pointer)
{VBO* vbo = context.current_vbo;VAO* vao = context.current_vao;// 将顶点属性数组的数据格式和位置存储在VAO中vao.vertexAttribs[index].enable = true;vao.vertexAttribs[index].size = size;vao.vertexAttribs[index].type = type;vao.vertexAttribs[index].normalized = normalized;vao.vertexAttribs[index].stride = stride;vao.vertexAttribs[index].offset = offset;vao.vertexAttribs[index].buffer = vbo;
}

通过上述伪代码,你应该可以大致了解 OpenGL API 做哪些事情,它们之间有什么联系。写到这里也写累了,更多解释和说明就不写了,聪明的你应该可以理解的。

6. 总结

本文尝试去向刚入门 OpenGL 的新手解释 VAO 和 VBO 之间的关系,从顶点着色器出发解释了渲染过程中顶点是如何送给 GPU 的;接着引出 vbo 概念,vbo 其实就是指向显存的指针;为了从内存中拷贝数据到显存,我们需要指定很多参数,如果每次渲染一个模型都要重新指定一遍参数,会让整个过程变得很繁琐,由于是引入 vao 对象来存放这些参数,使得只需要设置参数一次就能都重复使用;最后,利用伪代码来解释刚开始那些令人困惑的 OpenGL 函数。

相关文章:

OpenGL - 如何理解 VAO 与 VBO 之间的关系

系列文章目录 LearnOpenGL 笔记 - 入门 01 OpenGLLearnOpenGL 笔记 - 入门 02 创建窗口LearnOpenGL 笔记 - 入门 03 你好&#xff0c;窗口LearnOpenGL 笔记 - 入门 04 你好&#xff0c;三角形 文章目录系列文章目录1. 前言2. 渲染管线的入口 - 顶点着色器2.1 顶点着色器处理过…...

Linux中sed的使用

语法&#xff1a; sed [选项] [sed内置命令字符] [输入文件]选项&#xff1a; 参数说明-n取消默认色的输出常与sed内置命令p一起使用-i直接将修改结果写入文件&#xff0c;不用-i&#xff0c;sed修改的是内存数据-e多次编译&#xff0c;不需要管道符了-r支持正则扩展 sed的内…...

[软件工程导论(第六版)]第1章 软件工程学概述(复习笔记)

文章目录1.1 软件危机1.1.1 软件危机的介绍1.1.2 产生软件危机的原因1.1.3 消除软件危机的途径1.2 软件工程1.2.1 软件工程的介绍1.2.2 软件工程的基本原理1.2.3 软件工程方法学1.3 软件生命周期组成1.4 软件过程概念1.4.1 瀑布模型1.4.2 快速原型模型1.4.3 增量模型1.4.4 螺旋…...

ISP相关

Internet Service Provider&#xff0c;网络提供商/运营商&#xff0c;如电信、联通、移动等。 1. 与ISP互联的出口带宽 IDC或云提供商会与各运营商互联&#xff0c;互联的具体带宽数值一旦泄露&#xff0c;就会被恶意的攻击者利用。例如&#xff0c;若DDos攻击者知道了被攻击…...

vTESTstudio - VT System CAPL Functions - VT2004(续1)

成熟,就是某一个突如其来的时刻,把你的骄傲狠狠的踩到地上,任其开成花或者烂成泥。vtsStartStimulation - 启动激励输出功能&#xff1a;自动激励输出注意&#xff1a;在启动激励输出之前&#xff0c;一定要设置好输出模式Target&#xff1a;目标通道变量空间名称&#xff0c;例…...

WeakMap弱引用

let obj{name:张三} //{name:张三}这个对象能够被读取到&#xff0c;因为obj这个变量名对它的引用 ​ //将引用覆盖掉 objnull //这个对象将会被从内存中移除&#xff0c;因为我们已经失去了对他的所有引用 let obj{name:张三} let arr[obj] ​ objnull //对象{name:张三}不会…...

Springboot 使用quartz 定时任务 增删改查

前段时间公司项目用到了 定时任务 所以写了一篇定时任务的文章 &#xff0c;浏览量还不错 &#xff0c; Springboot 整合定时任务 ) 所以就准备写第二篇&#xff0c; 如果你是一名Java工程师&#xff0c;你也可以会看到如下的页面 &#xff0c;去添加定时任务 定时任务展示 :…...

华为OD机试 - 猜字谜(Python) | 机试题+算法思路 【2023】

最近更新的博客 华为OD机试 - 热点网络统计 | 备考思路,刷题要点,答疑 【新解法】 华为OD机试 - 查找单入口空闲区域 | 备考思路,刷题要点,答疑 【新解法】 华为OD机试 - 好朋友 | 备考思路,刷题要点,答疑 【新解法】 华为OD机试 - 找出同班小朋友 | 备考思路,刷题要点…...

Linux常用命令汇总

1、tcpdump抓包 tcpdump这个命令是用来抓包的&#xff0c;默认情况下这个命令是没有的&#xff0c;需要安装一下&#xff1a; yum install -y tcpdump 使用这个命令的时候最好是加上你网卡的名称&#xff0c;不然可能使用不了&#xff1a; tcpdump -nn -i {网卡名称} 网卡名称…...

1.TCP、UDP区别、TCP/IP七层、四层模型、应用层协议(计网)

文章目录1.OSI 七层模型是什么&#xff1f;每一层的作用是什么&#xff1f;2.TCP/IP 四层模型是什么&#xff1f;每一层的作用是什么&#xff1f;应用层&#xff08;Application layer&#xff09;传输层&#xff08;Transport layer&#xff09;网络层&#xff08;Network lay…...

气敏电阻的原理,结构,分类及应用场景总结

🏡《总目录》 目录 1,概述2,结构3,工作原理4,分类4.1,加热方式分类4.2,材料分类4.3,氧化还原分类5,应用场景6,总结1,概述 气敏电阻是指电阻值随着环境中某种气体的浓度变化而变化的电阻,本文对其工作原理,结构,分类和应用场景进行总结。 2,结构 气敏电阻由防爆…...

实验10 拓扑排序与最短路径2022

A. DS图—图的最短路径&#xff08;无框架&#xff09;题目描述给出一个图的邻接矩阵&#xff0c;输入顶点v&#xff0c;用迪杰斯特拉算法求顶点v到其它顶点的最短路径。输入第一行输入t&#xff0c;表示有t个测试实例第二行输入顶点数n和n个顶点信息第三行起&#xff0c;每行输…...

C/C++每日一练(20230218)

目录 1. 整数转罗马数字 2. 跳跃游戏 II 3. 买卖股票的最佳时机 IV 1. 整数转罗马数字 罗马数字包含以下七种字符&#xff1a; I&#xff0c; V&#xff0c; X&#xff0c; L&#xff0c;C&#xff0c;D 和 M。 字符 数值 I 1 V 5 X …...

【C语言】预编译

&#x1f6a9;write in front&#x1f6a9; &#x1f50e;大家好&#xff0c;我是謓泽&#xff0c;希望你看完之后&#xff0c;能对你有所帮助&#xff0c;不足请指正&#xff01;共同学习交流&#x1f50e; &#x1f3c5;2021年度博客之星物联网与嵌入式开发TOP5&#xff5…...

音频信号处理笔记(一)

相关课程&#xff1a;【音频信号处理及深度学习教程】 文章目录01 信号的时域分析1.1 分帧1.1.1 幅值包络1.1.2 均方根能量0 信号的叠加&#xff1a;https://teropa.info/harmonics-explorer/ 一个复杂信号分解成若干简单信号分量之和。不同个频率信号的叠加: 由于和差化积&a…...

【深度学习】模型评估

上一章——多分类问题和多标签分类问题 文章目录算法诊断模型评估交叉验证测试算法诊断 如果你为问题拟合了一个假设函数&#xff0c;我们应当如何判断假设函数是否适当拟合了&#xff1f;我们可以通过观察代价函数的图像&#xff0c;当代价函数达到最低点的时候&#xff0c;此…...

AcWing《蓝桥杯集训·每日一题》—— 3777 砖块

AcWing《蓝桥杯集训每日一题》—— 3777. 砖块 文章目录AcWing《蓝桥杯集训每日一题》—— 3777. 砖块一、题目二、解题思路三、解题思路本次博客我是通过Notion软件写的&#xff0c;转md文件可能不太美观&#xff0c;大家可以去我的博客中查看&#xff1a;北天的 BLOG&#xf…...

CleanMyMac X软件下载及详细功能介绍

mac平台的知名系统清理应用CleanMyMac在经历了一段时间的测试后&#xff0c;全新设计的X正式上线。与CleanMyMac3相比&#xff0c;新版本的UI设计焕然一新&#xff0c;采用了完全不同的风格。使用Windows电脑时&#xff0c;很多人会下载各类优化软件&#xff0c;而在Mac平台中&…...

pytorch零基础实现语义分割项目(一)——数据概况及预处理

语义分割之数据加载项目列表前言数据集概况数据组织形式数据集划分数据预处理均值与方差结尾项目列表 语义分割项目&#xff08;一&#xff09;——数据概况及预处理 语义分割项目&#xff08;二&#xff09;——标签转换与数据加载 语义分割项目&#xff08;三&#xff09…...

ARM+LINUX嵌入式学习路线

嵌入式学习是一个循序渐进的过程&#xff0c;如果是希望向嵌入式软件方向发展的话&#xff0c;目前最常见的是嵌入式Linux方向&#xff0c;关注这个方向&#xff0c;大概分3个阶段&#xff1a; 1、嵌入式linux上层应用&#xff0c;包括QT的GUI开发 2、嵌入式linux系统开发 3、…...

echart在微信小程序的使用

echart在微信小程序的使用 echarts不显示在微信小程序 <!-- 微信小程序的echart的使用 --> <view class"container"><ec-canvas id"mychart-dom-bar" canvas-id"mychart-bar" ec"{{ ec }}"></ec-canvas> &l…...

51单片机最强模块化封装(5)

文章目录 前言一、创建timer文件,添加timer文件路径二、timer文件编写三、模块化测试总结前言 今天这篇文章将为大家封装定时器模块,定时器是工程项目中必不可少的,希望大家能够将定时器理解清楚并且运用自如。 一、创建timer文件,添加timer文件路径 这里的操作就不过多…...

链表学习之判断链表是否回文

链表解题技巧 额外的数据结构&#xff08;哈希表&#xff09;&#xff1b;快慢指针&#xff1b;虚拟头节点&#xff1b; 判断链表是否回文 要求&#xff1a;时间辅助度O(N)&#xff0c;空间复杂度O(1) 方法1&#xff1a;栈&#xff08;不考虑空间复杂度&#xff09; 遍历一…...

【Linux06-基础IO】4.5万字的基础IO讲解

前言 本期分享基础IO的知识&#xff0c;主要有&#xff1a; 复习C语言文件操作文件相关的系统调用文件描述符fd理解Linux下一切皆文件缓冲区文件系统软硬链接动静态库的理解和制作动静态编译 博主水平有限&#xff0c;不足之处望请斧正&#xff01; C语言文件操作 #再谈文件…...

c++协程库理解—ucontext组件实践

文章目录1.干货写在前面2.ucontext初接触3.ucontext组件到底是什么4.小试牛刀-使用ucontext组件实现线程切换5.使用ucontext实现自己的线程库6.最后一步-使用我们自己的协程库1.干货写在前面 协程是一种用户态的轻量级线程 首先我们可以看看有哪些语言已经具备协程语义&#x…...

英语基础-状语

1. 课前引语 1. 形容词使用场景 (1). 放在系动词后面作表语 The boy is handsome. (2). 放在名词前面做定语 I like this beautiful girl. (3). 放在宾语后面做补语 You make your father happy. 总结&#xff1a;形容词无论做什么&#xff0c;都离不开名词&#xff0c…...

目标检测笔记(八):自适应缩放技术Letterbox完整代码和结果展示

文章目录自适应缩放技术Letterbox介绍自适应缩放技术Letterbox流程自适应缩放Letterbox代码运行结果自适应缩放技术Letterbox介绍 由于数据集中存在多种不同和长宽比的样本图&#xff0c;传统的图片缩放方法按照固定尺寸来进行缩放会造成图片扭曲变形的问题。自适应缩放技术通…...

2023年全国最新高校辅导员精选真题及答案1

百分百题库提供高校辅导员考试试题、辅导员考试预测题、高校辅导员考试真题、辅导员证考试题库等&#xff0c;提供在线做题刷题&#xff0c;在线模拟考试&#xff0c;助你考试轻松过关。 一、选择题 11.李某与方某签订房屋租赁合同期间&#xff0c;李某欲购买租赁房屋&#xff…...

【Python】Python读写Excel表格

简要版&#xff0c;更多功能参考资料1。1 Excel文件保存格式基础概念此处不提&#xff0c;详见资料1。Excel的文件保存格式有两种&#xff1a; xls 和 xlsx。如果你看不到文件后缀&#xff0c;按下图设置可见。xls是Office 2003及之前版本的表格的默认保存格式。xlsx 是 Excel …...

Python每日一练(20230218)

目录​​​​​​​ 1. 旋转图像 2. 解码方法 3. 二叉树最大路径和 1. 旋转图像 给定一个 n n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。 你必须在原地旋转图像&#xff0c;这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像…...

软装设计网站排名/北京百度竞价

关于ADO.NET ADO.NET是微软提供的一种数据库访问方式。他使得.NET程序员对于不同的数据库都能采用相同的访问方式。 Connection 连接 Connection是一个数据库连接类&#xff0c;他负责打开&#xff0c;关闭数据库连接。 和数据库交互&#xff0c;就必须连接他。他让后续的对象&…...

做网站的注意什么问题/房地产市场现状分析

更多Java全套学习资源均在专栏&#xff0c;持续更新中↑↑戳进去领取~ &#x1f357;MySQL的安装及登陆基本操作&#xff08;附图&#xff09;手把手带你安装 &#x1f357;MySQL基础&#xff1a;通过SQL对数据库进行CRUD &#x1f357;MySQL基础&#xff1a;通过SQL对表、数据…...

做网站的抬头标语怎么/网络营销软件

偶然间在微信公众号奇舞周刊上看到了这篇文章《CSS Painting API》&#xff0c;算是对 conic-gradient的初次见面。 后来有空的时候&#xff0c;百度搜了一下&#xff0c;看了这篇文章《CSS神奇的conic-gradient圆锥渐变》后&#xff0c;对conic-gradient有了深刻的了解。 概述…...

全国企业信用信息查询/东莞网络优化哪家公司好

可以添加一列&#xff0c;命名为NEW&#xff0c;此列的值用随机函数表示。 然后选中NEW列&#xff0c;点A-Z&#xff0c;此时EXCEL会让我们选择是扩展其他列&#xff0c;还是仅排序当前选中列&#xff0c;选扩展。 然后其他列就被随机排序了。 注意到&#xff1a;由于排序后&am…...

wordpress 侧边栏跟随/现在广告行业好做吗

js图片上传组件&#xff1a;基本要求&#xff1a;1.上传的图片可预览&#xff0c;可删除&#xff0c;可被覆盖更新2.要求图片格式为jpg和png&#xff0c;大小不能超过2M新加需求&#xff1a;1.模拟回显&#xff0c;可用本地存储&#xff08;实际上的回显是通过后台传过来的url&…...

文章类型网站/网站seo优化

&#xff08;1&#xff09;r分量更大&#xff0c;则为红色 &#xff08;2&#xff09;判断深浅&#xff1a; 亮度越亮&#xff0c;则颜色越浅。 if(r*0.299 g*0.578 b*0.114 > T){ //浅色... }else{ //深色... }...