OpenGL学习脚印:创建更多的实例(instancing object)

2023-10-17 06:20

本文主要是介绍OpenGL学习脚印:创建更多的实例(instancing object),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

写在前面
前面我们学习了模型加载的相关内容,并成功加载了模型,令人十分兴奋。那时候加载的是少量的模型,如果需要加载多个模型,就需要考虑到效率问题了,例如下图所示的是加载了400多个纳米战斗服机器人的效果图:

更多的纳米战斗服

渲染一个模型更多的实例,需要使用到实例化技术,就是本节要介绍的instancing object方法。本节示例代码均可以从我的github下载。

本节内容整理自:
www.learnopengl.com

渲染多个实例的方法

要渲染多个实例,基本的想法就是,在主程序中使用循环,在不同位置绘制多个物体,伪代码如下所示:

   for(GLuint i = 0; i < instanceCount; ++i){// 分别设置每个物体的模型变换矩阵 model matrix// glDrawArrays(GL_TRIANGLES, ...)}

这种方式存在的缺点是,当要渲染多个模型的实例时,需要多次调用glDraw这类命令,而这类命令从CPU–>GPU是需要花费时间的,因为使用绘制命令时OpenGL需要做一些工作,例如通知GPU从哪个buffer里面读取数据。虽然GPU绘图很快,但是CPU–>GPU的命令发送,当量比较大时还是会成为瓶颈。

因此OpenGL提供了glDrawArrays和glDrawElements的绘制实例版本,分别对应为glDrawArraysInstanced和glDrawElementsInstanced 。实例版本的函数,多了一个参数,就是最后一个指定渲染多少个实例的参数。

下面以一个简单的绘制多个矩形的例子作为引例,开始熟悉绘制多个实例。

使用多个uniform传递实例数据

假设我们要绘制100个矩形,在顶点着色器中,我们使用一个uniform数组:

   #version 330 corelayout(location = 0) in vec2 position;
layout(location = 1) in vec3 color;uniform vec2 offsets[100]; // 每个实例的位移量out vec3 fColor;void main()
{vec2 offset = offsets[gl_InstanceID]; // 通过gl_InstanceID索引每个实例的位移量gl_Position = vec4(position + offset, 0.5f, 1.0f);fColor = color;
}

通过gl_InstanceID来索引每个实例,而在主程序中,我们通过循环设置这个uniform数组的内容:

   //准备多个实例的位移量数据
glm::vec2 translations[100];
int index = 0;
GLfloat offset = 0.1f;
for (GLint y = -10; y < 10; y += 2)
{
for (GLint x = -10; x < 10; x += 2)
{glm::vec2 translation;translation.x = (GLfloat)x / 10.0f + offset;translation.y = (GLfloat)y / 10.0f + offset;translations[index++] = translation;
}
}
// 接着 向shader传递这100个translate uniform

最后通过实例版本函数绘制多个矩形:

shader.use();
glBindVertexArray(quadVAOId);
glDrawArraysInstanced(GL_TRIANGLES, 0, 6, 100); // 使用instance方法绘制

得到的效果如下图所示:

绘制多个矩形

我们看到使用这个方法,确实渲染了多个矩形,但存在的问题时GLSL中支持的uniform受到限制,可以使用 GL_MAX_VERTEX_UNIFORM_COMPONENTS等枚举通过glGetIntegerv​函数查询。一般情况下uniforms数组也够用,但是对于需要实例比较多的情形,这种方案变得不合适。

使用instance array 传递实例数据

同顶点属性中位置、纹理坐标等其他属性一样,我们可以通过VBO来充当一个instance array,传递每个实例的数据。一般地顶点属性,当顶点着色器执行时需要获取每个顶点的这些属性信息,而充当instance array的顶点属性需要每个实例更新一次。这是instance array与普通顶点属性之间的差别。

创建一个instance array的包括两个步骤,第一步同普通顶点属性一样,创建VBO,填充数据;第二步是通知OpenGL如何解析VBO中的数据。在顶点着色器中,我们定义一个layout=2表示这个instance array,如下:

#version 330 corelayout(location = 0) in vec2 position;
layout(location = 1) in vec3 color;
layout(location = 2) in vec2 offset; // 通过VBO传递位移量// uniform vec2 offsets[100];  // 不再使用out vec3 fColor;void main()
{gl_Position = vec4(position + offset, 0.5f, 1.0f);fColor = color;
}

在主程序中,创建VBO,填充translations数组的数据,如下:

GLuint instanceVBOId;
glGenBuffers(1, &instanceVBOId);
glBindVertexArray(quadVAOId);
glBindBuffer(GL_ARRAY_BUFFER, instanceVBOId);
glBufferData(GL_ARRAY_BUFFER, sizeof(glm::vec2) * 100, &translations[0], GL_STATIC_DRAW);

并通知OpenGL解析这个VBO数据的方式:

glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 0, NULL);
glEnableVertexAttribArray(2);
glVertexAttribDivisor(2, 1); // 注意这里 指定1表示每个实例更新一次数据
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);

这里关键是使用glVertexAttribDivisor来指定数据更新方式,第一个参数2表示layout索引,第二个参数指定顶点属性的更新方式,默认是0表示着色器每次执行时更新属性数据,填写1表示每个实例更新一次属性数据,填写2则表示每2个实例更新一次属性数据,依次类推。上面填写1则通知了OpenGL这是一个instance array,每个实例更新一次数据。

运行上述代码,我们得到的效果与上面相同,当设置:

glVertexAttribDivisor(2, 4);

每4个实例更新一次数据时,我们将会得到100 / 4 =25个矩形,因为每4个矩形的模型变换矩阵相同,因此放在了同一个位置,重合了,效果如下图所示:

divisor=4

上面是一个简单的引例,下面我们通过两个案例,深入对比下instance array方式的性能差别。

绘制行星带

通过加载一个行星模型和石头模型来模拟一个行星带,这里我们通过下面的函数,来构造一个石头模型随机环绕行星的模型变换矩阵:

 // 这里通过随机方式 构造多个石头模型的模型变换矩阵void prepareInstanceMatrices(std::vector<glm::mat4>& modelMatrices, const int amount)
{
srand(glfwGetTime()); // 初始化随机数的种子
GLfloat radius = 50.0;
GLfloat offset = 2.5f;
for (GLuint i = 0; i < amount; i++)
{
glm::mat4 model;
// 1. 平移
GLfloat angle = (GLfloat)i / (GLfloat)amount * 360.0f;
GLfloat displacement = (rand() % (GLint)(2 * offset * 100)) / 100.0f - offset;
GLfloat x = sin(angle) * radius + displacement;
displacement = (rand() % (GLint)(2 * offset * 100)) / 100.0f - offset;
GLfloat y = displacement * 0.4f; 
displacement = (rand() % (GLint)(2 * offset * 100)) / 100.0f - offset;
GLfloat z = cos(angle) * radius + displacement;
model = glm::translate(model, glm::vec3(x, y, z));// 2. 缩放 在 0.05 和 0.25f 之间
GLfloat scale = (rand() % 20) / 100.0f + 0.05;
model = glm::scale(model, glm::vec3(scale));// 3. 旋转
GLfloat rotAngle = (rand() % 360);
model = glm::rotate(model, rotAngle, glm::vec3(0.4f, 0.6f, 0.8f));// 4. 添加作为模型变换矩阵
modelMatrices.push_back(model);
}
}

上面随机方式构造变换矩阵的计算细节,可以不用深究,我们需要重点理解的是对比使用普通方式和使用instance array的效率问题。

不使用instance array的绘制方式

构造了多个实例的矩阵后,我们使用普通的绘制方式如下:

// 这里填写场景绘制代码
shader.use();
glUniformMatrix4fv(glGetUniformLocation(shader.programId, "projection"),
1, GL_FALSE, glm::value_ptr(projection));
glUniformMatrix4fv(glGetUniformLocation(shader.programId, "view"),
1, GL_FALSE, glm::value_ptr(view));
glm::mat4 model;
model = glm::translate(model, glm::vec3(0.0f, -3.0f, 0.0f));
model = glm::scale(model, glm::vec3(4.0f, 4.0f, 4.0f));
glUniformMatrix4fv(glGetUniformLocation(shader.programId, "model"),
1, GL_FALSE, glm::value_ptr(model));planet.draw(shader); // 先绘制行星// 绘制多个小行星实例
for (std::vector<glm::mat4>::size_type i = 0; i < modelMatrices.size(); ++i)
{
glUniformMatrix4fv(glGetUniformLocation(shader.programId, "model"),
1, GL_FALSE, glm::value_ptr(modelMatrices[i]));
rock.draw(shader);
}

使用instance array的绘制方式

同上面使用的instance array有些不同,这里使用的instance array是mat4类型的矩阵,因为顶点属性允许的最大数据为vec4,因此我们需要使用4 * vec4表示这个mat4类型的instance array。在顶点着色器中定义这个mat4 instance array如下:

   #version 330 corelayout(location = 0) in vec3 position;
layout(location = 1) in vec2 textCoord;
layout(location = 2) in vec3 normal;
layout(location = 3) in mat4 instanceMatrix;  // 顶点属性最多vec4 输入 实际上有4个vec4输入构造这个mat4uniform mat4 projection;
uniform mat4 view;out vec2 TextCoord;void main()
{gl_Position = projection * view * instanceMatrix * vec4(position, 1.0);TextCoord = textCoord;
}

同时我们还需要在主程序中向着色器传递这个instance array。之前设计的mesh.h类,需要少量修改,允许获取mesh相关信息,修改后的mesh.h类。我们这里不去大量修改mesh类,采用的策略是为每个mesh使用这个instance array,实现如下:

   void prepareInstanceMatrices(std::vector<glm::mat4>& modelMatrices, const int amount, const Model& instanceModel)
{// 构造modelMatrices 同上面函数实现// 创建instance arrayGLuint modelMatricesVBOId;glGenBuffers(1, &modelMatricesVBOId);glBindBuffer(GL_ARRAY_BUFFER, modelMatricesVBOId);glBufferData(GL_ARRAY_BUFFER, sizeof(glm::mat4) * amount, &modelMatrices[0], GL_STATIC_DRAW);glBindBuffer(GL_ARRAY_BUFFER, 0);// 为模型里每个mesh 传递model matrix// 用4个vec4传递这个mat4类型const std::vector<Mesh>& meshes = instanceModel.getMeshes();for (std::vector<Mesh>::size_type i = 0; i < meshes.size(); ++i){glBindVertexArray(meshes[i].getVAOId());glBindBuffer(GL_ARRAY_BUFFER, modelMatricesVBOId);// 第一列glEnableVertexAttribArray(3);glVertexAttribPointer(3, 4, GL_FLOAT, GL_FALSE, 4 * sizeof(glm::vec4), (GLvoid*)0);// 第二列glEnableVertexAttribArray(4);glVertexAttribPointer(4, 4, GL_FLOAT, GL_FALSE, 4 * sizeof(glm::vec4), (GLvoid*)(sizeof(glm::vec4)));// 第三列glEnableVertexAttribArray(5);glVertexAttribPointer(5, 4, GL_FLOAT, GL_FALSE, 4 * sizeof(glm::vec4), (GLvoid*)(2 * sizeof(glm::vec4)));// 第四列glEnableVertexAttribArray(6);glVertexAttribPointer(6, 4, GL_FLOAT, GL_FALSE,4 * sizeof(glm::vec4), (GLvoid*)(3 * sizeof(glm::vec4)));// 注意这里需要设置实例数据更新选项 指定1表示 每个实例更新一次glVertexAttribDivisor(3, 1);glVertexAttribDivisor(4, 1);glVertexAttribDivisor(5, 1);glVertexAttribDivisor(6, 1);glBindVertexArray(0);}
}

这个地方稍微有点绕,关键一点就是每个mesh都包含了这个modelMatrices数据,因此每个mesh绘制三角形时,都会在每个实例上更新modelMatrix,从而整体上绘制出的模型也用了这些模型变换矩阵。

上面绘制的效果如下图所示:

行星带效果

使用上面两种方法渲染包含1000, 10000, 100000个石头模型的行星带,在NVIDIA Graphics 上粗略的一个对比数据(这不是基准测试结果),如下表1所示:

实例数目普通绘制instancing方法
10000.05s0.01s
10,0000.45s0.12s
100,0004.0s1.25s

这个计时是通过glfwGetTime来实现的,更科学的对比可能是使用帧率,暂时不细究这个问题了。通过对比,可以看到使用instance array渲染多个实例速度比普通方式快了4到5倍。

渲染更多的纳米战斗服机器人

再给出一个使用instance方法,绘制多个机器人的方法,我们指定了要绘制的机器人数量,然后平铺在钢铁纹理上。绘制9个机器人的效果如下图所示:

9个机器人

121个机器人效果如下图所示:

121个机器人

渲染的441个机器人效果如下图所示:

441个机器人

你可以根据需要将机器人的摆放成其他形式,例如同心圆、心形图案等,可以自己玩会儿了。

最后的说明

本节学习了instance实例的方法,并对比了普通渲染方式和它在性能上的差别。实际应用中,instance实例一般应用在草地、树木等模型上面,来构成游戏场景中很好的布景。

这篇关于OpenGL学习脚印:创建更多的实例(instancing object)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/223493

相关文章

HarmonyOS学习(七)——UI(五)常用布局总结

自适应布局 1.1、线性布局(LinearLayout) 通过线性容器Row和Column实现线性布局。Column容器内的子组件按照垂直方向排列,Row组件中的子组件按照水平方向排列。 属性说明space通过space参数设置主轴上子组件的间距,达到各子组件在排列上的等间距效果alignItems设置子组件在交叉轴上的对齐方式,且在各类尺寸屏幕上表现一致,其中交叉轴为垂直时,取值为Vert

Ilya-AI分享的他在OpenAI学习到的15个提示工程技巧

Ilya(不是本人,claude AI)在社交媒体上分享了他在OpenAI学习到的15个Prompt撰写技巧。 以下是详细的内容: 提示精确化:在编写提示时,力求表达清晰准确。清楚地阐述任务需求和概念定义至关重要。例:不用"分析文本",而用"判断这段话的情感倾向:积极、消极还是中性"。 快速迭代:善于快速连续调整提示。熟练的提示工程师能够灵活地进行多轮优化。例:从"总结文章"到"用

【前端学习】AntV G6-08 深入图形与图形分组、自定义节点、节点动画(下)

【课程链接】 AntV G6:深入图形与图形分组、自定义节点、节点动画(下)_哔哩哔哩_bilibili 本章十吾老师讲解了一个复杂的自定义节点中,应该怎样去计算和绘制图形,如何给一个图形制作不间断的动画,以及在鼠标事件之后产生动画。(有点难,需要好好理解) <!DOCTYPE html><html><head><meta charset="UTF-8"><title>06

学习hash总结

2014/1/29/   最近刚开始学hash,名字很陌生,但是hash的思想却很熟悉,以前早就做过此类的题,但是不知道这就是hash思想而已,说白了hash就是一个映射,往往灵活利用数组的下标来实现算法,hash的作用:1、判重;2、统计次数;

【Python编程】Linux创建虚拟环境并配置与notebook相连接

1.创建 使用 venv 创建虚拟环境。例如,在当前目录下创建一个名为 myenv 的虚拟环境: python3 -m venv myenv 2.激活 激活虚拟环境使其成为当前终端会话的活动环境。运行: source myenv/bin/activate 3.与notebook连接 在虚拟环境中,使用 pip 安装 Jupyter 和 ipykernel: pip instal

零基础学习Redis(10) -- zset类型命令使用

zset是有序集合,内部除了存储元素外,还会存储一个score,存储在zset中的元素会按照score的大小升序排列,不同元素的score可以重复,score相同的元素会按照元素的字典序排列。 1. zset常用命令 1.1 zadd  zadd key [NX | XX] [GT | LT]   [CH] [INCR] score member [score member ...]

【机器学习】高斯过程的基本概念和应用领域以及在python中的实例

引言 高斯过程(Gaussian Process,简称GP)是一种概率模型,用于描述一组随机变量的联合概率分布,其中任何一个有限维度的子集都具有高斯分布 文章目录 引言一、高斯过程1.1 基本定义1.1.1 随机过程1.1.2 高斯分布 1.2 高斯过程的特性1.2.1 联合高斯性1.2.2 均值函数1.2.3 协方差函数(或核函数) 1.3 核函数1.4 高斯过程回归(Gauss

在cscode中通过maven创建java项目

在cscode中创建java项目 可以通过博客完成maven的导入 建立maven项目 使用快捷键 Ctrl + Shift + P 建立一个 Maven 项目 1 Ctrl + Shift + P 打开输入框2 输入 "> java create"3 选择 maven4 选择 No Archetype5 输入 域名6 输入项目名称7 建立一个文件目录存放项目,文件名一般为项目名8 确定

【学习笔记】 陈强-机器学习-Python-Ch15 人工神经网络(1)sklearn

系列文章目录 监督学习:参数方法 【学习笔记】 陈强-机器学习-Python-Ch4 线性回归 【学习笔记】 陈强-机器学习-Python-Ch5 逻辑回归 【课后题练习】 陈强-机器学习-Python-Ch5 逻辑回归(SAheart.csv) 【学习笔记】 陈强-机器学习-Python-Ch6 多项逻辑回归 【学习笔记 及 课后题练习】 陈强-机器学习-Python-Ch7 判别分析 【学

Java 创建图形用户界面(GUI)入门指南(Swing库 JFrame 类)概述

概述 基本概念 Java Swing 的架构 Java Swing 是一个为 Java 设计的 GUI 工具包,是 JAVA 基础类的一部分,基于 Java AWT 构建,提供了一系列轻量级、可定制的图形用户界面(GUI)组件。 与 AWT 相比,Swing 提供了许多比 AWT 更好的屏幕显示元素,更加灵活和可定制,具有更好的跨平台性能。 组件和容器 Java Swing 提供了许多