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

来源:互联网 发布:windows组件向导 编辑:程序博客网 时间:2024/05/29 04:26

 


   

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

开发环境

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_Hclass 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_OBJECTprivate:    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_OBJECTprivate:    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段,ufloat 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//ufloat 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


1 0
原创粉丝点击