资讯动态

三次B样条曲线VC++编程:de Boor算法与MFC交互实现

发布时间:2026/10/15 16:08:39 来源:尧图企业网站定制
简介三次B样条曲线是计算机图形学与CAD中常用的光滑曲线构造方法。这份VC工程资源实现了基于De Boor算法的核心计算并将控制点、Knot向量、基函数等关键概念封装在清晰可读的C代码中同时提供MFC交互界面允许手动拖动控制点实时观察曲线形状变化适合正在学习曲线拟合、准备图形学课程设计或从事CAD相关开发的读者直接参考。资源包共38个文件以头文件、C源文件、Visual C工程配置文件为主并附有可直接运行的exe程序与说明文档压缩包大小仅2.29MB部署和阅读都很方便。已有528人学习下载从算法推导到程序实现完整闭环既能辅助理解B样条的理论细节也能作为后续扩展NURBS或曲面建模的基础。1. 三次B样条曲线算法VC编程不经过控制点的曲线为什么是工业设计的主力第一次接触B样条的人最直观的感受往往是“控制点明明画在那里曲线却不经过它们”看起来有点玄学。但正是这种“不经过”换来了比Bezier曲线更重要的工程特性局部修改不影响全局、段与段之间天然二阶连续C2连续、曲线形状可以通过节点向量灵活调控。在VC环境里落地这套算法核心不是背公式而是把节点向量、基函数递推、de Boor求值变成能在屏幕上实时绘制、能随鼠标拖拽更新的可运行代码。这篇笔记面向的是要在MFC程序里实现自由曲线设计、测量点拟合、数控路径平滑的开发者以及需要完成图形学项目但不能只停留在调用现成库层面的学生。我会从数学定义讲到MFC工程落点再重点拆开那些一跑就翻车的细节全程按可复现的方式展开。2. 三次B样条的数学内核节点向量、基函数递推与Clamped选型真正动手写代码之前建议先把三个名词焊死在脑子里次数、节点向量、控制点。三次B样条里的“三次”是次数degree表示每一段曲线是参数u的三次多项式阶数order是次数加一也就是4。很多代码跑飞就是把次数和阶数混着用节点向量长度跟着错整个递推全乱。B样条曲线由n1个控制点P0..Pn和一个节点向量U共同定义。节点向量是一个非递减的实数序列U{u0,u1,...,um}其中m n p 1节点总数是n p 2。曲线只在参数区间[u_p, u_{n1}]上有完整定义前面的几个节点纯粹是为递推起点服务的。为什么定义域不从u0开始因为基函数N_i,p(u)只在[u_i, u_{ip1}]上非零要让所有p1个基函数同时有定义参数必须落在中间这段窗口内。对三次B样条来说p3至少需要4个控制点。当控制点恰好为4个时内部节点个数为0曲线只有一段三次B样条完全退化为三次Bezier曲线。这是理解B样条的好起点B样条就是一段一段拼起来的Bezier节点向量规定了每一段在参数轴上占多长。2.1 “三次”是次数不是阶数先理清控制点、节点向量与定义域写代码前先把变量名定死n是控制点最大下标控制点数减一p是次数U是节点向量。以6个控制点为例n5p3节点向量下标从0到np19一共10个节点。定义域是[U[3], U[6]]在这个区间之外基函数组合不完整算出来的点没有几何意义。这里有一个新手高频错误直接用控制点个数当n去套公式导致节点向量长度多一个或少一个最终曲线在某个区间上突然断掉。写代码时可以在BuildClampedKnotVector里加一句断言U.size() n p 2这个等式不满足后面一切免谈。另一个容易混的是“段数”。6个控制点、三次B样条定义域被内部节点分成n - p 2段不对段数是内部节点数量加一。n5, p3时内部节点2个曲线分3段每段由4个控制点决定。段数和控制点数的关系是段数 控制点数 - 次数。这是判断程序输出曲线段数是否正确的快速心算法。2.2 Cox-de Boor基函数递推递归起点是分段常数零分母要按0处理基函数是整个B样条的“黑匣子”里最核心的部分。0次基函数最简单参数u落在节点区间[u_i, u_{i1})内就是1否则是0。高次基函数由两个低次基函数加权合成权重分别是(u - u_i) / (u_{ip} - u_i)和(u_{ip1} - u) / (u_{ip1} - u_{i1})。写成代码// 计算第i个p次B样条基函数在参数u处的值 // U: 节点向量要求 u 在 [U[i], U[ip1]] 范围内 double BasisFunc(int i, int p, double u, const std::vectordouble U) { if (p 0) { // 半开区间约定u 等于右端点时不属于本区间 return (u U[i] u U[i 1]) ? 1.0 : 0.0; } double left 0.0, right 0.0; double dLeft U[i p] - U[i]; double dRight U[i p 1] - U[i 1]; // 分母小于 1e-12 视为重节点该权重按 0 处理 if (dLeft 1e-12) left (u - U[i]) / dLeft * BasisFunc(i, p - 1, u, U); if (dRight 1e-12) right (U[i p 1] - u) / dRight * BasisFunc(i 1, p - 1, u, U); return left right; }这个函数递归调用自身最大递归深度等于次数p三次情况下最多3层完全不用担心栈溢出。两个边界要注意u取节点区间右端点时0次基函数返回0这是半开区间约定分母小于1e-12时直接认为权重为0这是重节点的预处理。调用时要保证i p 1不超过U.size()-1不然数组越界。实际工程里我很少直接用这个递归版本去逐点求和因为它每个点都要重复计算大量基函数。它更大的价值是用来验证后面de Boor算法的结果随机取若干u两种方法算出来坐标误差小于1e-6说明实现没跑偏。2.3 节点向量选型均匀、Clamped与准均匀工程默认选Clamped节点向量的选型直接决定曲线的边界行为。最简单的均匀节点向量节点等距分布曲线首尾不会经过控制点。Clamped节点向量在首尾各重复p1个节点三次就是4个0和4个1曲线强制通过第一个和最后一个控制点。交互设计里基本都选Clamped因为路径的首尾固定是刚需。节点向量类型构造方式首尾是否经过控制点典型场景均匀节点在参数轴等距分布不经过周期闭曲线、单纯形状展示Clamped首尾各重复p1个节点经过首尾控制点交互设计、路径规划、数控加工准均匀/非均匀按弦长或曲率分布视情况数据拟合、插值逼近第一版实现我一般直接写Clamped曲线首尾钉在控制点上调试时最直观。构造代码// 构造 Clamped 三次 B 样条节点向量 // controlPtCount: 控制点总数次数 p 3 std::vectordouble BuildClampedKnotVector(int controlPtCount) { int n controlPtCount - 1; // 控制点最大下标 int p 3; int m n p 1; // 节点向量最大下标 std::vectordouble U(m 1); int i; // 首部 p1 个 0 for (i 0; i p; i) U[i] 0.0; // 内部节点均匀分布个数为 n - p int innerCnt n - p; for (i 1; i innerCnt; i) U[p i] static_castdouble(i) / (innerCnt 1); // 尾部 p1 个 1 for (i n 1; i m; i) U[i] 1.0; return U; }这段代码的关键是innerCnt n - p。当6个控制点n5, p3时innerCnt2内部节点把(0,1)三等分得到U{0,0,0,0,1/3,2/3,1,1,1,1}定义域被分成3段。当只有4个控制点n3时innerCnt0U{0,0,0,0,1,1,1,1}整条曲线就是一段三次Bezier。这个退化关系可以用来做自检4个控制点时B样条输出必须和Bezier一致。3. 用VC MFC搭出绘图框架文档类存数据视图类画曲线VC里做曲线交互最常见的载体是MFC单文档应用。不要在对话框里硬画文档/视图的分工更清晰文档类负责存控制点、算曲线参数视图类负责画点和响应鼠标。这样后续加序列化保存、多视图同步都顺理成章。工程创建不多说VS向导里选MFC单文档应用基类选CView。建好之后先别急着画曲线把数据结构定死这是后面所有代码的地基。3.1 创建MFC单文档工程先定数据结构和数据归属控制点是什么类型很多示例代码用CPoint因为MFC自带、画图方便。但CPoint内部是LONG整数类型而B样条递推过程全是浮点插值用整数中间计算会累积截断误差。我习惯在文档类里自己定义一个DPoint结构坐标用double// CMyDoc.h放在类声明之前 struct DPoint { double x; double y; }; // CMyDoc 类内 class CMyDoc : public CDocument { public: std::vectorDPoint m_ctrlPts; // 控制点数组 // ... };控制点放在文档类就是告诉所有人这是数据不是显示。视图类只是观察者。初始化控制点时先手动填6个点覆盖一个能看出曲线形态的布局// CMyDoc::OnNewDocument 或其他初始化入口 void CMyDoc::InitCtrlPts() { m_ctrlPts.clear(); m_ctrlPts.push_back({ 100.0, 400.0 }); m_ctrlPts.push_back({ 200.0, 200.0 }); m_ctrlPts.push_back({ 350.0, 150.0 }); m_ctrlPts.push_back({ 500.0, 300.0 }); m_ctrlPts.push_back({ 650.0, 180.0 }); m_ctrlPts.push_back({ 780.0, 350.0 }); }注意这里坐标是设备坐标语义y向下。如果你的控制点来自数学模型测量数据、DXF导入一般是y向上的数学坐标绘制时要做映射这个坑在第5章单独讲。选这6个点是为了让曲线有一段明显“鼓起来”、一段“凹下去”方便观察算法是否正确。3.2 用OnDraw画控制多边形与控制点GDI的画笔、画刷与整点坐标视图类的OnDraw是绘图的唯一入口窗口刷新、缩放、滚动都会回调它。先把控制点和控制多边形画出来曲线还没画但这一步能验证数据和坐标映射是否正确void CMyView::OnDraw(CDC* pDC) { CMyDoc* pDoc GetDocument(); if (!pDoc || pDoc-m_ctrlPts.size() 2) return; // 控制点从 double 转成 int 设备坐标GDI 画不了浮点 std::vectorCPoint devPts; devPts.reserve(pDoc-m_ctrlPts.size()); for (const auto pt : pDoc-m_ctrlPts) devPts.emplace_back((int)pt.x, (int)pt.y); // 控制多边形用灰色点线 CPen dotPen(PS_DOT, 1, RGB(160, 160, 160)); CPen* oldPen pDC-SelectObject(dotPen); pDC-Polyline(devPts.data(), (int)devPts.size()); pDC-SelectObject(oldPen); // 控制点用红色小方块 CBrush redBrush(RGB(220, 40, 40)); CBrush* oldBrush pDC-SelectObject(redBrush); for (const auto pt : devPts) pDC-Rectangle(pt.x - 3, pt.y - 3, pt.x 3, pt.y 3); pDC-SelectObject(oldBrush); }这里有几个细节。第一GetDocument()在视图还没有挂接文档时可能返回NULL开头判空是必须的。第二用Polyline一次连完所有点比循环MoveTo/LineTo少很多GDI调用。第三从double转int是有意为之的精度损失发生在这一个边界上且只影响显示不影响算法数据。画完之后运行程序应该看到6个点、5段灰色连线。如果控制多边形看起来是镜像的比如原本应该向上的折线变成了向下直接跳到第5章避坑5.1处理坐标方向。3.3 设备坐标方向与“看起来反了”的处理统一坐标系比算对公式更重要B样条算法本身不关心坐标方向它只是对x和y做同样的线性插值。出问题的是坐标系混用。如果你用鼠标点击拿控制点那控制点本身就是设备坐标y向下计算和绘制用同一套坐标画出来自然正确。但如果控制点来自实际模型通常是数学坐标y向上直接拿去OnDraw画整条曲线就是上下镜像的。我一般会在视图类里加一个转换函数把数学坐标映射到设备坐标y_dev clientHeight - y_math。控制多边形的Polyline、控制点的命中检测、曲线的采样点绘制全部统一走这个映射。最忌讳的是控制多边形用一套坐标、曲线用另一套两套叠加后曲线和控制点对不上看起来像见鬼了。具体到代码就是在OnDraw开头先取客户区高度遍历m_ctrlPts做转换时套用这个公式。注意clientHeight要取当前客户区高度因为窗口大小会变不能缓存死值。这一步处理完后续所有算法逻辑都跟坐标方向解耦了。4. de Boor求值算法落地采样绘制一条完整的三次B样条曲线数学定义清楚了控制点也画出来了接下来是主角de Boor算法。这是目前工程上最主流的B样条求值方法本质是在控制点数组上做重复线性插值每插值一层参与的点就少一圈三次情况下做3层最后剩下的那个点就是曲线上的点。4.1 为什么用de Boor而不是基函数求和直接对基函数求和也能算出曲线点但每求一个点要计算p1个基函数每个基函数又递归展开开销大且数值上有小误差累积。de Boor算法直接把坐标插值和基函数计算合并成一组公式循环次数少代码也更直观。尤其在做拖拽交互时鼠标每动一次要重算几十上百个采样点de Boor的性能优势是能感知到的。另外de Boor还有一个隐性优势它天然处理了节点重复的情况。基函数求和在重节点处要小心处理分母de Boor只需在插值前判断分母是否为零。这也是我推荐用它作为主力求值函数的原因。4.2 实现de Boor区间定位、局部拷贝与三层插值// de Boor 求值返回控制点数组 P 对应的 p 次 B 样条曲线上参数 u 处的点 // U: 节点向量要求是 Clamped 且长度 P.size() p 1 DPoint DeBoor(int p, const std::vectordouble U, const std::vectorDPoint P, double u) { int n (int)P.size() - 1; // 定义域右端点保护半开区间不包含 U[n1] if (u U[n 1]) u U[n 1] - 1e-10; // 找到参数 u 所在节点区间 [U[k], U[k1]) int k p; while (u U[k 1]) k; // 局部拷贝避免修改原始控制点 std::vectorDPoint d(P); // 从 r1 到 rp 逐层线性插值 for (int r 1; r p; r) { for (int i k; i k - p r; --i) { double denom U[i p - r 1] - U[i]; // 重节点分母为0alpha按0处理这一项不参与插值 double alpha (denom 1e-12) ? 0.0 : (u - U[i]) / denom; d[i].x (1.0 - alpha) * d[i - 1].x alpha * d[i].x; d[i].y (1.0 - alpha) * d[i - 1].y alpha * d[i].y; } } return d[k]; }参数含义说明P是控制点数组DPointdouble精度U是节点向量u是参数值p是次数本文固定为3。查找区间时k从p开始因为定义域起点就是U[p]小于这个值的参数没有几何意义。if (u U[n 1])这行是端点保护后面细说。循环部分是最容易写错的地方。内层循环的i从k递减到k - p r每层参与插值的点比上一层少一个。以p3为例第一层处理3组插值第二层处理2组第三层处理1组最终d[k]就是曲线上的点。d是P的深拷贝因为DPoint只有两个double拷贝整个数组的开销很小控制点在几十个以内时完全不需要优化成只拷贝k-p到k的窗口。重节点时会遇到denom为0的情况这时alpha按0处理。数学上这对应“该项不参与插值”d[i]直接继承d[i-1]的值。如果直接用continue跳过更新会破坏递推链曲线在重节点处会出尖角。4.3 采样成折线步长怎么定200个点够不够曲线上单个点算出来了要画整条线就得采样。常见做法是从U[3]到U[n1]按固定步长取200个点依次连成折线。分段再多也基本够用因为三次曲线段内的弯曲不会特别剧烈// 绘制三次 B 样条曲线 // ctrlPts: 控制点数组数学坐标或设备坐标需与 GDI 映射一致 void DrawSpline(CDC* pDC, const std::vectorDPoint ctrlPts) { int n (int)ctrlPts.size() - 1; std::vectordouble U BuildClampedKnotVector((int)ctrlPts.size()); CPen solidPen(PS_SOLID, 2, RGB(0, 120, 220)); CPen* oldPen pDC-SelectObject(solidPen); int samples 200; double uMin U[3]; // 定义域起点 double uMax U[n 1]; // 定义域终点 DPoint prev DeBoor(3, U, ctrlPts, uMin); for (int i 1; i samples; i) { double u uMin (uMax - uMin) * i / samples; DPoint cur DeBoor(3, U, ctrlPts, u); pDC-MoveTo((int)prev.x, (int)prev.y); pDC-LineTo((int)cur.x, (int)cur.y); prev cur; } pDC-SelectObject(oldPen); }这里的BuildClampedKnotVector参数传的是控制点总数所以是ctrlPts.size()而不是n注意别传错。采样数200对段数少于10的情况足够如果控制点很多比如20个以上固定200会把每段曲线的采样点稀释到很少建议动态设置samples max(200, (int)ctrlPts.size() * 20)。固定步长的局限在于节点分布不均匀时节点密集的区段采样点相对少可能丢失细节。但对第一版交互工具来说200个点完全够看实际导出数控路径时再按弦长误差细分这个放到第6章讲。4.4 用6个控制点验证u区间与参与控制点的对应关系拿第3章的6个控制点跑一遍完整流程生成节点向量U{0,0,0,0,1/3,2/3,1,1,1,1}定义域从0到1。在这个参数范围内不同区间的曲线段参与插值的控制点不同参数区间参与插值的控制点说明[0, 1/3]P0, P1, P2, P3第一段起点就是P0[1/3, 2/3]P1, P2, P3, P4中间段P0完全退出[2/3, 1]P2, P3, P4, P5最后一段终点就是P5验证代码可以这样写void TestBSpline() { std::vectorDPoint P { { 100.0, 400.0 }, { 200.0, 200.0 }, { 350.0, 150.0 }, { 500.0, 300.0 }, { 650.0, 180.0 }, { 780.0, 350.0 } }; int n (int)P.size() - 1; std::vectordouble U BuildClampedKnotVector((int)P.size()); // 取定义域起点、中点、终点各算一个点 double params[] { U[3], 0.5, U[n 1] }; for (double u : params) { DPoint pt DeBoor(3, U, P, u); // 在调试输出里检查数值 } }u0时结果应该等于P0u1时结果应该等于P5这是Clamped节点向量的必然结果。两个端点对上了说明节点向量和de Boor实现基本正确。重点关注u0.5这个点它在中间段的某个位置上可以用上一版的基函数求和交叉验证误差应该在浮点精度范围内。表格里能看出B样条最核心的局部支撑性质拖拽控制点P4参数u1/3的第一段完全不受影响因为它根本不在P4的支撑区间内拖拽P1最后一段也完全不变。这就是B样条相对Bezier的工程价值修改局部不拖累全局。验证时故意拖拽中间某个控制点观察只有相邻两段曲线变化其他段保持不动就说明实现正确。5. 三次B样条VC编程的5个避坑实录从镜像错位到鼠标拖拽失控这一章的坑都是实际写代码时遇到的按“现象→原因→解决”展开。每一条都值得在你自己的工程里加一条注释省得以后重新踩一遍。5.1 曲线上下颠倒控制多边形和曲线对不上现象控制点连线顺序看着正常但曲线像是照镜子画的上下完全翻转。原因MFC的CDC设备坐标原点在窗口左上角y轴向下而数学模型里的坐标通常是y轴向上。控制点用设备坐标计算用数学坐标或者反过来曲线必然镜像。解决统一坐标系。我的习惯是控制点存储和算法计算全用数学坐标只在OnDraw显示时把y翻转为y_dev clientHeight - y_math。控制多边形的Polyline、曲线采样点、鼠标命中检测全部走同一个映射不要把转换散落在各处。如果你用鼠标点击获取控制点那控制点本身就是设备坐标直接用一套坐标即可不要多此一举再翻转。5.2 曲线两端飘在空中不经过第一个和最后一个控制点现象曲线整体形状对但端点离控制点有一段距离怎么调都不贴上去。原因节点向量用了均匀分布首尾没有重复节点。只有Clamped节点向量首尾各重复p1个节点才会让曲线强制通过首尾控制点。三次B样条里如果节点向量是{0, 1/10, 2/10, ...}这种普通均匀分布曲线端点是“悬空”的。解决检查节点向量前4个元素是否全为0、最后4个是否全为1。用第2章的BuildClampedKnotVector生成即可。4个控制点时U{0,0,0,0,1,1,1,1}曲线退化为Bezier端点自然通过控制点。如果第一步验证发现端点不对优先查这里别急着怀疑de Boor写错了。5.3 曲线有锯齿状轻微抖动采样数加到500也没用现象曲线看起来整体平滑但放大后能看到微小的折线抖动尤其在曲线弯曲剧烈的区段。原因de Boor中间计算用了CPoint坐标是整数每次插值都截断取整。三次B样条要经过3层递推每层的误差累积起来就在曲线上形成了像素级噪音。这类问题最坑的是不明显缩小时根本看不出一旦截图放大就露馅。解决所有几何计算变量统一用double就是前面定义的DPoint只在MoveTo/LineTo转换成int。自查方法在DeBoor返回前加一行调试输出把中间变量打印出来如果发现全是.000结尾说明中间量被int化了。这是B样条VC编程里最典型的“精度陷阱”。5.4 拖拽控制点时曲线飞走松开鼠标还在动现象鼠标按住控制点拖动曲线跟着鼠标跑出窗口松开左键后鼠标在窗口外移动控制点还在跟着动。原因鼠标捕获没有正确配对。OnLButtonDown里调用了SetCapture但OnLButtonUp没有调用ReleaseCapture或者OnMouseMove里没有判断“当前是否处于拖拽状态”。捕获未释放时窗口会持续接收鼠标消息即使光标已经在窗口外。解决用状态变量加三件套消息处理// 视图类成员int m_dragIdx -1; void CMyView::OnLButtonDown(UINT nFlags, CPoint point) { // 命中检测遍历控制点距离小于6像素视为命中 for (size_t i 0; i pDoc-m_ctrlPts.size(); i) { DPoint p pDoc-m_ctrlPts[i]; if (abs((int)p.x - point.x) 6 abs((int)p.y - point.y) 6) { m_dragIdx (int)i; SetCapture(); return; } } CView::OnLButtonDown(nFlags, point); } void CMyView::OnMouseMove(UINT nFlags, CPoint point) { if (m_dragIdx 0 pDoc) { pDoc-m_ctrlPts[m_dragIdx].x point.x; pDoc-m_ctrlPts[m_dragIdx].y point.y; Invalidate(FALSE); // 不擦背景减少闪烁 } CView::OnMouseMove(nFlags, point); } void CMyView::OnLButtonUp(UINT nFlags, CPoint point) { if (m_dragIdx 0) { m_dragIdx -1; ReleaseCapture(); } CView::OnLButtonUp(nFlags, point); }m_dragIdx是状态标志等于-1表示不在拖拽状态。OnMouseMove里先判断再更新就不会在没按下鼠标时误改控制点。ReleaseCapture和SetCapture必须成对这是MFC鼠标交互的铁律。5.5 某些控制点布局下曲线出尖角甚至数值变成NaN现象控制点密集排列或拖拽到特定位置时曲线局部出现一个很尖的角严重时整个曲线消失调试发现坐标是NaN。原因节点向量里存在重节点U[ip-r1] - U[i]分母为0alpha计算出现0/0。这在拖拽控制点动态重建节点向量时很容易触发尤其是当用户把两个控制点拖到几乎同一位置时。解决DeBoor里对分母加容差判断denom 1e-12时alpha按0处理见4.2的代码。不要用continue跳过要按“alpha0、该项不参与插值”的语义继续执行递推否则递推链断裂。同样的问题也可能出现在基函数求和的实现里处理方式一致所有除零前都加容差不给NaN任何机会。6. 从画出来到用起来拖拽控制点实时变形与曲线切线输出6.1 让曲线跟着鼠标走命中检测与三件套消息处理5.4给了拖拽的消息框架这里补上性能细节。拖拽时OnMouseMove每触发一次就重算200个采样点并重绘在MFC里如果不做处理会有明显闪烁感。我一般先Invalidate(FALSE)只重绘客户区不擦背景减少闪烁。如果曲线控制点很多、采样点上千就把曲线先画到内存CDC再一次性BitBlt到屏幕这是MFC双缓冲的标准做法。命中检测的阈值用6像素比较顺手太小难点中太大容易误触。控制点数量多时可以倒序遍历保证画在最上层的点优先命中。拖拽过程中控制点坐标是double鼠标坐标是int赋值时直接转换不做舍入因为OnDraw显示时还会再转一次精度损失只剩显示层的那一次。6.2 曲线上任意点坐标作为路径数据差分求切线的技巧B样条曲线算出来不只是为了画在屏幕上。数控加工、路径规划里经常要取曲线上某点的坐标和方向角。坐标直接用DeBoor拿方向角用相邻两点差分算// 取参数 u 附近的曲线方向角弧度 double u 0.5; DPoint pL DeBoor(3, U, P, u - 1e-4); DPoint pR DeBoor(3, U, P, u 1e-4); double dx pR.x - pL.x; double dy pR.y - pL.y; double tangentAngle atan2(dy, dx);差分步长取1e-4对三次样条足够稳定比解析求导实现简单得多精度也够用。如果要做速度规划再考虑对de Boor公式解析求导那是进阶话题。导出路径数据时按弦长误差做自适应细分比固定步长更省点简单做法是相邻采样点距离超过阈值就插入中点曲率大的地方会自动变密。我做这类曲线功能时最深的一个教训所有几何数据统一用double存储和计算只有OnDraw画点和鼠标命中检测用int节点向量在控制点数量不变时缓存起来不要每次重绘都重新构造。这个习惯从MFC换到别的框架同样成立算一次B样条和算一百次B样条的差距在交互程序里用肉眼就能感觉到。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑