游戏编程 - 从Godot引擎中的lerp(...)函数到线性插值

野性酷女 2023-01-04 13:29 448阅读 0赞

从Godot引擎中的lerp(…)函数到线性插值

在这里插入图片描述

1。 What’s LERP?

  • lerp < linear interpolation < 线性插值 < LERP索引页 < 黑客词典

2。lerp(…)函数做了什么?

  • 先让我们给出godot中lerp的函数签名,方便我们下面讨论:
  • Variant lerp(from: Variant, to: Variant, weight: float)

  • Variant是gdscript中的一种元数据,这里暂且可以理解成一种数据类型的占位符,表示这个函数有针对多个数据类型的版本,比如:int、float、Vector2、Vector3、Color。

2。1。对两个数做lerp

  • 比如:为什么lerp(0, 4, 0.75)的返回值是3
  • 可以将这里的lerp看成针对int(或float)的版本:int lerp(from: int, to: int, weight: float)
  • 3这个返回值是怎么得来的?lerp在内部是这样干的:return = (1- weight) * from + weight * to;也就是:(1 - 0.75) * 0 + 0.75 * 4 = 3
  • 为什么要这样做?上面的式子是两个数之间的插值公式。当然了,也没必要非得往数学上扯,这只不过是我们想在0~4范围内取一个数,于是我们对lerp说:“我要取0~4这个范围内的0.75比重处的那个数”。lerp寻思了一会说:“好,那个数是3”。
  • 当我们在说比重(weight)时,那意味着什么?再看一眼那个公式,没错,这个公式的确是from与to的加权平均值,毕竟两个数之间的插值就是这样定义的。所以“比重”就是“权重”。(1)当weight是0时,我们的意思是第一个数的权重是1,第二个数的权重是0,所以加权平均值等于第一个数;(2)当weight是1时,我们的意思是第一个数的权重是0,第二个数的权重是1,所以加权平均值等于第二个数;(3)当weight是0.5时,我们的意思是两个数的权重各占一半,这个时候的加权平均值也就是两个数的平均值,也就是两个数中点处的那个数值;……
  • 两个数的加权平均值,本质上是在干一件什么事?试着回答一个问题(如下图):如果把范围0~1当成范围from~to,那么0~1内的0.75处相当于from~to的哪一处?0~1内的weight处呢?
    在这里插入图片描述
  • 对于lerp两个数(一维),更好的理解是看成一种范围映射操作。lerp的源码可能更有说服力:

    static ALWAYS_INLINE float lerp(float p_from, float p_to, float p_weight)
    {

    1. return p_from + (p_to - p_from) * p_weight;

    }

  • weight的范围当然可以在[0,1]之外,只不过那并不常用,这个我们稍后说~

  • 让我们想个演示,实数可以表示什么呢,也许角度(弧度表示)是个不错的开始:
    在这里插入图片描述

    extends Node2D

    const fan_speed = 1

    func _process(delta):

    1. $Sprite.rotation = lerp($Sprite.rotation, 8 * PI, delta * fan_speed)

2。2。对两个点做lerp

  • 比如:为什么lerp(Vector2(1, 5), Vector2(3, 2), 0.5)的返回值是Vector2(2, 3.5)
  • 所以这里的lerp是针对Vector2的版本:Vector2 lerp(from: Vector2, to: Vector2, weight: float)
  • 针对某个类型的lerp版本和int/float的稍有些不同,当我们对某个类型调用lerp时,实际上lerp内部转而调用了对应类型的linear_interpolate(...)成员函数,这时的lerp函数只是一个包装,为的是对外提供统一的接口。
    在这里插入图片描述
  • 下面是Vector2的linear_interpolate函数源码,这和【2。1】中一样,只不过现在是对两个维度上分别做加权平均。所以1 + 0.5 * (3-1) = 2;5 + 0.5 * (2-5) = 3.5=> Vector2(2, 3.5)

    Vector2 Vector2::linear_interpolate(const Vector2 &p_a, const Vector2 &p_b, real_t p_t) {

  1. Vector2 res = p_a;
  2. res.x += (p_t * (p_b.x - p_a.x));
  3. res.y += (p_t * (p_b.y - p_a.y));
  4. return res;
  5. }
  • 从根本上说,这是两个点之间线性插值公式。(具体看第三部分)
  • 看看下面这个对点做lerp演示,我们对红圆圈的位置做了lerp,而白圆圈的位置只是单纯的赋值:
    在这里插入图片描述

    extends Node2D

    const FOLLOW_SPEED = 4.0

    func _physics_process(delta):

    1. var mouse_pos = get_local_mouse_position()
    2. $Sprite.position = mouse_pos
    3. $Sprite2.position = $Sprite2.position.linear_interpolate(mouse_pos, delta * FOLLOW_SPEED)

2。3。对两个颜色做lerp

  • 这里lerp的版本是针对Color这个类的:Color lerp(from: Color, to: Color, weight: float)
  • 这里lerp内部也是对Color类的linear_interpolate(...)成员函数的调用,下面是它的源码,和Vector2基本一样,如果非要统一起来,那或许是Vector4?

    FORCE_INLINE Color linear_interpolate(const Color &p_b, float p_t) const {

  1. Color res = *this;
  2. res.r += (p_t * (p_b.r - r));
  3. res.g += (p_t * (p_b.g - g));
  4. res.b += (p_t * (p_b.b - b));
  5. res.a += (p_t * (p_b.a - a));
  6. return res;
  7. }
  • Vector2的改变意味着位置的变化,那么Color的改变意味着……
    在这里插入图片描述

    extends Node2D

    const changed_speed = 1

    func _process(delta):

    1. # 初始为蓝色rgba(0, 0, 1, 1)
    2. $ColorRect.color = $ColorRect.color.linear_interpolate(Color(1, 1, 1, 1), delta * changed_speed)

2。4。对两个矩阵做lerp

  • 对矩阵做lerp的内容是我中途加的,可能是我觉得对lerp的铺垫还不够。
  • 这里我们说到矩阵时,我们的意思是2D变换。
  • 2D变换在godot引擎里对应的类是Transform2D,这个类有自己的做lerp的成员函数interpolate_with(...)
  • 简单回忆一下2D变换的内容:

在这里插入图片描述

  • 图片来自 Wikipedia - Transformation matrix
  • 如果在使用godot引擎中的Transform2D时硬套用上面数学书中的理论,也许并不好过,原因在于:(1)大多数游戏引擎(包括godot)在2D中使用的是左手坐标系,它的Y轴朝下,和数学中坐标系的Y轴相反;(2)Transform2D内对矩阵数据的组织方式也和我们在数学中所使用的2D变换矩阵不同(看下图)。
    在这里插入图片描述
  • 庆幸的是,看懂“将怪物扔出去”的演示并不需要掌握Transform2D的底层细节。
    在这里插入图片描述

    extends Node2D

    const transform_speed = 1

    func _process(delta):

    1. # 初始位置是Vector2(20, 40)
    2. $Sprite.transform = $Sprite.transform.interpolate_with( \
    3. Transform2D(PI, Vector2(300, 40)), delta * transform_speed)
  • 当然了,godot中肯定存在Transform(3D)和Transform(3D)::interpolate_with(...)这种东西了~

  • 如果仍然对前面几个例子中精灵所表现出来的行为感到怪异,试图把握一点:每次做lerp时,起始状态在变,而weight不变!

2。5。对某某某做lerp

  • What we did?
  • 我们总是在找一些可以变化的东西,比如,实数、位置、颜色、矩阵……;我们总是提供给了lerp操作这些可变化东西的两个状态,比如,两个不相等的实数、两个不同的位置、两个不一样的颜色、两个不等价的矩阵……;当然,最后,我们还给了lerp一个比重weight,意思是:“我要这两个状态的混合物,并且混合后的重心在weight处。”
  • 线性:两个变量成比例变化,叫线性关系,所以weight的变化量和状态的变化量也应该成比例。
  • 插值:我们明明只有两个边界状态,却试图得到两个边界状态的某个中间状态,于是我们给这中间插值。

3。 线性插值

3。1。事出何为

  • 在数学中,线性插值是一种使用线性多项式进行曲线拟合的方法,可以在一组离散的已知数据点范围内构造新的数据点。- Wikipedia - Linear interpolation
  • 游戏中,使用线性插值从根本上是为了获得一种过渡。运用于平滑动作、控制相机、制作动画等。
  • 拿两个点的线性插值来研究,现在我们有平面上两个点A、B,注意我的意思是我们只有这两个点,中间是空白一片(看下图),当然它们的坐标(xA, yA)(xB, yB)是已知的。当我们在区间xAxB上随便找一点x?,并试图计算出这一点对应的纵坐标y?时,实际上我们就是在研究一个插值问题。
    在这里插入图片描述
  • 通常已知一个点的横坐标,求它对应的纵坐标,是一个水到渠成的问题,这里的水也就是过这点的函数方程,题目中经常会给出各种暗示,很重要的一点是:这些暗示对于确定出这个函数方程是完备的。
  • 我们现在面对的似乎也是这种问题,不过我们的已知条件有且只有A、B两点的坐标,很明显,这没法确定出过(x?, y?)点的方程(看下图)。的确,没人知道纵坐标是多少那么任何答案都是正确答案,“猜一个”,这种想法很好,但是当别人问你为什么的时候,你应该能扯出来,也就是可以自圆其说,这才比较数学。
    在这里插入图片描述
  • 我们完全没必要这么被动,我想是不是我们在学校刷题太多,遇到条件不完备的题目总感觉题目出错了?
  • 题目的意思是计算一个点的纵坐标,而且没有限制我们过这点的曲线(方程)应该是什么样,唯一那一点要求是过A、B两个点,换句话说,题目给了我们充分的自由度,我们学过什么一次(线性、直线)函数二次(抛物线)函数三次(回归抛物线)函数三角函数等等。(1)随便选一个,比如一次函数(图中圈4),(2)用两点式求出这条直线方程,(3)把先前自己臆想的那个点的横坐标x?代入直线方程待定系数,(4)于是求y?。当别人问你为什么是y?而不是y??y???时,告诉它们,你用的是线性方程,在A、B两点之间插入(x?, y?)点的方式是线性方式,依据的数学理论是线性插值理论。
  • 至于圈1、圈2、圈3也许叫二次插值或非线性插值什么的。

3。2。推导

  • 根据两点写出直线方程:
    在这里插入图片描述
  • 移项:
    在这里插入图片描述
  • 恒等变形:
    在这里插入图片描述
  • 换个元:
    在这里插入图片描述
  • 所以x是:
    在这里插入图片描述
  • 代回原式,所以y是:
    在这里插入图片描述
  • 回到这篇blog的开头。
  • 把上述两个表示xy的式子运用结合律变形:
    在这里插入图片描述
  • 回到godot引擎Vector2的线性插值函数linear_interpolate(...)的源码:

    Vector2 Vector2::linear_interpolate(const Vector2 &p_a, const Vector2 &p_b, real_t p_t) {

  1. Vector2 res = p_a;
  2. res.x += (p_t * (p_b.x - p_a.x));
  3. res.y += (p_t * (p_b.y - p_a.y));
  4. return res;
  5. }

3。3。Where are we?

在这里插入图片描述

  • 恩,线性插值是最简单也是最常用的一种插值方式。
  • 每种插值方式游戏中都可能用的到,比如Bezier曲线?

3。4。内插与外推(线性)

  • 还记得之前说过weight的范围并不需要非得在[0, 1]之间吗?当weight不在这个范围时,x?就会跑出[xA, xB]区间。相当于我们要在A、B两点之外(而非之间)插值,又叫外推。
    在这里插入图片描述

4。_lerp_函数族(不完全)

  • lerp(...)
  • linear_interpolate(...)
  • interpolate_with(...)
  • lerp_angle(...) - 对两个弧度表示的角度插值
  • inverse_lerp(...) - 反插值,也许也可以叫unlerp
  • range_lerp(...) - 真正的范围映射,也许也可以叫remap
  • cubic_interpolate(...) - 三次插值,构造Bezier曲线

5。参考文章

  • https://en.wikipedia.org/wiki/Linear_interpolation
  • https://docs.godotengine.org/en/stable/tutorials/math/interpolation.html
  • https://docs.godotengine.org/en/stable/tutorials/math/beziers_and_curves.html#doc-beziers-and-curves
  • https://github.com/godotengine/godot/tree/3.2
  • https://limnu.com/sketch-lerp-unlerp-remap/
  • 《数值计算方法》 科学出版社 魏毅强等编

6。其它

个人公众号,偶尔发些技术文,欢迎一起讨论游戏开发~

在这里插入图片描述

发表评论

表情:
评论列表 (有 0 条评论,448人围观)

还没有评论,来说两句吧...

相关阅读

    相关 在UnityLerp函数

    介绍 在Unity中,Lerp函数是一种用于线性插值的方法。它可以在两个给定的值之间进行插值,根据一个介于0和1之间的插值因子来确定插值的程度。这个函数通常用于平滑过渡一

    相关 unity,Color.Lerp函数

    介绍 Color.Lerp函数是Unity引擎中的一个静态函数,用于在两个颜色值之间进行线性插值,从而实现颜色渐变效果 -------------------- 方

    相关 Unity3D线性Lerp()函数

    在unity3D中经常用线性插值函数Lerp()来在两者之间插值,两者之间可以是两个材质之间、两个向量之间、两个浮点数之间、两个颜色之间,其函数原型如下: Material.

    相关 U3d Vector3.Lerp 理解

    有时,我们在做游戏时会发现有些跟随动作不够圆滑或者需要一个缓冲的效果,这时,一般会考虑到插值。所以对插值的理解是必需的。(比如摄像机跟随主角) 插值是数学上的一个概念,在这里

    相关 Mac M1 编译 Godot 引擎

    Mac M1 编译 Godot 引擎 目前 `Godot` 官方未提供 `Mac m1` 芯片的二进制程序下载,不过`3.2.3`版本已经支持了。可以下载源码下来编译。