目录
在设计矢量图案的时候,我们常常需要用到曲线来表达物体造型,单纯用鼠标轨迹绘制显然是不足的。于是我们希望能够实现这样的方法:通过设计师手工选择控制点,再通过插值得到过控制点(或在附近)的一条平滑曲线。在这样的需求下,样条曲线诞生了。简而言之,样条曲线是由多个多项式按比例系数组成的多项式函数,而比例系数是由控制点决定的。Hermite曲线、Cardinal曲线在平时的开发中,经常用于模拟运动物体的轨迹,如下:

关于Hermite曲线、Cardinal曲线的数学理论,参见如下博文:
如下为用Qt实现的Cardinal曲线
Cardinal.h
- #pragma once
-
- #include
- #include "ui_Cardinal.h"
-
- QT_BEGIN_NAMESPACE
- namespace Ui { class CardinalClass; };
- QT_END_NAMESPACE
-
- class Cardinal : public QWidget
- {
- Q_OBJECT
-
- public:
- Cardinal(QWidget *parent = nullptr);
- ~Cardinal();
-
- private:
- Ui::CardinalClass *ui;
- };
Cardinal.cpp
- #include "Cardinal.h"
-
- Cardinal::Cardinal(QWidget *parent)
- : QWidget(parent)
- , ui(new Ui::CardinalClass())
- {
- ui->setupUi(this);
- setWindowState(Qt::WindowMaximized);
-
- ui->doubleSpinBox->setMinimum(0);
- ui->doubleSpinBox->setMaximum(1);
- ui->doubleSpinBox->setValue(0.5);
- ui->doubleSpinBox->setSingleStep(0.1);
-
- connect(ui->doubleSpinBox, QOverload<double>::of(&QDoubleSpinBox::valueChanged), ui->myCardinalPanel, &CardinalPanel::valueChanged);
- connect(ui->startDrawBtn, &QAbstractButton::clicked, ui->myCardinalPanel, &CardinalPanel::startDraw);
- connect(ui->clearBtn, &QAbstractButton::clicked, ui->myCardinalPanel, &CardinalPanel::clear);
- }
-
-
- Cardinal::~Cardinal()
- {
- delete ui;
- }
-
CardinalPanel.h
- #pragma once
-
- #include
- #include "ui_CardinalPanel.h"
- #include
- using std::list;
-
- class CardinalPanel : public QWidget
- {
- Q_OBJECT
-
-
- public:
- CardinalPanel(QWidget *parent = nullptr);
- ~CardinalPanel();
- public:
-
- void valueChanged(double value);
-
- void startDraw();
-
- void clear();
- private:
-
- virtual void mousePressEvent(QMouseEvent* event) override;
-
- virtual void paintEvent(QPaintEvent* event) override;
-
- private:
-
- // 画鼠标左键按下选中的点
- void drawPoint();
-
- // 画Cardinal曲线
- void drawCardinal();
-
- // 计算MC矩阵
- void calMcMatrix(double s);
-
- // 压入头部和尾部两个点,用于计算
- void pushHeadAndTailPoint();
- private:
- Ui::CardinalPanelClass ui;
- bool m_bStartDraw{false};
- double m_dfMcMatrix[4][4];
- list
m_lstPoint; - QPainterPath path;
- };
CardinalPanel.cpp
- #include "CardinalPanel.h"
- #include
- #include
- #include
- #include
- using std::vector;
-
- CardinalPanel::CardinalPanel(QWidget* parent)
- : QWidget(parent)
- {
- ui.setupUi(this);
-
- valueChanged(0.5);
- }
-
- CardinalPanel::~CardinalPanel()
- {}
-
- void CardinalPanel::valueChanged(double value)
- {
- auto s = (1 - value) / 2.0;
-
- // 计算MC矩阵
- calMcMatrix(s);
-
- update();
- }
-
- // 计算MC矩阵
- void CardinalPanel::calMcMatrix(double s)
- {
- m_dfMcMatrix[0][0] = -s, m_dfMcMatrix[0][1] = 2 - s, m_dfMcMatrix[0][2] = s - 2, m_dfMcMatrix[0][3] = s;//Mc矩阵
- m_dfMcMatrix[1][0] = 2 * s, m_dfMcMatrix[1][1] = s - 3, m_dfMcMatrix[1][2] = 3 - 2 * s, m_dfMcMatrix[1][3] = -s;
- m_dfMcMatrix[2][0] = -s, m_dfMcMatrix[2][1] = 0, m_dfMcMatrix[2][2] = s, m_dfMcMatrix[2][3] = 0;
- m_dfMcMatrix[3][0] = 0, m_dfMcMatrix[3][1] = 1, m_dfMcMatrix[3][2] = 0, m_dfMcMatrix[3][3] = 0;
- }
-
- void CardinalPanel::clear()
- {
- m_bStartDraw = false;
-
- m_lstPoint.clear();
-
- update();
- }
-
- // 压入头部和尾部两个点,用于计算
- void CardinalPanel::pushHeadAndTailPoint()
- {
- // 随便构造两个点
- auto ptBegin = m_lstPoint.begin();
- auto x = ptBegin->x() + 20;
- auto y = ptBegin->y() + 20;
- m_lstPoint.insert(m_lstPoint.begin(), QPoint(x, y));
-
- auto ptEnd = m_lstPoint.back();
- x = ptEnd.x() + 20;
- y = ptEnd.y() + 20;
- m_lstPoint.insert(m_lstPoint.end(), QPoint(x, y));
- }
- void CardinalPanel::startDraw()
- {
- m_bStartDraw = true;
-
- pushHeadAndTailPoint();
-
- update();
- }
-
- void CardinalPanel::mousePressEvent(QMouseEvent* event)
- {
- if ((Qt::LeftButton != event->button()))
- {
- return QWidget::mousePressEvent(event);
- }
-
- m_lstPoint.insert(m_lstPoint.end(), event->pos());
-
- update();
-
- QWidget::mousePressEvent(event);
- }
-
- // 画鼠标左键按下选中的点
- void CardinalPanel::drawPoint()
- {
- QPainter painter(this);
- painter.setBrush(QColor(Qt::red));
-
- const auto iPointSize = 8;
-
- // 先画鼠标左键按下选中的点
- auto nPointIndex = 0;
- for (auto iter = m_lstPoint.begin(); iter != m_lstPoint.end(); ++iter)
- {
- // 头部、尾部的两个控制点不绘制
- if (m_bStartDraw && ( (iter == m_lstPoint.begin()) || (*iter == m_lstPoint.back()) ))
- {
- continue;
- }
-
- painter.drawEllipse(*iter, iPointSize, iPointSize);
- }
- }
-
- // 画Cardinal曲线
- void CardinalPanel::drawCardinal()
- {
- if (m_lstPoint.size() < 4)
- {
- return;
- }
-
- QPainter painter(this);
- QPen pen(QColor(Qt::green), 6);
- painter.setPen(pen);
-
- path.clear();
-
- auto iter = m_lstPoint.begin();
- ++iter; // 第1个点(基于0的索引)
- path.moveTo(*iter);
- --iter;
-
- auto endIter = m_lstPoint.end();
- int nIndex = 0;
- while (true)
- {
- --endIter;
- ++nIndex;
- if (3 == nIndex)
- {
- break;
- }
- }
-
- for (; iter != endIter; ++iter)
- {
- auto& p0 = *iter;
- auto& p1 = *(++iter);
- auto& p2 = *(++iter);
- auto& p3 = *(++iter);
-
- --iter;
- --iter;
- --iter;
-
- vector
vtTempPoint; - vtTempPoint.push_back(p0);
- vtTempPoint.push_back(p1);
- vtTempPoint.push_back(p2);
- vtTempPoint.push_back(p3);
-
- //double value[4][1];
- for (auto i = 0; i < 4; ++i)
- {
- vtTempPoint[i] = m_dfMcMatrix[i][0] * p0 + m_dfMcMatrix[i][1] * p1 + m_dfMcMatrix[i][2] * p2 + m_dfMcMatrix[i][3] * p3;
- }
-
- double t3, t2, t1, t0;
- for (double t = 0.0; t < 1; t += 0.01)
- {
- t3 = t * t * t; t2 = t * t; t1 = t; t0 = 1;
- auto newPoint = t3 * vtTempPoint[0] + t2 * vtTempPoint[1] + t1 * vtTempPoint[2] + t0 * vtTempPoint[3];
- path.lineTo(newPoint);
- }
-
- }
-
- painter.drawPath(path);
- }
-
- void CardinalPanel::paintEvent(QPaintEvent* event)
- {
- drawPoint();
-
- // 再画Cardinal曲线
- if (m_bStartDraw)
- {
- drawCardinal();
- }
- }
运行效果如下:

可以看到当u值越大时,曲线越尖锐,当变为1时,就成了直线;越小越光滑。
如果想实现3D版的Cardinal曲线,请参考:osg实现三次样条Cardinal曲线