如何在Unity中实现UMetaLod一个通用的增强版LOD(3)

发表于2016-09-10
评论0 5k浏览
  各位朋友大家好。这一次我们来聊一聊,如何在游戏中实现一个通用的增强版 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 区别不大。


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

工程实现
  代码简单说一下吧,先说一下伪码的运算流程。为了简明起见,我们把影响因子称为 FOI (factor of impact)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
计算目标物体的活跃度()
 
{
 
// ==== 第一阶段 ====
 
获取目标物体与热点(摄像机或玩家的位置)的距离
 
分别计算四种内建 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 PerfLevelScaleLut = new Dictionary
 
{
 
{ 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 允许用户使用自定义的公式替换掉默认的热力衰减运算。
  其他的代码就不一一说明了,感兴趣可自行查看,文末附有 下载链接

优化和扩展
  这里先简单地提两点吧。
  一个是可以与上篇《Unity教程之-基于四叉树UQuadtree在Unity中实现场景资源的动态管理》 结合使用,把每个叶节点上的数据集作为一个 UMetaLod 的 Lod Target,这样的好处是可以以区域为单位批量化运算,避免以单个对象为粒度所产生的大量近似的冗余运算。
  另一个是如果单帧的运算量过大,更新时可以划分为四个象限,逐象限计算和更新,也就是分拆到不同的帧去做增量更新。由于整个系统更新频率较低 (默认为 0.2s 更新一次),相邻的不同帧之前可以看做是等同的。即使万一由于玩家的移动漏更了一两个对象,也会在下一个 0.2s 周期就会处理,问题不大。
  正如你可能已经发觉的那样,本文中一些细节并未充分地展开说明,如果你对背后的思路感兴趣,希望了解更多的实现细节,可以阅读此文,这是我此前实现的一个类似系统的一些开发日志的整理,也是此文中一些概念的来源。

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引