最稳妥方法是手写展开三次方而非用std::pow:B(t)=(1−t)³·P₀+3(1−t)²t·P₁+3(1−t)t²·P₂+t³·P₃,对每个分量分别计算,t∈[0,1]需断言或clamp,批量计算应复用中间系数,整数坐标下定点数更准。

用 std::pow 或直接展开计算最稳妥
三次贝塞尔曲线的参数方程是确定的:B(t) = (1−t)³·P₀ + 3(1−t)²t·P₁ + 3(1−t)t²·P₂ + t³·P₃,其中 t ∈ [0,1]。别直接套 std::pow(x, 3) 算三次方——它引入浮点误差且慢。更推荐手写展开:(1-t)*(1-t)*(1-t) 或先算 u = 1-t 再算 u*u*u。
常见错误是把控制点当成标量处理。实际中 P₀ 到 P₃ 是二维或三维点(如 struct Vec2 { float x,y; };),需对每个分量分别计算:
Vec2 bezier(const Vec2& p0, const Vec2& p1, const Vec2& p2, const Vec2& p3, float t) {
float u = 1.f - t;
float u2 = u * u;
float u3 = u2 * u;
float t2 = t * t;
float t3 = t2 * t;
return {
u3 * p0.x + 3.f * u2 * t * p1.x + 3.f * u * t2 * p2.x + t3 * p3.x,
u3 * p0.y + 3.f * u2 * t * p1.y + 3.f * u * t2 * p2.y + t3 * p3.y
};
}避免 t 超出 [0,1] 导致意外外插
数学上公式对任意实数 t 都成立,但工程中绝大多数场景只关心线段本身(t ∈ [0,1])。如果传入 t = 1.5,结果虽可算,但已不是原曲线的一部分,容易和路径裁剪、碰撞检测等逻辑冲突。
建议在调试阶段加断言:
立即学习“C++免费学习笔记(深入)”;
assert(t >= 0.f && t <= 1.f && "t must be in [0,1]");
生产环境可做 clamping(但要清楚这是掩盖输入问题,不是修复):
-
t = std::clamp(t, 0.f, 1.f);(C++20) - 或手动写
t = t < 0.f ? 0.f : (t > 1.f ? 1.f : t);
批量求点时注意缓存友好性
如果要生成 1000 个等距点(如用于渲染或导出),别写成“每次调用都重算所有系数”。把系数拆成独立变量并复用中间结果,能省掉约 40% 的乘法运算。
例如,循环中提前算好 u、u2、t2 等,而不是每轮重复 (1-t)*(1-t) 和 t*t;更进一步,若步长固定(如 t += 0.001f),可用差分法递推系数——但仅当点数极大(>10⁴)且 profile 确认瓶颈在此时才值得做。
整数坐标下用定点数反而更准
如果最终目标是光栅化到整数像素(比如游戏 UI 绘制),用 float 算完再 round() 可能因累积误差导致相邻点跳变。一种实践是:全程用 16.16 定点数(即 int32_t 存放大于 2¹⁶ 倍的值),乘法后手动右移 16 位。这样无浮点舍入抖动,且现代 CPU 上整数乘加不比 float 慢多少。
关键点:定点实现必须统一缩放因子,且注意中间结果溢出——3 * u2 * t * p1.x 这类项最大可能达 3×2³²,在 32 位 int 中会爆。此时要么升到 64 位中间量,要么限制输入范围。
真正麻烦的不是公式本身,而是控制点精度丢失、t 值采样不均、以及不同平台 std::pow 实现差异带来的微小偏差——这些在动画关键帧或 CAD 导出里会层层放大。


















