smoothstep
最后同步日期:2026-04-15 | Godot 官方原文 — smoothstep
smoothstep
定义
smoothstep() 是一个"带缓入缓出"的插值函数。你可以把它理解为一个更丝滑的 lerp()——普通的 lerp() 是匀速直线前进,而 smoothstep() 起步时慢慢加速、快到终点时慢慢减速,走的是一条 S 形曲线。
打个比方:想象你开车从 A 点到 B 点。普通的 lerp() 就像你一脚油门踩到底,到达 B 点时猛然刹车——速度瞬间从有到无。而 smoothstep() 就像一个老司机,起步时缓缓踩油门,中途匀速,快到时提前松油门轻踩刹车,整个过程丝滑平稳。
为什么需要缓入缓出? 因为现实世界中的运动几乎不会突然开始或突然停止。如果你让 UI 元素、角色移动、相机动画用 lerp() 来实现,看起来会很"机械"——像机器人一样生硬。加上 smoothstep() 的 S 形过渡后,动画会变得自然、舒适、有"弹性"。
函数签名
public static float SmoothStep(float from, float to, float val)func smoothstep(from: float, to: float, val: float) -> float参数说明
| 参数 | 类型 | 必需 | 说明 |
|---|---|---|---|
from | float | 是 | 过渡的起始值。当 val 小于等于此值时,返回 0.0 |
to | float | 是 | 过渡的结束值。当 val 大于等于此值时,返回 1.0 |
val | float | 是 | 输入值。函数会把这个值映射到 0~1 的 S 形曲线上 |
返回值
float -- 返回一个 0.0 到 1.0 之间的值。输入值在 from 和 to 之间时,返回值按照 S 形曲线平滑过渡。
- 当
val <= from时返回 0.0 - 当
val >= to时返回 1.0 - 当
val在from和to之间时,按 S 形曲线返回 0~1 之间的值
代码示例
基础用法:对比 lerp 和 smoothstep
using Godot;
public partial class SmoothstepExample : Node
{
public override void _Ready()
{
// smoothstep 的输入是 val(在 from 和 to 之间),输出是 0~1
float result1 = Mathf.SmoothStep(0f, 100f, 0f); // 在起点
GD.Print(result1); // 运行结果: 0
float result2 = Mathf.SmoothStep(0f, 100f, 50f); // 在中间
GD.Print(result2); // 运行结果: 0.5(中间点也正好是 0.5)
float result3 = Mathf.SmoothStep(0f, 100f, 100f); // 在终点
GD.Print(result3); // 运行结果: 1
// 关键区别:在 25% 位置时,smoothstep 的值比 0.25 小(缓入)
float result4 = Mathf.SmoothStep(0f, 100f, 25f);
GD.Print(result4); // 运行结果: 约 0.1042(比 0.25 小,因为起步慢)
// 在 75% 位置时,smoothstep 的值比 0.75 小(缓出)
float result5 = Mathf.SmoothStep(0f, 100f, 75f);
GD.Print(result5); // 运行结果: 约 0.8958(比 0.75 大,因为快到终点减速了)
}
}func _ready():
# smoothstep 的输入是 val(在 from 和 to 之间),输出是 0~1
var result1 = smoothstep(0.0, 100.0, 0.0) # 在起点
print(result1) # 运行结果: 0.0
var result2 = smoothstep(0.0, 100.0, 50.0) # 在中间
print(result2) # 运行结果: 0.5(中间点也正好是 0.5)
var result3 = smoothstep(0.0, 100.0, 100.0) # 在终点
print(result3) # 运行结果: 1.0
# 关键区别:在 25% 位置时,smoothstep 的值比 0.25 小(缓入)
var result4 = smoothstep(0.0, 100.0, 25.0)
print(result4) # 运行结果: 约 0.1042(比 0.25 小,因为起步慢)
# 在 75% 位置时,smoothstep 的值比 0.75 小(缓出)
var result5 = smoothstep(0.0, 100.0, 75.0)
print(result5) # 运行结果: 约 0.8958(比 0.75 大,因为快到终点减速了)实际场景:平滑的相机缩放
using Godot;
public partial class SmoothCameraZoom : Camera2D
{
// 导出属性:最小缩放
[Export] public Vector2 ExMinZoom = new Vector2(0.5f, 0.5f);
// 导出属性:最大缩放
[Export] public Vector2 ExMaxZoom = new Vector2(2.0f, 2.0f);
// 导出属性:缩放过渡的起始距离
[Export] public float ExZoomStartDistance = 100.0f;
// 导出属性:缩放过渡的结束距离
[Export] public float ExZoomEndDistance = 500.0f;
// 内部变量:追踪目标
private Node2D _target = null;
public override void _Process(double delta)
{
if (_target == null) return;
float distance = GlobalPosition.DistanceTo(_target.GlobalPosition);
// 用 smoothstep 实现平滑的缩放过渡
float t = Mathf.SmoothStep(ExZoomStartDistance, ExZoomEndDistance, distance);
// 根据 smoothstep 的输出在最小和最大缩放之间插值
Zoom = ExMinZoom.Lerp(ExMaxZoom, t);
}
}extends Camera2D
# 导出属性:最小缩放
@export var min_zoom: Vector2 = Vector2(0.5, 0.5)
# 导出属性:最大缩放
@export var max_zoom: Vector2 = Vector2(2.0, 2.0)
# 导出属性:缩放过渡的起始距离
@export var zoom_start_distance: float = 100.0
# 导出属性:缩放过渡的结束距离
@export var zoom_end_distance: float = 500.0
# 内部变量:追踪目标
var _target: Node2D = null
func _process(delta):
if _target == null:
return
var distance = global_position.distance_to(_target.global_position)
# 用 smoothstep 实现平滑的缩放过渡
var t = smoothstep(zoom_start_distance, zoom_end_distance, distance)
# 根据 smoothstep 的输出在最小和最大缩放之间插值
zoom = min_zoom.lerp(max_zoom, t)进阶用法:平滑的时间进度动画
using Godot;
public partial class SmoothAnimation : Node2D
{
// 导出属性:动画持续时间
[Export] public float ExDuration = 2.0f;
// 导出属性:起始位置
[Export] public Vector2 ExStartPosition = Vector2.Zero;
// 导出属性:目标位置
[Export] public Vector2 ExEndPosition = new Vector2(500f, 0f);
// 内部变量:已过时间
private float _elapsed = 0.0f;
// 内部变量:是否正在播放
private bool _isPlaying = true;
public override void _Process(double delta)
{
if (!_isPlaying) return;
_elapsed += (float)delta;
// 计算线性进度(0~1)
float linearProgress = Mathf.Clamp(_elapsed / ExDuration, 0f, 1f);
// 用 smoothstep 把线性进度变成 S 形曲线进度
float smoothProgress = Mathf.SmoothStep(0f, 1f, linearProgress);
// 用平滑后的进度计算位置
Position = ExStartPosition.Lerp(ExEndPosition, smoothProgress);
// 动画结束
if (_elapsed >= ExDuration)
{
_isPlaying = false;
GD.Print("动画结束");
}
}
}extends Node2D
# 导出属性:动画持续时间
@export var duration: float = 2.0
# 导出属性:起始位置
@export var start_position: Vector2 = Vector2.ZERO
# 导出属性:目标位置
@export var end_position: Vector2 = Vector2(500.0, 0.0)
# 内部变量:已过时间
var _elapsed: float = 0.0
# 内部变量:是否正在播放
var _is_playing: bool = true
func _process(delta):
if not _is_playing:
return
_elapsed += delta
# 计算线性进度(0~1)
var linear_progress = clampf(_elapsed / duration, 0.0, 1.0)
# 用 smoothstep 把线性进度变成 S 形曲线进度
var smooth_progress = smoothstep(0.0, 1.0, linear_progress)
# 用平滑后的进度计算位置
position = start_position.lerp(end_position, smooth_progress)
# 动画结束
if _elapsed >= duration:
_is_playing = false
print("动画结束")注意事项
自动 clamp:和
lerp()不同,smoothstep()会自动把结果限制在 0~1 之间。当val小于等于from时返回 0,大于等于to时返回 1。不需要手动 clamp。参数含义和
lerp()不同:lerp(from, to, weight)的第三个参数是权重(0~1),而smoothstep(from, to, val)的第三个参数是实际值(在from和to之间的某个数)。smoothstep的输出永远是 0~1,不是from到to之间的值。S 形曲线的数学原理:
smoothstep使用的是 Hermite 提值公式3t^2 - 2t^3。这个公式的特点是:在 t=0 和 t=1 处斜率(导数)为零,所以"起步"和"到站"都是零速度,产生自然的缓入缓出效果。与
ease()的关系:smoothstep(from, to, val)等价于先做inverse_lerp(from, to, val)得到 0~1 的值,然后对它应用ease(x, 2.0)的缓动效果。当
from等于to时:如果起始值和结束值相同,函数的行为不可预期(可能返回 0 或 NaN)。请确保from和to不同。
