贝塞尔曲线计算核心是伯恩斯坦基函数加权求和;二次用三个系数,三次用(1−t)³、3t(1−t)²、3t²(1−t)、t³;t步长建议0.01并显式补eval(1.0);避免std::pow和递归deCasteljau,优先循环实现与std::array存储。

怎么用 C++ 算贝塞尔曲线上的点(不依赖图形库)
贝塞尔曲线本质是参数方程,核心就是实现 bernstein 基函数 + 控制点加权求和。别想着先找“画图库”,先算出点,才能决定怎么画。
常见错误是直接抄递归版 deCasteljau 算单点——它稳定但慢;而循环版容易索引越界或权重累加错位。
- 二次贝塞尔:用
float B2(int i, float t)手写三个伯恩斯坦系数,t从 0.0 到 1.0 步进(比如 0.01),每次算B2(0,t)*p0 + B2(1,t)*p1 + B2(2,t)*p2 - 三次更常用:对应系数是
(1-t)^3、3*t*(1-t)^2、3*t^2*(1-t)、t^3,别用阶乘函数现场算,浮点误差+性能双杀 -
t步长不是越小越好:小于 1e-4 容易因浮点精度导致终点跳变,尤其在t==1.0时,建议显式补上终点eval(1.0)
用 OpenGL / SDL / Qt 绘制时,为什么曲线看起来断断续续
不是算法错,是采样密度和绘制方式不匹配。贝塞尔是连续曲线,但你只给了离散点,靠直线段连起来,线段太长就锯齿明显。
典型场景:用 glBegin(GL_LINE_STRIP) 画 20 个点,结果弧度大的地方发虚。
立即学习“C++免费学习笔记(深入)”;
- 控制点间距越大,所需采样点越多——粗略按最大控制点距离 / 2 做初始步长估算
- Qt 中别用
QPainter::drawPolyline直接喂点,开启setRenderHint(QPainter::Antialiasing),否则亚像素边缘直接糊成块 - SDL2 没内置抗锯齿线,
SDL_RenderDrawLines画出来就是硬边,要么自己做 MSAA 插值,要么换SDL_RenderDrawLine循环画短段(代价高)
std::vector<:array>> 能不能当控制点容器
能,但要注意内存布局和访问模式。很多教程用 std::vector<point></point>,其中 Point 是 struct,这没问题;但若用嵌套 vector,每层都有额外指针开销,且缓存不友好。
实际跑起来,1000 条三次贝塞尔,用 std::vector<:array>></:array> 比 std::vector<:vector>></:vector> 快 15%~20%,尤其在批量计算 t=0.5 的切线方向时。
- 避免
std::vector<:pair>></:pair>:pair成员非连续存储,CPU 预取失效 - 如果控制点数量固定(如全是三次),直接用
std::array<:array>,4></:array>,栈上分配,零成本 - 别在循环里反复调用
points.size(),C++17 后编译器常能优化,但老编译器可能真每次查 size
计算切线或曲率时,derivative 函数总返回 NaN
导数公式本身没问题,问题出在 t 接近边界时的数值稳定性。三次贝塞尔导数是二次多项式,但直接套公式 3*(1-t)^2*(p1-p0) + ... 在 t==1.0 附近,(1-t)^2 可能下溢成 0,再乘大数就崩。
真实项目里,曲率用于速度规划或法向偏移,一个 NaN 就让整个路径生成中断。
- 对
t < 0.01或t > 0.99,改用前向/后向差分近似:比如(eval(t+dt) - eval(t)) / dt,dt=1e-4 - 别用
std::pow(t,2)算幂,直接写t*t,std::pow对 float 可能触发域错误 - 检查控制点是否共线——三点共线时二次贝塞尔退化为直线,曲率恒为 0,但中间除零风险仍在,得提前判断
贝塞尔最麻烦的从来不是公式,而是边界、精度、和你没意识到的“控制点其实也是数据,会脏、会重复、会超限”。算之前,先打印几个 eval(0.0)、eval(1.0) 和控制点坐标对齐一下,省掉半天调试。











