Qt实现三次样条Cardinal曲线

目录

1. 前言

2. 预备知识

3. 代码实现

4. 附录


1. 前言

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

2. 预备知识

       关于Hermite曲线、Cardinal曲线的数学理论,参见如下博文:

3. 代码实现

如下为用Qt实现的Cardinal曲线

Cardinal.h

#pragma once

#include <QtWidgets/QWidget>
#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 <QWidget>
#include "ui_CardinalPanel.h"
#include<vector>
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<QPoint> m_lstPoint;
	QPainterPath path;
};

CardinalPanel.cpp

#include "CardinalPanel.h"
#include<QMouseEvent>
#include<QPainter>
#include <QPainterPath>
#include<vector>
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<QPoint>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时,就成了直线;越小越光滑。

4. 附录

 如果想实现3D版的Cardinal曲线,请参考:osg实现三次样条Cardinal曲线

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值