测量性能(Measuring Performance) ms
Posted 大哥大嫂过年好啊
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了测量性能(Measuring Performance) ms相关的知识,希望对你有一定的参考价值。
使用 game窗口的stats,frame debugger, 和 profiler
对比 dynamic batching, GPU instancing, 和 SRP batcher
显示一个帧率计数
通过函数自动循环(Cycle through functions automatically.)
功能之间平滑转变(Smoothly transition between functions.)
unity版本2020.3.6f1
1.分析unity(Profiling Unity)
unity连续的渲染新的帧。使所有的东西足够快的出现,让我们感觉图像的序列是连续的运动。典型的30帧每秒--缩写FPS--是最小的目标,理想状态是60FPS。我们的到这些数字是因为许多设备的显示刷新频率是60赫兹。你不能在没有关闭垂直同步的情况下超过这个帧率,那将导致图像撕裂。如果不能达到保持60FPS,那么最佳的帧率是30FPS,即每两个显示刷新为一帧。在降一步的话应是15FPS,但是不足以保持顺畅。
能否达到目标帧率依赖于处理每个单独的帧的时长。要达到60FPS,我们必须在16.67毫秒内update和render每一帧。30FPS的时长则加倍,33.33毫秒每帧。
当游戏运行的时候我们可以简单的通过观察来感受图像是否流程,但这是一个非常不严谨的测量性能的方式。如果看起来顺畅它肯能超过了30FPS,如果卡顿则可能不足30FPS。它也可能时顺畅时卡顿,因为性能不能保持一致。这可能是因为我们的app的变化,也可能是其他在运行的app导致的。unity提供了一些工具帮助我们测量性能。
1.1 游戏窗口统计(Game Window Statistics)
游戏窗口有一个统计面板,我们可以通过上方工具栏的状态按钮激活它。它显示了上一帧的测量数据。数据并不丰富,但它是我们能用来指示当前情况的最简单的工具。在编辑模式时,游戏窗口只在有东西改变时偶然的刷新。在play模式下则每帧刷新。
下面是我们圆环功能游戏的统计,使用默认渲染管线但从现在起我将用DRP作为参考。我开启了垂直同步,所以刷新频率同步于我的60赫兹显示器。
Statistics for DRP.
这个统计显示了这一帧,cpu主线程耗时31.7ms,render线程耗时29.2ms。基于你的硬件和游戏窗口尺寸,你会得到一个不同的结果。我这种情况渲染整个一帧耗时60.9ms,但是统计面板报告31.5FPS,和CPU耗时相关。这是极简单的统计方式,它只统计了CPU的数据,忽略了GPU。真实的帧率应该会更低。
除了时间和FPS,统计面板还显示了渲染的多种细节。有30003 batches, 0 saved by batching。有绘制命令被送往GPU。我们的画面有10000个点,所有好像每个点被渲染了3次。一次是depth pass,一次是shadow casters,一次是最终的cube。多出的三个batches是像天空盒和阴影这些附加的工作,独立于我们的画面。还有6个set-pass calls,可以认为是GPU以不同的方式重新读取配置渲染,比如使用不同的材质。
Statistics for URP.
如果我们使用URP,统计会有所不同。它渲染的更快。很容易看出原因:只有20 002 batches,比DRP少10 001。那是因为URP的directional shadows不使用分离的depth pass(separate depth pass)。它确实有更多的set-pass calls,但是这并不影响。
虽然Saved by batching报告为0,URP默认使用SRP batcher,但是统计面板不明白这些。SRP batcher 不会消除单个绘制命令但是会使他们更有效率。为了阐明这些,选择我们的URP asset,在advanced选项下,不勾选SRP Batcher 和 Dynamic Batching。
URP advanced settings.
此时URP性能会糟糕很多。
Statistics for URP without SRP batcher.
1.2 动态批处理(Dynamic Batching)
除了SRP Batcher,URP还有另一个动态批处理的开关(toggle)。这是一个古老的技术,将多个小meshes动态绑定为一个大mesh然后渲染。开启它后URP将batched缩减到10 024 并且统计面板指示9 978 draws 被消除。
Statistics for URP with dynamic batching.
在我的例子中,SRP batcher 和 dynamic batching 的性能表现差不多,因为我们图像里的cube meshes 正是 dunamic batching 的理想人选。
在DRP中不能使用SRP batches,但是我们能使用dunamic batching。此时我们可以在player project settings中的other settings 选项中找到开关,就在我们设置颜色空间的下面。只有在没有使用可编程渲染管线(scriptable render pipeline)时才能看到它。
Statistics for DRP with dynamic batching.
Dynamic batching 是非常有效的,消除了29 964个batches,减少到39,但是并没有表现出很大的帮助。
1.3 CPU 实例化(GPU Instancing)
另一个提升渲染性能的方法是开启GPU instancing。它可以仅使用一条绘制命令,让GPU绘制使用相同材质的同一网格(mesh)的多个实体。在这种情况下,我们需要开启每个材质的GPU Instancing。
Material with GPU instancing enabled.
URP优先选择SRP batcher而不是GPU instancing,所以我们需要先关闭SRPbatcher。此时我们可以看到batches的数量减少到了46,比dynamic batching好很多。我们稍后会发现这种差异的原因。
Statistics for URP with GPU instancing.
我们可以从这些数据推断,对于URP ,GPU Instancing 是最好的,接着是dynamic batching,最后是SRP batcher,但其区别不大。
对于DRP,GPU instancing 比 dynamic batching 导致了更多的batches,但是帧率更高了一点。
Statistics for DRP with GPU instancing.
1.4 Frame Debugger
统计面板可以告诉我们dynamci batching 和 GPU instancing 是不同的,但是不能告诉我们原因。为了更好的理解在发生的事,我们可以使用frame debugger,通过Window / Analysis / Frame Debugger打开。打开后它显示游戏窗口最后一帧发送给GPU的渲染命令的列表。这个列表在它的左侧。右侧是选中的绘制命令的详细信息。同时,在选中命令后,游戏窗口显示渐进式绘制状态。
在我们的例子中我们必须进入play模式,因为此时我们的图形才绘制。开启frame debugger后会暂停play 模式,以使我们可以检视绘制命令层级。我们先为DRP查看这些,即不使用dynamic batching 也不使用 GPU instancing。
Frame debugger for DRP.
我们看到一共有30 007 个draw calls,比统计面板多,因为有些命令不会作为batches统计,比如清空目标缓冲(clearing a target buffer)。我们点的30 000 个命令被分别列在DepthPass.Job、Shadows.RenderDirJob、RenderForward.RenderLoopJob之下。
如果我们再次启用dynamic batching,命令结构仍然是一样的,不同的是每个10 000命令组减少到了12个 Draw Dynamic calls。这是CPU--GPU通信开销方面的重大改进。
DRP with dynamic batching.
如果我们使用GPU instancing,每个命令组减少到20。又是一个巨大的改进,只是方法不同。
DRP with GPU instancing.
对于URP也是一样的,只是命令层级会不同。在这个例子中点被绘制了两次,第一次在Shadows.Draw 下面,第二次在RenderLoop.Draw下面。一个重大的不同是,dynamic batching貌似对 shadow map是无效的,这也解释了为何它对URP不太有效。我们最终也有22个batches而不是仅仅12个,表明URP材质比DRP依赖更多的网格顶点数据,所以单个批处理(batch)合适的点更少。不同于dynamic batching ,GPU instancing适用于阴影,因此在这个例子中它是最好的。
URP with nothing, dynamic batching, and GPU instancing.
最后,在SRP batcher 启用的情况下,10 000个点被列为11 个SRP Batch 命令,但是请记住,这些仍然是独立的绘制命令,只是非常有效的命令。
URP with SRP batcher.
1.5 一个额外的灯光(An Extra Light)
目前为止,我们得到的结果都是针对我们的图形,其基于单个平行光和其他我们使用的项目设置。让我们看一下我们在场景中再加一个灯光会发生什么,通过GameObject / Light / Point Light.添加一个点光源。将其位置设置为0并确保它不投射阴影,这也是它的默认行为。DRP支持点光源的阴影,但是URP还不支持。
对于额外的灯光,DRP现在需要额外的时间绘制所有的点。frame debugger 显示了RenderForward.RenderLoopJob 像之前一样渲染两次。更糟的是,dynamic batching 现在只对深度和阴影通道有效(depth and shadow passes),而对前向通道(forward passes)无效。
DRP with nothing and dynamic batching.
这是因为DPR为每个灯光绘制所有对象。有一个主通道工作与一个平行光,然后在它上面渲染额外的通道。发生这种情况是因为它是一种老式的向前附加渲染管线。Dynamic batching 不能处理这些不一样的通道,所以无法使用。
对于GPU instancing也是一样的,除了它仍然工作与主通道。只有额外的灯光通道不能从其受益。
DRP with GPU instancing.
对URP来讲第二个灯光好像没有什么区别,那是因为它是一个现代前向渲染器,可以在一个通道应用所有灯光。所以命令列表保持不变,即使GPU每次绘制需要计算更多灯光。
这些总结是相对于一个额外的作用于所有点的灯光。如果你增加更多灯光并且移动它们使不同的灯管作用于不同的点,事情会变得更加复杂,并且当使用GPU instancing时batches会分离增加。对一个简单场景适用的情况可能不适用一个复杂的场景。
1.6 (分析器)Profiler
为了更好的了解CPU发生的情况,我们可以打开分析器窗口。关闭点光源,打开窗口Window / Analysis / Profiler. 在play模式时它会记录性能数据并存储以为稍后视察。
分析器被分为两部分。它的顶部包含显示各种性能图像的模块列表。最上面是CPU Usage, 也是我们将要重点关注的。选中模块后,窗口底部部分显示我们选中的帧的详细信息。
Profiler showing CPU usage timeline, for DRP and URP.
CPU使用率的默认的底部视图是时间线。它显示了一帧中哪些东西消耗了多少时间。它显示了每一帧都开始于PlayerLoop, 它消耗最多的时间用在调用RunBehaviourUpdate.在往下两步我们看到它主要是调用我们的Graph.Update
方法。你可以选择一个时间线的块来看它的全名和时长。
在初始的玩家循环片段之后是一个短的EditorLoop 部分,接下来是另一个玩家片段,用于帧的渲染部分,此时CPU告诉GPU要做什么。这些工作被分在主线程、渲染线程和一些job 线程,但是对DRP和URP具体做法不同。
渲染部分之后--如果使用URP渲染线程仍在工作--接下来是另一个编辑器片段,这之后是另一帧的开始。线程也可能跨越帧边界。这是因为Unity可以在渲染线程结束前在主线程开启下一帧的更新循环。我们将在下一节稍后在讨论这一点。
如果你对线程精确的时间没兴趣,你可以通过左侧的下拉菜单用Hierarchy 视图替换Timeline 视图。
Profiler showing hierarchy.
1.7 分析构建(Profiling a Build)
分析器显示编辑器增加了很多额外开销。因此当我们的程序自己运行时我们去分析它会非常有用。为此我们需要构建我们的程序,专门用于调试。我们可以在Build Settings 窗口配置我们的程序如何构建,通过File / Build Settings.... 打开。如果你还没有配置过,Scenes in Build 是空的。这也没什么因为默认会使用当前打开的场景。
你可以选择你的目标平台,然后勾选Development Build 和 Autoconnect Profiler .
Development build for profiling.
当构建的程序运行一会儿之后切换回Unity。分析器现在应该包含了它执行的信息。有时第一次构建后不显示信息,此时就在构建一次。同时也要记住分析器在附加到构建后不会清除旧的数据,即使打开了Clear on Play ,所以要确保你正在查看正确的帧。
Profiling a build, DRP and URP
2 显示帧率(Showing the Frame Rate)
我们并不总是需要详细的分析信息,一个粗略的帧率通常够用了。我们也可能会在没有Unity编辑器的情况下运行我们的程序。此时我们能做的是在程序中测量和显示帧率,在一个小的面板上。
2.1 UI面板(UI Panel)
一个小的覆盖面板可以用Unity内置的UI创建。我们还会用TextMeshPro 来创建字体以显示帧率。TextMeshPro 是一个独立的插件(package),包含了高级的文字显示功能,比默认UI文字组件要优秀。
UI插件安装好后通过GameObject / UI / Panel创建一个面板。这将创建一个覆盖整个UI画布的半透明面板。画布会匹配游戏窗口尺寸,但在场景窗口会非常大。用2D模式观察它会比较容易。
Panel covering entire canvas.
每个UI有一个canvas root 对象,我们添加面板时会自动创建。面板时画布的字对象。一个EventSystem 游戏对象也会被创建,这是用来处理UI输入事件的。我们不会使用它所以可以忽略或删除它。
UI game object hierarchy.
画布有一个缩放组件可以用来配置UI的缩放。默认设置是constant pixel size 。如果你在用一个高清屏或视网膜屏那么你需要提升scale factor 否则UI会非常小。你也可以试验其他的试验模式。
UI canvas game object.
UI游戏对象有一个特别的的RectTransform
组件,它替换了通常的Transform 组件。除了通常的position,rotation,和scale,它还基于锚点有额外的属性。锚点控制着对象相对于其父对象的位置和缩放。最简单的改变它的方式是通过点击正方形的锚点图片打开的弹窗。
UI panel.
我们将帧率面板放置在窗口的右上,所以设置面板锚点为右上,中心点XY为1。设置宽度为38,高为70,位置为0。然后设置图片的颜色组件为黑色,保持透明度不变。
Dark panel in top right corner.
2.2 文字(Text)
为了给面板添加文字,通过GameObject / UI / Text - TextMeshPro创建一个TextMeshPro UI 文字组件。如果这是你第一次创建TextMeshPro 对象,会出现一个 Import TMP Essentials弹窗。根据提示导入essentials。 这会创建一个包含一些资源的TextMesh Pro 资源文件夹,我们不必亲自处理这些。
文字游戏对象创建完后,使其作为面板的子对象,设置它的锚点为两个维度的拉伸模式。通过设置上、下、左、右为0,将其覆盖整个面板。
UI Text.
接着,对TextMeshPro - Text (UI) 组件做一些调整。设置Font Size 为14,Alignment 为center middle。
Text settings.
我们现在可以看到我们的帧率计数器大概的样子。
Frame rate text.
2.3 更新显示(Updating the Display)
为了更新计数器我们需要一个自定义组件。用c#脚本创建一个FrameRateCounter
组件。给他一个序列化的TMPro.TextMeshProUGUI
字段来引用要显示数据的文字组件。
using UnityEngine;
using TMPro;
public class FrameRateCounter : MonoBehaviour {
[SerializeField]
TextMeshProUGUI display;
}
将这个组件添加到文字对象并连接显示。
Frame rate counter component.
为了显示帧率我们需要知道上一帧与当前帧之间的时长。可以通过Time.deltaTime
.获得这个信息。然而这个值会受到time scale影响。我们需要使用Time.unscaledDeltaTime
来代替。
void Update () {
float frameDuration = Time.unscaledDeltaTime;
}
下一步是修改显示的文字。我们可以通过调用它的SetText
函数并传入字符串参数来实现。
display.SetText("FPS\\n{0:0}\\n000\\n000", 1f / frameDuration);
2.4 平均帧率(Average Frame Rate)
显示的帧率最终变化迅速,因为连续帧之间的时间几乎不会完全一样。我们可以通过显示帧率的平均值来减少其不稳定。我们通过追踪整个期间内有多少帧被渲染来实现,然后显示帧的数量除以他们的总时间。
int frames;
float duration;
void Update () {
float frameDuration = Time.unscaledDeltaTime;
frames += 1;
duration += frameDuration;
display.SetText("FPS\\n{0:0}\\n000\\n000", frames / duration);
}
它运行一段时间后,将使我们的计数器趋向于一个稳定的平均值,但是这是我们程序整个运行时间的平均值。如果我们想要最近的信息,我们需频繁的重置和开始,取样一个新的平均值。我们通过添加一个序列化sample duration 字段来配置,设置它的默认值为1秒。给他一个合理的区间,比如0.1--2。
[SerializeField]
TextMeshProUGUI display;
[SerializeField, Range(0.1f, 2f)]
float sampleDuration = 1f;
Sample duration set to a single second.
从现在开始,当累计的时长等于或大于配置的时长时,我们只需调整显示。更新显示后将累计帧和时长设回0。
void Update () {
float frameDuration = Time.unscaledDeltaTime;
frames += 1;
duration += frameDuration;
if (duration >= sampleDuration) {
display.SetText("FPS\\n{0:0}\\n000\\n000", frames / duration);
frames = 0;
duration = 0f;
}
}
2.5 最好的和最坏的(Best and Worst)
平均帧率在波动因为我们的程序性能并非一成不变。它有时会降低或许因为它临时有更多的工作要做或者因为运行在这个机器上的其他程序在繁忙。为了搞清楚这些波动有多大我们也记录并显示最好和最坏的帧时长。
float duration, bestDuration = float.MaxValue, worstDuration;
每一帧都检查当前帧时长是否比最好的时长小。若如此将其设为最好时长。同时检查当前帧时长是否大于最坏时长。若如此将其设为最坏时长。
void Update () {
float frameDuration = Time.unscaledDeltaTime;
frames += 1;
duration += frameDuration;
if (frameDuration < bestDuration) {
bestDuration = frameDuration;
}
if (frameDuration > worstDuration) {
worstDuration = frameDuration;
}
…
}
我们现在将最好帧率放在第一行,平均帧率在第二行,最坏帧率在最后。
if (duration >= sampleDuration) {
display.SetText(
"FPS\\n{0:0}\\n{1:0}\\n{2:0}",
1f / bestDuration,
frames / duration,
1f / worstDuration
);
frames = 0;
duration = 0f;
bestDuration = float.MaxValue;
worstDuration = 0f;
}
2.6 帧时长(Frame Durations)
每秒帧数对测量性能是非常有用的,但当试图达到一个目标帧率时,显示帧时长会更有用。比如,当尝试实现稳定的60FPS时。所以让我们添加一个帧率计数器之外的显示模式。
在FrameRateCounter
中为FPS 和 MS 定义一个DisplayMode
枚举,然后添加一个这个类型的序列化字段,默认值为FPS。
[SerializeField]
TextMeshProUGUI display;
public enum DisplayMode { FPS, MS }
[SerializeField]
DisplayMode displayMode = DisplayMode.FPS;
Configurable display mode.
接着,当我们在update 中刷新显示时,检查模式是否是FPS。如果不是,用MS 替换 FPS 数据头,并将参数翻转。同时将参数乘以1000 以从毫秒转换到秒。
if (duration >= sampleDuration) {
if (displayMode == DisplayMode.FPS) {
display.SetText(
"FPS\\n{0:0}\\n{1:0}\\n{2:0}",
1f / bestDuration,
frames / duration,
1f / worstDuration
);
}
else {
display.SetText(
"MS\\n{0:1}\\n{1:1}\\n{2:1}",
1000f * bestDuration,
1000f * duration / frames,
1000f * worstDuration
);
}
frames = 0;
duration = 0f;
bestDuration = float.MaxValue;
worstDuration = 0f;
}
2.7 内存分配(Memory Allocations)
我们的帧率计数器已经完成了,但是继续进行之前让我们检查它影响了多少性能。显示UI每帧需要更多的draw calls,但是这并没有影响。在play模式中使用profile,在我们更新文字时查找一帧。可以看到它并不消耗很多时间,但是它需要分配内存。在层级窗口,根据GC Alloc 列排序,可以轻易的发现这种情况。
Allocations shown in profiler hierarchy.
字符串也是对象。当我们通过SetText
创建一个新文字时就产生一个新字符串对象,表示要分配1
06bytes。Unity 的 UI 刷新后增长到4.5 KB。这并不多但它会增长,在某个时间点触发一个内存垃圾回收流程,然后导致一个不希望有的帧时长峰值。
意识到临时对象的内存分配,并避尽量免重复出现是非常重要的。幸运的是,由于许多原因,SetText
和Unity的UI Update 只在编辑模式分配内存,比如更新文本输入域。如果分析一个构建,我们会发现一些初始分配但并不多。所以分析构建是必要的的。分析编辑器的play模式只是用于第一印象。
3 自动功能转换(Automatic Function Switching)
现在我们知道了如何分析我们的程序,我们可以在显示不同功能时对比其性能。如果一个功能需要更多计算,CPU就需要做更多工作,所以可能会使帧率降低。点如何计算对GPU来讲没有区别。如果分辨率一样GPU做的工作就一样。
最大的不同来自wave 和 torus 函数。我们可以通过分析区对比他们的CPU使用率。
Spike during switch from torus to wave.
CPU图像显示,从tours切换到wave后,负载确实减少了。切换的时候仍有一个帧时长峰值。这是因为通过编辑器做修改时,play模式会临时的暂停。接着还有一些小峰值,因为取消选择和编辑器焦点改变。
峰值属于other 分类。CPU图表可以通过开关左侧的类别标签进行过滤,我们可以只看相关的数据。将 other 分类禁用,计算量的改变会更显而易见。
Other category not shown.
通过inspector切换功能是非常笨拙的,因为会暂停。我们可以通过给我们的图标添加切换功能的能力来改善,除了通过它的inspector,还可以自动或者通过用户输入。在这个教程里我们将使用第一种。
3.1 循环功能(Looping Through Functions)
我们将自动循环所有功能。每个功能显示一个固定时长,然后切换显示下一个。为了使功能时长可配置,添加一个序列化字段,默认值为1秒。给它赋予Min属性使其最小值为0。
[SerializeField]
FunctionLibrary.FunctionName function = default;
[SerializeField, Min(0f)]
float functionDuration = 1f;
Function duration.
现在起我们就需要记录当前功能运行的时间并在需要时切换到下一个。我们需要修改 Update
函数。它目前仅仅处理更新当前的功能,所以我们将它移到一个独立的UpdateFunction
函数并让Update调用它。这使我们的函数更容易识别。
void Update () {
UpdateFunction();
}
void UpdateFunction () {
FunctionLibrary.Function f = FunctionLibrary.GetFunction(function);
float time = Time.time;
float step = 2f / resolution;
float v = 0.5f * step - 1f;
for (int i = 0, x = 0, z = 0; i < points.Length; i++, x++) { … }
}
现在添加一个持续时间字段,在 Update
开始时按delta time增加它。当持续时间等于或超过配置时长时将其重置为0。这些完成后再调用UpdateFunction
Transform[] points;
float duration;
…
void Update () {
duration += Time.deltaTime;
if (duration >= functionDuration) {
duration = 0f;
}
UpdateFunction();
}
函数运行大概永远不会精确的匹配配置的时长,我们会超过一点点。我们可以忽略这些,但是为了与函数时间精确的保持同步,我们需要减去额外的时长。为此我们需要从当前时长减去预期时长而不是设为0.
if (duration >= functionDuration) {
duration -= functionDuration;
}
为了循环功能,我们需要为FunctionLibrary
添加一个GetNextFunctionName
函数,它输入一个功能名字并返回下一个。由于枚举就是整型,我们可以对其参数加一并将其返回。
public static FunctionName GetNextFunctionName (FunctionName name) {
return name + 1;
}
但我们仍需循环回第一个功能而不是移过最后一个,否则我们将得到一个非法的名字。因此只有提供的名字小于torus时我们才增加它。否则我们返回第一个,也就是wave。
if (name < FunctionName.Torus) {
return name + 1;
}
else {
return FunctionName.Wave;
}
我们可以通过将名称--作为整型--与功能数组的长度减一进行比较,是该函数与功能名字无关。如果是最后一个,我们仍可以返回0,也就是第一个索引。这个方法的好处是如果我们后面修改了功能名字我们也不需要修改函数。
if ((int)name < functions.Length - 1) {
return name + 1;
}
else {
return 0;
}
也可以使用 ? : 三目运算符将函数体缩减到一个表达式。
public static FunctionName GetNextFunctionName (FunctionName name) {
return (int)name < functions.Length - 1 ? name + 1 : 0;
}
在 Graph.Update
中使用新函数来切换功能
if (duration >= functionDuration) {
duration -= functionDuration;
function = FunctionLibrary.GetNextFunctionName(function);
}
Cycling through functions.
现在我们可以在profile中按顺序查看功能的性能。
Profiling a build of looping functions.
在我的例子中所有功能的帧率是一样的,因为它从没有降到60FPS以下。通过等待垂直同步消除了那些差异。在profiler中只查看scripts差异会比较明显。
Only showing scripts.
结果是Wave是最快的,接下来是Ripple,然后是Multi Wave,Sphere 和 torus。这和我们的预期是一样的。
3.2 随机功能(Random Functions)
让我们做些有趣的修改,添加一个选项,在切换功能时可以随机而不是按一个固定的顺序。给FunctionLibrary添加一个GetRandomFunctionName
函数。它可以通过调用Random.Range
函数来选择一个随机的索引。
public static FunctionName GetRandomFunctionName () {
var choice = (FunctionName)Random.Range(0, functions.Length);
return choice;
}
回到Graph,
为转换模式加一个配置选项,cycle 或 random。
[SerializeField]
FunctionLibrary.FunctionName function;
public enum TransitionMode { Cycle, Random }
[SerializeField]
TransitionMode transitionMode;
当选择下一个功能时,检查转换模式是否是cycle。如果是调用GetNextFunctionName,否则GetRandomFunctionName。由于这会使选择变得复杂,我们将这些代码放到一个独立的函数中,以保持 Update
简洁。
void Update () {
duration += Time.deltaTime;
if (duration >= functionDuration) {
duration -= functionDuration;
//function = FunctionLibrary.GetNextFunctionName(function);
PickNextFunction();
}
UpdateFunction();
}
void PickNextFunction () {
function = transitionMode == TransitionMode.Cycle ?
FunctionLibrary.GetNextFunctionName(function) :
FunctionLibrary.GetRandomFunctionNameOtherThan(function);
}
Picking random functions.
3.3 插值功能(Interpolating Functions)
我们通过使功能转换变得更有趣来结束本教程。我们将平滑的转换我们的图形而不是突然切换。这对性能检测来说也很有趣,因为在转换时它需要同时计算两个功能。
先给FunctionLibrary
添加一个Morph
函数,它将处理转换。给它和其他功能函数一样的参数,添加两个Function
参数和一个float
参数来控制图形合成过程。
public static Vector3 Morph (
float u, float v, float t, Function from, Function to, float progress
) {}
progress 参数是一个0--1的值,我们将使用它从第一个功能插值到第二个功能。我们可以使用Vector3.Lerp
函数,将两个函数的结果和progress传给他。
public static Vector3 Morph (
float u, float v, float t, Function from, Function to, float progress
) {
return Vector3.Lerp(from(u, v, t), to(u, v, t), progress);
}
Lerp 是线性插值的简写。在功能之间,他会导致一个连续、等速的转换。我们可以在开始和结束时降低progress来使其看起来更顺滑。通过将原始的progress替换为调用Mathf.Smoothstep
来实现,Mathf.Smoothstep
参数为0, 1和 progress。它提供了一个 函数,俗称平滑函数。Smoothstep
的前两个参数是偏移和缩放,我们不需要所以使用0和1
0–1 Smoothstep and linear.
return Vector3.Lerp(from(u, v, t), to(u, v, t), SmoothStep(0f, 1f, progress));
Lerp
函数会限制它的第三个参数在0--1范围内。Smoothstep
函数也是。我们配置后者输出0--1 的值。有一个LerpUnclamped
函数可以满足这种情况,所以让我们使用它。
return Vector3.LerpUnclamped(
from(u, v, t), to(u, v, t), SmoothStep(0f, 1f, progress)
);
3.4 变换(Transitioning)
功能间的变换周期需要一个持续时长,所以给Graph添加一个配置项。
[SerializeField, Min(0f)]
float functionDuration = 1f, transitionDuration = 1f;
Transition duration.
我们的图形现在有两种模式,变换或不变换。我们将用一个布尔字段记录它,它有一个bool
类型。我们还要记录将要变换的功能的名字。
float duration;
bool transitioning;
FunctionLibrary.FunctionName transitionFunction;
UpdateFunction
函数用于显示单个功能。将其复制让后重命名为UpdateFunctionTransition。修改它,使其获得两个功能并计算progress,即当前时长除以变换时长。然后在循环中调用Morph
。
void UpdateFunctionTransition () {
FunctionLibrary.Function
from = FunctionLibrary.GetFunction(transitionFunction),
to = FunctionLibrary.GetFunction(function);
float progress = duration / transitionDuration;
float time = Time.time;
float step = 2f / resolution;
float v = 0.5f * step - 1f;
for (int i = 0, x = 0, z = 0; i < points.Length; i++, x++) {
…
points[i].localPosition = FunctionLibrary.Morph(
u, v, time, from, to, progress
);
}
}
在Update
最后检查是否在变换,如果是,调用UpdateFunctionTransition,否则UpdateFuction。
void Update () {
duration += Time.deltaTime;
if (duration >= functionDuration) {
duration -= functionDuration;
PickNextFunction();
}
if (transitioning) {
UpdateFunctionTransition();
}
else {
UpdateFunction();
}
}
如果持续时长超过了功能时长,我们继续下一个。在选择下个功能前,指出我们正在变换并使变换功能等于当前功能。
if (duration >= functionDuration) {
duration -= functionDuration;
transitioning = true;
transitionFunction = function;
PickNextFunction();
}
但是如果我们已经变换完了我们也需要做一些其他事。所以首先要检查我们是否在变换。只有在没再变换时我们需要检查我们是否超过了功能持续时长。
duration += Time.deltaTime;
if (transitioning) {}
else if (duration >= functionDuration) {
duration -= functionDuration;
transitioning = true;
transitionFunction = function;
PickNextFunction();
}
如果我们正在变换,我们需要检查是否超过了变换持续时长。
if (transitioning) {
if (duration >= transitionDuration) {
duration -= transitionDuration;
transitioning = false;
}
}
else if (duration >= functionDuration) { … }
Transitioning between functions.
如果我们现在分析,我们可以看到在转换期间Graph.Update
确实消耗更多。消耗的确切时间由切换的功能决定。
以上是关于测量性能(Measuring Performance) ms的主要内容,如果未能解决你的问题,请参考以下文章
测量性能(Measuring Performance) ms
测量性能(Measuring Performance) ms