找回密码
 注册帐号

扫一扫,访问微社区

RaymondChan Unity中Lerp与SmoothDamp函数使用误区浅析

62
回复
13903
查看
[ 复制链接 ]
3偶尔光临
281/300
排名
12481
昨日变化

4

主题

8

帖子

281

积分

Rank: 3Rank: 3Rank: 3

UID
254628
好友
2
蛮牛币
308
威望
0
注册时间
2017-11-16
在线时间
43 小时
最后登录
2018-10-19

专栏作家

2017-12-18 20:55:07 显示全部楼层 阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有帐号?注册帐号

x
本帖最后由 RaymondChan 于 2017-12-18 20:59 编辑

写在前面
Unity的Lerp和SmoothDamp这两个函数我们在处理物体移动、一些渐进变化或者摄像机的跟随等场景会较常用到,虽然Mathf、Vector2、Vector3等都有这两个函数,但用法上是基本一样的,用起来也比较简单。不过在实际的运用中,往往一不小心却会掉进坑里,下面我们一起来分析一下。

Lerp函数
Lerp函数使用的最常见的误区是把线性移动用成了弹性移动(关键是自己还不知道,还奇怪,咦,不是说好的线性移动吗?怎么有点弹性的赶脚……),如下图所示:
01.gif


这样子的代码一般是这么写的:
[C#] 纯文本查看 复制代码
private Vector3 target = new Vector3(0, 0, 5);

void Update()
    {
            transform.position = Vector3.Lerp(transform.position, target, 0.1f);
    }



这个错误主要是没有理解好Lerp的第三个参数t的作用,这里我们为了便于理解,我们拿Mathf.Lerp函数来分析,思路是一样的。我们先来看一下Mathf.Lerp函数的具体实现:
[C#] 纯文本查看 复制代码
        /// <summary>
        ///   <para>Clamps value between 0 and 1 and returns value.</para>
        /// </summary>
        /// <param name="value"></param>
        public static float Clamp01(float value)
        {
            float result;
            if (value < 0f)
            {
                result = 0f;
            }
            else if (value > 1f)
            {
                result = 1f;
            }
            else
            {
                result = value;
            }
            return result;
        }

        /// <summary>
        ///   <para>Linearly interpolates between a and b by t.</para>
        /// </summary>
        /// <param name="a">The start value.</param>
        /// <param name="b">The end value.</param>
        /// <param name="t">The interpolation value between the two floats.</param>
        /// <returns>
        ///   <para>The interpolated float result between the two float values.</para>
        /// </returns>
        public static float Lerp(float a, float b, float t)
        {
            return a + (b - a) * Mathf.Clamp01(t);
        } 



估计大家一看函数的实现就明白了关键点,想要线性移动,应该只控制t的变化,t的值在0-1,它就类似一个百分比的值,代表着a点到b点之间的位置,比如想要到a和b之间的3/10的位置,t就应该是0.3,想要一步到位t的值就应该为1。在上述错误的用法中,实际的效果就是第一次到达1/10位置,第二次到达1/10+9/10*1/10的位置……理论上讲永远到不了b点,每一次都是a与b点之间的1/10的位置,所以移动的幅度越来越小,有一种弹性感觉。正确的使用方法如下:
[C#] 纯文本查看 复制代码
    private Vector3 target = new Vector3(0, 0, 5);
    private Vector3 startPos;
    private float t1;

    void Start()
    {
        startPos = transform.position;
    }

    void Update()
    {           
            t1 += 1f * Time.deltaTime;
            transform.position = Vector3.Lerp(startPos, target, t1);
    }


效果如下:

02.gif


SmoothDamp函数
SmoothDamp函数在使用过程中比较容易出现的一个问题就是容易在代码较复杂的情形下将currentVelocity这个参数需要的变量定义为局部变量,如下:
[C#] 纯文本查看 复制代码
   private Vector3 target = new Vector3(0, 0, 5);
    public float smoothTime = 0.3F;

    void Update()
    {
            Vector3 velocity = Vector3.zero;
            transform.position = Vector3.SmoothDamp(transform.position, target, ref velocity, smoothTime);
        }
    }


使用局部变量的效果如下图所示,越来越慢,定义的平滑时间明明是0.3f,怎么用了10几秒的时间都还在缓慢移动?
03.gif


为了便于理解,我们来看一下Mathf.SmoothDamp的具体实现:
[C#] 纯文本查看 复制代码
public static float SmoothDamp(float current, float target, ref float currentVelocity, float smoothTime, [DefaultValue("Mathf.Infinity")] float maxSpeed, [DefaultValue("Time.deltaTime")] float deltaTime)
        {
            smoothTime = Mathf.Max(0.0001f, smoothTime);
            float num = 2f / smoothTime;
            float num2 = num * deltaTime;
            float num3 = 1f / (1f + num2 + 0.48f * num2 * num2 + 0.235f * num2 * num2 * num2);
            float num4 = current - target;
            float num5 = target;
            float num6 = maxSpeed * smoothTime;
            num4 = Mathf.Clamp(num4, -num6, num6);
            target = current - num4;
            float num7 = (currentVelocity + num * num4) * deltaTime;
            currentVelocity = (currentVelocity - num * num7) * num3;
            float num8 = target + (num4 + num7) * num3;
            if (num5 - current > 0f == num8 > num5)
            {
                num8 = num5;
                currentVelocity = (num8 - num5) / deltaTime;
            }
            return num8;
        }


通过具体的实现我们就可以很清楚的发现,currentVelocity这个变量是先使用,然后再赋值,通过ref传递就是为了获取到上一次计算得到的速度值,如果我们使用局部变量,每一次速度都是零,相当于刚开始进行平滑移动,平滑的距离(transform.position到target)不断在缩短,然而平滑时间却没有变化,为了保证大约在平滑的时间内完成平滑移动,这个起步速度肯定是越来越慢的,所以就导致了上图中的问题。
我们把currentVelocity改为全局变量,就可以看到正常效果了

[C#] 纯文本查看 复制代码
private Vector3 target = new Vector3(0, 0, 5);
    public float smoothTime = 0.3F;
    private Vector3 velocity = Vector3.zero;
    void Update()
    {
            transform.position = Vector3.SmoothDamp(transform.position, target, ref velocity, smoothTime);
    }


效果如下:
04.gif


写在最后
好了,以上就是这一次想要和大家一起讨论的内容了,希望对大家有那么一丢丢的帮助。

本文工程文件(Unity2017.2.0f3):

链接:https://pan.baidu.com/s/1kV7rd4b  密码:vft2





点评

小妞牛和RaymondChan(楼主)的相关点评,已在楼下置顶大家可以查看.  发表于 2017-12-20 09:44
参与人数 21鲜花 +61 收起 理由
清风绕 + 2 赞一个!
堕落的囹圄 + 1 很给力!
Tlrbs + 1 赞一个!
yilianxin + 5 淡定
没动00 + 5 赞一个!
RyeCat + 2 赞一个!
美ぁ无与伦比 + 5 很给力!
YT4444 + 5 很给力!
步行骑士 + 5 很给力!
煮粥侠 + 5 很给力!
X-eJack + 5 赞一个!
lilexy + 5 不哭不哭
jordanseed + 1 淡定
xifg2007 + 2 很给力!
仅为年时 + 20 淡定
盖世花满楼 + 1 不错,讲得通俗易懂
陌上君无邪 + 2 很给力!
Mill + 2 赞一个!
小妞牛 -20 所谓误区十分可笑!请看官方文档,明确线性.
perfecthacker + 1 很给力!

查看全部评分总评分 : 鲜花 +61

回复

使用道具 举报

排名
10707
昨日变化

13

主题

169

帖子

496

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
70381
好友
8
蛮牛币
439
威望
0
注册时间
2015-1-22
在线时间
200 小时
最后登录
2020-7-6
2017-12-19 13:36:31 显示全部楼层
本帖最后由 小妞牛 于 2017-12-20 13:16 编辑

这个帖子是水的吧,居然还顶置了
首先这个所谓的误区根本就是楼主自己过去的使用习惯有问题,请发帖前先看看官方文档好伐,不然很可笑啊,
   t1 += 1f * Time.deltaTime;这个1f有何意义
而且官方文档明确案例说明了,线性lerp要使用的t是time.time
本身人家设置这个api的时候用的就是时间增量,你用deltaTime这个衡量自加不是搞笑得嘛

我看这个帖子尴尬病都快犯了……
===

有些人回复我,觉得我说的有问题,我统一在这里补充一下—
(下面是我回复别人的,我直接粘过来了)

我没说他这点说的有问题

我是说上一步自加deltatime的这个步骤完全多余,我说的官方文档明确说明的就是指time。time这个累计值的问题,并不需要衡量的deltatime来自加,这个浅析误区的帖子,弄的好像unity设定的api有逻辑漏洞一样,所以我才那么说的。
====

刚才没仔细看,你理解错了,我不是说t就是time.time,我是说官方文档上使用的t参数这个位置上用的值是时间的增量time.time,因为只有这样,lerp才是线性的,原理和帖子里说的一样。他的原理没问题,但是本身unity api就是线性的,就是没有问题的,我指的是这个!将api的time改成deltatime形成了非线性移动,是程序员自身的问题,就算是出帖子也要标上官方给的正确优化方法才比较hao。

也是我多事。。。


-------------------------------------------


好吧,我已经觉得我自己多事了,其实都无所谓,我也是闲的……请楼主不要介意。
说出去的话泼出去的水,之前我还觉得终点值有问题,刚刚有测试了一下,发现是通的,但我就是觉得在update里写deltatime自加冗余,可能是我自己的习惯问题。回复我就不改了,就当有两个方法。


微信截图_20171220101405.png


刚刚看了楼主发的最新api,这个是我没考虑到中途使用lerp的情况。嗯,最初对这个帖子的评价是我的错。原理和解决方式都没有问题。如果lerp不是在运行一开始就用,需要在update里自加,如果是在一开始就用,还是推荐老版本里的方法。就是这样。




回复 支持 2 反对 2

使用道具 举报

3偶尔光临
281/300
排名
12481
昨日变化

4

主题

8

帖子

281

积分

Rank: 3Rank: 3Rank: 3

UID
254628
好友
2
蛮牛币
308
威望
0
注册时间
2017-11-16
在线时间
43 小时
最后登录
2018-10-19

专栏作家

楼主 2017-12-19 21:15:47 显示全部楼层
小妞牛 发表于 2017-12-19 13:36
这个帖子是水的吧,居然还顶置了
首先这个所谓的误区根本就是楼主自己过去的使用习惯有问题,请发帖前先看 ...

首先,这个误区的确我自己也犯过,但正是因为我自己犯过这个错,然后发现身边不少开发者也犯了这个错,所以才写出来给大家,希望大家能避免错误。

第二,请看Mathf.Lerp官方文档示例,如下。怎么没有使用Time.deltaTime呢,示例中使用了0.5f,是要在1/0.5 也就是2秒内走完,而我使用1f就是要在1s内走完这个线性流程。而Vector3.Lerp的示例中虽然没有使用Time.deltaTime,但是使用Time.time-startTime同样是为了得到时间的累计值。 请问我哪里错了呢?你这边直接给我减了20分,感觉有点气啊。

[C#] 纯文本查看 复制代码
    void Update()
    {
        // animate the position of the game object...
        transform.position = new Vector3(Mathf.Lerp(minimum, maximum, t), 0, 0);

        // .. and increate the t interpolater
        t += 0.5f * Time.deltaTime;

        // now check if the interpolator has reached 1.0
        // and swap maximum and minimum so game object moves
        // in the opposite direction.
        if (t > 1.0f)
        {
            float temp = maximum;
            maximum = minimum;
            minimum = temp;
            t = 0.0f;
        }
    }
回复 支持 反对

使用道具 举报

8常驻蛮牛
7267/10000
排名
455
昨日变化

258

主题

1338

帖子

7267

积分

Rank: 8Rank: 8

UID
73452
好友
100
蛮牛币
192
威望
0
注册时间
2015-2-6
在线时间
2278 小时
最后登录
2020-9-17

专栏作家蛮牛译员活力之星七夕浪漫情人原创精华达人蛮牛哥认证开发者

QQ
2017-12-20 10:17:22 显示全部楼层
RaymondChan 发表于 2017-12-19 21:15
首先,这个误区的确我自己也犯过,但正是因为我自己犯过这个错,然后发现身边不少开发者也犯了这个错,所 ...

这篇文章从细节解释了lerp的作用,通俗易懂,对于不是特别理解lerp的童鞋们其实很有帮助,对于deltatime的使用其实很多人也都是这样用的,包括我,下面回复说的我觉得也没什么错,程序员嘛,应该追求用最合适的方式,最简洁的代码来完成工作,论坛就是用来谈论的,无可厚非,我给楼主加20分,都别生气,OK啦
回复 支持 1 反对 0

使用道具 举报

4四处流浪
310/500
排名
8332
昨日变化

0

主题

86

帖子

310

积分

Rank: 4

UID
26826
好友
0
蛮牛币
63
威望
0
注册时间
2014-5-26
在线时间
77 小时
最后登录
2020-6-3
2017-12-20 11:26:29 显示全部楼层
小妞牛 发表于 2017-12-19 13:36
这个帖子是水的吧,居然还顶置了
首先这个所谓的误区根本就是楼主自己过去的使用习惯有问题,请发帖前先看 ...

对于你的评论,别的我不敢质疑,我也是新手,不过你说的“而且官方文档明确案例说明了,线性lerp要使用的t是time.time”这句话肯定有问题,我之前就是认为t是Time.time,所以一直没弄懂这个函数的用法。楼主说的没错啊,这个t就是0到1之间的一个值,官方文档不就是这么说的吗“Interpolates between the vectors a and b by the interpolant t. The parameter t is clamped to the range [0, 1]. This is most commonly used to find a point some fraction of the way along a line between two endpoints (e.g. to move an object gradually between those points).”
官方案例只是用了Time.time作为参数而已。
回复 支持 反对

使用道具 举报

排名
10707
昨日变化

13

主题

169

帖子

496

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
70381
好友
8
蛮牛币
439
威望
0
注册时间
2015-1-22
在线时间
200 小时
最后登录
2020-7-6
2017-12-20 11:42:49 显示全部楼层
本帖最后由 小妞牛 于 2017-12-20 12:49 编辑
xifg2007 发表于 2017-12-20 11:26
对于你的评论,别的我不敢质疑,我也是新手,不过你说的“而且官方文档明确案例说明了,线性lerp要使用的 ...

我没说他这点说的有问题

我是说上一步自加deltatime的这个步骤完全多余,我说的官方文档明确说明的就是指time。time这个累计值的问题,并不需要衡量的deltatime来自加,这个浅析误区的帖子,弄的好像unity设定的api有逻辑漏洞一样,所以我才那么说的。
====

刚才没仔细看,你理解错了,我不是说t就是time.time,我是说官方文档上使用的t参数这个位置上用的值是时间的增量time.time,因为只有这样,lerp才是线性的,原理和帖子里说的一样。他的原理没问题,但是本身unity api就是线性的,就是没有问题的,我指的是这个!将api的time改成deltatime形成了非线性移动,是程序员自身的问题,就算是出帖子也要标上官方给的正确优化方法才比较。
也是我多事。。。

回复 支持 反对

使用道具 举报

3偶尔光临
281/300
排名
12481
昨日变化

4

主题

8

帖子

281

积分

Rank: 3Rank: 3Rank: 3

UID
254628
好友
2
蛮牛币
308
威望
0
注册时间
2017-11-16
在线时间
43 小时
最后登录
2018-10-19

专栏作家

楼主 2017-12-20 12:46:41 显示全部楼层
本帖最后由 RaymondChan 于 2017-12-20 12:56 编辑
小妞牛 发表于 2017-12-20 10:43
亲,我们说的是一个官方文档么

Time.time - startTime和Time.deltaTime累加的这两种方法都是为了应对不是从程序一运行开始就执行线性移动的这种情况。不过我这里确实不够严谨,因为我并没有用在这种情形下,所以如果是一运行就直接的移动的确实只需使用Time.time就可以了。不过Unity的官方文档确实都是这么用的,因为一运行就执行的情况下相对比较少。以下是官方文档的截图和链接
https://docs.unity3d.com/ScriptReference/Mathf.Lerp.html
https://docs.unity3d.com/ScriptReference/Vector3.Lerp.html





回复 支持 反对

使用道具 举报

3偶尔光临
281/300
排名
12481
昨日变化

4

主题

8

帖子

281

积分

Rank: 3Rank: 3Rank: 3

UID
254628
好友
2
蛮牛币
308
威望
0
注册时间
2017-11-16
在线时间
43 小时
最后登录
2018-10-19

专栏作家

楼主 2017-12-20 12:59:39 显示全部楼层
小妞牛 发表于 2017-12-20 12:33
用deltatime自加是多余的,为什么不直接用time.time

如果从程序运行中途突然要使用Lerp,就不能直接使用Time.time啦
回复 支持 反对

使用道具 举报

排名
10707
昨日变化

13

主题

169

帖子

496

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
70381
好友
8
蛮牛币
439
威望
0
注册时间
2015-1-22
在线时间
200 小时
最后登录
2020-7-6
2017-12-20 13:10:33 显示全部楼层
RaymondChan 发表于 2017-12-20 12:59
如果从程序运行中途突然要使用Lerp,就不能直接使用Time.time啦

你说得对,这个我确实没考虑到
回复 支持 反对

使用道具 举报

2初来乍到
114/150
排名
18795
昨日变化

0

主题

28

帖子

114

积分

Rank: 2Rank: 2

UID
257856
好友
0
蛮牛币
8
威望
0
注册时间
2017-12-4
在线时间
54 小时
最后登录
2020-8-17
2017-12-18 22:51:58 显示全部楼层
haodongxia dajiaweiguan
回复 支持 反对

使用道具 举报

7日久生情
1789/5000
排名
3008
昨日变化

5

主题

915

帖子

1789

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
246489
好友
1
蛮牛币
2468
威望
0
注册时间
2017-9-28
在线时间
375 小时
最后登录
2020-6-26

活力之星

2017-12-19 08:33:52 显示全部楼层
谢谢分享
回复

使用道具 举报

7日久生情
2630/5000
排名
1826
昨日变化

14

主题

395

帖子

2630

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
217870
好友
1
蛮牛币
1445
威望
0
注册时间
2017-4-15
在线时间
1493 小时
最后登录
2020-9-27
2017-12-19 08:35:08 显示全部楼层
不错不错
回复

使用道具 举报

5熟悉之中
649/1000
排名
13486
昨日变化

3

主题

160

帖子

649

积分

Rank: 5Rank: 5

UID
216830
好友
2
蛮牛币
570
威望
0
注册时间
2017-4-9
在线时间
421 小时
最后登录
2019-8-22
2017-12-19 08:59:42 显示全部楼层
谢谢分享
回复

使用道具 举报

7日久生情
1752/5000
排名
5397
昨日变化

2

主题

395

帖子

1752

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
159858
好友
1
蛮牛币
907
威望
0
注册时间
2016-8-1
在线时间
1090 小时
最后登录
2020-9-28
2017-12-19 09:06:25 显示全部楼层
通俗易懂
回复

使用道具 举报

7日久生情
1559/5000
排名
3391
昨日变化

2

主题

371

帖子

1559

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
238701
好友
1
蛮牛币
1303
威望
0
注册时间
2017-8-21
在线时间
746 小时
最后登录
2020-9-10
2017-12-19 09:07:40 显示全部楼层
很好
回复

使用道具 举报

7日久生情
2427/5000
排名
1820
昨日变化

5

主题

832

帖子

2427

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
239879
好友
1
蛮牛币
17352
威望
0
注册时间
2017-8-26
在线时间
859 小时
最后登录
2020-8-19
2017-12-19 09:10:45 显示全部楼层
看看学习一下
回复

使用道具 举报

7日久生情
2938/5000
排名
2100
昨日变化

13

主题

1164

帖子

2938

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
168159
好友
6
蛮牛币
6747
威望
0
注册时间
2016-9-12
在线时间
1089 小时
最后登录
2020-9-16
2017-12-19 09:18:27 显示全部楼层
回复

使用道具 举报

5熟悉之中
857/1000
排名
4968
昨日变化

2

主题

371

帖子

857

积分

Rank: 5Rank: 5

UID
213486
好友
1
蛮牛币
1924
威望
0
注册时间
2017-3-21
在线时间
192 小时
最后登录
2018-10-10
2017-12-19 09:20:00 显示全部楼层
学习了,谢谢分享
回复

使用道具 举报

5熟悉之中
832/1000
排名
4751
昨日变化

2

主题

84

帖子

832

积分

Rank: 5Rank: 5

UID
201386
好友
0
蛮牛币
258
威望
0
注册时间
2017-1-12
在线时间
438 小时
最后登录
2020-9-15
2017-12-19 09:33:02 显示全部楼层
很不错
回复

使用道具 举报

7日久生情
2051/5000
排名
1993
昨日变化

8

主题

783

帖子

2051

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
83438
好友
3
蛮牛币
17912
威望
0
注册时间
2015-3-23
在线时间
567 小时
最后登录
2019-10-17
2017-12-19 09:38:27 显示全部楼层
好东西 真细致
回复

使用道具 举报

7日久生情
2422/5000
排名
2126
昨日变化

0

主题

362

帖子

2422

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
228538
好友
11
蛮牛币
4385
威望
0
注册时间
2017-6-24
在线时间
1408 小时
最后登录
2020-9-28
2017-12-19 09:44:09 显示全部楼层
感谢分享
回复

使用道具 举报

6蛮牛粉丝
1307/1500
排名
3654
昨日变化

2

主题

372

帖子

1307

积分

Rank: 6Rank: 6Rank: 6

UID
122160
好友
1
蛮牛币
1576
威望
0
注册时间
2015-9-10
在线时间
523 小时
最后登录
2020-9-3
2017-12-19 09:44:50 显示全部楼层
感谢分享,很赞
回复

使用道具 举报

7日久生情
4215/5000
排名
118
昨日变化

1

主题

189

帖子

4215

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
19245
好友
2
蛮牛币
8403
威望
0
注册时间
2014-3-27
在线时间
1565 小时
最后登录
2020-9-28
2017-12-19 09:47:58 显示全部楼层
谢谢分享
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册帐号

本版积分规则

蛮牛教育10.1大促销!全站6折扣!