Modern OpenGL - 渲染矩体/矩形体/立方体/正方体/长方体

本文简述如何在OpenGL 4.3以上渲染一个矩体(cuboid),有时也称为 矩形体/立方体/正方体/长方体

前言

Minecraft 中的模型由一个个cube组成,这里的cube不是正方体而是矩体,也就是6个面中的每个面都是矩形。
在 Minecraft 1.16 中,一个cube包含6个quad。假定我们渲染一个普通的完整不透明方块,即一个正方体,那么这个模型的6个面中的每个面有一个quad。一个quad即一个矩形,包含了4个顶点的属性(attribute),因此一个cube便有 4 * 6 = 24 个顶点。
Minecraft 会创建 VBO,将这些 attributes 批量传入OpenGL内置shader,即使用固定管线,传入投影矩阵(projection)并使用 glDrawArraysGL_QUADS 模式下渲染。
那么我们如何在 Modern OpenGL(core-profile)中渲染这个矩体呢?


1. Vertex Buffer Object

VBO(Vertex Buffer Object)即顶点缓冲对象,规范于OpenGL 1.5 (2003)。VBO是在显存中分配出的一块内存空间,用于存放大量顶点的属性(attribute),如坐标 position、颜色 color、法向量 normal、纹理映射 uv、光照映射 lightmap等。渲染时,由于VBO的数据是存储在显存中而不是内存中,不需要从CPU向GPU传入数据,GPU可以直接从VBO中按顺序读取数据,且这些数据是对齐的,性能更高。

创建并使用VBO

所有的缓冲对象都由下面的方法所生成一个ID表示:

GLuint VBO_id; // a generated buffer object name
glGenBuffers(/* number */ 1, &VBO_id);

OpenGL中有各种类型的缓冲对象,VBO所对应的类型为 GL_ARRAY_BUFFER。我们可以使用 glBindBuffer 同时绑定多个不同类型的缓冲对象。下面的方法可以将当前的 GL_ARRAY_BUFFER 绑定到 VBO_id,当第二个参数为 0 时,即解除绑定(unbind):

glBindBuffer(/* target */ GL_ARRAY_BUFFER, VBO_id);

绑定之后,我们对所有 targetGL_ARRAY_BUFFER 的操作都会应用到当前所绑定的缓冲对象上,即我们刚刚生成的 VBO_id。接着我们使用 glBufferData 来复制内存中的数据到缓冲对象中去,即显存里:

glBufferData(/* target */ GL_ARRAY_BUFFER, /* size */ sizeof(vertices), /* data */ vertices, /* usage */ GL_DYNAMIC_DRAW);

这里,size 即数据的大小,单位为字节。usage 即用途,由频率(frequency)和性质(nature)两部分组成,每个部分有三种选择,可以组合出9种用途。

频率包括:

  • STREAM
    数据的内容只能修改一次,并至多使用一次。
  • STATIC
    数据的内容只能修改一次,并可以使用很多次。
  • DYNAMIC
    数据的内容可以反复修改,也可以使用很多次。

性质包括:

  • DRAW
    数据的内容由CPU传入,在GPU中获得该数据用于渲染和图像相关指令。
  • READ
    数据的内容来自于GPU自身,而在CPU中获得该数据使用。
  • COPY
    数据的内容来自于GPU自身,并在GPU中用于渲染和图像相关指令。

不同的 usage 所分配的内存空间是不同的,这里我们的VBO用于实时渲染,并且数据内容可能会发生变化(比如坐标),因此我们选择 GL_DYNAMIC_DRAW

2. 连接顶点属性

在顶点着色器(vertex shader)中我们可以定义输入的每个顶点属性的变量名与类型。

  • 这里所说的顶点属性为通用顶点属性(generic vertex attribute),相比之下固定管线中存在内置顶点属性,但这些在可编程管线中已全部废弃,所以我们通常说的顶点属性都指通用顶点属性,也就是我们自己定义的。

我们必须要指定我们VBO的数据是如何布局的,这样着色器才能正确地得到每个顶点的每个属性,这就需要使用 glVertexAttribPointer

假设我们的顶点属性中只包含一个坐标,类型为 vec3,它的布局如下:

vertex_attribute_pointer.png

一个 vec3 包含3个分量(component),即 x, y, z 分量。如果每个顶点的属性之间没有额外空间,如 pos1,pos2,color1,color2 称为紧密排布(tightly packed)。而对于 pos1,color1,pos2,color2 则称为交错排布(interleaved),需要指定 stride。通常情况下,使用交错排布性能更好,因其内存对齐和序列化存储,但会占用更多显存空间。

glVertexAttribPointer(/* index */ 0, /* size */ 3, /* type*/ GL_FLOAT, /* normalized */ GL_FALSE, /* stride */ 3 * sizeof(float), (void*) 0);
glEnableVertexAttribArray(0);
  • index - 指定了要配置的通用顶点属性的索引。假设我们在顶点着色器中指定的坐标属性带有 layout (location = 0),我们这里就要输入 0。
  • size - 指定了该顶点属性的大小,即分量数,该值只能是1,2,3,4。这里使用的是一个 vec3,所以分量数是 3。
  • type - 指定了VBO数据中对应此属性的类型,这里是 GL_FLOAT。可选的值有 GL_BYTE, GL_UNSIGNED_BYTE, GL_SHORT, GL_UNSIGNED_SHORT, GL_INT, GL_UNSIGNED_INT, GL_HALF_FLOAT, GL_FLOAT, GL_DOUBLE, GL_FIXED, GL_INT_2_10_10_10_REV, GL_UNSIGNED_INT_2_10_10_10_REVGL_UNSIGNED_INT_10F_11F_11F_REV
  • normalized - 指定了数据是否要被标准化。如果数据类型的符号型数值,则转换为[-1, 1]的浮点值,如果是无符号型数值,则转换为[0, 1]的浮点值。如果不标准化,则直接转换为浮点值。坐标不需要标准化,所以这里输入 GL_FALSE
  • stride - 指定了每组顶点属性数组的长度,这里只有循环交错的三个浮点数,所以步数为 3 * 4 = 12 字节。当我们确定顶点属性是紧密排布的,可以传入 0。当有更多复杂的顶点属性时,我们可能需要添加padding来保证数据对齐。
  • pointer - 指定了该属性的第一个值在每个顶点属性数组的偏移量,这里的坐标就在数组的开始,所以传入 0。

注意:glVertexAttribPointer 最终都会将数据转成浮点型。如果使用 glVertexAttribIPointer,则会转成整数类型,并且 type 只能 GL_BYTE, GL_UNSIGNED_BYTE, GL_SHORT, GL_UNSIGNED_SHORT, GL_INTGL_UNSIGNED_INT。这个方法不含 normalized 参数。

调用 glVertexAttribPointer 后,所有的这些配置会被保存成一个状态,这个状态也包括当前所绑定的VBO,这样以后就会从这个VBO中读取数据。如果使用VAO,则该状态会保存进VAO,这样就不再需要绑定VBO,即省事也能提高性能。
所有顶点属性默认都是关闭的,需要使用 glEnableVertexAttribArray 来给出它的 location,即通用顶点属性的索引。

3. Element Buffer Object

EBO(Element Buffer Object)即元素缓冲对象,或称为索引缓冲对象,规范于OpenGL 1.5 (2003)。假设我们要渲染的矩体中的顶点属性只包含坐标,那么事实上我们只需要8个坐标而不是24个,这样8个顶点就可以满足需求,同时可以节省显存占用,提高利用率和性能。EBO存储了一个组索引,OpenGL能按照这种顺序使用顶点,这也称为索引绘制(indexed drawing)。

GLuint EBO_id;
glGenBuffers(/* number */ 1, &EBO_id);

与VBO类似,我们需要绑定EBO并使用 glBufferData 上传数据,但这里的 targetGL_ELEMENT_ARRAY_BUFFER。EBO的数据一般不需要修改,所以这里的 usage 使用 GL_STATIC_DRAW

glBindBuffer(/* target */ GL_ELEMENT_ARRAY_BUFFER, EBO_id);
glBufferData(/* target */ GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, /* usage */ GL_STATIC_DRAW); 

4. Vertex Array Object

VAO(Vertex Array Object)即顶点数组对象,规范于OpenGL 3.0 (2008)。在Modern OpenGL(core-profile)中必须绑定VAO才能进行渲染。VAO可以保存我们的顶点属性配置,这样我们只需要配置好一个VAO,就不需要在每次渲染时调用 glVertexAttribPointer 等操作,并且可以轻松在不同的VBO或者是相同的VBO中使用不同的顶点属性配置之间进行切换。当绑定VAO时,会切换 glEnableVertexAttribArrayglDisableVertexAttribArray 状态,进行调用 glVertexAttribPointer 的顶点属性配置,并使用某个EBO。

注意,VAO只会与某个EBO相关联而不会与某个VBO相关联,它会与在绑定VAO时调用 glVertexAttribPointer 的顶点属性配置时所绑定的VBO相关联,也就是那时的 GL_ARRAY_BUFFER,所以不同的VAO可以使用相同的VBO,同时也能使用不同的顶点属性配置。此外,顶点属性默认就是关闭的,所以在某个的VAO内我们只需使用 glEnableVertexAttribArray 而几乎无需使用 glDisableVertexAttribArray,除非需要重新配置顶点属性。

生成一个VAO:

GLuint VAO_id;
glGenVertexArrays(/* number */ 1, &VAO_id);

接着使用 glBindVertexArray 绑定并配置VAO,配置好后解绑表示不再配置该VAO,当需要渲染时,我们只需重新绑定这个VAO即可。注意,VAO只需要在渲染前创建并配置好,通常不在渲染循环中创建。

// 1. first bind our VAO
glBindVertexArray(VAO_id);
// 2. bind VBO for vertex attribute configuration, VBO data has already been uploaded.
glBindBuffer(GL_ARRAY_BUFFER, VBO_id);
// 3. connect a EBO with VAO, EBO data has already been uploaded.
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO_id);
// 4. then set our vertex attributes pointers
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*) 0);
glEnableVertexAttribArray(0);
// 5. unbind and finish creation
glBindVertexArray(0);

开始渲染工作

假定渲染这样一个正方体,0为坐标原点,01的方向向量为x03的方向向量为y40的方向向量为z

cube_8vertices.png

则顶点数据:

float vertices[] = {
     0.0f,  0.0f,  0.0f,  // 0
     1.0f,  0.0f,  0.0f,  // 1
     1.0f,  1.0f,  0.0f,  // 2
     0.0f,  1.0f,  0.0f   // 3
     0.0f,  0.0f, -1.0f,  // 4
     1.0f,  0.0f, -1.0f,  // 5
     1.0f,  1.0f, -1.0f,  // 6
     0.0f,  1.0f, -1.0f   // 7
};

OpenGL中渲染任何图形最终只会转换为3种:点,线,三角形。所以我们要渲染正方体的6个面,每个面都是一个矩形,则由两个三角形组成,一共需要渲染12个三角形,即 modeGL_TRIANGLES。一个三角形需要3个顶点,在OpenGL中默认以逆时针方向为正面,OpenGL 2.0起可以通过 glFrontFace 来设置顺时针还是逆时针方向为正面,这里我们遵循默认行为,则EBO数据:

unsigned int indices[] = {
    0, 1, 3, // front
    3, 1, 2, 
    1, 5, 2, // right
    2, 5, 6, 
    5, 4, 6, // back
    6, 4, 7, 
    4, 0, 7, // left
    7, 0, 3, 
    3, 2, 7, // top
    7, 2, 6, 
    0, 4, 1, // bottom
    1, 4, 5
};

接着我们再使用 glDrawElements 来进行绘制。因为我们在VAO中已经绑定了EBO,所以 indices 需要传入所使用的EBO数据的指针即可,即起始偏移量(如果没绑定EBO,则为EBO数组的指针)。type 则指定了EBO的数据类型。VBO只包含一个正方体的数据,所以 count 为 3 * 12 = 36。则渲染循环:

glUseProgram(shaderProgram);
glBindVertexArray(VAO_id);
glDrawElements(/* mode */ GL_TRIANGLES, /* count */ 36, /* type */ GL_UNSIGNED_INT, /* indices */ 0)
glBindVertexArray(0);

编写shader、投影矩阵、坐标系等较为复杂,这里暂不介绍。回到 Minecraft 中,如果每个面使用不同的纹理,我们还是需要24个顶点,即使有3个顶点的坐标值是相同的,但是其他属性不同,因此不同情况下要采取不同的策略,比如把所有顶点坐标存在一个VBO,其他数据存在另一个VBO等。

本文部分内容翻译自https://learnopengl.com/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343

推荐阅读更多精彩内容