|
OGRE-Next 4.0.0unstable
面向对象的图形渲染引擎 |
ArrayMemoryManager 是处理 SoA 内存的基础抽象系统。派生类(如 NodeArrayMemoryManager 或 ObjecDataArrayMemoryManager)会提供每个 SoA 指针所需字节的列表。我编写了一些幻灯片来[^4]帮助理解这些概念。
每个节点父层级对应一个节点数组内存管理器 。根节点属于父层级 0;根节点的子节点属于父层级 1。而根节点子节点的子节点则属于父层级 2。
每个渲染队列也对应一个对象数据数组内存管理器 。
节点内存管理器是主要公共接口,负责处理所有节点数组内存管理器 。
同样地, 对象数据内存管理器作为主要公共接口,负责处理所有对象数据数组内存管理器 。
NodeMemoryManager
Transform(包含 4x4 矩阵、位置、旋转、缩放,以及派生的位置、旋转、缩放等),所有数据均为 SoA 指针结构。ObjectDataMemoryManager
ObjectDataArrayMemoryManager。每个渲染队列对应一个。ObjectData(包含局部空间中的 AABB、世界空间中的 AABB、半径、可见性掩码等),所有数据均为 SoA 指针结构。
采用 SSE2 单精度浮点运算时,OGRE 通常每次处理 4 个节点/实体。但当仅存在 3 个节点时,我们需要为 4 个节点分配内存(ArrayMemoryManager 已实现此机制),并将未使用的节点初始化为合理默认值(例如将四元数与矩阵设为恒等变换),以防止计算过程中出现非数值(NaN)。若 xmm 寄存器中的某个元素包含 NaN 值,某些特定架构会出现运行降速。
此外, 但空指针无效,替代使用的是哑指针 。
SIMD 一致性对稳定性和性能都至关重要,且 99%的情况下由内存管理器负责维护
ArrayMemoryManagers 采用槽位概念进行运作。当节点请求变换时,实质是申请一个槽位;当可移动对象请求对象数据时,同样是在申请槽位。
在 SSE2 构建中,4 个槽位构成一个块。构成块所需的槽位数量取决于宏 ARRAY_PACKED_REALS 的设定值。
当槽位以 LIFO 顺序请求和释放时效率最高。
若不遵循 LIFO 顺序,释放操作(即销毁节点)会将释放的槽位加入列表。当请求新槽位时,将从该列表中取出并使用对应槽位并将其移除。若此列表过大,将执行清理操作。该清理阈值可进行调整。
当非 LIFO 顺序释放的槽位数量过大时触发清理。清理操作将重新移动内存使其恢复连续状态。
为何需要清理操作?简而言之, 性能 。设想以下示例(假设 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 的世界 Aabb、执行视锥体裁剪),所有访问操作都是顺序执行的。
代码将循环 4 次 ,依次处理 A、空槽、K、M。若 ARRAY_PACKED_REALS 值为 1,则代码需循环 13 次。
若该状态长期持续将显著降低效率。实际应用中处理4个节点时不会影响性能,但当此类"碎片化"现象发生在数千个节点上时,将出现可察觉的性能下降。
清理操作将移动所有节点以使其再次连续。
AKM* **** **** **** ****
因此,对于 SSE 构建,代码将只循环一次(如果 ARRAY_PACKED_REALS = 1,则循环 3 次)
ArrayMemoryManagers 预分配固定数量的插槽(总是向上取整到 ARRAY_PACKED_REALS 的倍数),在初始化时指定。
当达到此限制时,可能发生以下情况:
注意: 撰写本文时,内存管理器尚未提供直接设置预分配内存量或清理操作执行频率的方法。
高级用户通常习惯于对渲染目标进行底层操作。因此他们习惯于设置自定义视口并调用 RenderTarget::update。
这种方式过于底层。 现在建议用户通过设置合成器节点和多重工作区来实现多渲染目标(RT)的渲染操作,即使是为自定义内容服务时也应如此 。新版合成器比旧版(已被移除)灵活得多。更多信息请参阅合成器相关章节。
视口不再与摄像机关联,因为它们现在是无状态的(过去会缓存当前使用的摄像机),并且它们原先持有的许多设置(如背景色、清除设置等)已迁移至节点。详见 CompositorPassClearDef 与 CompositorPassClearDef。
RenderTarget::update 已被移除,因为渲染场景更新被拆分为两个阶段:剔除(cull)与渲染(render)。
若仍坚持使用底层操作,请参考 CompositorPassScene::execute 中的代码来理解如何准备 RenderTarget 并手动渲染。但我们再次强调,您应当尝试使用合成器(Compositor)。
您可以直接从 1.x 版本移植到 2.1 版本;但分阶段移植是更好的做法,因为改动是渐进式的,您可以在逐步测试新功能的同时适应这些变化,避免因一次性面对所有功能而手忙脚乱,也能更轻松地排查问题。
主要变更包括:
CompositorWorkspaceListener。若需自定义渲染或注入新可渲染对象,建议通过 CompositorPassProvider 创建自定义通道,这是最清晰且可扩展性最强的方式。Node::_getTransformUpdated 可强制节点更新自身,但仍需谨慎操作。例如移动摄像机节点时,物体和光源的视锥体裁剪可能已完成。因此若物体消失或光照异常无需惊讶。务必在调试版本中进行测试,我们的强断言机制将协助捕捉此类问题。
若您已完成1.9到2.0版本的迁移,这将是重大进展。您已成功过半。此时可测试合成器设置是否生效并熟悉其运作,确保无断言触发,所有内容正常显示。
2.0 至 2.1 版本间的两大突破性变革在于:v2 对象体系与 Hlms 材质系统(即 HlmsDatablock)
v2 对象中,Item 就是最典型的例子。它相当于全新的"实体"。Item 运行速度更快(具体快多少取决于场景复杂度)。
若现有引擎过于复杂而难以将 Entity 迁移至 Item,保留 Entity 是合理的选择。但对于新启动的项目,Item 是最佳选择(某些特殊情况除外)。v1 对象仍然可用,由于自动实例化功能对其生效,其性能表现依然相当出色。
Items 与 Entity 的区别在于,若存在 100 个相同网格实例且使用同一贴图阵列的 5 种材质,最终可能只需一次绘制调用即可完成渲染——即使这些对象属于 Entity 类型也不例外。
然而,如果你有 100 个不同的网格模型 (不同于前例中相同网格的实例);使用 Entity 需要 100 次绘制调用,而 Items 仍只需 1 次绘制调用即可完成。
Items 在 CPU 端的内存占用也低于 Entities。
若你决定沿用 Entity(这对现有代码库可能最合理;若是新项目或简单项目,则应完全采用 Items),最繁琐的部分可能是为命名空间添加"`v1::`"前缀。例如原先的 Entity 现在需写作 v1::Entity,许多其他类也需如此修改。
接下来是材质系统。这里的改动差异显著但通常更便捷。在 1.x 和 2.0 版本中,存在 Renderable::setMaterial 方法(及 setMaterialName)。而在 2.1 版本中,材质被强烈不推荐使用,你应当改用 Hlms 数据块(后处理除外)。
有个函数 Renderable::setDatablockOrMaterialName ,顾名思义,“会先检查是否存在指定名称的 HLMS 数据块,若不存在则尝试在底层材质中搜索”——这在移植时相当方便。
你需要将材质移植为 PBS 和无光照数据块等效形式。在材质中,通常需要定义顶点着色器程序和像素着色器程序,然后将这些程序链接到材质上。设置自动参数,设置纹理单元,再设置投射器——整套流程工作量巨大(还得用 HLSL、GLSL 等需要支持的着色语言编写着色器)。
注:严格遵循翻译规则,保持原文段落结构(此处为单段落),专业术语如 PBS/HLSL/GLSL 保留原名,技术术语"vertex shader program"等采用中文游戏开发领域通用译法。将原文六个动作节点(define/link/setup×3)处理为中文流水句形式,括号补充说明保持原位,通过破折号衔接体现原文语势。
当需要选择性开关功能时(如开启/关闭法线贴图;硬件骨骼动画等),情况会变得更糟。你甚至不得不定义多个着色器程序。
使用 Hlms 数据块就无需处理这些繁琐操作。只需设置纹理和颜色参数即可完成。Hlms 在配置数据块时会分析实体/物品对象,并基于模板生成对应的着色器。
最令人震惊的部分可能是:如果你曾编写大量直接操作 Material 的 C++代码(及其类成员:Technique、Pass、TextureUnitState),因为 HlmsDatablock 的机制完全不同。不过若通过脚本操作则毫无问题。不过,那些直接操作材质的代码功能很可能已被 Hlms 取代,今后不再需要了。
这样就搞定了 :)
如需自定义 Hlms,本手册提供了详尽的内部机制解析章节。内容涵盖其如何分析几何信息并与通道信息结合(这是阴影投射通道?接收通道?),最终才生成着色器的全过程。
大多数用户无需接触这部分内容,但若您追求特定视觉效果或需迁移旧框架的着色器,工作量将取决于具体需求:您可选择扩展或修改现有着色器模板;或创建专属模板;亦或修改 Hlms 实现的 C++部分。建议先查看生成的着色器效果(输出路径由 Hlms::setDebugOutputPath 指定),再反向追溯至模板以理解现有实现逻辑。