美文网首页OpenGL
Modern OpenGL - 渲染矩体/矩形体/立方体/正方体

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

作者: IcyllisMilica | 来源:发表于2021-03-14 13:02 被阅读0次

    本文简述如何在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/

    相关文章

      网友评论

        本文标题:Modern OpenGL - 渲染矩体/矩形体/立方体/正方体

        本文链接:https://www.haomeiwen.com/subject/slpxcltx.html