ogre-next 学习笔记 - [ogre 2.0移植手册 - 3 技术概述](翻译)

3 技术概述

3.1 概述

3.1.1 SIMD一致性

ArrayMemoryManager 是用于处理 SoA 内存的基本抽象系统。派生类(如 NodeArrayMemoryManager 或 ObjecDataArrayMemoryManager)提供了一个列表,其中包含将要使用的每个 SoA 指针所需的字节数。我写了几张幻灯片到1帮助理解这些概念。

每个 Node 父级别都有一个 NodeArrayMemoryManager。根节点是父级 0;根的子级是父级 1。而Root的孩子的孩子是父母2级。

每个渲染队列还有一个ObjectDataArrayMemoryManager。

NodeMemoryManager 是主要的公共接口,它处理所有NodeArrayMemoryManagers。

同样,ObjectDataMemoryManager是主要的公共接口,它处理所有ObjectDataArrayMemoryManager。

NodeMemoryManager

  • 多个NodeArrayMemoryManager。每个父级别一个。
    • 每个都返回一个变换(包含4x4矩阵,位置,旋转,比例,派生的pos,rot,scale;等),这都是SoA指针
      ogre-next 学习笔记 - [ogre 2.0移植手册 - 3 技术概述](翻译)

ObjectDataMemoryManager

  • Multiple ObjectDataArrayMemoryManager.每个渲染队列一个。
    • 每个都返回一个ObjectData(在本地空间,世界空间的Aabb,半径,可见性掩码等),这都是SoA指针。
      ogre-next 学习笔记 - [ogre 2.0移植手册 - 3 技术概述](翻译)

3.1.1 SIMD一致性

使用SSE2单精度,OGRE通常一次处理4个节点/实体。但是,如果只有3个节点;我们需要为其中4个分配内存(ArrayMemoryManager已经处理了这一点),并将值初始化为正常的默认值,即使它们没有被使用(例如。将四元数和矩阵设置为标识)以在计算期间防止NaNs。如果 xmm 寄存器中的一个元素包含 NaN,则某些体系结构会变慢。

此外,空指针无效,但使用虚拟指针代替。

SIMD 一致性对于稳定性和性能都非常重要,并且 99% 的时间由内存管理器负责

3.2 内存管理器使用模式

ArrayMemoryManagers使用插槽的概念。当节点请求转换时,它要求一个槽。当MovableObject请求ObjectData时,它正在请求一个插槽。

在 SSE2 版本中,4 个插槽构成一个块。制作一个块需要多少个槽取决于宏ARRAY_PACKED_REALS的值。

插槽在请求时效率最高,并按后进先出顺序释放。

当后进先出顺序不被遵守时,释放(即销毁节点)会将释放的插槽放入列表中。请求新槽时,将使用列表中的槽并从中删除该槽位。当此列表变得太大时,将执行清理。可以调整清理阈值。

3.2.1 清理

当以非后进先出顺序释放的插槽数量增长过大时,就会进行清理。清理将移动内存,使其再次连续。

为什么需要清理?简而言之,性能。假设以下示例(假设 ARRAY_PACKED_REALS = 4):用户创建了 20 个节点,命名为 A 到 T:

ABCD EFGH IJKL MNOP QRST

然后,用户决定删除节点 B、C、D、E、F、G、H、I、J、L、N、O、P、Q、R、S、T;生成的内存布局将如下所示:

A * * * * * * * * * K * M * * * * * * *

其中星号"*"表示空插槽。解析 SoA 数组时(即在更新场景节点、更新 MovableObject 的世界 Aabbs、视锥体剔除时),所有内容都是按顺序访问的。

代码将循环4次,以处理A,然后什么都没有,然后是K,它们是M。如果ARRAY_PACKED_REALS为 1;代码将循环 13 次。
如果长时间停留,这显然是低效的。在实际应用中,如果只对 4 个节点执行此操作,则此问题不会影响性能,但如果这种"碎片"发生在数千个节点上,则性能下降会很明显。

清理将移动所有节点以使它们再次连续:

AKM* **** **** **** ****

因此,对于 SSE 构建,代码只会循环一次(如果 ARRAY_PACKED_REALS = 1,则为 3 次)

3.3内存预分配

ArrayMemoryManagers 预先分配在初始化时指定的固定数量的插槽(始终四舍五入到ARRAY_PACKED_REALS的倍数)。

达到此限制时,可能会发生以下情况:

  • 调整缓冲区大小。任何指向插槽的指针都将失效(即持有对 Transform::mPosition 的外部引用)。节点 &MovableObjects 内部指针会自动更新。

  • 达到硬限制(硬限制也在初始化时设置,默认值为无限制)。发生这种情况时,内存管理器将抛出。

3.4 配置内存管理器

注意:在撰写本文时,内存管理器没有直接的方法来设置预分配的数量或它们执行清理的频率。

3.5 RenderTarget::update在哪里?为什么我在视口中收到错误?

高级用户可能习惯于渲染目标的低级操作。因此,他们习惯于设置其自定义视口并调用 RenderTarget::update。

这太低了。相反,现在鼓励用户设置合成器节点和多个工作区以执行对多个RT的渲染,即使它是针对您自己的自定义内容。新的合成器比旧的合成器(已被删除)灵活得多。有关详细信息,请参阅有关合成器的部分。

视口不再与摄像机相关联,因为它们现在是无状态的(它们用于缓存当前正在使用的摄像机),并且它们曾经保存的许多设置(如背景颜色,清晰设置等)已移至节点。请参阅CompositorPassClearDef和CompositorPassClear。

RenderTarget::update已消失,因为渲染场景更新已分为两个阶段:剔除和渲染

如果您仍然坚持使用低级别,请参阅CompositorPassScene::execute上的代码,了解如何准备 RenderTarget 并手动渲染它。但同样,我们坚持认为您应该尝试合成器。

3.6 从 1.x 到 2.0 的移植

您可以直接从1.x移植到2.1;但是,分步进行操作可能很棒,因为更改是渐进式的,您可以适应它们,同时也可以逐步测试新功能,而不会一次被所有功能所淹没,然后找出出了什么问题。

主要变化是:

  • 合成器。绝对是最明显的变化。合成器不再只是用于后期处理花哨的效果。它是ogre的关键组成部分。你需要告诉 Ogre 你希望它如何渲染你的场景。您可以通过合成器执行此操作。有关详细信息,请参阅合成器一章。

  • 监听器。ogre是重监听器的。每个事件都有一个侦听器。这使得性能瘫痪。许多监听器被删除,少数可能不起作用。现在最重要的监听器是CompositorWorkspaceListener。此外,对于自定义渲染和注入新的可渲染对象,请考虑通过 CompositorPassProvider 创建自定义Pass,这是最干净、最可扩展的方式。

  • 场景节点、灯光和摄像机的行为:灯光必须附加到场景节点。一旦 Ogre 开始渲染(例如,在侦听器内部),就不应该修改 SceneNodes & Cameras。如果您仍然这样做,在某些情况下调用 Node::_getTransformUpdated 可以强制 Node 自行更新,但您仍然必须小心。例如,如果移动摄像机的节点,则很可能已经对对象和灯光进行了视锥体剔除。因此,如果物体消失或未正确点亮,请不要感到惊讶。请务必在调试版本中进行测试,以便我们的主动断言可帮助您捕获这些问题。

3.7 从2.0移植到2.1

如果你已经从1.9到2.0,那就太好了。你已经成功了一半。您可以测试您的合成器设置是否正常工作,并且您习惯了它,没有触发断言,所有内容都显示出来。

在2.0和2.1之间引入的两个突破性变化是:v2对象和Hlms材质(又名HlmsDatablock)

v2 对象,Item 就是它的明显示例。这是新的"实体"。Item更快(根据您的场景,速度会快得多)。

如果预先存在的引擎太复杂,无法将实体迁移项目;放弃实体是一个合理的选择。但是,如果您要开始一个新项目,则 Items 是最佳选择(某些例外)。v1 对象确实有效,但它们仍然获得相当不错的性能,因为自动实例化适用于它们。

Items和实体之间的区别在于,如果您有100个相同Mesh的实例,其中5个材质使用相同的纹理数组;它们可能最终只是一个Drawcall。即使它们是实体。

但是,如果您有 100 个不同的Mesh(相同Mesh但不是Instance);实体需要 100 个绘制调用,而 Items 仍然可以在 1 个绘制调用中执行此操作。

item在 CPU 端的内存占用量也低于实体。

如果你决定保留 Entity(这可能是对现有代码库最有意义的;如果这是一个新的代码库或一个简单的代码库,那么 Items 一直都是这样)可能最令人讨厌的部分将是为命名空间编写"v1::"前缀。它现在不是实体,而是 v1::Entity,依此类推,具有许多多个类。

然后是材质。这是完全不同的地方,但往往令人愉快。在1.x和2.0中;有一个 Renderable::setMaterial 函数(和 setMaterialName)。在2.1中;强烈建议不要使用材质,您应该改用 Hlms 数据块(后处理除外)。

有一个函数 Renderable::setDatablockOrMaterialName,顾名思义,“首先尝试查看是否存在具有给定名称的HLMS数据块,如果没有,则尝试在低级材料之间搜索”,这在移植时非常方便。

您必须将材料移植到PBS和Unlit Datablock等效项。在材质中,您通常会定义一个顶点着色器程序,一个像素着色器程序。然后将这些程序链接到材料。设置自动参数。然后设置纹理单元。然后设置转换器。这是一项繁重的工作。(还可以在HLSL,GLSL以及您需要支持的任何其他内容中编写着色器)。

如果您想有选择地切换功能(例如,打开/关闭法线映射;硬件骨架动画等)。您甚至必须定义多个着色器程序。

使用Hlms数据块,您将无法处理任何此类问题。只需设置纹理,颜色参数,您就完成了。Hlms 将在设置数据块时分析Entiy/Item,并根据模板创建适当的着色器。

也许最令人震惊的部分是,如果你有很多直接操作材质的C++代码(及其类成员:Technique,Pass,TextureUnitState),因为HlmsDatablocks是完全不同的。但是,如果您通过脚本处理它们,则不会有任何问题。但是,很有可能,如果你有直接操纵材料的代码,它可能是为了做Hlms现在为你做的事情,你将不再需要它了。

然后你准备好:)

如果您想自定义Hlms,本手册中有一个关于其内部工作的非常长且内容丰富的部分。它如何分析几何信息并将其与通道信息相结合(这是阴影投射器通道吗?接收器通道?),然后才生成着色器

大多数用户不必接触它,但是如果您正在寻找非常特定的外观或想要从以前的框架移植着色器,这将取决于您需要执行的工作量:您可能只想扩展或修改着色器模板;或创建您自己的模板。或者修改 Hlms 实现的C++端。作为建议,首先查看生成的着色器的外观(它们被转储到Hlms::setDebugOutputPath所说的位置),然后倒退到模板以了解现有的实现。


  1. Mirror 1; Mirror 2 ↩︎

上一篇:【Airtest相关】收集一些Airtest的介绍


下一篇:LeetCode#860: 柠檬水找零