如何在 Godot 中最长的路线或路径的 2 个角度之间进行 LERP

How to LERP between 2 angles going the longest route or path in Godot

这是 Godot 中 2 个角度之间的 LERP 代码:

 func angle_dist(from, to):
    var max_angle = PI * 2
    var difference = fmod(to - from, max_angle)
    return (fmod(2 * difference, max_angle) - difference)

 func lerp_angle(from, to, weight):
    return from + angle_dist(from, to) * weight

我想做的不是走最短路径,而是希望它 LERP 最长路径,因为以最宽的角度向另一方向旋转

就如上图,头直接旋转到最近的角度,我想把头转回另一边,还有一个角度限制,防止头不切实际地转360

最短路线

首先,lerp_angle是一个GDScript内置函数。而且它会走更短的路。

例如:

extends Sprite

var from := 0.0
var to := 0.0
var weight := 0.0

func _process(delta: float) -> void:
    if Input.is_action_pressed("Click"):
        var mouse_position = get_viewport().get_mouse_position()
        from = rotation
        to = Vector2.UP.angle_to(mouse_position - global_position)
        weight = 0.0

    weight = min(weight + delta * 10, 1.0)
    rotation = lerp_angle(from, to, weight)

如果你自己定义,你需要给它起别的名字,否则,Godot 不会使用它。例如:

func my_lerp_angle(from, to, weight):
    var difference = fmod(to - from, TAU)
    var distance = fmod(2.0 * difference, TAU) - difference
    return from + distance * weight

此实现基于 Godot 源码。说服自己这等同于您的代码。记住TAU是一圈(助记:以t开头),也就是你的max_anglePI * 2).

您可能还对 fposmod and wrapf 函数感兴趣。事实上,我们可以像这样更短的实现:

func my_lerp_angle(from, to, weight):
    var distance = wrapf(to - from, -PI, PI)
    return from + distance * weight

这里 wrapf 会给我们一个介于 -PIPI 之间的值。或者,如果您愿意:

func short_angle_dist(from, to):
    return wrapf(to - from, -PI, PI)

func my_lerp_angle(from, to, weight):
    return from + short_angle_dist(from, to) * weight

最长的路

最长的路就是走最短的路,加上一个反方向的旋转。即加入一个符号相反的TAU,即:

func my_lerp_angle(from, to, weight):
    var distance = wrapf(to - from, -PI, PI)
    distance -= TAU * sign(distance)
    return from + distance * weight

或者如果您愿意:

func complement_angle(angle):
    return angle - TAU * sign(angle)

func short_angle_dist(from, to):
    return wrapf(to - from, -PI, PI)

func long_angle_dist(from, to):
    return complement_angle(short_angle_dist(from, to))

func my_lerp_angle(from, to, weight):
    return from + long_angle_dist(from, to) * weight

顺其自然

你认为你想要最长的路。你不知道。你想要的是避免角色的脖子在动画中被勒死。

返回基本 lerp:

func my_lerp_angle(from, to, weight):
    var distance = to - from
    return from + distance * weight

这会给你一个永远不会超过半圈的旋转。

好吧,除非你的角度没有标准化,但我们可以解决这个问题:

func my_lerp_angle(from, to, weight):
    var distance = wrapf(to, -PI, PI) - wrapf(from, -PI, PI)
    return from + distance * weight

如果你想移动它永远不会交叉的角度,那就是抵消角度。结果是这样的:

func my_lerp_angle(from, to, zero, weight):
    to = wrapf(to - zero, -PI, PI)
    from = wrapf(from - zero, -PI, PI)
    var distance = to - from
    return from + distance * weight + zero

其中zero为中立位置(半转远离角度避让)。

或者像这样:

func my_lerp_angle(from, to, avoid, weight):
    to = wrapf(to - avoid + PI, -PI, PI)
    from = wrapf(from - avoid + PI, -PI, PI)
    var distance = to - from
    return from + distance * weight + avoid - PI

其中 avoid 是旋转不应交叉的角度。


此外,头部的范围有限。如果目标角度 (to) 在它之外,则根本不要 lerp。像这样:

extends Sprite

var from := 0.0
var to := 0.0
var weight := 0.0
var zero := deg2rad(90)

func _process(delta: float) -> void:
    if Input.is_action_pressed("Click"):
        var mouse_position = get_viewport().get_mouse_position()
        from = rotation
        to = Vector2.UP.angle_to(mouse_position - global_position)
        weight = 0.0

    if abs(wrapf(to - zero, -PI, PI)) > deg2rad(170):
        return

    weight = min(weight + delta * 10, 1.0)
    rotation = my_lerp_angle(from, to, zero, weight)

func my_lerp_angle(from, to, zero, weight):
    to = wrapf(to - zero, -PI, PI)
    from = wrapf(from - zero, -PI, PI)
    var distance = to - from
    return from + distance * weight + zero

总是顺时针,总是逆时针

为了完整起见,这里有一个简单的顺时针插补:

func my_lerp_angle(from, to, weight):
    var distance = fposmod(to - from, TAU)
    return from + distance * weight

逆时针:

func my_lerp_angle(from, to, weight):
    var distance = fposmod(to - from, -TAU)
    return from + distance * weight

附录:角度范围

虽然上面我建议当角度超出其范围时不要 lerp。如果这不是您想要的……我想您仍然想要 lerp,但将其限制在一个范围内。

要到达那里,我们需要从夹紧角度开始。但是,我们需要确保对其进行归一化(例如wrapf(angle, -PI, PI)),然后钳制:

func clamp_angle(angle, min_angle, max_angle):
    return clamp(wrapf(angle, -PI, PI), min_angle, max_angle)

或者只是从零开始的 deviation 角:

func clamp_angle(angle, deviation):
    return clamp(wrapf(angle, -PI, PI), -deviation, deviation)

说到zero,我们需要考虑如何处理。一种选择是钳制输出,它曾经像这样与 lerp 代码合并:

func my_lerp_angle(from, to, zero, deviation, weight):
    to = wrapf(to - zero, PI, -PI)
    from = wrapf(from - zero, PI, -PI)
    var distance = to - from
    var angle = wrapf(from + distance * weight, PI, -PI)
    return clamp(angle, -deviation, deviation) + zero

但是,请记住这会截断 lerp。因此,动画将比在此处停止时更快地达到限制。

为避免这种情况,不要限制输出,而是限制目标角度 (to):

func my_lerp_angle(from, to, zero, deviation, weight):
    to = clamp(wrapf(to - zero, PI, -PI), -deviation, deviation)
    from = wrapf(from - zero, PI, -PI)
    var distance = to - from
    return from + distance * weight + zero