C++实现B样条曲线:从数学原理到交互式图形绘制 1. 项目概述从Bezier到B样条平滑曲线的工程实践在图形学、CAD/CAM乃至游戏开发中平滑曲线的生成与绘制是一个基础且核心的需求。很多朋友可能都接触过Bezier曲线它通过控制点来定义一条曲线但有一个显著特点曲线会精确地穿过首尾控制点并被中间的控制点所“吸引”。然而在实际工程中我们常常需要一种局部控制性更强、且不强制通过所有控制点的曲线这就是B样条曲线B-spline大显身手的地方。今天我们就来深入探讨如何用C从零开始实现B样条曲线的计算与绘制并完整解析其源码背后的数学原理与工程考量。无论你是正在学习计算机图形学的学生还是需要在实际项目中集成曲线功能的开发者这篇结合了理论推导与实战代码的解析都能让你不仅“知其然”更“知其所以然”最终拥有一套可以随手拿来即用、亦可深度定制的工具代码。2. B样条曲线核心原理与设计思路拆解2.1 为什么是B样条从Bezier的局限性说起Bezier曲线无疑是伟大的发明但其全局控制的特性也是一把双刃剑。移动曲线上的任何一个控制点整条曲线的形状都会发生变化。想象一下你在设计汽车外壳调整车头的一个控制点结果车尾的线条也跟着变了这显然不符合设计师精细、局部调整的需求。此外高阶Bezier曲线控制点很多数值稳定性会变差且难以生成复杂形状。B样条曲线正是为了解决这些问题而生的。它的核心优势在于局部支撑性每个曲线段只受有限的几个控制点影响移动一个控制点只会影响曲线局部的形状其余部分保持不变。这使得曲线编辑变得非常直观和高效。同时通过引入节点向量和基函数的概念B样条可以灵活地控制曲线的连续性C0 C1 C2连续和逼近程度既能生成插值曲线通过控制点也能生成逼近曲线靠近但不通过控制点除首尾特定条件外。我们本次实现的重点就是最常见的、均匀节点向量下的三次B样条曲线它在计算复杂度和曲线光滑度C2连续之间取得了很好的平衡广泛应用于路径规划、字体轮廓描述和工业设计。2.2 核心三要素控制点、节点向量与基函数要定义一条B样条曲线你需要三个核心要素控制点一个包含n1个点的数组P0, P1, ..., Pn。这些点构成了曲线的“骨架”决定了曲线的大致走向和形状。与Bezier不同B样条曲线通常不通过除特定条件外的内部控制点。节点向量一个非递减的实数序列U [u0, u1, ..., u_{m}]。其中m n k 1n是控制点最大索引k是曲线次数degree 我们常说的3次曲线其次数k3。节点向量定义了参数u的定义域如何被划分以及基函数在何处“活跃”或“失效”。对于均匀B样条节点通常是等间距的如[0, 1, 2, 3, 4, 5, 6]。次数曲线多项式的最高次数记为k。k1是折线k2是二次曲线k3是三次曲线最常用能保证曲率连续。曲线上的点C(u)由以下公式给出C(u) Σ_{i0}^{n} (N_{i, k}(u) * P_i)其中N_{i, k}(u)是第i个k次B样条基函数。这个公式意味着曲线上任意一点是所有控制点的加权平均权重就是对应基函数在参数u处的值。而基函数的值又完全由节点向量U和次数k通过一个递归的Cox-de Boor公式决定。我们的实现将围绕高效、稳定地计算这些基函数并最终合成曲线点来展开。3. 关键算法实现Cox-de Boor递归与均匀B样条优化3.1 Cox-de Boor递推算法的C实现这是B样条所有计算的基石。给定节点向量U、次数k、控制点索引i和参数u计算基函数N_{i,k}(u)的公式如下如果 k 0: 如果 u 在 [U[i], U[i1]) 区间内返回1.0否则返回0.0。 否则 N_{i,k}(u) (u - U[i]) / (U[ik] - U[i]) * N_{i, k-1}(u) (U[ik1] - u) / (U[ik1] - U[i1]) * N_{i1, k-1}(u)这里有一个关键细节分母可能为零。当节点向量中出现重节点时U[ik] - U[i]或U[ik1] - U[i1]可能为0。标准约定是0/0 0。在代码中我们必须对除法进行保护。下面是一个健壮的C实现片段/** * 计算B样条基函数的值 * param i 控制点索引从0开始 * param k 曲线次数 * param u 参数值 * param knots 节点向量 * return 基函数 N_{i, k}(u) 的值 */ double basisFunction(int i, int k, double u, const std::vectordouble knots) { // 递归基条件 if (k 0) { // 注意通常约定区间为左闭右开 [u_i, u_{i1})最后一个节点处理为闭区间 if ( (i knots.size() - 1) || (u knots[i] || u knots[i1]) ) { return 0.0; } else { return 1.0; } } double coef1, coef2; double denom1 knots[ik] - knots[i]; double denom2 knots[ik1] - knots[i1]; // 处理分母为零的情况遵循 0/0 0 的约定 if (std::abs(denom1) 1e-10) { coef1 0.0; } else { coef1 (u - knots[i]) / denom1; } if (std::abs(denom2) 1e-10) { coef2 0.0; } else { coef2 (knots[ik1] - u) / denom2; } return coef1 * basisFunction(i, k-1, u, knots) coef2 * basisFunction(i1, k-1, u, knots); }注意这是一个清晰的递归实现便于理解但直接用于绘制效率较低因为存在大量重复计算。在实际的绘制循环中我们通常采用更高效的非递归方法或预计算基函数表。3.2 针对均匀B样条的优化计算对于均匀B样条节点向量是等间距的例如[0, 1, 2, 3, 4, 5, ...]。这种情况下基函数具有平移不变性N_{i,k}(u)只与(u - i)的小数部分有关。我们可以将参数u规范化到某个标准区间如[0, 1]并预先计算出该区间内k次基函数的解析形式。以三次均匀B样条k3为例在一个标准区间u ∈ [0, 1]内影响该段的4个控制点对应的基函数值为N0(u) (1 - u)^3 / 6 N1(u) (3u^3 - 6u^2 4) / 6 N2(u) (-3u^3 3u^2 3u 1) / 6 N3(u) u^3 / 6这样在绘制每一段曲线时我们无需递归只需计算参数u规范化后然后代入这四个多项式即可速度极快。这是工程中常用的优化手段。// 计算三次均匀B样条在规范区间[0,1]内的基函数值 void uniformCubicBasis(double u, double basis[4]) { double u2 u * u; double u3 u2 * u; double inv6 1.0 / 6.0; basis[0] (1 - 3*u 3*u2 - u3) * inv6; // 等价于 (1-u)^3 / 6 basis[1] (4 - 6*u2 3*u3) * inv6; basis[2] (1 3*u 3*u2 - 3*u3) * inv6; basis[3] (u3) * inv6; }3.3 曲线点的计算与采样策略有了基函数计算曲线上一点就很简单了遍历所有控制点将每个控制点坐标乘以其对应的基函数值然后累加。Point2D evaluateBSpline(const std::vectorPoint2D controlPoints, int k, const std::vectordouble knots, double u) { int n controlPoints.size() - 1; // 控制点最大索引 Point2D result(0.0, 0.0); // 确保参数u在有效区间内 [knots[k], knots[n1]) u std::max(knots[k], std::min(u, knots[n1] - 1e-10)); for (int i 0; i n; i) { double weight basisFunction(i, k, u, knots); result.x controlPoints[i].x * weight; result.y controlPoints[i].y * weight; } return result; }为了绘制连续的曲线我们需要在参数域[u_min, u_max]内进行采样。采样步长delta_u的选择至关重要步长太大曲线看起来由离散的线段组成不平滑。步长太小计算点过多渲染效率下降且可能超出显示精度。一个实用的经验是根据显示分辨率动态调整。例如可以设定步长使得相邻采样点在屏幕上的距离大约为1-2个像素。更简单的方法是对每一段曲线两个节点区间进行固定数量的细分如20-50次。std::vectorPoint2D sampleBSpline(const std::vectorPoint2D ctrlPts, int degree, const std::vectordouble knots, int samplesPerSegment) { std::vectorPoint2D curvePoints; int n ctrlPts.size() - 1; double uStart knots[degree]; double uEnd knots[n1]; // 对于clamped B样条有效区间是 [knots[k], knots[n1]] for (int seg degree; seg n; seg) { // 遍历每个非零的节点区间 [knots[seg], knots[seg1]) for (int s 0; s samplesPerSegment; s) { double t static_castdouble(s) / samplesPerSegment; double u knots[seg] * (1 - t) knots[seg1] * t; curvePoints.push_back(evaluateBSpline(ctrlPts, degree, knots, u)); } } // 注意上面的循环会导致区间连接点被重复采样一次可以在后续绘制时跳过或在这里去重。 // 更优雅的方式是内层循环用 s samplesPerSegment最后再补上终点。 return curvePoints; }4. 完整C项目架构与源码解析一个完整的B样条绘制程序不仅仅是计算几个点。我们需要考虑数据结构、图形交互、实时渲染等多个方面。下面我将分模块解析一个基于控制台和简单图形库如SDL2或SFML的实现架构。4.1 核心数据结构定义首先定义一些基础类型和类。// Point2D.h #ifndef POINT2D_H #define POINT2D_H struct Point2D { double x, y; Point2D(double x_ 0.0, double y_ 0.0) : x(x_), y(y_) {} // 一些实用运算符重载 Point2D operator(const Point2D other) const { return Point2D(x other.x, y other.y); } Point2D operator-(const Point2D other) const { return Point2D(x - other.x, y - other.y); } Point2D operator*(double scalar) const { return Point2D(x * scalar, y * scalar); } friend Point2D operator*(double scalar, const Point2D p) { return p * scalar; } }; #endif // POINT2D_H// BSpline.h #ifndef BSPLINE_H #define BSPLINE_H #include vector #include Point2D.h class BSpline { public: // 构造函数传入控制点、曲线次数是否生成clamped夹紧曲线首尾过控制点 BSpline(const std::vectorPoint2D controlPoints, int degree, bool clamped true); // 计算曲线上参数u对应的点 Point2D evaluate(double u) const; // 采样整条曲线生成用于绘制的点序列 std::vectorPoint2D sample(int samplesPerSegment 30) const; // 获取/设置控制点修改后可能需要重新初始化节点向量 const std::vectorPoint2D getControlPoints() const { return m_controlPoints; } void setControlPoint(int index, const Point2D pt); // 获取节点向量只读 const std::vectordouble getKnots() const { return m_knots; } int getDegree() const { return m_degree; } bool isClamped() const { return m_clamped; } private: std::vectorPoint2D m_controlPoints; int m_degree; // 曲线次数 bool m_clamped; std::vectordouble m_knots; // 节点向量 // 初始化节点向量 void initializeKnots(); // 计算基函数内部使用可优化为查表或非递归 double basisFunction(int i, int k, double u) const; }; #endif // BSPLINE_H4.2 核心类BSpline的实现细节实现文件BSpline.cpp包含了算法的核心逻辑。// BSpline.cpp #include BSpline.h #include algorithm #include cassert #include cmath BSpline::BSpline(const std::vectorPoint2D controlPoints, int degree, bool clamped) : m_controlPoints(controlPoints), m_degree(degree), m_clamped(clamped) { assert(controlPoints.size() degree 1); // 控制点数量至少为次数1 assert(degree 1); initializeKnots(); } void BSpline::initializeKnots() { int n m_controlPoints.size() - 1; // 控制点最大索引 int m n m_degree 1; // 节点向量长度 n k 1 m_knots.resize(m 1); // 通常使用0-index多一个方便计算 // 生成节点向量 if (m_clamped) { // Clamped B样条前k1个节点为0后k1个节点为1中间均匀分布 for (int i 0; i m_degree; i) { m_knots[i] 0.0; } for (int i m_degree 1; i n; i) { m_knots[i] static_castdouble(i - m_degree) / (n - m_degree 1); } for (int i n 1; i m; i) { m_knots[i] 1.0; } } else { // 均匀非clampedB样条节点从0开始均匀递增 for (int i 0; i m; i) { m_knots[i] static_castdouble(i); } } // 注意许多文献和库的节点向量是从0到m我们这里存储了m1个值u的有效范围是 [knots[k], knots[n1]] } double BSpline::basisFunction(int i, int k, double u) const { // 递归实现同前文。注意处理分母为零。 if (k 0) { if (i m_knots.size() - 1) return 0.0; return (u m_knots[i] u m_knots[i1]) ? 1.0 : 0.0; } double denom1 m_knots[ik] - m_knots[i]; double denom2 m_knots[ik1] - m_knots[i1]; double coef1 (std::abs(denom1) 1e-10) ? ((u - m_knots[i]) / denom1) : 0.0; double coef2 (std::abs(denom2) 1e-10) ? ((m_knots[ik1] - u) / denom2) : 0.0; return coef1 * basisFunction(i, k-1, u) coef2 * basisFunction(i1, k-1, u); } Point2D BSpline::evaluate(double u) const { // 将u钳制到有效参数域 [knots[degree], knots[n1]) int n m_controlPoints.size() - 1; double uMin m_knots[m_degree]; double uMax m_knots[n1]; u std::max(uMin, std::min(u, uMax - 1e-10)); // 减一个小量避免取到右端点导致基函数计算越界 Point2D result(0.0, 0.0); for (size_t i 0; i m_controlPoints.size(); i) { double weight basisFunction(i, m_degree, u); result.x m_controlPoints[i].x * weight; result.y m_controlPoints[i].y * weight; } return result; } std::vectorPoint2D BSpline::sample(int samplesPerSegment) const { std::vectorPoint2D sampledPoints; int n m_controlPoints.size() - 1; // 遍历每个曲线段 for (int seg m_degree; seg n; seg) { double uStart m_knots[seg]; double uEnd m_knots[seg1]; // 跳过零长度区间由重节点引起 if (std::abs(uEnd - uStart) 1e-10) continue; // 在每个区间内均匀采样 for (int s 0; s samplesPerSegment; s) { // 注意这里是 不是 double t static_castdouble(s) / samplesPerSegment; double u uStart t * (uEnd - uStart); sampledPoints.push_back(evaluate(u)); } } // 补上曲线终点 sampledPoints.push_back(evaluate(m_knots[n1] - 1e-10)); return sampledPoints; } void BSpline::setControlPoint(int index, const Point2D pt) { assert(index 0 index m_controlPoints.size()); m_controlPoints[index] pt; // 注意修改控制点不需要重新生成节点向量因为节点向量只与数量和clamped属性有关。 }4.3 图形交互与绘制模块以SFML为例为了可视化我们需要一个简单的图形窗口来显示控制点和B样条曲线并允许交互如拖动控制点。这里以SFML库为例展示主循环和绘制逻辑。// main.cpp #include SFML/Graphics.hpp #include BSpline.h #include iostream const int WINDOW_WIDTH 800; const int WINDOW_HEIGHT 600; const float CONTROL_POINT_RADIUS 8.0f; int main() { sf::RenderWindow window(sf::VideoMode(WINDOW_WIDTH, WINDOW_HEIGHT), B-Spline Curve Demo); window.setFramerateLimit(60); // 初始化一组控制点 std::vectorPoint2D controlPoints { {100, 300}, {200, 200}, {350, 400}, {500, 100}, {650, 300}, {700, 500} }; // 创建三次B样条曲线clamped模式首尾过控制点 BSpline spline(controlPoints, 3, true); // 采样曲线点 auto curvePoints spline.sample(30); // 将几何点转换为SFML的顶点用于绘制 std::vectorsf::Vertex curveVertices; for (const auto pt : curvePoints) { curveVertices.emplace_back(sf::Vector2f(pt.x, pt.y), sf::Color::Blue); } // 创建控制点的图形表示圆 std::vectorsf::CircleShape controlPointShapes; for (const auto pt : controlPoints) { sf::CircleShape shape(CONTROL_POINT_RADIUS); shape.setFillColor(sf::Color::Red); shape.setOrigin(CONTROL_POINT_RADIUS, CONTROL_POINT_RADIUS); // 将原点设为中心 shape.setPosition(pt.x, pt.y); controlPointShapes.push_back(shape); } // 绘制控制多边形连线 std::vectorsf::Vertex polylineVertices; for (const auto pt : controlPoints) { polylineVertices.emplace_back(sf::Vector2f(pt.x, pt.y), sf::Color(255, 150, 150, 128)); // 半透明粉色 } int selectedPointIndex -1; // 当前被拖拽的控制点索引 while (window.isOpen()) { sf::Event event; while (window.pollEvent(event)) { if (event.type sf::Event::Closed) window.close(); // 鼠标按下检查是否点中了某个控制点 if (event.type sf::Event::MouseButtonPressed event.mouseButton.button sf::Mouse::Left) { sf::Vector2f mousePos(event.mouseButton.x, event.mouseButton.y); selectedPointIndex -1; for (size_t i 0; i controlPointShapes.size(); i) { float dx mousePos.x - controlPointShapes[i].getPosition().x; float dy mousePos.y - controlPointShapes[i].getPosition().y; if (dx*dx dy*dy CONTROL_POINT_RADIUS * CONTROL_POINT_RADIUS) { selectedPointIndex i; break; } } } // 鼠标释放停止拖拽 if (event.type sf::Event::MouseButtonReleased event.mouseButton.button sf::Mouse::Left) { selectedPointIndex -1; } // 鼠标移动如果正在拖拽控制点则更新其位置并重新计算曲线 if (event.type sf::Event::MouseMoved selectedPointIndex ! -1) { Point2D newPt(event.mouseMove.x, event.mouseMove.y); spline.setControlPoint(selectedPointIndex, newPt); controlPointShapes[selectedPointIndex].setPosition(newPt.x, newPt.y); // 更新控制多边形连线 polylineVertices[selectedPointIndex].position sf::Vector2f(newPt.x, newPt.y); // 重新采样曲线 curvePoints spline.sample(30); curveVertices.clear(); for (const auto pt : curvePoints) { curveVertices.emplace_back(sf::Vector2f(pt.x, pt.y), sf::Color::Blue); } } // 按键事件例如按空格键增加一个控制点在末尾 if (event.type sf::Event::KeyPressed event.key.code sf::Keyboard::Space) { // 简单示例在最后一个控制点附近添加一个新点 if (!controlPoints.empty()) { Point2D lastPt controlPoints.back(); Point2D newPt(lastPt.x 50, lastPt.y (rand() % 100 - 50)); controlPoints.push_back(newPt); // 需要重新创建BSpline对象因为控制点数量变了节点向量需要重新生成 spline BSpline(controlPoints, 3, true); // 更新图形对象 sf::CircleShape shape(CONTROL_POINT_RADIUS); shape.setFillColor(sf::Color::Red); shape.setOrigin(CONTROL_POINT_RADIUS, CONTROL_POINT_RADIUS); shape.setPosition(newPt.x, newPt.y); controlPointShapes.push_back(shape); polylineVertices.emplace_back(sf::Vector2f(newPt.x, newPt.y), sf::Color(255, 150, 150, 128)); curvePoints spline.sample(30); curveVertices.clear(); for (const auto pt : curvePoints) { curveVertices.emplace_back(sf::Vector2f(pt.x, pt.y), sf::Color::Blue); } } } } // 渲染 window.clear(sf::Color::White); // 绘制控制多边形连线 if (polylineVertices.size() 1) { window.draw(polylineVertices[0], polylineVertices.size(), sf::LineStrip); } // 绘制B样条曲线 if (curveVertices.size() 1) { window.draw(curveVertices[0], curveVertices.size(), sf::LineStrip); } // 绘制控制点 for (const auto shape : controlPointShapes) { window.draw(shape); } window.display(); } return 0; }5. 性能优化与高级话题探讨5.1 递归计算的性能瓶颈与优化策略前面展示的basisFunction递归实现虽然清晰但存在大量的重复计算时间复杂度是指数级的。在实际绘制中我们需要对每个采样点u计算多个基函数这种开销是不可接受的。优化策略1查表法预计算对于均匀B样条我们可以预先计算好标准区间[0, 1)内基函数在离散采样点上的值存储在一个表中。计算时根据u找到对应的区间和偏移量通过查表和线性插值快速得到基函数值。这是游戏等实时应用中最常用的方法。优化策略2非递归计算De Boor算法对于给定的参数u和次数k我们真正需要的是所有非零基函数的值。可以改用De Boor算法的变体直接计算k1个非零基函数的值避免递归。其核心是一个迭代过程计算量是O(k^2)远优于递归。// 非递归方式计算在参数u处所有非零的k次基函数值结果存入数组N中 void computeBasisFunctions(int span, double u, int k, const std::vectordouble knots, double N[]) { // 临时数组用于存储中间结果 std::vectordouble left(k1, 0.0); std::vectordouble right(k1, 0.0); N[0] 1.0; for (int j 1; j k; j) { left[j] u - knots[span 1 - j]; right[j] knots[span j] - u; double saved 0.0; for (int r 0; r j; r) { double temp N[r] / (right[r1] left[j-r]); N[r] saved right[r1] * temp; saved left[j-r] * temp; } N[j] saved; } } // 调用时先找到u所在的节点区间span然后调用此函数。 // 得到的N[0]到N[k]就是基函数 N_{span-k, k}, ..., N_{span, k} 的值。5.2 处理Clamped与Unclamped B样条我们的实现支持clamped模式。在这种模式下曲线起点和终点会分别与第一个和最后一个控制点重合前提是节点向量前k1个值相同后k1个值相同。这通常是我们想要的行为。unclamped均匀非夹紧模式下曲线不会通过首尾控制点所有节点均匀分布局部性更纯粹。在initializeKnots函数中我们通过一个布尔标志m_clamped来区分这两种情况。理解这两种模式的差异对于正确设置节点向量至关重要。5.3 节点向量的生成与重节点的影响节点向量的生成并非只有均匀一种方式。除了均匀和clamped还有准均匀类似clamped但内部节点也是均匀的。非均匀节点间距任意可以用于调整曲线在特定参数区间的“密度”。重节点重复的节点值是一个强大的工具。在节点向量中重复一个节点值p次会使曲线在该节点处失去p阶连续性。例如在clamped B样条中起点和终点处的k1重节点使得曲线具有C0连续性即位置连续并穿过控制点但失去了高阶连续性。在内部插入重节点可以制造曲线的“尖角”或中断这在设计有棱角的形状时非常有用。在代码中我们通过判断denom是否为零来处理重节点自动遵循0/0 0的约定。6. 常见问题排查与调试心得在实际实现和调试B样条曲线的过程中你几乎一定会遇到下面这些问题。这里我把踩过的坑和解决方法记录下来希望能帮你节省大量时间。问题1曲线显示不出来或者形状完全不对。检查控制点坐标首先确认你的控制点坐标是否在合理的范围内例如是否匹配你的窗口坐标系。一个常见的错误是坐标系Y轴方向屏幕坐标系通常Y轴向下为正与数学坐标系混淆。检查参数u的有效范围这是最容易出错的地方对于有n1个控制点次数为k的B样条其有效参数范围是[knots[k], knots[n1])。在我们的clamped实现中这通常是[0, 1)。如果你在[0, 1]内采样确保处理右端点u1的情况可能需要一个微小偏移如1 - 1e-10来避免基函数数组越界。验证基函数计算选择一个简单的测试用例比如2次B样条3个控制点(0,0), (1,1), (2,0)手动计算在u0.5时的基函数值和曲线点与程序输出对比。确保基函数权重之和为1对于u在有效区间内。问题2曲线在端点处行为异常不过控制点或突然拐弯。确认clamped设置如果你希望曲线过首尾控制点必须使用clamped节点向量。检查initializeKnots函数中对于m_clampedtrue的节点生成逻辑是否正确确保前k1个节点为0后k1个节点为1。检查节点向量值打印出生成的节点向量确认其符合你的预期。对于n5, k3的clamped曲线节点向量应该类似于[0,0,0,0, 0.333, 0.666, 1,1,1,1]具体值取决于你的生成算法。问题3拖动控制点时曲线更新卡顿。采样点过多samplesPerSegment设置得太高比如1000。对于屏幕显示每段20-50个采样点已经非常平滑。可以尝试降低这个值。递归计算效率低这是主要原因。务必使用前面提到的非递归De Boor算法或查表法来替代递归的basisFunction调用。在我的测试中优化后性能可提升数十倍。不必要的重采样在鼠标移动事件中每次移动都重新采样整个曲线。可以加入一个阈值比如鼠标移动超过一定像素再触发重采样或者使用脏矩形等局部更新技术但对于简单演示通常不需要。问题4曲线出现不希望的“震荡”或“过冲”。控制点过于密集或排列奇异B样条具有“变差递减性”通常不会比控制多边形振荡得更厉害。但如果控制点排成很尖锐的锯齿形曲线也可能出现抖动。尝试让控制点分布更平滑。次数过高次数k越高曲线越光滑但也会更“松驰”可能产生意想不到的波动。对于一般设计三次k3是最佳选择。除非有特殊需求不要轻易使用高于5次的B样条。一个实用的调试技巧可视化基函数。编写一个简单的函数将某个控制点对应的基函数N_{i,k}(u)在整个参数域上的图像画出来。这能帮你直观地理解该控制点的影响范围局部支撑性。当你移动一个控制点时只有那些基函数值非零的参数区间对应的曲线段会发生变化这个图像能清晰地展示这一点。实现B样条曲线绘制是一个将优雅的数学公式转化为健壮、高效代码的经典过程。从理解递推公式开始到处理各种边界条件重节点、参数越界再到进行性能优化和交互设计每一步都需要仔细推敲。希望这篇长文和附带的源码能成为你图形学工具箱里一件称手的兵器。当你看到随着鼠标拖动曲线平滑而局部地改变形状时你会感受到数学与代码结合带来的独特成就感。

本月热点