4

据我了解:VAO 代表某种状态。如果我绑定一个 VAO,为索引和东西添加一些 VBO 和元素缓冲区,我可以保存我想要绘制和激活的对象的特定状态,并在以后想要渲染东西时轻松绘制它们。正确的?

所以 VBO 保存实际数据,而 VAO 只是一个“包装对象”,它保存指向我为它定义的所有缓冲区的指针?

更改 VAO 的成本很高(更改 VBO 也是如此?!)。目前我加载网格并将它们组合到模型中。每个模型都使用自己的 VAO,并有一个 VBO(带有顶点)和一个带有索引的元素缓冲区。

现在据我了解,这是胡说八道,因为我世界中的每个对象(具有模型)都使用它自己的 VAO。我的世界中大约 30 个对象不是问题,但我想正确地做到这一点,将来可能有成百上千个对象,然后性能会大大下降。

因此,就模型而言,许多对象是“相同的”。我的意思是,例如,如果您在世界上有某种树类型,您可能会多次使用相同的模型,只是在不同的位置。

现在我该怎么做呢?我是否应该手动跟踪我的 VAO,例如:(伪代码如下!)

treesVAOId = 1;
rabbitsVAOId = 2;

然后如果我加载一个模型,只需检查 ID 是否已经绑定(如何?)并在那里添加另一组 VBO(或者甚至添加到正确的 VBO?如果是,如何?)

我正在考虑在这里举办一场大型比赛。假设游戏中有数千个角色。当然不是所有这些都是同时渲染的,但是我不能为它们中的每一个创建一个 VAO 和 VBO,可以吗?

我觉得缺少了很大一部分……例如为不同目的有效地再次实例化和使用(或多或少)相同的数据。

这是我缺少的一步,即如何真正优化现实世界中 VAO 和 VBO 的使用。

4

2 回答 2

7

您在这里描述的称为资源管理器或至少是资源管理器的一部分。在外部文件中描述资源是一种很好的做法,因此您需要一个资源文件,其中所有网格都以某种方式描述(考虑使用 XML 或 JSON)。

类层次结构

这是类层次结构的一种可能方法:

每个 VAO 代表一个网格,定义它的顶点坐标、纹理坐标、法线、顶点颜色等。我认为没有理由在几个 VAO 中使用相同的 VBO,除非你有一个非常特殊的可视化案例。因此,假设您只使用每组数据一次,即使用 VAO 的类不应该知道任何有关底层 VBO 的信息,并且没有必要为 VBO 编写类包装器。

一组网格(可能只包含一个网格)代表一个模型。最小模型类应包括 VAO 的句柄和几何变换信息(旋转、平移,任何你想要的)。为什么不严格每个模型一个网格?有时您可能需要对一组网格应用一个变换,而它们中的哪一个又具有它自己的模型局部变换。例如,这种组合可以用于一种骨骼动画或仅用于使用从可能的武器库中获取的任意武器来渲染角色。此外,您可以将这些模型组合在一起,以获得具有相同界面的更复杂的模型,因此您将获得场景图的相似之处。无论如何,对模型类使用复合模式是个好主意。

场景应包括模型、光源、力场等的集合。

资源管理器

但是场景(或类似的游戏对象)从哪里得到它的模型呢?资源经理应该回答这个问题。使每个模型都由某种唯一标识符定义。在最简单的情况下,可以将真实或虚拟文件系统中的路径视为标识符,但它不是很灵活。在我看来,最好使用富有表现力的人类可读名称定义资源文件中的所有网格,并将每个名称绑定到数据集(所有类型的坐标、颜色等)和属性。您的所有代码都不应直接使用模型,而应使用资源管理器提供给您的句柄。显然,资源管理器必须在程序执行期间和来自不同地方的调用之间保持状态。它旨在跟踪哪些网格已经存储在内存中,并保留所有存储的网格的 VAO 标识符。考虑使用资源管理器的单例模式。

例子:

ModelHandle footman = resMan->getModel("footman.model");
//.....
footman->setLocation(x,y,z);
footman->draw();

这里对 getModel("footman.model") 的调用开始构建模型,导致调用如下

MeshHandle resMan->getMesh("footman1.mesh");

获取所有网格。getMesh工作是否引起了所有这些解释。它检查之前是否加载了此类网格,如果是,则仅将句柄返回给包含此网格的 VAO。否则,它会创建新的 VAO 对象,将请求的数据加载到其中并返回新创建的对象的句柄。此对象的所有后续请求都不会导致新的 VAO 分配。

当然,所描述的场景图组织只是它应该是什么样子的粗略近似。例如,它不区分模型和抽象场景图节点,但是为您的引擎开发和微调这种层次结构取决于您。

资源管理器类的最终接口是另一个需要讨论和设计的话题。一些问题和想法:

  • 您会使用单例还是出于某种原因决定使用全局变量?
  • 如果您决定使用单例,也许您希望为某些有限的资源集使用一些其他私有的非单例资源管理器?然后考虑将单例设计为包装模板类,以使这样的代码成为可能:
    ResourceHandle h1 = Singleton<ResourceMan>::instance->getResource("foo");
    ResourceMan myPrivateManager;
    ResourceHandle h2 = myPrivateManager.getResource("bar");
  • 您是使用一个综合管理器管理所有类型的资源,还是为每种资源类型使用专门的管理器类?第二种方法更好。第二种方法的开发思路,让你的编译器为你编写代码!将模板资源管理器类与专用方法的小子集一起使用。只需针对每种资源类型专门化一种资源创建方法,并保持所有其他资源管理代码不变!
  • 考虑资源生命周期。什么时候应该销毁特定的 VAO?考虑实现引用计数器和/或借用引用。
  • 缓存?将数据加载到设备(视频卡)后立即从主机内存中删除数据还是保留一段时间?多长时间?
  • 流媒体呢?它不应该是资源管理器的域,但流支持会影响它。
  • glIsVertexArray函数及其类似物可能很有用。

排序

VAO 不是渲染场景时需要更改的唯一资源。您还需要更改纹理、着色器甚至帧缓冲区 减少状态更改次数的常用方法是按某些属性对可显示对象进行排序。

例如,您很可能只使用一个着色器来渲染给定的网格。这就是为什么首先您可以按着色器对所有网格进行排序,因此您可以最大限度地减少着色器更改的数量。然后对于每个着色器(即在给定着色器的网格列表中),您可以按 VAO 对网格进行排序,以将 VAO 更改的数量减少到可行最低限度。按质地排序?如果您需要按纹理对对象进行排序以及在哪里进行排序,这取决于您的应用程序。

结论

总而言之,如果您正在编写游戏引擎,那么无论如何您都需要一个资源管理器。如果您为 VAO 编写了一个快速而简单的部分解决方案,那么您将面临完全相同的问题和处理纹理、附加帧缓冲区和许多其他对象的问题,因此最好实现一次好的资源管理器。

有用的文章开始:

http://www.gamedev.net/page/resources/_/technical/game-programming/a-resource-manager-for-game-assets-r3807

http://www.gamedev.net/page/resources/_/technical/game-programming/a-simple-fast-resource-manager-using-c-and-stl-r2503

很实用的书:

http://www.gameenginebook.com/

于 2015-04-08T17:52:58.913 回答
1

更换 VAO 并不昂贵确切的数字显然高度依赖于硬件和平台。但只是为了给你一个粗略的想法,几年前我在笔记本电脑上测量了每秒数百万的 VAO 开关数量。假设您的机器每秒可以切换 600 万次 VAO。如果您想以该速率达到 60 fps,您可以每帧切换 VAO 100,000 次。

现在,您当然不想将所有 CPU 时间用于切换 VAO。您的应用程序将有很多其他状态要更改,您必须处理自己的应用程序逻辑,理想情况下,您不希望将整体 CPU 负载保持在尽可能低的水平。所以我不想接近上面的数字。尽管如此,在相当高性能的计算机/设备上,每帧切换 1000 次 VAO 应该不是问题。

它确实可以与您通常在绘图调用之间进行的其他状态更改相媲美。您总是希望最小化它们(以及绘制调用本身的数量)。但就状态变化而言,绑定不同的 VAO 通常是相对便宜的。

如果您有共享相同几何图形的对象,例如示例中的树,那么您当然不应该拥有相同数据的多个副本。这只是常识,甚至与图形没有太大关系。浪费内存当然是不可取的。即使您没有陷入绝望的内存紧缩,拥有相同数据的多个副本仍然会损害性能,因为它可能会降低您的缓存命中率。

对于这种格式来说,如何设计您的游戏以使其发挥最佳效果在某种程度上是一个广泛的问题。好吧,我从来没有写过严肃的游戏,所以无论如何我都没有资格给你建议。我的第一直觉是拥有一组定义不同形状的类。例如,您可以拥有一个TreeShape拥有一棵树的几何图形(VAO 和 VBO)的 a。每种角色都一样。然后,如果您的场景包含一堆树,则您有Tree描述特定树的类,其中实例可能只包含有关树的位置/大小的信息,但它们都共享对相同的引用TreeShape。所以所有的树都使用相同的 VAO/VBO TreeShape,并且每个特定的Tree仅包含每个树实例实际上不同的信息。

于 2015-04-09T02:54:07.503 回答