cubic_interpolate_angle
最后同步日期:2026-04-15 | Godot 官方原文 — cubic_interpolate_angle
cubic_interpolate_angle
定义
cubic_interpolate_angle() 是一个专门用于角度值的三次插值函数。它的工作原理和 cubic_interpolate() 完全一样——在两个值之间画出一条平滑的曲线——但它额外做了一件重要的事:自动处理角度环绕问题。
打个比方:想象你手里拿着一个指南针。当前指针指向 350°(差不多朝北偏西一点),你想让它转到 10°(差不多朝北偏东一点)。如果用普通的三次插值,指针会逆时针转一大圈,经过 270°、180°、90° 最后才到 10°——足足转了 340°!但 cubic_interpolate_angle() 会聪明地选择最短路径,让指针顺时针轻轻一转就到了,只转 20°。
为什么不能用普通的 cubic_interpolate()? 因为角度是一个"首尾相连"的值——在 -PI 到 PI 的范围内(或说 0° 到 360°),-PI 和 PI 其实是同一个方向。普通插值函数不知道这一点,所以当角度跨越这个"接缝"时,就会走弯路。cubic_interpolate_angle() 会自动识别最短的角度路径,让过渡始终走"近路"。
函数签名
// Mathf.CubicInterpolateAngle 返回 float
public static float CubicInterpolateAngle(
float from, // 起始角度(弧度)
float to, // 目标角度(弧度)
float pre, // 起始角度之前的关键点
float post, // 目标角度之后的关键点
float weight // 插值权重(0 到 1 之间)
)func cubic_interpolate_angle(
from: float, # 起始角度(弧度)
to: float, # 目标角度(弧度)
pre: float, # 起始角度之前的关键点
post: float, # 目标角度之后的关键点
weight: float # 插值权重(0 到 1 之间)
) -> float参数说明
| 参数 | 类型 | 必需 | 说明 |
|---|---|---|---|
| from | float | 是 | 起始角度(弧度制)。当 weight 为 0 时,返回此值 |
| to | float | 是 | 目标角度(弧度制)。当 weight 为 1 时,返回此值 |
| pre | float | 是 | 起始角度之前的关键点角度。它影响曲线在起始处的弯曲方向,帮助曲线平滑地"出发" |
| post | float | 是 | 目标角度之后的关键点角度。它影响曲线在结束处的弯曲方向,帮助曲线平滑地"刹车" |
| weight | float | 是 | 插值权重,范围 0 到 1。0 表示完全在 from 处,1 表示完全在 to 处 |
关于弧度制:Godot 中的角度统一使用弧度制,不是角度制。PI 弧度 = 180°,2 * PI 弧度 = 360°。如果你习惯角度制,可以用
Mathf.DegToRad()/Mathf.RadToDeg()进行转换。
关于 pre 和 post 的理解:和
cubic_interpolate()一样,你需要提供 4 个角度点:pre->from->to->post。函数根据这 4 个点算出from和to之间的平滑曲线。pre和post是"引导点",告诉曲线该从哪个方向进入、往哪个方向离开。
返回值
类型: float
返回在 from 和 to 之间按 weight 权重进行三次角度插值后的结果(弧度制)。函数会自动选择最短的角度路径进行插值。
不同 weight 值的行为:
| weight | 返回值 | 说明 |
|---|---|---|
0.0 | from | 完全处于起始角度 |
0.5 | 中间的曲线角度值 | 处于曲线中段(不一定是中点) |
1.0 | to | 完全处于目标角度 |
代码示例
基本用法:角度环绕效果
using Godot;
public partial class CubicAngleExample : Node
{
public override void _Ready()
{
// 注意:角度使用弧度制
// from = 350° ≈ 6.11 弧度, to = 10° ≈ 0.17 弧度
float from = Mathf.DegToRad(350.0f);
float to = Mathf.DegToRad(10.0f);
float pre = Mathf.DegToRad(320.0f); // from 之前的关键点
float post = Mathf.DegToRad(40.0f); // to 之后的关键点
// 普通三次插值 vs 角度三次插值
float normalResult = Mathf.CubicInterpolate(from, to, pre, post, 0.5f);
float angleResult = Mathf.CubicInterpolateAngle(from, to, pre, post, 0.5f);
GD.Print($"普通插值结果: {Mathf.RadToDeg(normalResult):F1}°"); // 走远路
GD.Print($"角度插值结果: {Mathf.RadToDeg(angleResult):F1}°"); // 走近路
}
}func _ready():
# 注意:角度使用弧度制
# from = 350° ≈ 6.11 弧度, to = 10° ≈ 0.17 弧度
var from := deg_to_rad(350.0)
var to := deg_to_rad(10.0)
var pre := deg_to_rad(320.0) # from 之前的关键点
var post := deg_to_rad(40.0) # to 之后的关键点
# 普通三次插值 vs 角度三次插值
var normal_result := cubic_interpolate(from, to, pre, post, 0.5)
var angle_result := cubic_interpolate_angle(from, to, pre, post, 0.5)
print("普通插值结果: %.1f°" % rad_to_deg(normal_result)) # 走远路
print("角度插值结果: %.1f°" % rad_to_deg(angle_result)) # 走近路实际场景:角色平滑转向
using Godot;
public partial class SmoothRotator : Node2D
{
// 角色当前朝向的目标角度(弧度)
[Export] public float ExTargetRotation = 0.0f;
// 转向速度
[Export] public float ExTurnSpeed = 2.0f;
// 记录前几个角度用于三次插值的引导点
private float _preRotation = 0.0f;
private float _currentRotation = 0.0f;
private float _postRotation = 0.0f;
private float _weight = 0.0f;
public override void _Ready()
{
_currentRotation = Rotation;
_preRotation = Rotation;
_postRotation = Rotation;
}
public override void _Process(double delta)
{
if (_weight < 1.0f)
{
_weight += (float)delta * ExTurnSpeed;
_weight = Mathf.Min(_weight, 1.0f);
// 使用角度三次插值让转向走最短路径
Rotation = Mathf.CubicInterpolateAngle(
_currentRotation,
ExTargetRotation,
_preRotation,
_postRotation,
_weight
);
}
}
// 调用此方法来设定新的目标角度
public void SetTargetRotation(float newTarget)
{
_preRotation = Rotation; // 当前位置变成 pre 引导点
_currentRotation = Rotation; // 从当前位置出发
_postRotation = newTarget * 2 - _currentRotation; // 简单估算 post
ExTargetRotation = newTarget;
_weight = 0.0f;
}
}extends Node2D
# 角色当前朝向的目标角度(弧度)
@export var ex_target_rotation: float = 0.0
# 转向速度
@export var ex_turn_speed: float = 2.0
# 记录前几个角度用于三次插值的引导点
var _pre_rotation: float = 0.0
var _current_rotation: float = 0.0
var _post_rotation: float = 0.0
var _weight: float = 0.0
func _ready():
_current_rotation = rotation
_pre_rotation = rotation
_post_rotation = rotation
func _process(delta: float):
if _weight < 1.0:
_weight += delta * ex_turn_speed
_weight = minf(_weight, 1.0)
# 使用角度三次插值让转向走最短路径
rotation = cubic_interpolate_angle(
_current_rotation,
ex_target_rotation,
_pre_rotation,
_post_rotation,
_weight
)
# 调用此方法来设定新的目标角度
func set_target_rotation(new_target: float):
_pre_rotation = rotation # 当前位置变成 pre 引导点
_current_rotation = rotation # 从当前位置出发
_post_rotation = new_target * 2 - _current_rotation # 简单估算 post
ex_target_rotation = new_target
_weight = 0.0实际场景:多个角度关键帧之间的平滑过渡
using Godot;
public partial class AngleKeyframeAnimator : Node2D
{
// 一组角度关键帧(弧度制)
private readonly float[] _angleKeyframes = {
0.0f, // 0°
Mathf.DegToRad(90.0f), // 90°
Mathf.DegToRad(180.0f), // 180°
Mathf.DegToRad(270.0f), // 270°
Mathf.DegToRad(350.0f) // 350°(测试跨越 0° 边界)
};
// 动画总时长
[Export] public float ExDuration = 4.0f;
private float _elapsed = 0.0f;
public override void _Process(double delta)
{
_elapsed += (float)delta;
if (_elapsed > ExDuration)
_elapsed -= ExDuration;
// 计算当前处于哪两个关键帧之间
float segmentLength = ExDuration / (_angleKeyframes.Length - 1);
int segmentIndex = (int)Mathf.Min(
Mathf.Floor(_elapsed / segmentLength),
_angleKeyframes.Length - 2
);
// 计算当前段的局部权重(0~1)
float localWeight = (_elapsed - segmentIndex * segmentLength) / segmentLength;
// 获取前后四个关键点
float pre = _angleKeyframes[Mathf.Max(segmentIndex - 1, 0)];
float from = _angleKeyframes[segmentIndex];
float to = _angleKeyframes[segmentIndex + 1];
float post = _angleKeyframes[Mathf.Min(segmentIndex + 2, _angleKeyframes.Length - 1)];
// 使用角度三次插值(自动处理 270° -> 350° 的最短路径)
Rotation = Mathf.CubicInterpolateAngle(from, to, pre, post, localWeight);
}
}extends Node2D
# 一组角度关键帧(弧度制)
var _angle_keyframes: Array[float] = [
0.0, # 0°
deg_to_rad(90.0), # 90°
deg_to_rad(180.0), # 180°
deg_to_rad(270.0), # 270°
deg_to_rad(350.0) # 350°(测试跨越 0° 边界)
]
# 动画总时长
@export var ex_duration: float = 4.0
var _elapsed: float = 0.0
func _process(delta: float):
_elapsed += delta
if _elapsed > ex_duration:
_elapsed -= ex_duration
# 计算当前处于哪两个关键帧之间
var segment_length := ex_duration / (_angle_keyframes.size() - 1)
var segment_index := int(minf(
floorf(_elapsed / segment_length),
_angle_keyframes.size() - 2
))
# 计算当前段的局部权重(0~1)
var local_weight := (_elapsed - segment_index * segment_length) / segment_length
# 获取前后四个关键点
var pre := _angle_keyframes[maxi(segment_index - 1, 0)]
var from := _angle_keyframes[segment_index]
var to := _angle_keyframes[segment_index + 1]
var post := _angle_keyframes[mini(segment_index + 2, _angle_keyframes.size() - 1)]
# 使用角度三次插值(自动处理 270° -> 350° 的最短路径)
rotation = cubic_interpolate_angle(from, to, pre, post, local_weight)注意事项
角度使用弧度制:Godot 中所有角度相关的函数都使用弧度制,不是角度制。PI 弧度 = 180°。如果你习惯用角度,记得用
Mathf.DegToRad()转换输入、用Mathf.RadToDeg()转换输出。与
cubic_interpolate()的区别:cubic_interpolate()直接对数值进行插值,不知道"角度"这个概念。当角度从 350° 过渡到 10° 时,它会让数值从 350 往 10 的方向走(逆时针转一大圈)。而cubic_interpolate_angle()会自动识别最短路径(顺时针转 20°),让角度过渡更自然。与
lerp_angle()的区别:lerp_angle()是线性插值,过渡轨迹是直线(匀速旋转)。cubic_interpolate_angle()是三次插值,过渡轨迹是平滑曲线(可以缓入缓出),动画效果更流畅自然。pre 和 post 也要用弧度:所有四个角度参数(
from、to、pre、post)都是弧度制。pre和post作为曲线的引导点,同样会被当作角度来处理(自动选择最短路径)。如果需要控制时间分布:如果你希望关键帧之间的时间间隔不一样(比如从 A 到 B 花了 2 秒,从 B 到 C 只花了 0.5 秒),请使用
cubic_interpolate_angle_in_time(),它多了时间参数来控制变速效果。
