推荐:使用 NSDT场景设计器 快速搭建 3D场景。

在这个教程中,我们将了解 Open Scene Graph 如何表示 OpenGL 图形状态,并探索 Open Scene Graph 优化渲染以最小化状态更改次数的一些方法。

1、OpenGL状态机

Open Scene Graph 最重要的优化之一是最大限度地减少 OpenGL 图形状态的更改次数。

OpenGL 被定义为一个复杂的状态机,其状态包含影响多边形渲染方式的所有内容。 构成状态的值范围从非常简单的开/关切换(例如启用深度测试或照明)到复杂的结构(例如纹理和着色器程序)可能占用图形处理器上数千或数百万字节的内存。 即使 OpenGL 驱动程序可能不需要将所有数据重新传输到显卡以更改状态,它仍然可能非常昂贵。 通常,当状态发生变化时,GPU 上的所有渲染都必须停止。 更改着色器或纹理的设置时间可能会很长。

一个 3D 场景可以包含数千个不同的对象,但其中许多对象将共享相同的图形状态。 如果具有相同状态的对象可以一起渲染,那么冗余的状态变化将被避免并且状态变化的次数将被最小化。 对象可以由程序以这种方式组织,但在像 Open Scene Graph 这样的场景图系统中,对象被分组在逻辑和空间层次结构中,这不一定与对象使用的图形状态有任何关系。

2、Drawable 和 StateSet

在 Open Scene Graph 中,几何图形存储在 Drawable 对象中场景图的叶节点中。

对于渲染,Drawable 提供了一个简单的接口: draw() 函数。 调用 draw() 时,必须正确设置 OpenGL 模型视图矩阵和所有其他渲染状态。 在每个视频帧,Open Scene Graph 遍历场景图。 在剔除(culling)的过程中,它确定哪些 Drawable 对象可见,记录模型视图矩阵和图形状态,并将它们存储在称为 RenderBins 的列表中。 一些 RenderBins 的内容会根据与视点的距离排序,但大多数 Drawables 会根据图形状态进行分组。 然后,在绘制过程中,所有的Drawables都会被依次渲染。

OSG 如何表示图形状态并识别具有相同状态的对象? 如果你做过 OSG 程序开发,就会知道不需要为场景图中的对象指定整个图形状态; 相反,我们创建一个称为  StateSet 的对象,它指定 OpenGL 状态的一小部分并将其附加到场景图中的一个节点。 下图是场景图的示意。 包含几何图形的可绘制对象由底部的多面体表示, 它们在 Geode 对象中存在,而 Geode 对象又是 Transform 对象的子对象。 设置纹理和材质属性的 StateSets 附加到场景图中的几个节点。 包含屋顶瓦片纹理的 StateSet 由图中的两个节点共享。

StateSet 可能会指定纹理、照明的材质属性、着色器的uniform参数以及一些 OpenGL 模式更改,例如启用混合和 alpha 测试。

StateSet 可以包含两种类型的值:模式和指向 StateAttribute 对象的指针。

模式是一个整数常量,它在传递给 glEnable()glDisable() 函数时控制 OpenGL 功能。 StateAttribute 是 OpenGL 状态中的一个值或值的集合。 属性直接对应于 OpenGL 状态机中的参数,但一些 OpenGL 参数可能被组合到单个 StateAttribute 中,并且 StateAttribute 可能具有不对应于任何实际 OpenGL 参数的成员。

这方面的一个例子是材质状态属性,它将环境、漫反射、镜面反射和发射材质属性组合到一个对象中,即使这些参数中的每一个都可以在 OpenGL 中独立更改。

Material 还有一个 setAlpha() 便捷方法,可以在一次调用中更改所有材质参数的 alpha 值; 使用原始 OpenGL 时必须手动完成此操作。 StateSet 的内部表示是非常动态的; 从 StateSet 的角度来看,没有任何预定义的模式,只有一个通用的状态属性类型系统是已知的。 纹理模式和属性根据纹理单元号存储在StateSet中。

场景图中的任何节点,从场景的根向下到叶的 Drawable,都可能有一个关联的 StateSet。 实际渲染 Drawable 时有效的 OpenGL 状态是默认状态和场景图中 Drawable 祖先的 StateSet 对象中设置的模式和属性的组合。 在树中设置较低的属性优先于在树中较高的 StateSet 对象中设置的属性,属性可以保护自己不被覆盖。

3、osg::State

OSG 使用 State 对象管理全局 OpenGL 状态。 我们通常不会遇到此对象,除非需要使用自己的 drawImplementation() 编写自定义 Drawable 类。 但 Open Scene Graph 在渲染场景时广泛使用它。

State 使用与 StateSet 相同的模式和状态属性,以便调用 OpenGL 来设置图形状态。 当前状态使用 StateSet 对象以一种旨在最小化将进行的低级 OpenGL 调用数量的方式进行更改。 请务必注意,状态属性相等性仅基于 StateAttribute 对象的标识或指针值。 State 不会查看状态属性或对它们调用 compare() 方法进行比较。

4、StateSet 和 StateSet 栈

如前所述,一个StateSet 是应用于当前图形状态的模式和属性的小型集合。 State维护了一堆StateSet对象,改变图形状态的主要公共接口包含 pushStateSet()popStateSet()apply()方法。

在内部,State 维护所有模式和属性类型的堆栈。 当调用 pushStateSet()popStateSet() 时,将遍历 StateSet 并压入和弹出各个模式和属性堆栈。 重要的是要注意 pushStateSet()popStateSet() 实际上并不改变 OpenGL 状态,必须调用  apply() 以强制 OpenGL 状态与所有模式和属性堆栈的当前顶部一致。

当单个模式和属性被压入和弹出时,State 对象会记录堆栈顶部的值是否真的发生了变化。 apply() 方法仅更新实际更改的 OpenGL 模式和属性。 这种惰性更新允许在实际更改 OpenGL 状态之前推送和弹出多个 StateSet 对象,这很可能与原始状态相似或相同。 你可能会想象在遍历场景图时会发生 StateSet 堆栈修改。 Open Scene Graph 渲染遍历实际上并没有直接执行此操作,但它确实执行了一系列类似的从 State 到 State 的“移动”,以类似堆栈的方式推送和弹出 StateSet 对象。

你可能会在 OSG 代码中看到一些处理 StateSet 对象的便捷方法。  State::apply(const StateSet*) 压入一个 StateSet,调用 apply() 然后一次弹出 StateSet。 State::insertStateSet() 弹出多个 StateSet,插入一个新的 StateSet,然后将旧的推回。 这种和其他类似的方法用于渲染遍历中的各种效果。 还有 applyMode()applyAttribute() 方法可以进行更改并将参数的堆栈标记为已更改,即使没有推送或弹出任何内容。 这将强制在下一次调用 apply 时正确更新该参数。

5、在自定义Drawable中使用状态

如前所述,你可能不会在普通的 OSG 代码中使用 State 对象,但是如果使用自定义的 drawImplementation() 方法编写自己的 Drawable 类,就会用到State。 有关使用 State 的示例,请查看 Open Scene Graph 类的源代码,如 osgText::Text,它在其绘制方法中进行了许多 OpenGL 调用。

除了管理 StateSet 对象中保存的图形状态外,State 还控制较低级别的 OpenGL 状态,例如顶点属性数组的使用。 State 还具有用于调用可能作为扩展实现的 OpenGL 函数的成员函数。

我们应该在自己的代码中使用状态管理功能,而不是使用较低级别的 OpenGL 等效功能; 否则,OpenGL 状态将变得与 State 对象的模型不一致,从而导致视觉失真甚至崩溃。 另一种方法是使用 glPushAttrib()glPushClientAttrib() 函数将所有 OpenGL 状态保存在我们的函数中,运行图形代码并使用 glPopClientAttrib()glPopAttrib() 恢复状态。 这很慢,但如果需要包装遗留代码,可能没有其他选择。

6、中场小结

前面我们介绍了 Open Scene Graph 用于跟踪和修改 OpenGL 图形状态的机制、StateSet 和 State 类。 这些类在微观层面上优化了状态变化:当 StateSet 被压入和弹出时,State 对象只会调用 OpenGL 以获取实际变化的状态。

然而,用最少的状态变化渲染整个场景图的问题更复杂:应该用相同的 OpenGL 状态渲染的对象应该一起渲染,并且最好按以下顺序渲染所有对象 最小化状态变化的次数。 最后一项优化变得不那么重要了,因为在现代图形硬件上,对 OpenGL 状态进行任何更改的成本都非常昂贵,但是一起进行的多项更改由驱动程序分批进行,并不比一次更改的成本高多少。 尽管如此,场景中的对象仍需要排序以便按状态对对象进行分组,最好避免冗余的状态更改函数调用。

一些游戏引擎将完整的状态—着色器、纹理等—散列为一个整数,然后根据它对所有对象进行排序。 这可以很好地处理少量属性,尤其是当它们都可以存储在一个对象中时。 在那种情况下,如果状态对象与场景中的每一个几何体相关联,则可以简单有效地收集状态。 线性时间基数排序甚至是可能的。 然而,Open Scene Graph 的状态属性不仅存储在图形对象中,而且还贯穿于整个场景图中的 StateSet 对象中。 此外,由于 OpenGL 状态中有大量可能的属性,管理状态之间的转换和设置所有不同的属性变得很复杂。

Open Scene Graph 使用一种方案来保留状态集的应用顺序,因为它们出现在场景图的遍历中,同时将使用相同状态集的对象分组在一起,以便可以在不更改 OpenGL 图形状态的情况下渲染它们。 这称为StateGraph(状态图)。 它是由几个合作类构建和遍历的图 — 实际上是一棵树。

7、StateGraph

StateGraph 类表示 StateSet 对象树中的一个节点。 下图显示了与前面图中所示的场景图相对应的状态图。 这棵树中的每个节点都有一个关联的 OpenGL 状态:它是将在 StateSet 对象中找到的所有属性从树的根向下应用到该节点的结果。

前面我们了解到 StateSet 对象可以有效地从 State 中推入和弹出,以达到不同的 OpenGL 状态。 我们可以看到,要到达 StateGraph 节点的状态,我们会将节点中的 StateSet 对象从根向下推送到该节点。 如果我们随后想要到达图中另一个节点的图形状态,我们会将每个父节点中的 StateSet 对象弹出到两个节点的共同祖先,然后在下降到新节点时推送 StateSet 对象。

事实上,有一个函数 StateGraph::moveStateGraph() 就是这样做的。 每当 Open Scene Graph 渲染需要与上次使用的图形状态不同的图形状态的对象时,就会使用此函数。

请注意,指向 StateGraph 的指针是图形状态的非常紧凑的表示,就像我们前面提到的哈希码一样。 为了使其有用,必须构建状态图,以便场景图中的每个 StateSet 应用程序链在状态图中都有一个唯一的节点。 OSG 还必须跟踪当前状态图节点,以便使用 moveStateGraph() 设置新的图形状态。 向后工作并使用 State 的当前状态查找 StateGraph 指针将非常困难。

每个 StateGraph 节点都包含一个指向其父节点和子节点映射的指针,由 StateSet 对象索引。 map使得图的构建更加高效。 该节点还包含应使用相应图形状态呈现的对象列表。 稍后我们将看到场景图是如何构建的,但首先我们将了解 StateGraph 在开放场景图“后端”中的使用方式。

8、RenderLeaf 和 RenderBin

正如本文第一部分所述,场景图中可见的Drawable对象在渲染之前被收集到RenderBin对象中。 可以用几种不同的方式对对象进行排序和绘制。

RenderBin 类实际上与称为 RenderLeaf 的 Drawable 周围的包装器一起工作。 该对象包含一个指向 Drawable 的指针、一个指向 Drawable 图形状态的 StateGraph 节点的指针,以及指向投影和模型视图矩阵的指针。 Open Scene Graph 不使用 OpenGL 的矩阵操作例程; 而是使用双精度浮点数在 CPU 上计算最终矩阵值。 双精度数的使用解决了使用大数(例如地心坐标)作为顶点坐标时精度损失的问题。

RenderBin 包含三个对象列表:

  • StateGraph 节点的列表。 使用该 StateGraph 节点按顺序呈现与每个节点一起存储的 RenderLeaf 对象。
  • RenderLeaf 对象列表。 例如,这些对象通常在场景中从后到前排序。 使用 alpha 混合的透明效果使用此顺序。
  • 将在此之前和之后渲染的其他 RenderBin 对象的映射。 该映射由一个整数 bin 编号作为键控:负值在当前渲染 bin 之前按顺序渲染,正值在其之后。

RenderBin映射提供了一种强大的机制来控制渲染顺序。 Open Scene Graph 程序员可能熟悉可用于不透明和半透明几何体的默认RenderBin。 StateSet 可以包含选择全局“不透明容器”或“透明容器”的高级呈现提示。

不透明容器仅包含 StateGraph 对象及其关联的 RenderLeaf 对象。 它的 bin 编号为 0。透明 bin 将 RenderLeaf 对象保存在其“细粒度”叶子列表中,这些叶子将在渲染场景之前从后到前排序。 它的 bin 编号为 10,因此它将在不透明几何体之后渲染。

通过 StateSet::setRenderingHint() 设置的渲染提示是一种建立在能够在每个 StateSet 中指定 RenderBin 之上的高级机制。 通常,与 StateSet 关联的RenderBin是从父节点中的 StateSet 继承的。

9、CullVisitor

在每个视频帧中,我们提到的对象——RenderLeaf、RenderBin 和 StateGraph——必须从应用程序场景图中的高级对象组装而成。 这是 CullVisitor 类的工作。

顾名思义,CullVisitor类的高级任务是在 OpenGL 渲染之前从场景中移除或“剔除”场景中相机视野之外的所有内容。 但这项工作与其说是移除,不如说是将场景中所有可见的东西组装起来。

CullVisitor 是一个开放场景图 NodeVisitor,它遍历每个相机的场景图,使用节点上的边界球体来避免下降到落在相机视锥之外的节点。 每个节点都可以包含一个 StateSet,它会影响场景图中该节点下方对象的图形状态。 这些 StateSet 对象必须合并到最终将用于渲染几何图形的 StateGraph 中。

这个过程相当简单。 CullVisitor 跟踪当前的 StateGraph 节点,该节点将用于渲染遇到的任何几何体。 当它在节点中找到 StateSet 时,它会检查 StateSet 对象是否与作为当前状态图的子节点的任何状态图节点匹配。 如果是,则该状态图节点成为当前状态图; 否则,当前状态图的一个新的 StateGraph 孩子被创建并成为当前状态图。 在当前场景图节点及其子节点的遍历完成后,当前状态图恢复到父状态图或以前的状态图。 通过这种方式,状态图模拟了场景图中 StateSet 对象的关系。 一个重要的结果是,在场景图中的不同节点中找到的 StateSet 对象将与单个 StateGraph 节点相关联,只要“父”StateSet 对象的链对于每次使用都是相同的。

CullVisitor 还在其场景图遍历期间作为堆栈跟踪当前RenderBin,如 StateSet 对象中指定的那样。 当遍历到达场景图叶子的几何体时,直接使用当前状态图节点和渲染容器构造一个 RenderLeaf 来保存几何体及其状态图,必要时将叶子放在状态图节点中, 然后将叶子放在当前渲染箱的适当列表中。 CullVisitor 对象的工作完成后,生成的RenderBin集合就可以渲染了。

10、结束语

本文介绍了 Open Scene Graph 图形状态管理的一些非常技术性的细节。 需要注意的一点是,StateSet 不对应于唯一的 OpenGL 图形状态; 相反,从场景图的根开始的有序 StateSet 对象链指定图形状态。

单个 StateSet 对象可以用在多个这样的链中,每个链将指定不同的图形状态。 对于它们确实指定的状态部分,拥有唯一的 StateSet 对象仍然很重要,因为 OSG 仅使用指针比较来检查两个 StateSet 对象是否相等。

StateGraph 机制可以很好地识别不同的、唯一的图形状态,并对使用这些状态呈现的几何图形进行分组。 为了尽量减少渲染时的状态变化,除了 StateSet 对象本身之外,还必须考虑场景图中状态集的顺序,以及它们与最终图形状态的关系。


原文链接:Open Scene Graph States and StateSets

BimAnt翻译整理,转载请标明出处