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)。


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

推荐阅读更多精彩内容