您的位置:首页 > 理论基础

[计算机动画] 路径曲线与运动物体控制(Cardinal样条曲线)

2016-10-05 00:58 791 查看
 

   


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

开发环境

Qt 5.7

Hermite曲线

        Hermite曲线从一个点到另一个点利用Hermite插值产生一个三次多项式。

        如果想要在两点之间得到Hermite曲线,我们还需要给出两个点处曲线的切线斜率。

       


        Hermite曲线是由四个基函数组成的:

        h1(u) = 2u^3 – 3u^2 + 1

         h2(u)= -2u^3 + 3u^2

         h3(u)= u^3 – 2u^2 + u

         h4(u)= u^3 - u^2

       


        写成矩阵形式,如下:

       


        当然,我们需要注意到这仅仅是两个点之间的曲线。而对于多个点生成连续曲线的话,我们需要保证第一段曲线的末端和第二段曲线的初端,同时,斜率也要相等。可以看出,指明所有切线是比较麻烦的一件事,毕竟它不是像控制点一样那么直观的东西。


Cardinal曲线

        在这个基础上,我们考虑这样一个问题:能不能绕开切线,换句话说,让程序自动生成切线。

        为了得到一个点的切线,我们需要利用到这个点前后两个点来辅助构造:

        P’i=τ*( Pi+1
- Pi-1 ) (*)

        上式中,τ最好在(0,1)之间取值,它的值越大,曲线就越弯曲,反之则越接近直线。

        在这样一个想法下,我们把(*)式带入原矩阵,得到了Cardinal曲线的矩阵形式:

       


        在τ = 1/2 时,曲线被称为CatMull曲线。

        这样一来,一个曲线实际上是由四个点控制的,那么我们马上就能想到——最边界的点是没有邻居点的,所以为了算法正确运行,我们需要加入虚拟点。简单来说,我们可以直接把首部点复制作为首部虚拟点,尾部复制作为尾部虚拟点。

        我们需要明确一点,我们最终得到的实际上是一个分段函数,每两个控制点之间为一段,一共有n + 2 个控制点(含虚拟点),和 n - 1条曲线。我们得到的曲线是参数方程,u是参数,而不是普通的函数表达式。

        换言之,我们可以这样认识Cardinal曲线:

        X(u,i) = Ax(i) * u^3 + Bx(i) * u^2 + Cx(i) * u + Dx(i)

        Y(u,i) = Ay(i) * u^3 + By(i) * u^2 + Cy(i) * u + Dy(i)

         其中,u是参数,它的取值在[0,1],i是段数,它的取值在[0,n-2],A,B,C,D是不同段数的系数,这是一个参数方程,也是三次多项式。

构建Cardinal曲线

        为了生成Cardinal曲线,我们只需给出控制点,弯曲程度,插值点个数。

        1)得到控制点作为输入信息

        2)在两端加入虚拟点

        3)计算得到基矩阵M(见之前给出的矩阵)

        4)计算出每段曲线的A,B,C,D系数(矩阵M中每一行与控制点列向量相乘对应一个系数)

        5)根据插值点个数计算出每段曲线中,插值参数u的值,并计算出对应的X,Y值

        6)把所有的插值点用直线连接起来

控制小车的速度

        为了控制小车的速度,也就是要让小车在特定时间移动到特定的距离。

        为了计算距离,我们需要在给定弧长的情况下,得到对应的u值。

        我们可以根据参数方程,得到曲线的弧长公式:

       


        其中:

       


 

        这里的ax,bx,cx,ay,by,cy和Cardinal曲线的系数是对应的。(忽略az,bz,cz,因为我们并没有考虑三维空间)

        我们用数值分析的方法解决这个问题,也就是二分法:

        简言之,就是先从u在[0,1]中开始,计算u=1/2处弧长,如果实际弧长大于1/2处,则在[1/2,1]中继续计算,否则在[0,1/2]中继续计算。按此递归下去,直到实际结果与预期的误差小于某一精度。

        看起来很简单。但是需要考虑两个问题:

        1)我们的曲线方程是分段曲线,所以我们首先要计算出每段曲线的长度,然后判断我们预期长度属于哪个片段,然后截取预期长度在该片段中的长度,再进入递归计算。

        2)我们还需要实现根据参数u,计算对应弧长的方法。

         后者本质上就是求一个积分(在前面已经给出了)我们可以采用simpson方法,将其展开成n个区间(偶数),然后求和。

       


spline.cpp // 样条曲线

paintWindow.cpp //绘制窗口

window.cpp //主窗口

main.cpp //入口

代码

(明天会加入注释)

spline.h
#ifndef SPLINE_H
#define SPLINE_H

class point
{
public:
float x;
float y;
void setPoint(float _x, float _y);
};

class spline
{
private:
float *a[2],*b[2],*c[2],*d[2];//每段Spline曲线的参数
float *A, *B, *C, *D, *E;
float m[16];//矩阵M
point* knots;
point* Spline;
int grain;
int n;
int count;
float tension;
void CubicSpline();
void initLength();
void GetCardinalMatrix();
float f(int j,float x);

float Matrix(int i,int j,float u);
void init(int i,int j,float a0, float b0, float c0, float d0);
float simpson(int j,float x,float y);
public:
spline(point* p, int _n, int _grain, float _tension);
~spline();
void print();
float getX(int i);
float getY(int i);
float getXFromU(int i,float u);
float getYFromU(int i,float u);
float getLen(int i,float u);

float getU(int i,float s,float u1,float u2);
int size();
};

#endif // SPLINE_H


paintWindow.h
#ifndef PAINTWINDOW_H
#define PAINTWINDOW_H

#include"spline.h"
#include<QWidget>
#include<vector>
class QTimer;
class paintWindow : public QWidget
{
Q_OBJECT
private:
spline* s;
std::vector<point>vec;
int size;
QTimer* timer;
class car_t{
public:
QPixmap* p[3];
float speed;
float acce;
float getLen(int t);
}car;
float totalLen;
float radio;
float x1,y1,x2,y2;
bool isFirst;
float* length;
int index;
int time;
void setPoint(float x,float y);
int getSec(float s);
float getRatio();
float getRatio(int i,int j);
public:
paintWindow(QWidget *parent = 0);
~paintWindow();
void setSpline(int grain, float tension);
void setCar(float speed,float acce);
void clear();
float getTotalLen();
void startMove();
protected:
void paintEvent(QPaintEvent *);
void mousePressEvent(QMouseEvent *);
private slots:
void changeState();
};

#endif // WINDOW_H


window.h
#ifndef WINDOW_H
#define WINDOW_H

#include "paintWindow.h"
class QPushButton;
class QLabel;
class QHBoxLayout;
class QVBoxLayout;
class QLineEdit;

class window : public QWidget
{
Q_OBJECT
private:
QWidget* menuWindow;
QHBoxLayout* hlayout[5];
QVBoxLayout* vlayout;
paintWindow* w;

QLineEdit* grainLine;
QLineEdit* tensionLine;
QLineEdit* speedLine;
QLineEdit* acceLine;

QLabel* lenLabel;
QLabel* grainLabel;
QLabel* tensionLabel;
QLabel* speedLabel;
QLabel* acceLabel;

QPushButton* genButton;
QPushButton* startButton;
QPushButton* clearButton;

void layout();
public:
window(QWidget* parent = 0);
private slots:
void updatePaintWindow();
void clear();
void start();
};

#endif // WINDOW_H


spline.cpp
#include "spline.h"
#include<math.h>

void point::setPoint(float _x, float _y)
{
x = _x;
y = _y;
}

void spline::initLength()
{
A = new float[n-1];
B = new float[n-1];
C = new float[n-1];
D = new float[n-1];
E = new float[n-1];
for(int i=0;i<n-1;i++){
A[i] = 9*(a[0][i]*a[0][i]+a[1][i]*a[1][i]);
B[i] = 12*(a[0][i]*b[0][i]+a[1][i]*b[1][i]);
C[i] = 6*(a[0][i]*c[0][i]+a[1][i]*c[1][i]) + 4*(b[0][i]*b[0][i]+b[1][i]*b[1][i]);
D[i] = 4*(b[0][i]*c[0][i]+b[1][i]*c[1][i]);
E[i] = c[0][i]*c[0][i]+c[1][i]*c[1][i];
}
}

float spline::f(int i,float x)
{
return sqrt(((((A[i]*x+B[i])*x)+C[i])*x+D[i])*x+E[i]);
}

float spline::simpson(int j,float x,float y)
{

const int n = 10;
const float h = (y - x)/n;
float ans = 0.0f;
for(int i=1;i<=n-1;i++){
if(i%2){
ans += 4*f(j,x+1.0f*i/n*(y-x));
}
else ans += 2*f(j,x+1.0f*i/n*(y-x));
}
ans += f(j,x) + f(j,y);
ans *= h/3;

return ans;
}

//第i段,u
float spline::getLen(int i,float u)
{
return simpson(i,0,u);
}

float spline::getU(int i,float s,float u1,float u2)
{
float ms = getLen(i,(u1+u2)/2);
if(ms-s>-1.0f && ms-s<1.0f){
return (u1+u2)/2;
}
else if(ms > s)return getU(i,s,u1,(u1+u2)/2);
else if(ms < s)return getU(i,s,(u1+u2)/2,u2);
}

spline::spline(point* p, int _n, int _grain, float _tension)
{
n = _n;
grain = _grain;
tension = _tension;

knots = new point[n + 2];
for (int i = 1; i<=n; i++) {
knots[i].x = p[i-1].x;
knots[i].y = p[i-1].y;
}
knots[0].x = p[0].x;
knots[0].y = p[0].y;
knots[n + 1].x = p[n - 1].x;
knots[n + 1].y = p[n - 1].y;
Spline = new point[(n-1)* grain + 1];

a[0] = new float[n-1];
b[0] = new float[n-1];
c[0] = new float[n-1];
d[0] = new float[n-1];

a[1] = new float[n-1];
b[1] = new float[n-1];
c[1] = new float[n-1];
d[1] = new float[n-1];

count = 0;
CubicSpline();
initLength();
}

int spline::size()
{
return (n-1)*grain + 1;
}

float spline::getX(int i)
{
return Spline[i].x;
}

float spline::getY(int i)
{
return Spline[i].y;
}

void spline::CubicSpline()
{
point *s, *k0, *kml, *k1, *k2;
int i, j;
float* u = new float[grain];
GetCardinalMatrix();
for (i = 0; i<grain; i++) {
u[i] = ((float)i) / grain;//u [0,1]
}
s = Spline;
kml = knots;
k0 = kml + 1;
k1 = k0 + 1;
k2 = k1 + 1;
for (i = 0; i<n-1; i++) {
init(0,i,kml->x,k0->x,k1->x,k2->x);
init(1,i,kml->y,k0->y,k1->y,k2->y);
for (j = 0; j<grain; j++) {
s->x = Matrix(0, i, u[j]);
s->y = Matrix(1, i, u[j]);
s++;
}
k0++, kml++, k1++, k2++;
}
s->x = knots
.x;
s->y = knots
.y;
delete u;
}

void spline::print()
{
for (int i = 0; i < grain*(n-1)+1; i++) {
if (i%grain == 0)printf("\n");
printf("%f %f\n", Spline[i].x, Spline[i].y);
}
}

void spline::GetCardinalMatrix()
{
float a1 = tension;
m[0] = -a1, m[1] = 2 - a1, m[2] = a1 - 2, m[3] = a1;
m[4] = 2 * a1, m[5] = a1 - 3, m[6] = 3 - 2 * a1, m[7] = -a1;
m[8] = -a1, m[9] = 0, m[10] = a1, m[11] = 0;
m[12] = 0, m[13] = 1, m[14] = 0, m[15] = 0;
}

void spline::init(int i,int j,float a0, float b0, float c0, float d0)
{
a[i][j] = m[0] * a0 + m[1] * b0 + m[2] * c0 + m[3] * d0;
b[i][j] = m[4] * a0 + m[5] * b0 + m[6] * c0 + m[7] * d0;
c[i][j] = m[8] * a0 + m[9] * b0 + m[10] * c0 + m[11] * d0;
d[i][j] = m[12] * a0 + m[13] * b0 + m[14] * c0 + m[15] * d0;
}

//i为0:X,i为1:Y
//u
float spline::Matrix(int i, int j,float u)
{
return(d[i][j] + u*(c[i][j] + u*(b[i][j] + u*a[i][j])));
}

float spline::getXFromU(int i,float u)
{
return Matrix(0,i,u);
}

float spline::getYFromU(int i,float u)
{
return Matrix(1,i,u);
}

spline::~spline()
{
delete[] knots;
delete[] Spline;
}


paintWindow.cpp
#include"paintWindow.h"
#include<QMouseEvent>
#include<QPainter>
#include<QPixmap>
#include<paintWindow.h>
#include<cmath>
#include<QTimer>
#include<qDebug>

//可以用累加法
float paintWindow::car_t::getLen(int t)
{
return speed*t + acce*t*t/2;
}

paintWindow::paintWindow(QWidget *parent):
QWidget(parent)
{
s = NULL;
size = 0;

isFirst = true;
index = 0;
time = 0;

x1 = y1 = x2 = y2 = -1;
car.p[0] = new QPixmap;
car.p[1] = new QPixmap;
car.p[2] = new QPixmap;

car.p[0]->load("1.png");
car.p[1]->load("2.png");
car.p[2]->load("3.png");

timer = new QTimer();
connect(timer,SIGNAL(timeout()),this,SLOT(changeState()));
}

paintWindow::~paintWindow()
{
//delete[] p;
}

int paintWindow::getSec(float s)
{
float len = length[0];
if(s<len)return 0;
for(int i=1;i<vec.size()-1;i++){
if(s>len && s<len+length[i]){
return i;
}
len += length[i];
}
}

void paintWindow::changeState()
{
float len = car.getLen(time);
index = (index+1)%3;
if(len>totalLen||len<0){
time = 0;
x1 = y1 = x2 = y2 = -1;
timer->stop();
return;
}
time ++;
int sec = getSec(len);
for(int i=0;i<sec;i++){
len -= length[i];
}

float u = s->getU(sec,len,0,1);
if(isFirst){
x1 = s->getXFromU(sec,u);
y1 = s->getYFromU(sec,u);
isFirst = false;
}
else{
x2 = s->getXFromU(sec,u);
y2 = s->getYFromU(sec,u);
isFirst = true;
}
update();
}

void paintWindow::startMove()
{
index = 0;
time = 0;
timer->start(100);//fps:10
}

float paintWindow::getTotalLen()
{
totalLen = 0.0f;
for(int i=0;i<vec.size()-1;i++){
length[i] = s->getLen(i,1.0f);
totalLen += length[i];
}
return totalLen;
}

void paintWindow::setCar(float speed,float acce)
{
car.acce = acce;
car.speed = speed;
}

void paintWindow::setSpline(int grain, float tension)
{
length = new float[vec.size()-1];
if(!timer->isActive())index = 0;
if(!s)delete s;
if(vec.size()==0)return;
point* p = new point[vec.size()];
for(int i=0;i<vec.size();i++){
p[i].x = vec[i].x;
p[i].y = vec[i].y;
}
s = new spline(p,vec.size(),grain,tension);
size = s->size();
}

void paintWindow::clear()
{
size = 0;
vec.clear();
delete s;
time = 0;
index = 0;
timer->stop();
x1 = y1 = x2 = y2 = -1;
update();
}

float paintWindow::getRatio(int i,int j)
{
const float pi = 3.14159;
double tan = (s->getY(j)-s->getY(i))/(s->getX(j)-s->getX(i));
double theta = atan(tan);
return theta/(2*pi)*360;
}

float paintWindow::getRatio()
{
const float pi = 3.14159;

if((x2-x1)<0.01f&&(y2-y1)<0.01f){
return radio;
}
if(x2-x1==0)return 0;
float tan = (y2-y1)/(x2-x1);
float theta = atan(tan);
return radio = theta/(2*pi)*360;
}

void paintWindow::paintEvent(QPaintEvent *)
{

QPainter paint(this);
if(size>0){
float ratio;
if(x1==-1||x2==-1||y1==-1||y2==-1){
ratio = (s->getY(1)-s->getY(0))/(s->getX(1)-s->getX(0));
}
else ratio = getRatio();
if(isFirst)paint.translate(x1,y1);
else paint.translate(x2,y2);
paint.rotate(ratio);
paint.drawPixmap(-90,-90,180,90,*car.p[index%3]);

paint.rotate(-ratio);

if(isFirst)paint.translate(-x1,-y1);
else paint.translate(-x2,-y2);
}
paint.setBrush(QBrush(Qt::black,Qt::SolidPattern));//设置画刷形式
for(int i=0;i<size-1;i++){
paint.drawLine(s->getX(i),s->getY(i),
s->getX(i+1),s->getY(i+1));
}
for(int i=0;i<vec.size();i++){
paint.drawEllipse(vec[i].x,vec[i].y,5,5);
}
}

void paintWindow::mousePressEvent(QMouseEvent *e)
{
float x = e->pos().x();
float y = e->pos().y();
point p;
p.setPoint(x,y);
vec.push_back(p);
update();
}


window.cpp
#include"window.h"
#include<QPushButton>
#include<QLabel>
#include<QHBoxLayout>
#include<QVBoxLayout>
#include<QLineEdit>

window::window(QWidget* parent):QWidget(parent)
{
layout();
}

void window::layout()
{
w = new paintWindow();

menuWindow = new QWidget();

grainLabel = new QLabel("grain");
tensionLabel = new QLabel("tension");
speedLabel = new QLabel("speed");
acceLabel = new QLabel("accelerate");
lenLabel = new QLabel("total length:");

genButton = new QPushButton("生成轨迹");
startButton = new QPushButton("开始运动");
clearButton = new QPushButton("清空");

startButton->setDisabled(true);
grainLine = new QLineEdit();
tensionLine = new QLineEdit();
speedLine = new QLineEdit();
acceLine = new QLineEdit();

connect(genButton,SIGNAL(clicked()),this,SLOT(updatePaintWindow()));
connect(clearButton,SIGNAL(clicked()),this,SLOT(clear()));
connect(startButton,SIGNAL(clicked()),this,SLOT(start()));

grainLine->setText("20");
tensionLine->setText("0.5");
speedLine->setText("10");
acceLine->setText("0");
for(int i=0;i<5;i++){
hlayout[i] = new QHBoxLayout;
}
vlayout = new QVBoxLayout;

hlayout[0]->addWidget(grainLabel);
hlayout[0]->addWidget(grainLine);

hlayout[1]->addWidget(tensionLabel);
hlayout[1]->addWidget(tensionLine);

hlayout[3]->addWidget(speedLabel);
hlayout[3]->addWidget(speedLine);

hlayout[4]->addWidget(acceLabel);
hlayout[4]->addWidget(acceLine);

vlayout->addLayout(hlayout[0]);
vlayout->addLayout(hlayout[1]);
vlayout->addLayout(hlayout[3]);
vlayout->addLayout(hlayout[4]);
vlayout->addWidget(lenLabel);

vlayout->addWidget(genButton);
vlayout->addWidget(startButton);
vlayout->addWidget(clearButton);

menuWindow->setFixedWidth(200);
menuWindow->setLayout(vlayout);

resize(900,900);
hlayout[2]->addWidget(menuWindow);
hlayout[2]->addWidget(w);

setLayout(hlayout[2]);
}

void window::updatePaintWindow()
{
startButton->setDisabled(false);
w->setSpline(grainLine->text().toInt(),tensionLine->text().toFloat());
lenLabel->setText("total length:\n" + QString("%1").arg(w->getTotalLen()));
w->update();
}

void window::clear()
{
w->clear();
startButton->setDisabled(true);
}

void window::start()
{
w->setCar(speedLine->text().toFloat(),acceLine->text().toFloat());
w->startMove();
}


main.cpp
#include "window.h"
#include <QApplication>

int main(int argc, char *argv[])
{
QApplication a(argc, argv);

window w;
w.show();

return a.exec();
}


qwq不要用我的图呜哇


1.png



2.png



3.png

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: