OpenGL学习24——实例化渲染

实例化(Instancing)渲染

  • 在开发过程中,可能存在这样的情形,一个场景中有很多顶点数据一样的模型,但是进行了不同的世界坐标转换。虽然每个模型可能只是由少量三角形片元组成,能快速完成渲染。但是成千上万个模型的渲染调用会极大降低程序性能。如果我们对大量物体进行渲染,代码大致如下:
for (unsigned int i = 0; i < amount_of_models_to_darw; i++)
{
    DoSomePreparations();   // bind VAO, bind Textures, set uniform etc.
    glDrawArrays(GL_TRIANGLES, 0, amount_of_vertices);
}
  • 像上述这种方式调用glDrawArraysglDrawElements函数会快速地消耗性能,因为OpenGL必须进行必要的准备才能执行顶点数据绘制。即使渲染顶点数据很快,但是传递指令到GPU执行渲染则不是。
  • 实例化(Instancing):是使用单个渲染调用一次性绘制多个物体(等同于网格数据)的技术,为我们节省每次需要渲染一个物体时CPU->GPU之间的通讯时间。要使用实例化渲染,我只需将绘制调用更换为glDrawArraysInstancedglDrawElementInstanced函数。
  • 实例化渲染函数本身有点多余,因为如果在同一个位置渲染同一物体一千次,我们只能看到一个物体。因此GLSL在顶点着色器添加了另外一个内置变量gl_InstanceIDgl_InstanceID从0开始每执行一次实例化渲染调用则递增一次。
  • 为了感受实例化渲染,我们接下来展示一个简单例子,使用一次渲染调用以标准设备坐标渲染一百个2D四方形。每个四方形包含两个三角形,总共6个顶点,数据如下:
float quadVertices[] = {
    // position     // colors
    -0.05f,  0.05f, 1.0f, 0.0f, 0.0f,
     0.05f, -0.05f, 0.0f, 1.0f, 0.0f,
    -0.05f, -0.05f, 0.0f, 0.0f, 1.0f,

    -0.05f,  0.05f, 1.0f, 0.0f, 0.0f,
     0.05f, -0.05f, 0.0f, 1.0f, 0.0f,
     0.05f,  0.05f, 0.0f, 1.0f, 1.0f
}
  • 片元着色器中从顶点着色器接收颜色矢量。
#version 330 core
out vec4 FragColor;

in vec3 fColor;

void main()
{
    FragColor = vec4(fColor, 1.0);
}
  • 顶点着色器。
#version 330 core
layout (location = 0) in vec2 aPos;
layout (location = 1) in vec3 aColor;

out vec3 fColor;

uniform vec2 offsets[100];

void main()
{
    vec2 offset = offsets[gl_InstanceID];
    gl_Position = vec4(aPos + offset, 0.0, 1.0);
    fColor = aColor;
}
  • 使用循环生成偏移位置矢量数组。
glm::vec2 translations[100];
int index = 0;
float offset = 0.1f;
for (int y = -10; y < 10; y +=2)
{
    for (int x = -10; x < 10; x+=2)
    {
        glm::vec2 translation;
        translation.x = (float)x / 10.0f + offset;
        translation.y = (float)y / 10.0f + offset;
        translations[index++] = translation;
    }
}
  • 设置偏移位置矢量数组。
shader.use();
for (unsigned int i = 0; i < 100; i++)
{
    shader.setVec2(("offsets[" + std::to_string(i) + "]"), translations[i]);
}
  • 渲染效果。


    实例化渲染四方形

1. 实例数组

  • 当我们绘制的物体对象远远超过100个,那么顶点着色器中uniform偏移数组将很快达到我们可以发送uniform数据到着色器的极限。这时我们可以使用另外一种方法:实例化数组(Instanced arrays)。实例化数组被定义为每个实例而不是每个顶点进行更新的顶点属性
  • 下面我们将前面的示例修改为使用实例化数组的方法,首先我们调整顶点着色器,添加一个顶点属性,去掉uniform数组。
#version 330 core
layout (location = 0) in vec2 aPos;
layout (location = 1) in vec3 aColor;
layout (location = 2) in vec2 aOffset;

out vec3 fColor;

void main()
{
    gl_Position = vec4(aPos + aOffset, 0.0, 1.0);
    fColor = aColor;
}
  • 因为实例数组与位置、颜色等相似,是一种顶点属性,因此我们需要将数组内容存储到一个顶点缓冲区对象并配置属性指针。首先,我们将偏移量数组存储到一个新的缓冲区对象:
unsigned int instanceVBO;
glGenBuffers(1, &instanceVBO);
glBindBuffer(GL_ARRAY_BUFFER, instanceVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(glm::vec2) * 100, &translations[0], GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, 0);
  • 其次,我们设置顶点属性指针并启用该顶点属性。(注意这里绑定了顶点数组对象,如果前面没有取消绑定这里就不需要再次绑定)
glBindVertexArray(objectVAO);
glEnableVertexAttribArray(2);
glBindBuffer(GL_ARRAY_BUFFER, instanceVBO);
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
// 第二个参数
// 0: 顶点着色器每次迭代都更新顶点属性内容
// 1: 开始渲染一个新的实例时更新顶点着色器的内容
// 2: 没渲染两个实例进行更新
glVertexAttribDivisor(2, 1);
glBindVertexArray(0);
  • 运行程序后的渲染效果与前面使用uniform变量数组是一样的。这里我调整一下顶点着色器,让四方形随着实例的渲染逐渐增大。
void main()
{
    vec2 pos = aPos * (gl_InstanceID / 100.0);
    gl_Position = vec4(pos + aOffset, 0.0, 1.0);
    fColor = aColor;
}
  • 渲染效果。


    实例数组渲染

2. 小行星带

  • 为了展示实例渲染的效果,我们使用两种方法分别渲染一个小行星带进行对比。要渲染一个小行星带场景,我们需要为小行星周围的石头生成一个模型转换矩阵。转换矩阵首先将石头移动到行星环的某个位置,然后添加一个随机的偏移量让行星环看起来更加自然。接下来我们再应用一个随机缩放和旋转。
unsigned int amount = 1000;
glm::mat4* modelMatrices;
modelMatrices = new glm::mat4[amount];
srand(glfwGetTime());   // 初始化随机种子
float radius = 50.0;
float offset = 2.5f;
for (unsigned int i = 0; i < amount; i++)
{
    glm::mat4 model = glm::mat4(1.0f);
    // 1. 平移:放置到半径为[-offset, offset]的圆圈
    float angle = (float)i / (float)amount * 360.0f;
    float displacement = (rand() % (int)(2 * offset * 100)) / 100.0f - offset;

    float x = sin(angle) * radius + displacement;
    displacement = (rand() % (int)(2 * offset * 100)) / 100.0f - offset;
    float y = displacement * 0.4f;   // 行星域高度保持小于x/z
    displacement = (rand() % (int)(2 * offset * 100)) / 100.0f - offset;
    float z = cos(angle) * radius + displacement;
    model = glm::translate(model, glm::vec3(x, y, z));

    // 2. 缩放:在0.05和0.25f之间缩放
    float scale = (rand() % 20) / 100.0f + 0.05;
    model = glm::scale(model, glm::vec3(scale));

    // 3. 旋转:绕随机旋转轴旋转
    float rotAngle = (rand() % 360);
    model = glm::rotate(model, rotAngle, glm::vec3(0.4f, 0.6f, 0.8f));

    modelMatrices[i] = model;
}
  • 参考我们3D模型章节,加载行星和石头模型后,我们先绘制行星,然后循环绘制散落的石头。(注意将相机位置拉后程序运行后的渲染效果才比较好,不然很难调节。顶点着色器和片元着色器直接采用3D模型章节即可。)
objectShader.use();
glm::mat4 view = camera.GetViewMatrix();
// 将投影远平面拉远
glm::mat4 projection = glm::perspective(glm::radians(camera.Zoom), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 1000.0f);
objectShader.setMat4("view", view);
objectShader.setMat4("projection", projection);
glm::mat4 model = glm::mat4(1.0f);
model = glm::translate(model, glm::vec3(0.0f, -3.0f, 0.0f));
model = glm::scale(model, glm::vec3(4.0));
objectShader.setMat4("model", model);
planetModel.Draw(objectShader);
for (unsigned int i = 0; i < amount; i++)
{
    objectShader.setMat4("model", modelMatrices[i]);
    rockModel.Draw(objectShader);
}
  • 渲染效果。


    直接绘制行星带效果
  • 不使用实例化渲染直接进行绘制,随着行星周围的石头数量增加,渲染速度明显下降,大家可以尝试将石头数量提高感受渲染效果的变化。接下来我们使用实例渲染方法进行渲染。首先,我们调整顶点着色器,添加一个mat4类型的顶点属性。
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
layout (location = 2) in vec2 aTexCoords;
layout (location = 3) in mat4 instanceMatrix;

out vec2 TexCoords;

uniform mat4 view;
uniform mat4 projection;

void main()
{
    TexCoords = aTexCoords;    
    gl_Position = projection * view * instanceMatrix * vec4(aPos, 1.0);
}
  • 注意:顶点属性允许数据的最大数量是vec4,因此当我们声明的顶点属性的数据类型包含的数据大于vec4时,我们需要进行额外处理。因为mat4就是4个vec4,因此我们使用4个顶点属性充当一个矩阵顶点属性。
  • 下面我们分别设置4个vec4的顶点属性。
unsigned int buffer;
glGenBuffers(1, &buffer);
glBindBuffer(GL_ARRAY_BUFFER, buffer);
glBufferData(GL_ARRAY_BUFFER, amount * sizeof(glm::mat4), &modelMatrices[0], GL_STATIC_DRAW);

// 注意这里模型类的meshs原先是private,需要修改为public
for (unsigned int i = 0; i < rockModel.meshes.size(); i++)
{
    unsigned int VAO = rockModel.meshes[i].VAO;
    glBindVertexArray(VAO);
    // vertex attributes
    std::size_t v4s = sizeof(glm::vec4);
    glEnableVertexAttribArray(3);
    glVertexAttribPointer(3, 4, GL_FLOAT, GL_FALSE, 4 * v4s, (void*)0);
    glEnableVertexAttribArray(4);
    glVertexAttribPointer(4, 4, GL_FLOAT, GL_FALSE, 4 * v4s, (void*)(1 * v4s));
    glEnableVertexAttribArray(5);
    glVertexAttribPointer(5, 4, GL_FLOAT, GL_FALSE, 4 * v4s, (void*)(2 * v4s));
    glEnableVertexAttribArray(6);
    glVertexAttribPointer(6, 4, GL_FLOAT, GL_FALSE, 4 * v4s, (void*)(3 * v4s));

    glVertexAttribDivisor(3, 1);
    glVertexAttribDivisor(4, 1);
    glVertexAttribDivisor(5, 1);
    glVertexAttribDivisor(6, 1);

    glBindVertexArray(0);
}
  • 在渲染循环里面,行星还是按照原来的方式使用模型类进行绘制。行星周围的石头则使用实例渲染。
instanceShader.use();
instanceShader.setMat4("view", view);
instanceShader.setMat4("projection", projection);
for (unsigned int i = 0; i < rockModel.meshes.size(); i++)
{
    glBindVertexArray(rockModel.meshes[i].VAO);
    glDrawElementsInstanced(GL_TRIANGLES, rockModel.meshes[i].indices.size(),
        GL_UNSIGNED_INT, 0, amount);
    glBindVertexArray(0);
}
  • 渲染效果(半径150,偏移量25,相机位置155,石头数量100000)。


    实例化渲染行星带
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容