Unity实现平滑插值

对于那些不熟悉Unity的人来说,都知道每个脚本都有三个可以调用的update处理。需要更新处理的时候既可以调用Update,也可以调用更好用的LateUpdate。这两个都会用到全局变量Time.deltaTime来访问帧帧的时间间隔。FixedUpdate使用Time.fixedTimeDelta并以固定的时间步长运行,因此每帧可能会运行多次。

关于重要的Lerp问题。

这个问题似乎在论坛上一次又一次的被问到,如何实现完美平滑的插值和阻尼Damping。比如你有一个值a,并希望将其平滑地插值到另一个值b,你决定使用任意值插值系数r的线性插值。如果你在可变帧率函数(Update或LateUpdate)中执行了类似的操作,那么你就会遇到以下问题:

1个
a = Mathf.Lerp(a, b, r);

这段代码是有问题的,因为它在每个帧的ab 之间取出了一部分而已且我们知道帧频是可变的,因此平滑度是变化的。

一旦弄清楚了这一点,也许就可以进行一些研究了,在Unity文档中你会发现建议应该这样做:

1个
a = Mathf.Lerp(a, b, r * Time.deltaTime);

稍等,这也不太对。。。插值系数现在可能会超过1,这是不允许的。这是不能允许的。那接下来呢?咱们考虑一下不使用原始lerp,不适用deltatime了,但是这次将其放入FixedUpdate中,使用fixdeltatime。

但是这仍然是有潜在的问题的。这有点微妙,因为在FixedUpdate中运行的东西几乎永远不会处于当前帧的正确状态,因为更新速率不同。这意味着它们需要外插或内插才能实现平滑显示。Unity有一个选项可以为刚体启用外插或插值,因此,如果启用此选项并且要约束刚体属性,则刚体才将按预期工作。但是,如果您使用的不是外推值或内插值,则就FixedUpdate而言,平滑虽然在技术上是平滑的,但仍会在屏幕上看到卡顿现象。

使用固定更新无法避免的另一个问题是,如果您需要更改更新速率(例如,您想以100 Hz的频率运行物理)并且使用普通lerp,则都需要重新调整平滑值。

我们正在尝试做什么?

让我们简化一些事情,看看我们在这里实际要实现什么。现在,让我们假设我们总是向零插值。

解决此问题的一种方法是询问一秒钟后应保留多少初始值。假设我们的初始值为10,而每秒钟我们都会丢失当前值的一半:

  \ begin {align *} a(0)&= 10 \\ a(1)&= 5 \\ a(2)&= 2.5 \\ a(3)&= 1.25 \ end {align *}

让我们看一下它随时间变化的图形。我们可以看到,从起始值10到几乎为零,这是一条平滑的曲线。它永远不会完全达到零,但是会非常接近零。

Unity实现平滑插值_第1张图片

查看数字序列,我们可以很容易地将其概括为:

  \ begin {align *} a(t +1)= \ frac {a(t)} {2} \ end {align *}

或对于(0,1)范围内的任意比率r:

  \ begin {align *} a(t +1)= a(t)r \ end {align *}

如果我们比当前值领先一步,会发生什么?  

 Unity实现平滑插值_第2张图片

我希望这里的模式很清楚,所以我们可以更笼统地说:

  \ begin {align *} a(t + n)= a(t)\ space r ^ n \ end {align *}

这意味着我们可以在当前时间t取值,并在将来的t + n中计算任意时间的值。在这里必须意识到n不必是整数值,因此在这里使用deltaTime很好。这意味着我们现在可以编写一个帧速率感知函数,该函数将衰减为零,并在可变速率更新函数中使用它

1个
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18岁
// Smoothing rate dictates the proportion of source remaining after one second
//
public static float Damp(float source, float smoothing, float dt)
{
    return source * Mathf.Pow(smoothing, dt);
}
 
private void Update()
{
    a = Damp(a, 0.5f, Time.deltaTime);
}
 
// or
 
private void FixedUpdate()
{
    a = Damp(a, 0.5f, Time.fixedDeltaTime);
}

如果在一个指定范围插值呢?

如果您想从值a变为值b而不是零呢?在这里要实现的关键是,这只是图形在y轴上进行平移。如果我们现在从20衰减到10,则它看起来像这样:

Unity实现平滑插值_第3张图片

因此,我们需要使用(a – b)添加阻尼,然后再添加b。让我们更改阻尼功能来做到这一点:

  \ begin {align *} a(t + n)&= b +(a(t)-b)r ^ n \\&= b-br ^ n + a(t)r ^ n \\&= b( 1-r ^ n)+ a(t)r ^ n \ end {align *}

这看起来应该很熟悉……它的形式与标准Lerp相同,但rate参数具有指数:

1个
a(t + n) = Lerp(b, a(t), Pow(r, n))

您可能会在这里注意到这些参数与您期望的顺序不符,但这很容易解决,因为:

1个
Lerp(a, b, t) = Lerp(b, a, 1 - t)

因此:

1个
a(t + n) = Lerp(a(t), b, 1 - Pow(r, n))

我们可以直接编写此代码,或者更好的主意是将其包装到一个函数中,该函数将在两个任意值之间进行帧速率感知阻尼:

1个
2
3
4
5
6
// Smoothing rate dictates the proportion of source remaining after one second
//
public static float Damp(float source, float target, float smoothing, float dt)
{
    return Mathf.Lerp(source, target, 1 - Mathf.Pow(smoothing, dt))
}

平滑率为零会给您返回目标值(即不进行平滑),从技术上不允许率为1,而只会给您返回源值(即无限进行平滑)。请注意,这与lerp参数的工作方式相反,但是如果您愿意,可以在Pow中使用平滑参数的加法逆。

指数衰减

你们当中敏锐的眼睛可能已经看过该图,并认为它看起来非常像指数衰减函数。您将是对的,因为它实际上指数衰减函数。要了解原因,让我们回到不带b的阻尼功能:

  \ begin {align *} a(t + n)= a(t)r ^ n \ end {align *}

现在,将其与指数衰减公式进行比较:

  \ begin {align *} a(t + n)= a(t)e ^ {-\ lambda n} \ end {align *}

让我们将其等同,看看会发生什么

  \ begin {align *} a(t)r ^ n&= a(t)e ^ {-\ lambda n} \\ r ^ n&= e ^ {-\ lambda n} \\&=(e ^ { -\ lambda})^ n \ end {align *}

因此

  \ begin {align *} r&= e ^ {-\ lambda} \\ \ lambda&=-\ ln(r)\ end {align *}

因此,表示阻尼函数的另一种方法是使用lambda参数化。现在它的范围在零到无穷大之间,很好地表达了这样的事实,即在阻尼时您永远无法真正达到b。

1个
2
3
4
public static float Damp(float a, float b, float lambda, float dt)
{
    return Mathf.Lerp(a, b, 1 - Mathf.Exp(-lambda * dt))
}

如果查看其他代码,将会看到通常使用的指数衰减形式,但是只知道它只是帧速率感知的Lerp的另一种形式(反之亦然,这取决于您如何看待它)。

下图显示了根据平滑率计算出的两种形式的λ阻尼。如您所见,它们都完美匹配。

 

 

 

Unity实现平滑插值_第4张图片

最后,这是同一张图,但这次使用的是随机时间间隔。

Unity实现平滑插值_第5张图片

摘要

  • 不要在Update或FixedUpdate内部使用普通的Lerp进行阻尼。
  • 在FixedUpdate中使用Lerp时要小心,并确保结果将被内插或外推。
  • 尽可能在可能的情况下,甚至在FixedUpdate中,最好使用帧速率感知阻尼功能,因为它将允许您更改固定更新速率而无需重新调整阻尼。

我希望这可以消除在阻尼值时如何正确使用Lerp的一些困惑。

你可能感兴趣的:(Unity实现平滑插值)