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

news2024/11/18 19:33:14

系列文章目录

  • 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 330
    layout (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 10

vector<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 330
    layout (location = 0) in vec3 aPos; // 顶点位置数据
    void main()
    {
        // ...
    }
)";

const char *vertexShaderSource_two_input = R"(
    #version 330
    layout (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 330
    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;
    }
)";

假设现在顶点数据包括位置和颜色,全部放在一个 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,交织型为 6

const int vertex_rgb_index = 1;
const int vertex_index_1_offset = 9	 // 平面型为 9,交织型为 3
const int vertex_index_1_stride = 3; // 平面型为 3,交织型为 6

for(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和VBO
  vao = 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> vbos(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 函数。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/353057.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Day891.一主多从的切换正确性 -MySQL实战

一主多从的切换正确性 Hi&#xff0c;我是阿昌&#xff0c;今天学习记录的是关于一主多从的切换正确性的内容。 在切换任务的时候&#xff0c;要先主动跳过这些错误&#xff0c;通过主动跳过一个事务或者直接设置跳过指定的错误&#xff0c;用GTID解决找同步位点的问题 大多…

oracle查找各PDB密码过期账户

连接oracle的时候&#xff0c;又报 ORA-12516: TNS: 监听程序找不到符合协议堆栈要求的可用处理程序 的错误。这种现象之前遇到不少&#xff0c;猜测可能又是某个sde账号密码过期或快过期&#xff0c;导致arcgis不停地连数据库&#xff0c;因而连接耗尽了。详见拙作&#xff1a…

电脑c盘满了怎么清理,c盘空间清理

电脑c盘满了怎么清理&#xff1f;电脑C盘满了可能是因为您的操作系统、程序文件、下载文件、临时文件、垃圾文件等占用了太多的存储空间。所以&#xff0c;我们就需要进行一些操作和清理。 一.清理电脑C盘的方法 清理临时文件和垃圾文件。在Windows上&#xff0c;您可以使用系…

windows10 安装DOSbox_32 debug.exe

windows10 安装DOSbox_32 debug.exe1.下载2. 安装DOSBox0.74-3-win32-installer.exe3. 配置DOSBox3. 启动DOSBox.exe4. 测试执行debug命令1.下载 DOSBox0.74-3-win32-installer.exe安装包debug.exe 2. 安装DOSBox0.74-3-win32-installer.exe 解压 双击DOSBox0.74-3-win32-insta…

QT+OPenGL模型加载 - Assimp

QTOPenGL模型加载 - Assimp 本篇完整工程见gitee:QtOpenGL 对应点的tag&#xff0c;由turbolove提供技术支持&#xff0c;您可以关注博主或者私信博主 模型加载 先来张图&#xff1a; 我们不大可能手工定义房子、汽车或者人形角色这种复杂形状所有的顶点、法线和纹理坐标。我…

【surfaceflinger源码分析】surface与surfaceflinger之间的关系

本篇文章带着以下问题继续分析surfaceflinger的源码: 什么是surface ? surface与图形数据之间是什么关系&#xff1f;surface和surfaceflinger之间是什么关系&#xff1f; Surface定义 先看看Surface这个类的定义&#xff0c;主要是定义了很多与GraphicBuffer相关的操作。 …

k8s(存储)数据卷与数据持久卷

为什么需要数据卷&#xff1f; 容器中的文件在磁盘上是临时存放的&#xff0c;这给容器中运行比较重要的应用程序带来一些问题问题1&#xff1a;当容器升级或者崩溃时&#xff0c;kubelet会重建容器&#xff0c;容器内文件会丢失问题2&#xff1a;一个Pod中运行多个容器并需要共…

创邻科技荣获人行旗下《金融电子化》年度大奖

近日&#xff0c;创邻科技收到由中国人民银行旗下《金融电子化》杂志社寄来的奖牌。 在《金融电子化》杂志社主办的第十三届金融科技应用创新奖中&#xff0c;创邻科技凭借“原生分布式图数据库Galaxybase解决方案”&#xff0c;从近400个参报案例中脱颖而出&#xff0c;荣获“…

Linux下zabbix_proxy实施部署

简介 zabbix proxy 可以代替 zabbix server 收集性能和可用性数据,然后把数据汇报给 zabbix server,并且在一定程度上分担了zabbix server 的压力. zabbix-agent可以指向多个proxy或者server zabbix-proxy不能指向多个server zabbix proxy 使用场景: 1&#xff0c;监控远程区…

【React全家桶】reac组件通信

&#x1f39e;️&#x1f39e;️&#x1f39e;️ 博主主页&#xff1a; 糖 &#xff0d;O&#xff0d; &#x1f449;&#x1f449;&#x1f449; react专栏&#xff1a;react全家桶 &#x1f339;&#x1f339;&#x1f339;希望各位博主多多支持&#xff01;&#xff01;&a…

自媒体市场规模由2015年的296亿元增涨至2021年的2500亿元

自媒体&#xff0c;又称“个人媒体”&#xff0c;是指大众化、自主化的传播者以图文、音频或视频内容等各类形式向公众发布信息内容。随着&#xff15;&#xff27;时代的来临和智能设备的性能逐渐提高&#xff0c;网络基础环境得到很大的提升&#xff0c;自媒体开始了新的发展…

QT的下载和安装

这里介绍的是QT官方方式下载&#xff0c;每次都让我很糊涂&#xff0c;就记载一下。先是下载QT online installerhttps://www.qt.io/download 在下方有Go Open Sourcehttps://www.qt.io/download-open-source 在下方有Download the Qt Online installerhttps://www.qt.io/downl…

C#(NET Core3.1 MVC)生成站点地图(sitemap.xml)

要做SEO的肯定绕不开站点地图sitemap.xml。这玩意其实不难我也在搞写下来备忘一下也给新人指指路。 我先把代码放出来备忘下 #region CreateSiteMapXml/// <summary>/// /// </summary>/// <returns></returns>[Route("/art/CreateSiteMapXml&qu…

Windows下安装启动nginx.exe报错

Windows下安装启动nginx.exe报错 前言&#xff1a; 问题1&#xff1a; 在安装使用nginx服务器时遇到最大的问题是windows下命令行输入start nginx后&#xff0c;或者双击nginx.exe&#xff0c;一闪而过&#xff0c;启动不了&#xff0c;怀疑是以下几个方面的问题&#xff0c;…

高效率工作之关于进入心流的方法

本文是向大家介绍高效率工作体验——心流&#xff01;心流也叫最优体验&#xff0c;是一位叫米哈里的心理学家在调查研究的基础上提出的概念。心流指的是一种将大脑注意力毫不费力地集中起来的状态&#xff0c;这种状态使人忘记时间的概念&#xff0c;忘掉自我&#xff0c;也忘…

商家必读!超店有数分享,tiktok达人营销变现如何更快一步?

近几年来&#xff0c;“粉丝经济”发展越来越迅猛&#xff0c;“网红带货”已经成为了一种营销的方式。这种方式让商家能基于达人的影响下迅速抢占自己的私域流量池。消费者会基于对达人的信任&#xff0c;购买达人推荐的产品。达人效应可以助力品牌走出营销困境。如果商家想要…

cmake 引入第三方库(头文件目录、库目录、库文件)

程序的编写需要用到头文件&#xff0c;程序的编译需要lib文件&#xff0c;程序的运行需要dll文件&#xff0c;因此cmake引入第三方库其实就是将include目录、lib目录、bin目录引入工程。 目录 1、find_package&#xff08;批量引入库文件和头文件&#xff09; 2、include_dir…

什么是禅道?禅道可以做什么?如何自动推送禅道消息?

什么是禅道&#xff1f;禅道是一款国产的开源项目管理软件。它的核心管理思想基于敏捷方法 scrum&#xff0c;内置了产品管理和项目管理&#xff0c;同时又根据国内研发现状补充了测试管理、计划管理、发布管理、文档管理、事务管理等功能&#xff0c;在一个软件中就可以将软件…

北京地铁口免费的大白鹅,你领了吗?

最近这几天&#xff0c;北京很多个地铁口周围涌现了许多只又白又肥的大鹅。 不过&#xff0c;此大鹅非铁锅之中的炖大鹅&#xff0c;乃是又呆又萌的大鹅玩偶。 而且&#xff0c;还是免费送的&#xff01; 所以每天晚上的下班时间&#xff0c;在一群掐着细长的大鹅脖子的地推人…

02:入门篇 - 漫谈 CTK

作者: 一去、二三里 个人微信号: iwaleon 微信公众号: 高效程序员 十万个为什么 五千个在哪里?七千个怎么办?十万个为什么?。。。生活中,有很多奥秘在等着我们去思考、揭示! 同样地,在使用 CTK 时,很多小伙伴一定也存在诸多疑问: 为什么 CTK Plugin Framework 要借…