1

This is the code for LERP between 2 angles in Godot:

 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

what I would like to do is instead of going the shortest path, I want it to LERP the longest path, as of rotate the other way around with the widest angle instead

enter image description here

exactly as shown in the picture above, the head is rotating directly to the nearest angle, I would like the head to rotate back to the other side instead, there is also an angle limit which prevents the head from unrealistically turning 360

Theraot
  • 31,890
  • 5
  • 57
  • 86

1 Answers1

4

Shortest way

First of all, lerp_angle is a GDScript build-in function. And it would go the shorter way.

For example:

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)

If you define your own, you need to call it something else, otherwise, Godot is not going to use it. For example:

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

This implementation is based on Godot source. Convince yourself this is equivalent to your code. Remember that TAU is one turn (mnemonic: it starts with t), which is your max_angle (PI * 2).

You might also be interested in the fposmod and wrapf functions. In fact, we can a shorter implementation like this:

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

Here wrapf will give us a value between -PI and PI wrapping around. Or if you prefer:

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

Longest way

The longest way is simply taking the shortest way and adding a rotation in the opposite direction. That is, adding a TAU with the opposite sign, that is:

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

Or if you prefer:

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

Natural way

You think you want the longest way. You don't. What you want is to avoid the neck of the character getting strangled in the animation.

Go back to basic lerp:

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

This will give you a rotation that never crosses the half turn.

Well, unless your angles are not normalized, but we can fix that:

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

If you want to move the angle it never crosses, it is a matter offsetting the angles. It ends up like this:

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

Where zero is the neutral position (half turn away from the angle to avoid).

Or like this:

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

Where avoid is the angle that the rotation should not cross.


Furthermore, the head would have a limited range. If the target angle (to) is outside of it, do not lerp at all. Something like this:

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

Always clockwise, and always anticlockwise

For completeness sake, here is a simple always clockwise lerp:

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

And anticlockwise:

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

Addendum: Angle range

While above I suggest to not lerp when the angle is out of its range. If that is not what you want… I guess you still want to lerp but have it limited to a range.

To get there, we need to start by clamping the angle. However, we need to make sure to normalize it (e.g. wrapf(angle, -PI, PI)), and then clamp:

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

Or just a deviation angle from zero:

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

Speaking of zero, we need to consider how are we going to handle it. One option is to clamp the output, which once merged with the lerp code like this:

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

However, remember that will truncate the lerp. As a result the animation will reach the limit faster than it would if it were to stop there.

To avoid that, instead of clamping the output, clamp the destination angle (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
Theraot
  • 31,890
  • 5
  • 57
  • 86
  • The natural way script with the zero variable is not working, the head is jerking and not in the right rotation towards the mouse, and I would like the head to be limited in a certain FOV angle, such as from -170 to 170, just a small FOV angle the area behind the head, like a gap, is that possible? – Your Dearest Fan Aug 21 '21 at 22:44
  • @YourDearestFan I made a mistake inlined the variables in the method. See updated answer. – Theraot Aug 21 '21 at 23:01
  • @YourDearestFan I have expanded the answer. – Theraot Aug 23 '21 at 20:20
  • I have ended up using conditions for different angles, thank you for the good and expanded answer :) if anything arises I will comment it here, by the way when disabling the lerp function for the specified angle or fov the head stops at the current position and doesn't look realistic this is why I have implement several conditions to make the head reach the limit if the mouse has moved quickly to the limits – Your Dearest Fan Aug 26 '21 at 16:13