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


 


   

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

开发环境

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[n].x;
    s->y = knots[n].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


  • 8
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
Cardinal曲线是一种平滑的曲线,常用于计算机图形学和计算机辅助设计中。它是通过一种插值方法生成的,用于在给定的控制点上创建平滑的曲线Cardinal曲线的生成基于插值原理,通过在给定的控制点上创建一个平滑的曲线。它利用控制点及其相关参数来计算曲线上的其他点。Cardinal曲线的特点是它的形状平滑,而且可以通过调整参数来控制曲线的形状。 Cardinal曲线有几个重要的参数,包括张力(tension)、断点(continuity)和偏移(bias)。张力参数控制曲线的平滑程度,增加张力值会使曲线更加平滑。断点参数用于设置曲线的顺滑度,较大的断点值会导致曲线控制点之间更加接近。偏移参数则控制曲线控制点处的形状。 Cardinal曲线的计算基于一种特定的公式,将控制点、参数和插值函数结合起来,生成平滑的曲线。该曲线控制点之间进行插值,通过计算每个插值点的坐标,从而生成曲线上的其他点。 需要注意的是,Cardinal曲线是一种插值曲线,它通过在控制点之间进行插值来创建曲线。因此,曲线上的点并不一定都是控制点,而是通过插值计算得到的。这使得Cardinal曲线具有更好的平滑性和连续性。 总结来说,Cardinal曲线是一种平滑的曲线生成方法,通过插值计算在给定的控制点上生成曲线。它具有可调参数和优秀的平滑性,是计算机图形学和计算机辅助设计中常用的曲线生成方法。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值