OpenGL学习笔记30-Instancing

Instancing 实例化

Advanced-OpenGL/Instancing

想象一个充满草叶的场景:每个草叶都是一个只有几个三角形的小模型。你可能会想要画一些它们,你的场景可能最终会有成千上万的草叶子,你需要渲染每一帧。因为每个叶子只有几个三角形,叶子几乎是立即呈现的。然而,您必须进行的数千次渲染调用将极大地降低性能。

如果我们真的渲染这么多的对象,它看起来会有点像这样的代码:


for(unsigned int i = 0; i < amount_of_models_to_draw; i++)
{
    DoSomePreparations(); // bind VAO, bind textures, set uniforms etc.
    glDrawArrays(GL_TRIANGLES, 0, amount_of_vertices);
}


相比呈现实际的顶点,告诉GPU渲染你的顶点数据和函数像glDrawArrays或glDrawElements吃了一些性能由于OpenGL必须进行必要的准备工作,才能把你的顶点数据(像告诉缓冲区读取数据的GPU,在哪里可以找到顶点属性和所有这些在相对较慢的CPU GPU总线)。所以即使渲染你的顶点是超级快的,给你的GPU命令去渲染它们不是。

如果我们可以将数据发送给GPU一次,然后告诉OpenGL用一个绘图调用来使用这些数据绘制多个对象,这会更加方便。输入实例化。

实例化是一种技术,我们在一次渲染调用中绘制许多(相等的网格数据)对象,节省我们所有的CPU -&gt;GPU通信每次我们需要渲染一个对象。要使用实例化进行渲染,我们所需要做的就是将渲染调用glDrawArrays和glDrawElements分别更改为gldrawarraysinstated和gldrawelementsinstated。经典呈现函数的这些实例版本采用了一个名为实例计数的额外参数,用于设置我们想要呈现的实例数量。我们将所有需要的数据发送给GPU一次,然后告诉GPU它应该如何用一次调用绘制所有这些实例。然后GPU渲染所有这些实例而不必与CPU持续通信。

这个函数本身有点没用。渲染同一对象一千次对我们来说是没有用处的,因为每一个渲染对象都是完全相同的,因此也在相同的位置;我们只能看到一个物体!因此,GLSL在顶点着色器中添加了另一个内置变量gl_InstanceID。

当使用一个实例呈现调用绘制时,gl_InstanceID会为每个实例从0开始递增。例如,如果我们要渲染第43个实例,gl_InstanceID在顶点着色器中的值将是42。例如,为每个实例提供一个惟一的值意味着我们现在可以索引大量的位置值,以便将每个实例定位到世界上的不同位置。

为了获得对实例绘图的感觉,我们将演示一个简单的例子,它在规范化的设备坐标中仅用一个渲染调用渲染100个2D四边形。我们通过索引一个由100个偏移向量组成的统一数组来唯一地定位每个实例化的四轴。结果是一个整齐组织的四边形网格填充了整个窗口:

每个四边形由2个三角形组成,共6个顶点。每个顶点包含一个2D的NDC位置向量和一个颜色向量。下面是这个例子中使用的顶点数据——当三角形有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;
}  

这里我们定义了一个名为offsets的统一数组,它包含总共100个偏移向量。在顶点着色器中,我们通过使用gl_InstanceID索引偏移数组为每个实例检索偏移向量。如果我们现在用实例画出100个四边形我们会得到位于不同位置的100个四边形。

在我们进入渲染循环之前,我们确实需要设置我们在嵌套for循环中计算的偏移位置:


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

在这里,我们创建了一个包含100个平移向量的集合,其中包含一个10x10网格中所有位置的偏移向量。除了生成翻译数组,我们还需要将数据传输到顶点着色器的统一数组:


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

在这个代码片段中,我们将for循环计数器i转换为一个字符串,以动态创建一个位置字符串,用于查询统一位置。然后对偏移量均匀数组中的每一项设置相应的平移向量。

现在所有的准备工作都完成了,我们可以开始渲染四轴飞行器了。为了通过实例渲染绘制,我们调用gldrawarraysinstated或gldrawelementsinstated。因为我们没有使用元素索引缓冲区我们将调用glDrawArrays版本:


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

gldrawarraysinstinstance的参数与glDrawArrays完全相同,除了最后一个参数设置我们想要绘制的实例的数量。因为我们想在10x10的网格中显示100个四边形,所以我们将其设置为100。现在,运行该代码应该会给您带来100个彩色四轴的熟悉图像。

Instanced arrays

虽然之前的实现在这个特定的用例中工作得很好,但是当我们渲染超过100个实例时(这是很常见的),我们最终会达到发送到着色器的统一数据量的限制。另一个选项称为实例数组。实例数组被定义为一个顶点属性(允许我们存储更多的数据),每个实例更新而不是每个顶点。

对于顶点属性,在每次运行顶点着色器的开始,GPU将检索属于当前顶点的下一组顶点属性。当将一个顶点属性定义为一个实例数组时,顶点着色器只更新每个实例的顶点属性的内容。这允许我们为每个顶点的数据使用标准的顶点属性,并使用实例化数组来存储每个实例唯一的数据。

为了给您一个实例化数组的示例,我们将使用前面的示例并将偏移均匀数组转换为实例化数组。我们必须通过添加另一个顶点属性来更新顶点着色器:


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

我们不再使用gl_InstanceID,可以直接使用偏移量属性,而无需首先索引到大型统一数组中。

因为一个实例数组是一个顶点属性,就像位置和颜色变量一样,我们需要将它的内容存储在一个顶点缓冲区对象中,并配置它的属性指针。我们首先将translation数组(来自上一节)存储在一个新的缓冲区对象中:


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的最后一行。这个函数告诉OpenGL何时将一个顶点属性的内容更新到下一个元素。它的第一个参数是所讨论的顶点属性,第二个参数是属性除数。默认情况下,属性除数为0,这告诉OpenGL在每次迭代顶点着色器时更新顶点属性的内容。通过将这个属性设置为1,我们告诉OpenGL,当我们开始渲染一个新实例时,我们想要更新顶点属性的内容。通过将其设置为2,我们将每两个实例更新一次内容,以此类推。通过将属性除数设置为1,我们有效地告诉OpenGL在属性位置2的顶点属性是一个实例数组。

如果我们现在用gldrawarraysinstited再次渲染四块,我们会得到如下输出:

这和前面的例子完全一样,但是现在有了实例数组,它允许我们传递更多的数据(内存允许我们)到顶点着色器来进行实例绘制。

为了好玩,我们可以再次使用gl_InstanceID,将每个四轴从上至右缓慢降低到下至左,因为为什么不呢?


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

结果是,绘制实例的第一个实例非常小,而且我们绘制实例的过程越深入,gl_InstanceID就越接近100,从而使更多的四轴实例恢复到原来的大小。像这样将实例数组和gl_InstanceID一起使用是完全合法的。

如果您仍然不确定实例呈现的工作方式,或者希望了解所有内容如何结合在一起,您可以在这里找到应用程序的完整源代码。

虽然很有趣,但是这些例子并不是实例化的好例子。是的,它们确实让你简单地了解了instancing是如何工作的,但是instancing的大部分功能是在绘制大量相似对象时获得的。出于这个原因,我们要去太空探险。

An asteroid field

想象这样一个场景,我们有一个巨大的行星在一个巨大的小行星环的中心。这样一个小行星环可能包含成千上万的岩层,很快就无法在任何像样的显卡上显示。这个场景证明了它对于实例渲染特别有用,因为所有的小行星都可以用一个模型来表示。每个单独的小行星然后从每个小行星唯一的变换矩阵得到它的变化。

为了演示实例渲染的效果,我们首先渲染一个小行星在行星周围盘旋而没有实例渲染的场景。场景将包含一个大的行星模型,可以从这里下载和一个大的小行星岩石,我们正确的位置周围的行星。小行星岩石模型可以在这里下载。

在代码示例中,我们使用前面在模型加载章节中定义的模型加载器来加载模型。

为了达到我们正在寻找的效果,我们将为每个小行星生成一个模型转换矩阵。转换矩阵首先转换小行星环中的某处岩石-然后我们将添加一个小的随机位移值到偏移量,使戒指看起来更自然。从那里我们也应用一个随机比例和随机旋转。结果是一个转换矩阵,将行星周围的每个小行星转化为一个更自然和独特的外观,与其他小行星相比。


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

这段代码可能看起来有点吓人,但我们基本上改变了小行星的x和z位置沿一个圆的半径定义的半径,并随机移动每个小行星一点点围绕圆的偏移。我们给y位移较少的冲击,以创建一个更平的小行星环。然后应用缩放和旋转变换,并将得到的变换矩阵存储在相应大小的模型矩阵中。这里我们生成1000个模型矩阵,每颗小行星一个。

加载行星和岩石模型并编译一组着色器后,渲染代码看起来有点像这样:


// 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次是岩石模型。您可以在这里找到这个场景的源代码。

一旦我们开始增加这个数字,我们会很快注意到场景停止平稳运行,我们每秒能够渲染的帧数急剧减少。当我们设置接近2000的时候,场景在GPU上已经变得非常慢以至于很难移动。

现在让我们尝试渲染相同的场景,但是这次使用实例渲染。我们首先需要调整一点顶点着色器:


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

我们不再使用模型统一变量,而是将mat4声明为一个顶点属性,这样我们就可以存储一个转换矩阵的实例数组。然而,当我们将数据类型声明为一个比vec4大的顶点属性时,事情的工作方式就有点不同了。顶点属性允许的最大数据量等于一个vec4。因为mat4基本上是4个vec4,我们必须为这个特定的矩阵保留4个顶点属性。因为我们给它分配了一个位置为3,矩阵的列的顶点属性位置为3,4,5,6。

然后我们必须设置这4个顶点属性的每个属性指针,并将它们配置为实例数组:


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

注意,我们将网格的VAO变量声明为公共变量而不是私有变量,这样我们就可以访问它的顶点数组对象。这不是最干净的解决方案,但只是适合本示例的一个简单修改。除了这个小改动之外,这段代码应该很清楚。我们基本上声明了OpenGL应该如何为矩阵的每个顶点属性解释缓冲区以及每个顶点属性都是一个实例数组。

接下来,我们再次使用网格的VAO,这次使用gldrawelementsinstinstance绘制:


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

这里我们绘制了与上一个示例相同数量的小行星,但这次使用了实例渲染。结果应该是完全相同的,但是一旦我们增加渲染量,你就会真正开始看到实例渲染的威力。在没有实例渲染的情况下,我们能够平稳地渲染大约1000到1500颗小行星。通过实例渲染,我们现在可以将这个值设置为100000。如果rock模型有576个顶点,那么在没有显著性能下降的情况下,每一帧大约有5700万个顶点;只有2个抽牌!

这张图片是由100000颗半径为150.0f,偏移量为25.0f的小行星渲染的。你可以在这里here. 找到实例渲染演示的源代码。

在不同的机器上,10万颗的小行星数可能有点高,所以尝试调整值,直到你达到一个可接受的帧率。

正如您所看到的,在正确类型的环境中,实例化呈现可以极大地改变应用程序的呈现能力。基于这个原因,实例渲染通常用于草、植物、粒子和这样的场景——基本上任何有许多重复形状的场景都可以从实例渲染中受益。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
1 课程简介:本课程详细讲解了OpenGL从入门到精通的理论+实践知识,对于每一个知识点都会带领学员通过代码来实现功能。其中涵盖了基础图元绘制,基础光照,高级过程,高级光照等内容;当前图形引擎的应用已经越来越广泛,春晚以及各大综艺节目已经开始使用XR作为主流的内容制作技术,房地产漫游及Web渲染技术已经开始茁壮发展,VR也即将突破硬件瓶颈;普遍的游戏引擎在独特的领域已经无法完全实用,且我们国家要发展自主科技技术,图形引擎以及CAD等卡脖子技术一定会蓬勃发展,所以同学们要抓住机会,趁势而上,熟悉底层,博取更大发展,学习OpenGL底层接口的应用以及图形学算法,将是您向纵深发展的第一步!2 课程解决优势:很多同学学习OpenGL最难的是找到路径,并且其中牵扯到的理论知识点无法完全理解透彻(比如VAO与VBO的区别,MVP矩阵变换的推导及原理,光照系统的设计及算法推导,帧缓存的灵活应用等),我们的课程可以带领大家从原理+实践的角度进行学习,每一个知识点都会:a 推导基础公式及原理 b 一行一行进行代码实践从而能够保证每位同学都学有所得,能够看得懂,学得会,用得上,并且能够培养自主研究的能力。学习课程所得:学习本课程完毕之后,学员可以全方位的完全了解OpenGL当中的必要接口,并且可以对图形学的基础知识融会贯通,可以制作中级的特效。并且对于UnrealEngine以及Unity3D的学习更加轻松,对于各类商业引擎当中的算法以及内容制作手法更加深刻理解把控。学员也可以自行进行图形引擎的设计以及研究,并且将本课程的知识点进行代码模块化编写;能够自主推导图形学管线以及应用当中的各类公式,并且理解其几何含义。 代码与PPT资源,已随课程附赠,请同学们对应课程下载 

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值