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

news2024/9/29 9:22:00

系列文章目录

  • 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> 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 函数。

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

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

相关文章

关于IIC通讯协议的有关问题

问题&#xff1a;如何正确使用IIC这么优秀的通讯协议呢&#xff1f;解决:第一步&#xff1a;知道起始信号和终止信号当SCL为1的时候&#xff0c;SDA从1变成0&#xff0c;这个就是起始信号&#xff0c;说明可以开始传输&#xff1b;当SCL为1的时候&#xff0c;SDA从0变成1&#…

Spire.Office for Java 8.2.0 强大之喜Xaspose

Spire.Office for Java是由E-iceblue开发的一款Java系列的Office文档操作类库&#xff0c;用于操作MS Word、PDF、MS Power Point 以及条形码&#xff0c;可实现文档创建、编辑、转换、打印等功能&#xff0c;是E-iceblue 所有 Java组件&#xff0c;包括Spire.Doc for Java、Sp…

Java爬虫入门——HttpClient,JSoup

一&#xff0c;网络爬虫介绍爬虫也叫网络机器人&#xff0c;可以代替人工&#xff0c;自动的在网络上采集和处理信息。爬虫包括数据采集&#xff0c;分析&#xff0c;存储三部爬虫引入依赖<!--引入httpClient依赖--><dependency><groupId>org.apache.httpcom…

Netty服务端请求接受过程源码剖析

目标 服务器启动后&#xff0c;客户端进行连接&#xff0c;服务器端此时要接受客户端请求&#xff0c;并且返回给客户端想要的请求&#xff0c;下面我们的目标就是分析Netty 服务器端启动后是怎么接受到客户端请求的。我们的代码依然与上一篇中用同一个demo&#xff0c; 用io.…

Tektronix TAP3500/泰克TAP3500有源探头

产品概览 泰克 TAP3500 有源探头&#xff0c;2 .5 GHz 泰克 TAP3500 单端有源 FET 探头是一种多功能且易于使用的探头&#xff0c;可提供数字系统设计所需的高速电气和机械性能。泰克 TAP3500 探头专为使用和连接到 TekVPI™ 探头接口而设计。 泰克 TAP3500 有源探头的特性和规…

带你认识一下什么是函数式接口Comparator

函数式接口Comparator 1、函数式接口是什么&#xff1f; 所谓的函数式接口&#xff0c;实际上就是接口里面只能有一个抽象方法的接口。Comparator接口就是一个典型的函数式接口&#xff0c;它只有一个抽象方法compare。 有人会说equales方法也没有方法体&#xff0c;也是抽象…

江苏专转本考试倒计时,该如何自救?

专转本考试倒计时&#xff0c;该如何自救&#xff1f;第一点&#xff1a;回归考纲教材&#xff0c;刷题。 最后一段时间&#xff0c;一定要回归考纲及教材&#xff01;要把知识点看细&#xff0c;看明白。 另外大家也可以根据历年考试题对知识点进行系统地复习和梳理&#xff0…

【java】Spring Boot --深入SpringBoot注解原理及使用

步骤一 首先&#xff0c;先看SpringBoot的主配置类&#xff1a; SpringBootApplication public class StartEurekaApplication {public static void main(String[] args){SpringApplication.run(StartEurekaApplication.class, args);} }步骤二 点进SpringBootApplication来…

线程间通信的常用方式

线程间通信的常用方式 1.简介 线程通信简单来说就是实现线程的交替工作&#xff0c;传递信息。例如在一个方法中我有两个线程A和B在运行&#xff0c;我希望线程A先向一个集合里面循环新增数据&#xff0c;当增加到第五次的时候&#xff0c;线程B才开始执行其他的操作。 线程间…

博客系统测试用例

博客系统测试用例目录博客系统测试用例博客系统删除功能测试用例 (判定表)提交BUG 1测试用例 1测试用例 2提交BUG提交BUG 2博客系统测试用例 博客系统删除功能测试用例 (判定表) # 首先确定输入条件与输出条件 输入条件: 博客作者, 非博客作者, 点击删除博客输出条件: 删除成…

web移动端:rem适配布局(重点)

目录 1. rem基础 2.媒体查询 2.1 媒体查询的概念 2.2 语法规范 2.2.1 2.2.2 关键字 2.2.3 媒体特性 2.2 根据页面宽度改变颜色 2.3 媒体查询rem 实现元素动态大小变化 2.4 引入资源&#xff08;理解&#xff09; 2.4.1 语法规范 3. less基础 3.1 css弊端 3.2 less介绍…

基于SuperPoint与SuperGlue实现图像配准

基于SuperPoint与SuperGlue实现图像配准&#xff0c;项目地址https://github.com/magicleap/SuperGluePretrainedNetwork&#xff0c;使用到了特殊算子grid_sample&#xff0c;在转onnx时要求opset_version为16及以上&#xff08;即pytorch版本为1.9以上&#xff09;。SuperPoi…

计讯物联污染源自动监控系统,坚守“绿水青山就是金山银山”

近年来&#xff0c;“绿水青山就是金山银山”的理念在全国各地落地生根&#xff0c;各大城市积极构建环境监测体系&#xff0c;旨在让生态文明成色更足&#xff0c;绿色发展底色更亮。计讯物联污染源自动监控系统作为生态环境部门监督企业排污的“火眼金睛”&#xff0c;充分运…

apifox持续集成+java+企微机器人+xxljob定时推送

总览&#xff1a; apifox做接口测试后&#xff0c;把用例合并组装成测试套件&#xff0c;然后apifox-cli通过终端命令实现把套件执行后&#xff0c;输出本地文件的测试报告html或json。本地解析后拿到有用的解决通过定时执行推送到企微群里。 然后把html一起推到群里。 这个…

【Spark分布式内存计算框架——Spark SQL】8. Shuffle 分区数目、Dataset(上)

4.4 Shuffle 分区数目 运行上述程序时&#xff0c;查看WEB UI监控页面发现&#xff0c;某个Stage中有200个Task任务&#xff0c;也就是说RDD有200分区Partition。 原因&#xff1a;在SparkSQL中当Job中产生Shuffle时&#xff0c;默认的分区数&#xff08;spark.sql.shuffle.p…

基于STM32采用CS创世 SD NAND(贴片SD卡)完成FATFS文件系统移植与测试

一、前言 在STM32项目开发中&#xff0c;经常会用到存储芯片存储数据。 比如&#xff1a;关机时保存机器运行过程中的状态数据&#xff0c;上电再从存储芯片里读取数据恢复&#xff1b;在存储芯片里也会存放很多资源文件。比如&#xff0c;开机音乐&#xff0c;界面上的菜单图…

Selenium + python自动化测试环境搭建

selenium 是一个web的自动化测试工具&#xff0c;不少学习功能自动化的同学开始首选selenium &#xff0c;相因为它相比QTP有诸多有点&#xff1a; 免费&#xff0c;也不用再为破解QTP而大伤脑筋 小巧&#xff0c;对于不同的语言它只是一个包而已&#xff0c;而QTP需要下载安…

JSON字符串解析

目录 依赖 方法 示例 判断JSON是否合格 依赖 方法 JSON.parseObject() JSON.parseArray() 示例 Data public class OrderVo {public String name;public Integer price;public Integer count; } JSON数据 { "name": "苹果手机", "pric…

BIT.8_Linux 多线程

目录Linux线程概念什么是线程线程的优点线程的缺点线程异常线程用途Linux进程VS线程进程和线程总结Linux线程控制POSIX线程库创建线程线程ID及进程地址空间布局进程和线程ID区别内核层面&#xff1a;pid & tgid线程终止线程等待__thread 和 pthread_self()分离线程Linux线程…

《爆肝整理》保姆级系列教程python接口自动化(十七)--Json 数据处理---一次爬坑记(详解)

简介 有些 post 的请求参数是 json 格式的&#xff0c;这个前面发送post 请求里面提到过&#xff0c;需要导入 json模块处理。现在企业公司一般常见的接口因为json数据容易处理&#xff0c;所以绝大多数返回数据也是 json 格式的&#xff0c;我们在做判断时候&#xff0c;往往只…