虚拟世界的管理
UNIGINE引擎使用了大量优化技术和实用方法来管理虚拟世界。 它们可以在不损失较多图像质量的前提下降低渲染负荷。
细节层次#
具有平滑的alpha融合特性的细节层次(LOD)用来降低3D对象的几何复杂度;借助该技术,当3D对象远离摄像机时,渲染器的负荷就能减轻。
Visibility Distances(能见距离) |
LOD的能见度参数由能见范围定义。 为此设置有两组参数:最小能见距离和最大能见距离; 它们都用来测量到摄像机的距离。 如果节点表面位于这些指定范围之内,那它会被显示,否则,就会被隐藏。
节点表面的范围代表了同一对象的不同LOD,它们不应重叠。 |
Fade Distances(渐隐/渐显距离) |
当做LOD间的切换时,不连续的LOD很可能会呈现出一种明显且又十分分散注意力的不自然改变 - ”细节突变“。 在渐隐/渐显距离之外,LOD间可以彼此平滑融合,而不是做骤然切换,该距离能使LOD在不知不觉间完成过渡。 像能见距离一样,衰减区域的范围由【最小渐隐/渐显】距离和【最大渐隐/渐显】距离定义:
尽管LOD间的alpha融合使效果看起来会好很多,但这也意味着在衰减区域内,要在同一时间渲染同一对象的两种版本。 考虑到这一因素,衰减区域则应尽可能的短。 |
处于节点表面的能见距离之内
|
处于渐隐/渐显距离之内
|
假设我们有两组LOD:高密度多边形surface_lod_0和低密度多边形surface_lod_1,现在需要安排这两组LOD彼此间完成平滑过渡。
-
我们想在50个单位距离上做切换。 为此,需要让节点表面的能见距离相互“靠拢“:
- 第一个LOD的节点表面surface_lod_0在摄像机靠近对象时应该是一直显现的。 所以它的最小能见距离设置为-inf。 而到摄像机的距离为50个单位就是它的最大能见距离。
- 紧随其后显现的是第二个LOD的节点表面surface_lod_1。 它的可见范围从50个单位距离(最小能见距离)开始,一直到无穷远(最大能见距离 = inf)。
-
现在的LOD切换是急剧地而不是平滑地。 想要实现平滑融合,就要设置对称的渐隐(第一个LOD)和渐显(第二个LOD)距离。 比如说,衰减区域要是5个单位,那么:
- 对于要渐渐消失的第一个LOD来说,它的最大渐隐/渐显距离设置为5。
- 对于要渐渐显现的第二个LOD来说,它的最小渐隐/渐显距离也设置为5。
作为结果,LOD将按如下方式改变:
从对象的边界框到50个单位距离 | 只有第一个LOD的节点表面surface_lod_0是完全可见的 |
50 — 55个单位距离 | 第一个LOD渐渐消失,第二个LOD渐渐显现 |
从55个单位到更远距离 | 只有第二个LOD的节点表面surface_lod_1是完全可见的 |
参照对象#
与LOD相关的参数不止一种:其中参照对象就是用来测量切换LOD时所使用的距离。 它指明了应该测量的是到节点表面自身的距离,还是到所处层级分支上级的任意节点表面或节点的距离。 每个节点表面都拥有两类参照对象:
Min Parent(最小父级数) |
最小父级数作为参照对象用来测量摄像机到它的最小能见距离:
|
Max Parent(最大父级数) | 最大父级数作为参照对象用来测量摄像机到它的最大能见距离。 引擎采用相同(同上)原理对其父级数进行计数。 |
让我们以房屋模型为例。 当摄像机拉近的时候,我们将会看到高密度多边形的清晰表面,比如拱门,石屋拐角,园窗和屋顶瓦片这些部分的细节。 当摄像机拉远的时候,所有这些表面都应同时切换为一种一致的低密度多边形的LOD表面。
高密度多边形模型
|
用于远处的LOD的低密度多边形模型
|
不过问题出在所有的这些清晰表面都拥有不同的边界框(Bounding Box)。 假如它们的距离是通过自身来检测的(最小和最大父级数都为0),又因为清晰表面的边界框会更靠近摄像机,那么房屋不同部分的LOD在被不均匀开启(或关闭)时就很可能会出现状况。 这会造成不自然的深度冲突变化。 此时,远处的拐角还没切换为更加精细的LOD,近处的就已被绘制了两次:作为高密度多边形拐角的LOD被绘制一次,同时作为统一的低密度多边形房屋的LOD又被绘制了一次。
假如我们将整个房屋的边界框设置为参照对象(最小和最大父级数都为1),那么不管我们从哪一侧拉近摄像机,它的所有表面都会同时切换。
在做检测时不止一个选项可用于不同的参照对象。 例如,下限(最小距离)用于检测节点表面自身,上限(最大距离)用于检测父级。 这可能听起来有些复杂,那就让我们看下方的图片。 第一幅图显示的是一个圆环,按照细节层次我们将其划分为了不同的节点表面。
在上面的图片中,最右边一列的节点表面在摄像机非常靠近它们时将被显示。 最左边一列的节点表面在摄像机远远离开它们时将被显示。 将多个节点表面合并成一个可减少要绘制对象的数量,因此,这样做可减少DIP请求数量,提高渲染速度。
注意:此处的所有最小距离都用于节点表面自身的测量,不过几乎所有的最大距离都用于测量其它参照对象,也就是父级。 这里的多张图片会有助于您理解该原理。
五角星代表摄像机;我们现在不考虑透过它能确切地看到什么事物。 在以上两张图片中,必要的节点表面会根据摄像机的位置以及摄像机到相应参照对象的距离来绘制。 例如,左图中,圆环的左上部作为单一的节点表面,右上部被划分为了两个分开的节点表面,下半部也作为单一的节点表面。 右图中,整个上半部被划分为了更小的可能扇区。
在上面的图片中,对于不同参照对象的距离测量,我们要正确“关闭”较小的单一扇区,而不是显示较大的扇区。 最大距离由父级扇区计算,这是因为相邻子扇区的距离可能会有很大不同。 最小距离由当前扇区计算,原因是假如摄像机靠它太近的话我们就需要将其显示。
界限#
绑定对象表示包围整个节点的球形或立方体体积,用于描述节点的大小和位置。 在 UNIGINE 中,这可以是轴对齐的边界框或球体。 边界仅针对具有视觉表示或其自身大小的节点定义。 以下“抽象”对象根本没有边界,因此被排除在空间树之外:
- Dummy Node
- Node Reference
- Node Layer
- World Switcher
- World Transform Path
- World Transform Bone
- World Expression
- Dummy Object (如果它没有分配身体身体)
由于在转换此类节点时节省了重新计算边界的时间,因此这种方法显着减小了树的大小并提高了性能。
使用以下类型的边界:
- 局部边界 (Local Bounds) - 使用不考虑物理和子级的本地坐标绑定对象:BoundBox 和 BoundSphere。
- 世界边界 (World Bounds) - 与本地相同,但具有世界坐标:WorldBoundBox 和 WorldBoundSphere。
- 空间边界 (Spatial Bounds) - 使用空间树使用的世界坐标绑定对象,因此考虑物理(形状边界等):SpatialBoundBox 和 SpatialBoundSphere。
并且在需要分层边界的地方使用它们的分层类似物(考虑到所有孩子)(它们很慢,但提供正确的计算):
- 局部层次界限 (Local Hierarchical Bounds) - 使用本地坐标绑定对象,考虑所有节点的子节点的边界:HierarchyBoundBox 和HierarchyBoundSphere。
- 世界等级界限 (World Hierarchical Bounds) - same as local ones, but with world coordinates: 与本地相同,但具有世界坐标:HierarchyWorldBoundBox 和 HierarchyWorldBoundSphere。
- 空间层次界限 (Spatial Hierarchical Bounds) - 空间树使用的分层边界对象,因此将物理考虑在内(形状边界等):HierarchySpatialBoundBox 和 HierarchySpatialBoundSphere。
室外空间#
适用室内场景的技术在用来管理大量风景地貌的时候是没效率的。 渲染速度直接受制于场景中绘制的实体和多边形的数量,以及为对象所做的物理演算,对于室外场景而言,其运算量通常都是非常高的。 因此,虚拟世界管理的主要目标就是只渲染看的见的区域而剔除所有其它区域。 如果虚拟世界不能被缩小成一系列的封闭区域,那就要使用被称为【space partitioning(空间划分)】的方法了。
在UNIGINE引擎中,通过自适应轴对齐BSP树来实现空间划分。
二元空间分区#
二元空间分区 是一种将场景沿轴划分为单独处理的区域的方法,因此更易于管理。 BSP树是通过对所有场景数据进行划分和组织得到的层次结构。 自适应行为允许通过将区域的大小调整为处理的几何形状和世界中对象的分布来优化 BSP 算法。
BSP树的构建方式如下:
- 创建根节点。 它是通过在整个场景上简单地跨越一个轴对齐的边界框来完成的。
- 边界框的空间被一个垂直于三个主轴之一的分区平面递归地细分为两个区域。 结果,创建了树的两个节点。 这些节点中的每一个都再次包含在轴对齐的边界框中,并且对它们中的每一个重复此步骤,直到表示整个场景几何图形。
-
当达到所需编辑器节点数的级别时,细分将停止。
如果某一层的划分平面分割了一个对象,则该对象停留在上一层,不会从树上滑下。 它经常发生在大而广泛的物体上,比如天空或巨大的建筑物。
在渲染期间,引擎循环遍历 BSP 节点以确定它们的边界框是否与视锥体相交。 如果节点通过此测试,则对其子节点重复相同的操作,直到到达叶节点或位于视锥体之外的节点。 对可见区域执行所有必要的计算,而场景的其余部分(即对象、它们的照明)被丢弃。
每次在世界中添加或删除对象时,以及当对象将其状态更改为碰撞对象或杂乱对象时,都会动态地重新生成树。 如果没有变化,树保持不变。 这种质量使自适应 BSP 不仅在渲染静态几何体时高效,而且在处理动态对象时也高效。
为了能提供高效的场景管理,同时也为了实现更好的多树平衡,遂创建了单独的类型树以用于不同的编辑器节点类型:
- World(世界)树负责处理所有遮挡器(Occluders),触发器(Triggers )和对象簇(Clusters)。
- Objects(对象)树包含所有对象,但不包含带有【collider(碰撞机)】和【clutter(杂物)】标记的对象。
- Collider(碰撞机)对象构成了单独的类型树以方便碰撞检测和避免出现一组对象全部紧靠另一组对象这样的最坏检测情况 。 很显然,对象间只有在处于场景中的同一区域内并发生重叠时才可以相交。 该类型树可彻底减少成对测试的数量,加速计算。
-
Clutter(杂物)对象由于自身被大量使用遂也被单独分了出来,它们可以打乱主对象树的平衡。
此空间树包括启用了 Immovable 标志的静态对象,以优化节点管理。 - Light(灯光)树负责处理所有光源。
- Decal(贴花)树负责处理贴花。
- Player(玩家)树负责处理所有类型的玩家。
- Physical node(物理节点)树负责处理所有物理作用力。
- Sound(音效)树负责处理所有声源。
Mesh Partitioning(网格划分)#
在达到编辑器节点级别之后,仍然需要做进一步的网格划分。 其划分还是基于同样原理:二分和轴对齐。 唯一的不同之处就是这些树是预先计算好的(它们在虚拟世界加载的时候生成),这一做的原因是网格属于烘焙对象,它无需为相关的树做动态更改。 网格被划分为如下类型树:
- Surfaces(节点表面)树
- Polygon(多边形)树
这两种基于网格的类型树为网格的快速相交和碰撞计算提供了基础。
透视投影(Perspective Projection)#
当人眼观看场景时,远处的对象要显得比近处的对象小 - 这被称为透视。 而正交投影会忽略这一影响实现精确测量,透视的定义表明了远处的对象作为缩小体提供了额外的现实信息。
视锥体(Viewing Frustum或View Frustum)是用于透视投影的虚拟摄像机的视角(Field of View);换句话说,它是我们在屏幕上看到的虚拟世界空间的一部分。 它的具体形状要取决于被模拟的摄像机类型,不过它通常就是一种平截头四棱锥体。 视锥体的平面与屏幕平行,它们分别称为近端剪裁平面(Near Plane)和远端剪裁平面(Far Plane)。
作为虚拟摄像机的视角它不是没范围的,有些对象是不进入视角的。 例如,比近端剪裁平面更靠近观看者的对象将不可见。 如果位于远端剪裁平面后面的对象没在无穷远处,或是有些对象被侧面切断了,那它们也同样是不可见的。 因为这类对象无论如何都是不可见的,所以我们就可以跳过它们的绘图。 丢弃看不见的对象的过程就叫做视锥体剔除。
正交投影(Orthographic Projection)#
当人眼观看场景时,远处的对象要显得比近处的对象小。 正交投影会忽略这一影响,它可用于创建满足建筑和工程所需的等比例绘图。
正交投影所用的视景体是一个矩形的平行六面体,或者更通俗的讲它是个盒子。 不同于透视投影,视景体的大小不会从一端到另一端发生改变,因此对象到摄像机的距离不会影响它所呈现出的大小。
遮挡剔除(Occlusion Culling)#
另一种流行的实用方法是移除被其它对象完全隐藏的那些对象,例如,我们无需绘制空白墙面后的房间,或是绘制完全不透明栅栏后的花卉。 这项技术称之为遮挡剔除(Occlusion Culling)。遮挡剔除的具体情况如下:
- 遮挡
- 潜在可见集,将空间按成群区域划分空间,每个区域都包含有一系列的多边形,这些多边形在该区域内的任何位置都可见。 之后,渲染器只需实时查询预先计算好的带有视图位置的设置即可。 该技术经常用来加速二叉空间划分(Binary Space Partitioning,简称BSP)。
异步数据流技术#
数据流是一种优化技术,它所采用的是不一次将所有数据都加载进随机存储器(RAM)。相反,只有需要的数据被加载,并在独立的异步线程中传输到GPU,其余的都是按需逐步加载。
因使用了数据流技术,以下数据将被异步加载进RAM:
- 所有材质的纹理,包括 立方体映射,体素探测映射 和烘烤阴影的阴影映射。
- ObjectMeshStatic, ObjectMeshClutter, ObjectMeshCluster.
节点的多线程更新#
节点的多线程更新(前提是您通过控制台命令world_threaded启用了多线程)可以大幅提高性能。 例如,当需要在虚拟世界中渲染大量的粒子系统或动态网格时这种技术手段就非常奏效。
世界中节点的异步更新取决于它们的类型和层次结构。 对于性能要求最高的项目,考虑到这一点很重要。
不同类型的节点有三种模式:
- 无更新 - 对于不改变且无需更新的节点(Mesh Static, NodeDummy、Decals、PlayerDummy等),这些节点被跳过。
-
独立更新 - 对于保证没有任何基于层次的逻辑的节点,这些节点会在需要时自动放入单独的线程中:
- ObjectLandscapeTerrain
- LandscapeLayerMap
- ObjectGrass
- MeshDynamic, (only with a rope, a cloth, or a water body assigned)
- WaterMesh
- PlayerPersecutor
- SoundSource
-
依赖更新(基于层次结构) - 此类节点可以受相似节点的影响(子 粒子系统 根据 父节点的更新等等...),这样的节点在同一个线程中被分组和更新。
依赖节点组是根据层次结构自动构建的。 唯一需要注意的是通过在依赖节点之间插入独立或未更新的节点来避免破坏依赖节点的层次结构。 例如。 如果你有一个粒子系统的层次结构应该在一个线程中一起更新,在它们之间插入一个 NodeDummy 将打破这个层次结构并将它们分开 线程。
以下节点类型取决于层次结构:
节点可以在运行时更改其更新模式,例如,将物理主体分配给 ObjectDummy 会将其更新模式切换为依赖。 对象的可见性(无论是粒子系统还是播放动画的蒙皮网格)会影响 它的更新频率。
多线程与内部任务系统结合的扩展使用确保负载在所有可用线程之间均匀分布。
节点缓存#
节点缓存用于加速加载过程:加载节点的隐藏副本(或节点层次结构)被添加到世界节点列表中,从而可以简单地获取缓存节点的克隆 ,而不是解析 .node 文件并再次检索数据。
当节点被缓存并且您尝试访问它时,请考虑以下几点:
- 如果节点按名称加载 - 节点按其 名称 存储在缓存中。
- 如果节点是从父节点 Node Reference 加载的 - 节点按其 GUID存储在缓存中。