Unity教程之-在Unity中实现UMetaLod一个通用的增强版LOD方案

 

各位朋友大家好。这一次我们来聊一聊,如何在游戏中实现一个通用的增强版 LOD (Level-Of-Detail) 方案案。先解释一下为什么搞出一个很难念的名字 UMetaLod 吧——这实际上是前缀 u- 和 meta-lod 的组合。所谓 meta-lod 实际上是针对传统 LOD 而言的,用来表示一种更通用的广义的 LOD。


基本思路

我们知道,不管是 Unity 还是 Unreal,都有着内建的基于与摄像机距离的 LOD 机制。如果正确地设置了 LOD 的每个层级对应的模型,当摄像机移动时,引擎会以一定频率计算 LOD,并把目标切换为对应层级精度的模型。

那么为什么我们还要手动实现一个所谓的增强版本呢?

这主要有以下几个方面的考虑:


其一,手动定制的 LOD 系统,除了以该物体与摄像机的距离为基础,还会考虑

  • 影响因子 1 – Bounding Box Factor – 该物体的包围盒尺寸
  • 影响因子 2 – Geometry Complexity Factor – 该物体的顶点数量
  • 影响因子 3 – ParticleSys Complexity Factor – 该物体是否为粒子系统,如果是的话考虑粒子数量等参数
  • 影响因子 4 – Visual Impact Factor – 每个子物体的视觉影响,可由美术手动设置

这些影响以不同的可定制权重 (weight) 对整个 LOD 系统发挥作用,这样全面而综合地考虑后,呈现出来的渲染结果对实际画面的影响更小,优化也就会更有效。

除了这些内建的影响因子以外,用户还可以通过 AddUserFactor() 添加若干个定制的影响因子,参与到 LOD 系统的运算和评估中来。


其二,对当前系统的性能进行评估,并把结果以参数形式传入系统,可以有效地形成负反馈,提高系统的伸缩性和健壮性。这里主要可以考虑两个因素:

  • 一个是当前系统性能等级的评估,目前用一个枚举 Highend / Medium / Lowend 分别代表高中低档的目标机器
  • 一个是当前 5 秒内的平均 FPS 状况,用于表示当前游戏的运行时性能状况 这两个值健康程度越高,整个 LOD 系统就会调整至允许容纳更多的视觉元素;如果情况越恶劣,系统则倾向于使用更严格的约束,从而降低视觉元素的总量。

其三,传统的狭义 LOD 仅会在若干个不同精度的模型之间切换,而 UMetaLod 则是相对广义一些。UMetaLod 通过上面多因素的综合考虑和计算,得到一个针对当前物体的活跃度 (Liveness) 的概念,其值域为 [0, 1]。有了这个值,游戏内不同的系统,可以有针对性地对自己的对象做多种粒度,多个角度的不同处理,下面是一些常见的例子:

  • 对于常见的包含多个面片和粒子系统的技能特效,可以通过美术设置的权重 (即上面的 Visual Impact Factor),在活跃度发生变化时有选择地隐藏那些相对次要的部分,或者让其较早地淡出
  • 如果一个角色包含高中低的 shader 实现,可以在需要时,根据活跃度在不同复杂度的 shader 实现间切换
  • 可以开启/关闭对应的物理模拟,或更细粒度的调整 (调高/调低物理更新的频率)
  • 在需要时,根据活跃度使用更低面数的模型,更低骨骼数的骨骼动画,更低分辨率的贴图
  • 在需要时,根据活跃度简化或关闭动态的光照运算,调整和精简 shadow caster 的列表

对多种影响因子的综合评估负反馈的性能调节,和多层次细粒度的调整这三者结合起来,就构成了一个广义的 LOD 系统。UMetaLod 能够从整体上根据系统的负载能力和运行情况,自主地去调节和优化系统的性能表现。当然,如果需要的话,也可以通过暴露出来的大量参数去调整它的行为,是激进还是保守,还是每个子系统使用不同的策略,还是针对特定的游戏类型做定制,都是可以考虑的。


上个图吧,看上去跟传统的 LOD 区别不大。

UMetaLod

图中为了清晰起见,我隐藏了实际的物体,仅显示表示活跃度的调试线框,黑色表示活跃度为 0 而红色表示活跃度为 1,中间的过渡色则为环状的过渡区域。过渡区域的宽度直接关系到 popping 现象的多寡,也就是视觉跳跃感的强弱。


工程实现

代码简单说一下吧,先说一下伪码的运算流程。为了简明起见,我们把影响因子称为 FOI (factor of impact)

这个计算流程的实际代码在类 UMetaLod 的这个函数里:

下面是系统中内建的四个影响因子,均定义有各自的取值范围和权重。正如上面提到的,用户还可以通过 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); }

正如之前的 UQtConfigUMetaLod 也提供了一些可配置参数来调整行为


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 允许用户使用自定义的公式替换掉默认的热力衰减运算。

其他的代码就不一一说明了,感兴趣可自行查看,文末附有 下载链接


优化和扩展

这里先简单地提两点吧。

  1. 一个是可以与上篇《Unity教程之-基于四叉树UQuadtree在Unity中实现场景资源的动态管理》 结合使用,把每个叶节点上的数据集作为一个 UMetaLod 的 Lod Target,这样的好处是可以以区域为单位批量化运算,避免以单个对象为粒度所产生的大量近似的冗余运算。
  2. 另一个是如果单帧的运算量过大,更新时可以划分为四个象限,逐象限计算和更新,也就是分拆到不同的帧去做增量更新。由于整个系统更新频率较低 (默认为 0.2s 更新一次),相邻的不同帧之前可以看做是等同的。即使万一由于玩家的移动漏更了一两个对象,也会在下一个 0.2s 周期就会处理,问题不大。

正如你可能已经发觉的那样,本文中一些细节并未充分地展开说明,如果你对背后的思路感兴趣,希望了解更多的实现细节,可以阅读此文,这是我此前实现的一个类似系统的一些开发日志的整理,也是此文中一些概念的来源。


代码及对应的测试工程,在 Unity-5.0.1f1 下编译和运行通过。下面是下载地址

UMetaLOD.txt (下载181 )

好了,本篇unity3d教程到此结束,下篇我们再会!