这篇 blog 是这个系列的第三篇,主题依然是 Unity 相关的实践。这一次我们来聊一聊,如何在游戏中实现一个通用的增强版 LOD (Level-Of-Detail) 方案。
先解释一下为什么搞出一个很难念的名字 UMetaLod 吧——这实际上是前缀 u- 和 meta-lod 的组合。所谓 meta-lod 实际上是针对传统 LOD 而言的,用来表示一种更通用的广义的 LOD。
我们知道,不管是 Unity 还是 Unreal,都有着内建的基于与摄像机距离的 LOD 机制。如果正确地设置了 LOD 的每个层级对应的模型,当摄像机移动时,引擎会以一定频率计算 LOD,并把目标切换为对应层级精度的模型。
那么为什么我们还要手动实现一个所谓的增强版本呢?
这主要有以下几个方面的考虑:
其一,手动定制的 LOD 系统,除了以该物体与摄像机的距离为基础,还会考虑
这些影响以不同的可定制权重 (weight) 对整个 LOD 系统发挥作用,这样全面而综合地考虑后,呈现出来的渲染结果对实际画面的影响更小,优化也就会更有效。
除了这些内建的影响因子以外,用户还可以通过 AddUserFactor() 添加若干个定制的影响因子,参与到 LOD 系统的运算和评估中来。
其二,对当前系统的性能进行评估,并把结果以参数形式传入系统,可以有效地形成负反馈,提高系统的伸缩性和健壮性。这里主要可以考虑两个因素:
其三,传统的狭义 LOD 仅会在若干个不同精度的模型之间切换,而 UMetaLod 则是相对广义一些。UMetaLod 通过上面多因素的综合考虑和计算,得到一个针对当前物体的活跃度 (Liveness) 的概念,其值域为 [0, 1]。有了这个值,游戏内不同的系统,可以有针对性地对自己的对象做多种粒度,多个角度的不同处理,下面是一些常见的例子:
把对多种影响因子的综合评估,负反馈的性能调节,和多层次细粒度的调整这三者结合起来,就构成了一个广义的 LOD 系统。UMetaLod 能够从整体上根据系统的负载能力和运行情况,自主地去调节和优化系统的性能表现。当然,如果需要的话,也可以通过暴露出来的大量参数去调整它的行为,是激进还是保守,还是每个子系统使用不同的策略,还是针对特定的游戏类型做定制,都是可以考虑的。
上个图吧,看上去跟传统的 LOD 区别不大。
图中为了清晰起见,我隐藏了实际的物体,仅显示表示活跃度的调试线框,黑色表示活跃度为 0 而红色表示活跃度为 1,中间的过渡色则为环状的过渡区域。过渡区域的宽度直接关系到 popping 现象的多寡,也就是视觉跳跃感的强弱。
代码简单说一下吧,先说一下伪码的运算流程。为了简明起见,我们把影响因子称为 FOI (factor of impact)
计算目标物体的活跃度() { // ==== 第一阶段 ==== 获取目标物体与热点(摄像机或玩家的位置)的距离 分别计算四种内建 FOI 在不同权重下的影响度,并累加 分别计算所有用户添加的 FOI 在不同权重下的影响度,并累加 计算经过所有 FOI 修正过的距离 // ==== 第二阶段 ==== 使用当前系统的性能评级和 FPS 来修正活跃度区域的上下限(也即热力环的热力衰减运算) // ==== 第三阶段 ==== 使用上面两个阶段的计算结果得出该物体的活跃度 }
这个计算流程的实际代码在类 UMetaLod
的这个函数里:
private void _updateLiveness(IMetaLodTarget target)
下面是系统中内建的四个影响因子,均定义有各自的取值范围和权重。正如上面提到的,用户还可以通过 void AddUserFactor(UImpactFactor userFactor)
来添加定制的影响因子。
public class UMetaLodConst { // the bounding volume of the target public const string Factor_Bounds = "Bounds"; // currently corresponds to vertex count of the target mesh, would be 0 for particle system public const string Factor_GeomComplexity = "GeomComplexity"; // currently correspends to particle count of the target particle system, would be 0 for ordinary mesh public const string Factor_PSysComplexity = "PSysComplexity"; // a subjective factor which reveals the visual importance of the target in some degrees // for instance, skill effects casted by player would generally has a // pretty much higher visual impact than a static stone on the ground public const string Factor_VisualImpact = "VisualImpact"; }
这些影响因子还可以设置不同的 Normalizer 去归一化传进来的值
public delegate float fnFactorNormalize(float value, float upper, float lower); ... public struct UImpactFactor { ... // customized Normalizer for different Impact Factor public fnFactorNormalize Normalizer; } ... // use methods like InverseLerp() to transform the parameter value into a valid FOI Normalizer = (value, upper, lower) => { return UMetaLodUtil.Percent(lower, upper, value); }
正如之前的 UQtConfig
,UMetaLod
也提供了一些可配置参数来调整行为
public static class UMetaLodConfig { // the time interval of an update (could be done discretedly) public static float UpdateInterval = 0.5f; // the time interval of an FPS update (could be done discretedly) public static float FPSUpdateInterval = 5.0f; // debug option (would output debugging strings to lod target if enabled) public static bool EnableDebuggingOutput = false; // performance level (target platform horsepower indication) public static UPerfLevel PerformanceLevel = UPerfLevel.Medium; // performance level magnifier public static Dictionary<UPerfLevel, float> PerfLevelScaleLut = new Dictionary<UPerfLevel, float> { { UPerfLevel.Highend, 0.2f }, { UPerfLevel.Medium, 0.0f }, { UPerfLevel.Lowend, -0.2f }, }; // heat attenuation parameters overriding (including the formula) public static float DistInnerBound = 80.0f; public static float DistOuterBound = 180.0f; public static float FpsLowerBound = 15.0f; public static float FpsStandard = 30.0f; public static float FpsUpperBound = 60.0f; public static float FpsMinifyFactor = -0.2f; public static float FpsMagnifyFactor = 0.2f; public static fnHeatAttenuate HeatAttenuationFormula = UMetaLodDefaults.HeatAttenuation; }
可以看到末尾的 HeatAttenuationFormula
允许用户使用自定义的公式替换掉默认的热力衰减运算。
其他的代码就不一一说明了,感兴趣可自行查看,文末附有 GitHub 链接。
这里先简单地提两点吧。
一个是可以与之前的 UQuadtree
结合使用,把每个叶节点上的数据集作为一个 UMetaLod
的 Lod Target,这样的好处是可以以区域为单位批量化运算,避免以单个对象为粒度所产生的大量近似的冗余运算。
另一个是如果单帧的运算量过大,更新时可以划分为四个象限,逐象限计算和更新,也就是分拆到不同的帧去做增量更新。由于整个系统更新频率较低 (默认为 0.2s 更新一次),相邻的不同帧之前可以看做是等同的。即使万一由于玩家的移动漏更了一两个对象,也会在下一个 0.2s 周期就会处理,问题不大。
正如你可能已经发觉的那样,本文中一些细节并未充分地展开说明,如果你对背后的思路感兴趣,希望了解更多的实现细节,可以阅读此文,这是我此前实现的一个类似系统的一些开发日志的整理,也是此文中一些概念的来源。
代码及对应的测试工程在这里,在 Unity-5.0.1f1 下编译和运行通过。
需要说明一下,这几期的代码都在西山居于 GitHub 上的 SeaSunOpenSource 组织的工程页面内维护。如未明确说明,均以 MIT License 发布。
[完]
Gu Lu
[2015-07-19]