计算机图形学(OPENGL):实例化

本文同时发布在我的个人博客上:https://dragon_boy.gitee.io

实例化

  想一想某个游戏场景或动画场景,我们总会法线某些模型是重复的,也就是使用了同样的顶点数据,只不过有了不同的世界空间变换。比如草地,我们可能想通过一个简单的叶子模型进行重复绘制来形成草地,一株草可能只包含几个三角形,但整个草地的三角形数量是巨大的。
  如果真的逐个绘制重复的模型,我们大概会在渲染循环中这么实现:

for(unsigned int i = 0; i < amount_of_models_to_draw; i++)
{
    DoSomePreparations(); // 绑定VAO,绑定纹理,设置uniform之类的。
    glDrawArrays(GL_TRIANGLES, 0, amount_of_vertices);
}

  由于对绘制命令的大量调用,我们很容易到达性能的瓶颈,因为使用类似glDrawArrays或glDrawElements的命令会消耗大量的性能,OpenGL必须在绘制前做一些必要的准备(比如告知GPU从哪片缓冲区读取数据,在哪里找到顶点属性,这些操作都会在CPU和GPU之间进行)。所以说即使渲染出图像很快,但给GPU这些命令却不是那么快。
  但如果我们只向GPU运送一次数据,并使用一个渲染命令来绘制大量重复的物体,这将会更加有效率。而这种方法就是实例化。
  实例化在渲染大量重复物体时非常有用,只需要使用一次渲染命令,节省了大量CPU与GPU之间的通信次数。使用实例化绘制物体我们使用glDrawArraysInstanced和glDrawElementsInstanced命令。这两个命令需要额外的实例化数量参数来表明我们想绘制的重复物体的数量。我们将需要的数据送往GPU一次,然后告知GPU如何绘制这些实例化物体,接着GPU就可以大量绘制这些实例化物体了。
  这个方法本身由点局限性,因为它所有绘制的物体都和要实例化的物体一致,包括位置、纹理等一切信息。也就是说,我们只能在屏幕上看到一个物体显示。当然,考虑到这一点,GLSL在顶点着色器中提供了一个内建变量gl_InstanceID。
  每渲染一个实例物体,gl_InstanceID就会从0开始自增1。如果我们渲染了43个实例物体,那么gl_InstanceID=42。这样,我们就可以根据这个值为每一个实例物体分配一些特定的属性,比如改变每个实例物体的位置。
  比如下面我们绘制了100个相同的2d平面,我们,给每个平面一点偏移量,就可以得到下面的结果:



  每个四边形包含2个三角形即6个顶点。每个顶点包含1个2为标准坐标和一个颜色向量。为了保证100个三角形能铺满屏幕,我们让平面的大小尽可能地小:

float quadVertices[] = {
    // positions     // 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;
}  

  我们定义了一个100大小的uniform偏移数组,按顺序(gl_InstanceID)对每个平面进行偏移。
  接着我们需要为uniform偏移数组赋值。我们在进入渲染循环前线定义一下一个偏移数组:

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;
    }
}  

  接着将数据传入uniform偏移数组:

shader.use();
for(unsigned int i = 0; i < 100; i++)
{
    shader.setVec2(("offsets[" + std::to_string(i) + "]")), translations[i]);
}  

  最后,在渲染循环中,我们绘制这100个平面:

glBindVertexArray(quadVAO);
glDrawArraysInstanced(GL_TRIANGLES, 0, 6, 100);  

实例化数组

  上述的例子非常有用,但平常我们绘制实例化物体总会超过100个,而这就表明我们会超过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;
}  

  由于实例化数组是一个顶点属性,我们需要存储在一个顶点缓冲对象中,并设置相应的顶点属性指针。首先将我们定义的偏移数组传入一个新的VBO:

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

  接着设置顶点属性指针:

glEnableVertexAttribArray(2);
glBindBuffer(GL_ARRAY_BUFFER, instanceVBO);
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0);
glBindBuffer(GL_ARRAY_BUFFER, 0);   
glVertexAttribDivisor(2, 1);  

  注意最后,我们使用了glVertexAttribDivisor。这个方法告知何时更新顶点属性(不是每个顶点都更新)。第一个参数代表顶点属性的位置,第二个参数是属性除数。默认情况下,属性除数为0,表明每个顶点都会更新数据。这里使用1表明我们想在绘制一个新的实例时更新数据。如果设为2则是每绘制两个实例更新数据,以此类推。通过将属性除数设为1我们告知OpenGL在2位置的顶点属性是实例化数组。
  其它不变,结果如下:



  可以看到结果和第一种方法一致,但使用实例化数组允许我们存入更多的数据,绘制更多的实例物体。
  我们可以使用内建的gl_InstanceID来逐行缩小平面玩一玩:

void main()
{
    vec2 pos = aPos * (gl_InstanceID / 100.0);
    gl_Position = vec4(pos + aOffset, 0.0, 1.0);
    fColor = aColor;
} 

  运行结果如下:


  这里给出原文参考代码:Code

小行星带

  想象一个场景,中心有一个天体,又一圈大的小行星带环绕。这样一圈小行星带包含成千上万的岩石,如果就这么直接渲染的话,最好的显卡也招架不住。我们可以使用实例化技术来实现这一看似不可能的渲染。行星带的每块岩石都可以看作是一块源石进行一些不同的变换得到的。
  为了体现实例化渲染的优势,我们首先直接渲染模型。这里给出天体模型和行星带岩石模型:天体行星带
  我们为每个小行星带的岩石设置model变换矩阵。我们首先根据一个圆形的半径进行不同的半径替换(在原始半径的基础上在偏移范围内添加添加扰动),得到位置,接着随机缩放和旋转一下:

unsigned int amount = 1000;
glm::mat4 *modelMatrices;
modelMatrices = new glm::mat4[amount];
srand(glfwGetTime()); // initialize random seed 
float radius = 50.0;
float offset = 2.5f;
for(unsigned int i = 0; i < amount; i++)
{
    glm::mat4 model = glm::mat4(1.0f);
    // 1. translation: displace along circle with 'radius' in range [-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; // keep height of field smaller compared to width of x and 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. scale: scale between 0.05 and 0.25f
    float scale = (rand() % 20) / 100.0f + 0.05;
    model = glm::scale(model, glm::vec3(scale));

    // 3. rotation: add random rotation around a (semi)randomly picked rotation axis vector
    float rotAngle = (rand() % 360);
    model = glm::rotate(model, rotAngle, glm::vec3(0.4f, 0.6f, 0.8f));

    // 4. now add to list of matrices
    modelMatrices[i] = model;
}  

  在加载天体和岩石模型以及设定一系列着色器后,渲染代码长这样:

// draw planet
shader.use();
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.0f, 4.0f, 4.0f));
shader.setMat4("model", model);
planet.Draw(shader);
  
// draw meteorites
for(unsigned int i = 0; i < amount; i++)
{
    shader.setMat4("model", modelMatrices[i]);
    rock.Draw(shader);
}  

  运行结果如下:


  整个场景每帧包含1001次渲染命令调用,只是为了绘制1000个岩石。这里给出原文代码参考:Code
  很明显,这太浪费资源了,而且如果不断增加岩石的数量,很快就会到达性能的瓶颈。
  接下来我们使用实例化来渲染整个场景。首先调整顶点着色器:

#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 2) in vec2 aTexCoords;
layout (location = 3) in mat4 instanceMatrix;

out vec2 TexCoords;

uniform mat4 projection;
uniform mat4 view;

void main()
{
    gl_Position = projection * view * instanceMatrix * vec4(aPos, 1.0); 
    TexCoords = aTexCoords;
}

  我们直接将model矩阵定义为顶点属性。矩阵这里的大小为mat4,即4*vec4,我们的顶点属性的数据最大大小为vec4,所以即使我们我们将位置定义为3,我们还需要使用4、5、6的位置来存储矩阵数据。
  我们设置每个属性指针并配置实例化数组:

// vertex buffer object
unsigned int buffer;
glGenBuffers(1, &buffer);
glBindBuffer(GL_ARRAY_BUFFER, buffer);
glBufferData(GL_ARRAY_BUFFER, amount * sizeof(glm::mat4), &modelMatrices[0], GL_STATIC_DRAW);
  
for(unsigned int i = 0; i < rock.meshes.size(); i++)
{
    unsigned int VAO = rock.meshes[i].VAO;
    glBindVertexArray(VAO);
    // vertex attributes
    std::size_t vec4Size = sizeof(glm::vec4);
    glEnableVertexAttribArray(3); 
    glVertexAttribPointer(3, 4, GL_FLOAT, GL_FALSE, 4 * vec4Size, (void*)0);
    glEnableVertexAttribArray(4); 
    glVertexAttribPointer(4, 4, GL_FLOAT, GL_FALSE, 4 * vec4Size, (void*)(1 * vec4Size));
    glEnableVertexAttribArray(5); 
    glVertexAttribPointer(5, 4, GL_FLOAT, GL_FALSE, 4 * vec4Size, (void*)(2 * vec4Size));
    glEnableVertexAttribArray(6); 
    glVertexAttribPointer(6, 4, GL_FLOAT, GL_FALSE, 4 * vec4Size, (void*)(3 * vec4Size));

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

    glBindVertexArray(0);
}  

  接着我们使用glDrawElementsInstanced绘制岩石:

// draw meteorites
instanceShader.use();
for(unsigned int i = 0; i < rock.meshes.size(); i++)
{
    glBindVertexArray(rock.meshes[i].VAO);
    glDrawElementsInstanced(
        GL_TRIANGLES, rock.meshes[i].indices.size(), GL_UNSIGNED_INT, 0, amount
    );
}  

  为了体现实例化的优势,我们将绘制数量调整为10万,结果如下:


  这里给出原文代码参考:Code
  最后,贴出原文地址供参考:https://learnopengl.com/Advanced-OpenGL/Instancing

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