如何在 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_angle
(PI * 2
).
您可能还对 fposmod
and wrapf
函数感兴趣。事实上,我们可以像这样更短的实现:
func my_lerp_angle(from, to, weight):
var distance = wrapf(to - from, -PI, PI)
return from + distance * weight
这里 wrapf
会给我们一个介于 -PI
和 PI
之间的值。或者,如果您愿意:
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
这是 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_angle
(PI * 2
).
您可能还对 fposmod
and wrapf
函数感兴趣。事实上,我们可以像这样更短的实现:
func my_lerp_angle(from, to, weight):
var distance = wrapf(to - from, -PI, PI)
return from + distance * weight
这里 wrapf
会给我们一个介于 -PI
和 PI
之间的值。或者,如果您愿意:
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