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