cubic_interpolate_angle_in_time
最后同步日期:2026-04-15 | Godot 官方原文 — cubic_interpolate_angle_in_time
cubic_interpolate_angle_in_time
定义
cubic_interpolate_angle_in_time() 是一个结合了角度环绕处理和自定义时间分布的三次插值函数。你可以把它理解为 cubic_interpolate_angle() 和 cubic_interpolate_in_time() 的"合体版"——它既能自动处理角度跨越 -PI/PI 边界的问题,又能让你指定关键点之间的实际时间间隔。
打个比方:想象你在控制一个卫星天线的旋转。天线需要从 350° 转到 10°(角度环绕),而且从上一个位置到 350° 花了 3 秒(慢),从 350° 到 10° 只需要 0.5 秒(快),从 10° 到下一个位置需要 2 秒(中等)。这个函数同时考虑了"该走最短路径"和"每段路花了多少时间"两个因素。
与其他函数的关系:
| 函数 | 处理角度环绕 | 自定义时间 |
|---|---|---|
cubic_interpolate() | 否 | 否 |
cubic_interpolate_angle() | 是 | 否 |
cubic_interpolate_in_time() | 否 | 是 |
cubic_interpolate_angle_in_time() | 是 | 是 |
函数签名
// Mathf.CubicInterpolateAngleInTime 返回 float
public static float CubicInterpolateAngleInTime(
float from, // 起始角度(弧度)
float to, // 目标角度(弧度)
float pre, // 起始角度之前的关键点
float post, // 目标角度之后的关键点
float weight, // 插值权重(0 到 to_t 之间)
float to_t, // 从 from 到 to 的持续时间
float pre_t, // 从 pre 到 from 的持续时间
float post_t // 从 to 到 post 的持续时间
)func cubic_interpolate_angle_in_time(
from: float, # 起始角度(弧度)
to: float, # 目标角度(弧度)
pre: float, # 起始角度之前的关键点
post: float, # 目标角度之后的关键点
weight: float, # 插值权重(0 到 to_t 之间)
to_t: float, # 从 from 到 to 的持续时间
pre_t: float, # 从 pre 到 from 的持续时间
post_t: float # 从 to 到 post 的持续时间
) -> float参数说明
| 参数 | 类型 | 必需 | 说明 |
|---|---|---|---|
| from | float | 是 | 起始角度(弧度制)。当 weight 为 0 时,返回此值 |
| to | float | 是 | 目标角度(弧度制)。当 weight 等于 to_t 时,返回此值 |
| pre | float | 是 | 起始角度之前的关键点角度。影响曲线在起始处的弯曲方向 |
| post | float | 是 | 目标角度之后的关键点角度。影响曲线在结束处的弯曲方向 |
| weight | float | 是 | 插值权重,范围 0 到 to_t。0 表示完全在 from 处,to_t 表示完全在 to 处 |
| to_t | float | 是 | 从 from 到 to 这段路程的持续时间。决定了 weight 的最大值 |
| pre_t | float | 是 | 从 pre 到 from 这段路程的持续时间。影响曲线在起始处的"张力" |
| post_t | float | 是 | 从 to 到 post 这段路程的持续时间。影响曲线在结束处的"张力" |
关于弧度制:Godot 中所有角度相关的函数都使用弧度制。PI 弧度 = 180°,2 * PI 弧度 = 360°。可以用
Mathf.DegToRad()/Mathf.RadToDeg()进行转换。
返回值
类型: float
返回在 from 和 to 之间按 weight 权重进行三次角度插值后的结果(弧度制)。函数同时考虑角度的最短路径和各关键点之间的实际时间分布。
不同 weight 值的行为:
| weight | 返回值 | 说明 |
|---|---|---|
0.0 | from | 完全处于起始角度 |
to_t / 2 | 中间的曲线角度值 | 处于曲线中段(不一定是中点) |
to_t | to | 完全处于目标角度 |
代码示例
基本用法:对比四个插值函数
using Godot;
public partial class CompareInterpFunctions : Node
{
public override void _Ready()
{
// 从 350° 到 10°——跨越 0° 边界
float pre = Mathf.DegToRad(300.0f);
float from = Mathf.DegToRad(350.0f);
float to = Mathf.DegToRad(10.0f);
float post = Mathf.DegToRad(60.0f);
// 假设各段时间不均匀
float toT = 2.0f; // from -> to 花了 2 秒
float preT = 3.0f; // pre -> from 花了 3 秒
float postT = 1.0f; // to -> post 花了 1 秒
float weight = 1.0f; // 走到一半时间
// 对比四个函数的结果
float r1 = Mathf.CubicInterpolate(from, to, pre, post, 0.5f);
float r2 = Mathf.CubicInterpolateAngle(from, to, pre, post, 0.5f);
float r3 = Mathf.CubicInterpolateInTime(from, to, pre, post, weight, toT, preT, postT);
float r4 = Mathf.CubicInterpolateAngleInTime(from, to, pre, post, weight, toT, preT, postT);
GD.Print($"普通三次插值: {Mathf.RadToDeg(r1):F1}°"); // 可能走远路
GD.Print($"角度三次插值: {Mathf.RadToDeg(r2):F1}°"); // 走近路,但时间均匀
GD.Print($"带时间三次插值: {Mathf.RadToDeg(r3):F1}°"); // 时间不均匀,但可能走远路
GD.Print($"角度+时间三次插值: {Mathf.RadToDeg(r4):F1}°"); // 既走近路,又考虑时间
}
}func _ready():
# 从 350° 到 10°——跨越 0° 边界
var pre := deg_to_rad(300.0)
var from := deg_to_rad(350.0)
var to := deg_to_rad(10.0)
var post := deg_to_rad(60.0)
# 假设各段时间不均匀
var to_t := 2.0 # from -> to 花了 2 秒
var pre_t := 3.0 # pre -> from 花了 3 秒
var post_t := 1.0 # to -> post 花了 1 秒
var weight := 1.0 # 走到一半时间
# 对比四个函数的结果
var r1 := cubic_interpolate(from, to, pre, post, 0.5)
var r2 := cubic_interpolate_angle(from, to, pre, post, 0.5)
var r3 := cubic_interpolate_in_time(from, to, pre, post, weight, to_t, pre_t, post_t)
var r4 := cubic_interpolate_angle_in_time(from, to, pre, post, weight, to_t, pre_t, post_t)
print("普通三次插值: %.1f°" % rad_to_deg(r1)) # 可能走远路
print("角度三次插值: %.1f°" % rad_to_deg(r2)) # 走近路,但时间均匀
print("带时间三次插值: %.1f°" % rad_to_deg(r3)) # 时间不均匀,但可能走远路
print("角度+时间三次插值: %.1f°" % rad_to_deg(r4)) # 既走近路,又考虑时间实际场景:炮塔旋转(变速 + 角度环绕)
using Godot;
public partial class TurretRotation : Node2D
{
// 炮塔目标朝向(弧度)
[Export] public float ExTargetAngle = 0.0f;
// 旋转速度倍率
[Export] public float ExRotationSpeed = 1.0f;
// 插值引导点
private float _preAngle = 0.0f;
private float _startAngle = 0.0f;
private float _postAngle = 0.0f;
// 时间参数
private float _toTime = 1.0f;
private float _preTime = 1.0f;
private float _postTime = 1.0f;
private float _elapsed = 0.0f;
public override void _Ready()
{
_startAngle = Rotation;
_preAngle = Rotation;
_postAngle = Rotation;
}
public override void _Process(double delta)
{
if (_elapsed < _toTime)
{
_elapsed += (float)delta * ExRotationSpeed;
// 使用角度+时间三次插值
Rotation = Mathf.CubicInterpolateAngleInTime(
_startAngle,
ExTargetAngle,
_preAngle,
_postAngle,
Mathf.Min(_elapsed, _toTime),
_toTime,
_preTime,
_postTime
);
}
}
// 外部调用:设定新的目标角度
public void AimAt(float targetAngle, float duration)
{
_preAngle = Rotation;
_startAngle = Rotation;
_postAngle = targetAngle + (targetAngle - Rotation) * 0.5f;
_toTime = duration;
_preTime = duration * 0.8f;
_postTime = duration * 1.2f;
ExTargetAngle = targetAngle;
_elapsed = 0.0f;
}
}extends Node2D
# 炮塔目标朝向(弧度)
@export var ex_target_angle: float = 0.0
# 旋转速度倍率
@export var ex_rotation_speed: float = 1.0
# 插值引导点
var _pre_angle: float = 0.0
var _start_angle: float = 0.0
var _post_angle: float = 0.0
# 时间参数
var _to_time: float = 1.0
var _pre_time: float = 1.0
var _post_time: float = 1.0
var _elapsed: float = 0.0
func _ready():
_start_angle = rotation
_pre_angle = rotation
_post_angle = rotation
func _process(delta: float):
if _elapsed < _to_time:
_elapsed += delta * ex_rotation_speed
# 使用角度+时间三次插值
rotation = cubic_interpolate_angle_in_time(
_start_angle,
ex_target_angle,
_pre_angle,
_post_angle,
minf(_elapsed, _to_time),
_to_time,
_pre_time,
_post_time
)
# 外部调用:设定新的目标角度
func aim_at(target_angle: float, duration: float):
_pre_angle = rotation
_start_angle = rotation
_post_angle = target_angle + (target_angle - rotation) * 0.5
_to_time = duration
_pre_time = duration * 0.8
_post_time = duration * 1.2
ex_target_angle = target_angle
_elapsed = 0.0实际场景:多角度关键帧的变速动画
using Godot;
public partial class TimedAngleAnimator : Node2D
{
// 角度关键帧序列(弧度)
private readonly float[] _angles = {
0.0f,
Mathf.DegToRad(170.0f),
Mathf.DegToRad(190.0f), // 跨越 180° 边界
Mathf.DegToRad(350.0f), // 接近 360°/0° 边界
Mathf.DegToRad(20.0f) // 跨越 0° 边界
};
// 每段的时间长度(不均匀)
private readonly float[] _durations = { 1.0f, 0.3f, 2.0f, 0.5f };
private float _elapsed = 0.0f;
public override void _Process(double delta)
{
// 计算总时长
float totalDuration = 0.0f;
foreach (float d in _durations)
totalDuration += d;
_elapsed += (float)delta;
if (_elapsed > totalDuration)
_elapsed -= totalDuration;
// 找到当前处于哪一段
float accumulated = 0.0f;
int segIdx = 0;
for (int i = 0; i < _durations.Length; i++)
{
if (_elapsed < accumulated + _durations[i])
{
segIdx = i;
break;
}
accumulated += _durations[i];
}
float segElapsed = _elapsed - accumulated;
float toT = _durations[segIdx];
// 获取四个关键点
float pre = _angles[Mathf.Max(segIdx - 1, 0)];
float from = _angles[segIdx];
float to = _angles[segIdx + 1];
float post = _angles[Mathf.Min(segIdx + 2, _angles.Length - 1)];
float preT = segIdx > 0 ? _durations[segIdx - 1] : toT;
float postT = segIdx + 1 < _durations.Length ? _durations[segIdx + 1] : toT;
// 角度 + 时间三次插值
Rotation = Mathf.CubicInterpolateAngleInTime(
from, to, pre, post,
segElapsed, toT, preT, postT
);
}
}extends Node2D
# 角度关键帧序列(弧度)
var _angles: Array[float] = [
0.0,
deg_to_rad(170.0),
deg_to_rad(190.0), # 跨越 180° 边界
deg_to_rad(350.0), # 接近 360°/0° 边界
deg_to_rad(20.0) # 跨越 0° 边界
]
# 每段的时间长度(不均匀)
var _durations: Array[float] = [1.0, 0.3, 2.0, 0.5]
var _elapsed: float = 0.0
func _process(delta: float):
# 计算总时长
var total_duration := _durations.reduce(func(a, b): return a + b, 0.0)
_elapsed += delta
if _elapsed > total_duration:
_elapsed -= total_duration
# 找到当前处于哪一段
var accumulated := 0.0
var seg_idx := 0
for i in _durations.size():
if _elapsed < accumulated + _durations[i]:
seg_idx = i
break
accumulated += _durations[i]
var seg_elapsed := _elapsed - accumulated
var to_t := _durations[seg_idx]
# 获取四个关键点
var pre := _angles[maxi(seg_idx - 1, 0)]
var from := _angles[seg_idx]
var to := _angles[seg_idx + 1]
var post := _angles[mini(seg_idx + 2, _angles.size() - 1)]
var pre_t := to_t if seg_idx <= 0 else _durations[seg_idx - 1]
var post_t := to_t if seg_idx + 1 >= _durations.size() else _durations[seg_idx + 1]
# 角度 + 时间三次插值
rotation = cubic_interpolate_angle_in_time(
from, to, pre, post,
seg_elapsed, to_t, pre_t, post_t
)注意事项
是两个函数的"合体版":这个函数同时拥有
cubic_interpolate_angle()的角度环绕处理能力和cubic_interpolate_in_time()的自定义时间分布能力。如果你的场景同时需要这两个特性,就用这个函数;如果只需要其中一个,可以用对应的简化版本。weight 的范围是 0 到
to_t:和cubic_interpolate_in_time()一样,weight不是一个 0 到 1 的比例值,而是一个"时间"值。weight为 0 返回from,weight等于to_t返回to。时间参数不能为 0:
to_t、pre_t、post_t都应该是正数。设为 0 或负数会导致除零错误或不可预测的行为。所有角度参数使用弧度制:Godot 中所有角度函数统一使用弧度制。如果你习惯角度制,用
Mathf.DegToRad()/deg_to_rad()转换输入,用Mathf.RadToDeg()/rad_to_deg()转换输出。适用场景有限:在实际开发中,大多数旋转动画可以通过 Godot 的 Tween 或 AnimationPlayer 来实现,它们已经内置了缓动函数和角度环绕处理。只有在需要精细控制插值曲线的自定义动画系统中,才需要手动使用这个函数。
pre 和 post 也作为角度处理:四个角度参数(
pre、from、to、post)都会被当作角度来处理,函数会自动为每对相邻点选择最短路径。
