
简介本资源是一份面向嵌入式开发、图形算法学习者及C语言初学者的贝塞尔曲线平滑实现代码包聚焦离散点序列的曲线拟合与可视化优化问题适用于路径规划、轨迹生成、UI动画插值等实际场景。压缩包共6个文件3个cpp源文件、2个h头文件、1个bmp示例图像总大小仅5KB轻量易集成其中bezier.cpp/h封装了二次与三次贝塞尔曲线核心计算逻辑bmp.cpp/h提供简易位图绘制支持test.cpp含完整可运行测试用例及详细注释便于理解参数含义与调用方式。已有1832人学习下载代码结构清晰、接口简洁、注释充分读者可直接调用函数完成曲线平滑快速掌握贝塞尔算法原理与C语言工程化实现要点并基于测试用例举一反三拓展至更高阶曲线或实时绘图应用。1. 用纯 C 实现贝塞尔曲线平滑不是调库而是亲手算出控制点与插值点——嵌入式、工业 HMI、实时绘图场景下绕不开的数学落地能力你正在写一个电机轨迹规划模块上位机下发的是离散采样点但伺服驱动器要求平滑连续的速度/位置指令或者你在开发一款轻量级矢量图形渲染引擎需要在无浮点协处理器的 MCU 上绘制圆角矩形、路径描边又或者你刚接手一段遗留的工业 HMI 代码发现所有“曲线”都是折线硬连用户抱怨界面抖动、拖拽卡顿。这时“贝塞尔曲线平滑”不是美术软件里的点击操作而是一段必须可复现、可调试、可嵌入到while(1)主循环里的 C 代码。它不依赖 OpenGL、不调用 Cairo、不链接 math.h 的高精度函数——它用定点或单精度浮点在 64KB Flash 的 STM32F103 上跑得稳参数可在线调节插值点可逐帧生成。本文聚焦于三次贝塞尔曲线Cubic Bézier这一工程中最常用、控制性最强、计算开销最低的形态从数学定义出发给出完整可编译的 C 实现覆盖如何从原始点列生成控制点曲线拟合、如何高效求值避免递归与幂运算、如何控制平滑度与保真度的权衡、以及在无标准库环境下的内存与精度取舍。2. 三次贝塞尔曲线的数学本质与 C 实现选型为什么选 De Casteljau 算法而非 Bernstein 多项式展开2.1 贝塞尔曲线不是“画一条线”而是定义一条参数化路径P(t) (1−t)³·P₀ 3(1−t)²t·P₁ 3(1−t)t²·P₂ t³·P₃三次贝塞尔曲线由四个点唯一确定起点 P₀、终点 P₃、以及两个控制点 P₁、P₂。参数 t ∈ [0,1]当 t0 时位于 P₀t1 时位于 P₃中间形态由 P₁、P₂ 拉伸决定。关键在于P₁ 和 P₂ 并不落在曲线上它们是“力”的施加点。P₁ 决定曲线在 P₀ 处的切线方向与长度即初始速度矢量P₂ 决定在 P₃ 处的切线方向与长度即终止速度矢量。因此“平滑”不是视觉模糊而是保证相邻曲线段在连接点处一阶导数切线连续即 C¹ 连续。这正是工业轨迹规划中避免加速度突变、HMI 动画中消除视觉抖动的数学基础。提示不要混淆“贝塞尔曲线拟合”与“最小二乘拟合”。前者是构造一条通过或逼近给定点的贝塞尔曲线后者是找一条直线/多项式使误差平方和最小。本文处理的是前者——给定首尾点及中间若干数据点反解出最优控制点使曲线尽可能贴近这些点。2.2 为何放弃 Bernstein 基函数直接展开De Casteljau 算法才是嵌入式友好的数值稳定解法理论上P(t) 可直接按 Bernstein 多项式展开// ❌ 不推荐存在幂运算、系数大、浮点误差累积严重 float bernstein_cubic(float t, float p0, float p1, float p2, float p3) { float u 1.0f - t; float u2 u * u; float u3 u2 * u; float t2 t * t; float t3 t2 * t; return u3 * p0 3.0f * u2 * t * p1 3.0f * u * t2 * p2 t3 * p3; }问题在于t 接近 0 或 1 时u³ 与 t³ 极小3u²t 与 3ut² 项易因浮点舍入产生显著相对误差且需 3 次乘法计算幂对 Cortex-M3/M0 等无硬件乘法器的芯片不友好。De Casteljau 算法将计算分解为三层线性插值完全规避幂运算数值稳定性极佳且天然支持定点实现Layer 1: Q₀ lerp(P₀,P₁,t), Q₁ lerp(P₁,P₂,t), Q₂ lerp(P₂,P₃,t) Layer 2: R₀ lerp(Q₀,Q₁,t), R₁ lerp(Q₁,Q₂,t) Layer 3: P(t) lerp(R₀,R₁,t)其中lerp(a,b,t) a t*(b-a)。每层仅需 1 次减法、1 次乘法、1 次加法共 9 次基本运算远低于 Bernstein 展开的 12 次。2.3 定点 vs 浮点在资源受限系统中如何抉择精度与性能维度单精度 floatIEEE 75432 位定点Q15/Q16精度~7 位有效数字适合坐标范围 ±1000 的场景Q15±32767 / 32768 ≈ ±1.0小数位 15 bitQ16±32767 / 65536 ≈ ±0.5小数位 16 bit性能Cortex-M4F 有硬件 FPU单周期乘加M0/M3 需软浮点慢 10–50 倍所有运算为整数Cortex-M0 也能高效执行内存float 占 4 字节int32_t 占 4 字节但需额外缩放因子管理适用场景PC 工具链、带 FPU 的 MCU、调试阶段无 FPU 的 STM32F0/F1、RTOS 下低延迟任务、Flash/ROM 极其紧张的 bootloader本文默认采用单精度 float 实现兼顾可读性与通用性但所有函数均设计为易于迁移到定点版本——关键在于将t的范围严格限定在 [0.0f, 1.0f]并将lerp封装为宏或内联函数便于后续替换。2.4 核心 C 函数骨架bezier_cubic_eval与bezier_cubic_lerp#include stdio.h #include math.h // 仅用于 fabsf实际可移除 // 线性插值a t*(b - a)t ∈ [0,1] static inline float bezier_lerp(float a, float b, float t) { return a t * (b - a); } // 三次贝塞尔曲线单点求值De Casteljau 算法 // 输入4 个控制点 (x0,y0), (x1,y1), (x2,y2), (x3,y3)参数 t // 输出目标点 *out_x, *out_y void bezier_cubic_eval( float x0, float y0, float x1, float y1, float x2, float y2, float x3, float y3, float t, float *out_x, float *out_y ) { // Layer 1: Q0, Q1, Q2 float q0x bezier_lerp(x0, x1, t); float q0y bezier_lerp(y0, y1, t); float q1x bezier_lerp(x1, x2, t); float q1y bezier_lerp(y1, y2, t); float q2x bezier_lerp(x2, x3, t); float q2y bezier_lerp(y2, y3, t); // Layer 2: R0, R1 float r0x bezier_lerp(q0x, q1x, t); float r0y bezier_lerp(q0y, q1y, t); float r1x bezier_lerp(q1x, q2x, t); float r1y bezier_lerp(q1y, q2y, t); // Layer 3: P(t) *out_x bezier_lerp(r0x, r1x, t); *out_y bezier_lerp(r0y, r1y, t); }逻辑说明与参数说明bezier_lerp是核心原子操作内联确保无函数调用开销bezier_cubic_eval严格遵循 De Casteljau 三层结构变量命名q0x/q0y等直接对应算法步骤便于调试与验证t参数必须由调用者保证在 [0.0f, 1.0f] 区间内否则结果无意义可添加assert(t 0.0f t 1.0f)在调试版输出通过指针*out_x,*out_y返回避免结构体拷贝符合嵌入式低开销惯例该函数可安全用于中断服务程序ISR因其无动态内存分配、无全局状态、无浮点异常风险只要输入 t 合法。3. 从离散点列生成贝塞尔控制点实现“曲线拟合”的两种工程级策略3.1 场景还原你手头只有一组(x[i], y[i])共 N 个点首尾固定为 P₀ 和 P₃如何求出 P₁、P₂这是“贝塞尔曲线拟合”的核心问题。不存在唯一解——无限多组 (P₁,P₂) 都能生成通过 P₀、P₃ 的曲线但只有特定组合能让曲线“最贴近”中间点。工程上常用两种策略策略原理简述适用场景计算复杂度弦长参数化 最小二乘法用弦长估计各数据点在曲线上的参数 tᵢ建立关于 P₁、P₂ 的线性方程组求最小二乘解数据点较密集N≥5、对保真度要求高O(N³)几何启发式Catmull-Rom 变种利用相邻点斜率估算切线方向按固定比例如 1/3 弦长设置 P₁、P₂实时性要求高如 HMI 动画、点数少N3~4O(N)本文重点实现几何启发式策略因其简单、快速、鲁棒且在绝大多数嵌入式与交互场景中效果足够好。3.2 几何启发式用前向/后向差分估算切线再按比例生成控制点假设我们有 4 个点P[0], P[1], P[2], P[3]希望构造一条三次贝塞尔曲线使其“自然”地穿过这 4 点P₀→P₃ 为端点P₁、P₂ 为控制点。经典做法是让曲线在 P₀ 处的切线平行于P[0]→P[1]在 P₃ 处的切线平行于P[2]→P[3]并按弦长比例缩放切线长度。具体步骤计算弦长d01 dist(P[0],P[1]),d12 dist(P[1],P[2]),d23 dist(P[2],P[3])估算 P₀ 处切向量T0 (P[1] - P[0]) * α其中α d01 / (d01 d12)归一化权重估算 P₃ 处切向量T3 (P[3] - P[2]) * β其中β d23 / (d12 d23)设 P₁ P₀ T0P₂ P₃ - T3。该方法保证了 C¹ 连续性若相邻段使用相同策略且无需解方程组。3.3 C 实现bezier_fit_4points—— 四点拟合专用函数#include math.h // 计算两点欧氏距离的平方避免开方 static inline float dist_sq(float x1, float y1, float x2, float y2) { float dx x1 - x2; float dy y1 - y2; return dx * dx dy * dy; } // 四点贝塞尔拟合输入 P[0]~P[3]输出控制点 P0,P1,P2,P3 // 注意P[0] 和 P[3] 直接作为曲线端点P[1],P[2] 用于估算切线 void bezier_fit_4points( const float P[4][2], // 输入4 个点P[i][0]x, P[i][1]y float *out_x0, float *out_y0, float *out_x1, float *out_y1, float *out_x2, float *out_y2, float *out_x3, float *out_y3 ) { float x0 P[0][0], y0 P[0][1]; float x1 P[1][0], y1 P[1][1]; float x2 P[2][0], y2 P[2][1]; float x3 P[3][0], y3 P[3][1]; // 计算弦长平方避免 sqrt 开销 float d01_sq dist_sq(x0, y0, x1, y1); float d12_sq dist_sq(x1, y1, x2, y2); float d23_sq dist_sq(x2, y2, x3, y3); // 使用弦长平方的平方根作为近似弦长更鲁棒 float d01 sqrtf(d01_sq); float d12 sqrtf(d12_sq); float d23 sqrtf(d23_sq); // 防止除零 float denom0 (d01 d12 1e-6f) ? (d01 d12) : 1.0f; float denom3 (d12 d23 1e-6f) ? (d12 d23) : 1.0f; // 权重 α, β越靠近端点该段弦长影响越大 float alpha d01 / denom0; float beta d23 / denom3; // 估算切向量单位向量 × 权重 × 缩放因子 0.33f // 缩放因子 0.33f 是经验值控制曲线“拉伸”程度可调 float scale 0.33f; // P0 处切线从 P0 指向 P1按 alpha 加权 float tx0 (x1 - x0) * alpha * scale; float ty0 (y1 - y0) * alpha * scale; // P3 处切线从 P2 指向 P3按 beta 加权 float tx3 (x3 - x2) * beta * scale; float ty3 (y3 - y2) * beta * scale; // 生成控制点 *out_x0 x0; *out_y0 y0; // P0 data[0] *out_x1 x0 tx0; *out_y1 y0 ty0; // P1 P0 tangent0 *out_x2 x3 - tx3; *out_y2 y3 - ty3; // P2 P3 - tangent3 *out_x3 x3; *out_y3 y3; // P3 data[3] }参数说明与可调项scale 0.33f是平滑度调节旋钮值越小曲线越靠近折线保真度高、平滑度低值越大曲线越“饱满”平滑度高、可能偏离原始点。典型取值范围 0.2–0.5alpha/beta权重机制自动适应点间距不均的情况——若P[0]→P[1]很长而P[1]→P[2]很短则alpha接近 1P₁ 更远离 P₀使曲线在 P₀ 处更“陡峭”使用sqrtf而非sqrt明确指定单精度若目标平台无sqrtf可用查表法或牛顿迭代替代该函数输出 4 个控制点可直接喂给bezier_cubic_eval生成任意密度的插值点。3.4 批量点列拟合将 N 个点拆分为重叠的四元组构建分段贝塞尔曲线对于 N 4 的点列如传感器采样序列需将其划分为多个四点窗口生成多段三次贝塞尔曲线并保证段间 C¹ 连续。标准做法是重叠窗口点列[P0,P1,P2,P3,P4,P5]→ 拟合段1[P0,P1,P2,P3]段2[P1,P2,P3,P4]段3[P2,P3,P4,P5]。这样段1 的终点 P₃ 与段2 的起点 P₁ 重合但控制点需协调。更优方案是全局控制点优化固定所有数据点D[i]设曲线段 k 连接D[k]与D[k1]则其控制点为D[k],C1[k],C2[k],D[k1]。通过约束C2[k-1],D[k],C1[k]共线且等距可解出所有C1[k],C2[k]。但此法需解三对角方程组复杂度高。本文提供轻量级分段拟合函数bezier_fit_spline// 将点列拟合成分段贝塞尔曲线每段4点重叠 // 输入points[0..n-1]n4 // 输出control_points[0..3*(n-3)-1]每4点一组x0,y0,x1,y1,x2,y2,x3,y3 void bezier_fit_spline( const float *points, // 格式x0,y0,x1,y1,...,x_{n-1},y_{n-1} int n, float *control_points // 输出缓冲区大小至少 8*(n-3) floats ) { if (n 4) return; for (int i 0; i n - 3; i) { // 提取四点窗口points[i*2]~points[(i3)*2-1] float P[4][2]; for (int j 0; j 4; j) { P[j][0] points[(ij)*2]; // x P[j][1] points[(ij)*2 1]; // y } // 拟合当前窗口 float x0, y0, x1, y1, x2, y2, x3, y3; bezier_fit_4points(P, x0, y0, x1, y1, x2, y2, x3, y3); // 存入输出缓冲区 int base i * 8; control_points[base 0] x0; control_points[base 1] y0; control_points[base 2] x1; control_points[base 3] y1; control_points[base 4] x2; control_points[base 5] y2; control_points[base 6] x3; control_points[base 7] y3; } }使用示例float raw_data[] {0.0f,0.0f, 1.0f,2.0f, 3.0f,1.5f, 4.0f,0.0f, 5.0f,-1.0f}; int n 5; float cps[8*(n-3)]; // 8 floats per segment, (5-3)2 segments → 16 floats bezier_fit_spline(raw_data, n, cps); // cps[0..7] segment0: P0,P1,P2,P3 // cps[8..15] segment1: P0,P1,P2,P3 (for points[1..4])4. 生成平滑点序列步进策略、精度控制与抖动抑制技巧4.1 t 参数的步进不是均匀的自适应步长才是抗抖动的关键初学者常犯错误用for(t0.0f; t1.0f; t0.05f)生成 21 个点。问题在于贝塞尔曲线在 t 接近 0 或 1 时变化缓慢因导数小而在 t≈0.5 附近变化剧烈因导数大。均匀 t 步进会导致曲线平坦区点密冗余计算曲线弯曲区点疏出现明显折线感即“抖动”。正确做法是按弧长近似步进。虽精确弧长积分无解析解但可用弦长累加法逼近先以较小步长如 Δt0.01采样计算相邻点间欧氏距离累加得总弦长 L再按目标点数 N设定每段目标弦长L/N反向查找 t 位置。本文提供实用折中方案基于导数模长的自适应步长。三次贝塞尔曲线的一阶导数为P(t) 3(1−t)²(P₁−P₀) 6(1−t)t(P₂−P₁) 3t²(P₃−P₂)其模长|P(t)|正比于曲线局部速度。我们令步长Δt ∝ 1 / |P(t)|确保每步弧长近似相等。4.2 C 实现bezier_sample_adaptive—— 生成指定数量的平滑点// 计算贝塞尔曲线在 t 处的导数x,y 分量 void bezier_cubic_deriv( float x0, float y0, float x1, float y1, float x2, float y2, float x3, float y3, float t, float *dx, float *dy ) { float u 1.0f - t; float u2 u * u; float t2 t * t; // P(t) 3u²(P1-P0) 6ut(P2-P1) 3t²(P3-P2) *dx 3.0f * u2 * (x1 - x0) 6.0f * u * t * (x2 - x1) 3.0f * t2 * (x3 - x2); *dy 3.0f * u2 * (y1 - y0) 6.0f * u * t * (y2 - y1) 3.0f * t2 * (y3 - y2); } // 自适应采样生成 num_points 个近似等弧长点 void bezier_sample_adaptive( float x0, float y0, float x1, float y1, float x2, float y2, float x3, float y3, int num_points, float *out_x, float *out_y // 输出数组长度 num_points ) { if (num_points 2) return; // 初始 t0 out_x[0] x0; out_y[0] y0; float t 0.0f; float dt 0.01f; // 初始步长 for (int i 1; i num_points; i) { // 预估下一个 t float t_next t dt; // 边界保护 if (t_next 1.0f) { t_next 1.0f; } // 计算当前 t 处导数模长 float dx, dy; bezier_cubic_deriv(x0,y0,x1,y1,x2,y2,x3,y3, t, dx, dy); float speed sqrtf(dx*dx dy*dy); speed (speed 1e-6f) ? speed : 1e-6f; // 防零 // 调整步长目标速度为 1.0故 dt ∝ 1/speed // 期望每步移动弧长 ≈ 1.0 / (num_points-1) * total_length_est // 这里用经验系数 0.5 控制总点数 dt 0.5f / speed / (num_points - 1); // 确保 dt 不过小防死循环或过大防跳过 dt fmaxf(0.001f, fminf(0.1f, dt)); t t_next; bezier_cubic_eval(x0,y0,x1,y1,x2,y2,x3,y3, t, out_x[i], out_y[i]); } }参数说明与技巧dt初始值0.01f和上下限0.001f/0.1f是经验值可根据曲线曲率调整0.5f是抖动抑制系数值越小步长越保守点越密抖动越小但计算量越大值越大点越疏效率高但可能失真该函数生成的点列在视觉上均匀分布彻底解决“平滑抖动”问题特别适合驱动步进电机或 LCD 刷新若需更高精度可将bezier_cubic_deriv改为数值微分前后差分牺牲一点性能换取普适性。4.3 实战用 20 行主函数验证整个流程// 完整可运行示例gcc -o bezier bezier.c -lm #include stdio.h #include math.h // 此处插入前述所有函数bezier_lerp, bezier_cubic_eval, // bezier_fit_4points, bezier_cubic_deriv, bezier_sample_adaptive int main() { // 原始折线点模拟不平滑的采样数据 float data[] {0.0f, 0.0f, 2.0f, 3.0f, 4.0f, 1.0f, 6.0f, 0.0f}; // 拟合控制点 float cp[8]; float P[4][2] {{data[0],data[1]}, {data[2],data[3]}, {data[4],data[5]}, {data[6],data[7]}}; bezier_fit_4points(P, cp[0], cp[1], cp[2], cp[3], cp[4], cp[5], cp[6], cp[7]); printf(Control Points:\n); printf(P0(%.2f,%.2f) - P1(%.2f,%.2f) - P2(%.2f,%.2f) - P3(%.2f,%.2f)\n, cp[0],cp[1], cp[2],cp[3], cp[4],cp[5], cp[6],cp[7]); // 生成 50 个平滑点 float smooth_x[50], smooth_y[50]; bezier_sample_adaptive(cp[0],cp[1], cp[2],cp[3], cp[4],cp[5], cp[6],cp[7], 50, smooth_x, smooth_y); printf(\nFirst 5 smooth points:\n); for (int i 0; i 5; i) { printf((%.3f, %.3f)\n, smooth_x[i], smooth_y[i]); } return 0; }编译运行后你将看到控制点坐标与前 5 个平滑点——它们不再是原始折线的简单插值而是具有连续切线、无尖角、视觉平滑的轨迹。这就是“贝塞尔曲线平滑”在 C 语言中的真实落地。5. 嵌入式部署与性能调优如何在 64KB Flash 的 MCU 上跑贝塞尔曲线5.1 内存与 Flash 优化剥离浮点依赖启用定点运算当目标平台为 STM32F030无 FPUFlash 仅 32KB时sqrtf、sinf等函数会链接庞大的软浮点库瞬间吃光空间。解决方案替换sqrtf用整数牛顿迭代法计算isqrt整数平方根再转换为定点禁用math.h所有sqrtf替换为fast_sqrtf查表法256 项线性插值定点化bezier_cubic_eval定义typedef int32_t fixed_t;缩放因子SCALE 32768Q15lerp改为a ((b-a) * t) 15。示例定点lerp#define FIXED_SCALE 32768 typedef int32_t fixed_t; static inline fixed_t fixed_lerp(fixed_t a, fixed_t b, fixed_t t) { // t ∈ [0, SCALE], result a (b-a)*t/SCALE return a (((int64_t)(b - a) * t) 15); }5.2 编译器指令优化告诉 GCC 这段代码值得全力优化在函数声明前添加属性强制内联与向量化__attribute__((always_inline, optimize(O3,fast-math))) static inline float bezier_lerp(float a, float b, float t) { ... }并启用-ffast-math -mfloat-abihard -mfpuvfp对有 FPU 的芯片。5.3 实时性保障预计算与查表法对固定形状的曲线如 UI 中的圆角按钮可预先计算 64 个 t 值对应的点存入 ROM 数组const float corner_points_x[64] PROGMEM { /* precomputed */ }; const float corner_points_y[64] PROGMEM { /* precomputed */ };运行时只需查表 线性插值CPU 占用趋近于零。5.4 验证平滑度用导数连续性指标量化“抖动抑制效果”在调试阶段可计算相邻插值点间的角度变化float angle_delta fabsf(atan2f(dy_i, dx_i) - atan2f(dy_{i-1}, dx_{i-1})); if (angle_delta 0.1f) { /* 发现潜在抖动 */ }或直接监控|P(t)|的标准差σ 0.05 表示速度高度均匀即视觉无抖动。最终你得到的不是一段“能跑”的代码而是一个可配置、可验证、可嵌入、可量产的贝塞尔曲线平滑模块——它不依赖任何外部库不产生堆内存不触发浮点异常且每一行 C 代码都直指工业现场的真实需求。本文还有配套的精品资源点击获取