课程名称:计算机动画
实验项目名称:路径曲线与运动物体控制
实验日期:2020 年 9 月 27 日
一、 实验目的和要求
- 设计并实现一个路径曲线,通过不同参数控制曲线状态,并实现对物体沿生成路线运动的控制。
- 通过上述实验内容,了解动画动态控制的基本原理何方法,提高动画编程能力。
二、 实验内容和原理
- 选用 Cardinal 曲线表示运动路径,掌握它的表示和算法,了解不同控制参数对曲线形状和状态的影响。
- 编写代码实现 Cardinal 曲线算法,对照 cardinal 样条曲线的数学表示和程序之间的对应关系。
- 给定若干关键控制点的位置(这些控制点可以大致描述某个运动路径的形状),用上述程序计算出控制点之间的插值点,显示出样条曲线。
- 改变曲线弯曲程度的参数$τ∈[0,1]$大小和控制插值点数目的参数 grain ,观察曲线形状的变化。
- 在路径曲线上放置一小汽车,使其在沿生成的 cardinal 曲线运动,汽车速度和加速度可以调节。
三、 实验平台
Qt 5.14.2 @ Windows
四、 实验步骤
1. 首先,对照Cardinal 样条曲线的数学表达和程序中计算代码的对应关系。
Cardinal 样条曲线矩阵表示:
$$$ P(u) = U^T M B $$$
其中,u 是幂次最高为 3 的插值变量,且u∈[0,1], M 是 Hermite 多项式矩阵,B 是曲线中,用户指定的关键点数据。其矩阵展开表示。
其中,$P_i-1,P_i,P_i+1,P_i+2$,是用户指定的控制点控制点,参数τ 控制曲线的弯曲程度。
为了实现 Cardinal 样条曲线计算,创建 spline 类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| class spline { private: double *ax,*bx,*cx,*dx;//P(u)系数 double *ay,*by,*cy,*dy;//P(u)系数 double *A,*B,*C,*D,*E;//计算弧长所用系数 double* matrix[4];//计算矩阵 double tension;//参数τ int num;//关键点个数 int grain;//每两个关键点之间插值点的个数(含关键点) bool create_flag=false;//是否已经为指针分配空间(判断是否需要delete) vector<QPoint> all_points;//所有点
public: spline(); //生成CubicSpline曲线 void set_Spline(vector<QPoint>& vec,int _grain,double _tension); //计算生成的三次样条曲线上所有插值点 void CubicSpline(vector<QPoint>& vec); double calc_Total_length(); //计算曲线总长度
void init_Matrix(); //初始化矩阵 void init_spline_Coefficient(vector<QPoint>& vec);//计算P(u)系数
QPoint calc_Interpolation(int i,double u);//计算内部插值点 point calc_double_Interpolation(int i,double u);//计算内部插值点(坐标为double类型)
vector<QPoint>& get_all_points(); //返回储存所有插值点的vector
void init_length_Coefficient(int _num);//初始化长度计算参数 double f(int i,double u); //f函数 double simpson(int i,double a,double b);//求样条曲线长度 double calc_U(double s,int i,double u1,double u2);//根据长度计算参数u的值 void clear();//清除数据
~spline(){} };
|
其中 set_Spline 函数根据关键点数组 vec、插值点数目 _grain、和控制曲线弯曲程度的参数 _tension 生成样条曲线所需要的计算数据
(如矩阵数值,P(u)系数等)。具体算法见源码。
其中init_Matrix 函数计算矩阵中的数值,即为 P(u)公式中的矩阵M。init_spline_Coefficient 函数计算不同曲线段中的P(u)多项式系数,即为公式中的 M*B(分 x,y 两个方向计算)。具体算法见源码。
2. CubicSpline 函数.
根据_grain 值生成不同的u 值,并计算曲线上所有插值点的坐标,储存在名为 all_points 的 vector 中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| //计算曲线上所有插值点 void spline::CubicSpline(vector<QPoint>& vec) { //当没清除就再次点击生成曲线时,不清除之前的插值点,插入间隔点 QPoint temp(0,0); all_points.push_back(temp);
//根据设置的插值点个数参数,计算对应的u值 int num=vec.size(); double* u = new double[grain]; for (int i = 0; i<grain; i++) { u[i] = ((double)i) / grain; //u [0,1] }
//根据u值和曲线参数计算插值点坐标 for (int i = 0; i<num-1; i++) { QPoint p1=vec[i]; //加入关键点 all_points.push_back(p1); for (int j = 1; j<grain; j++) { QPoint temp=calc_Interpolation(i,u[j]); all_points.push_back(temp); } } //加入关键点 QPoint p1=vec[num-1]; all_points.push_back(p1);
delete []u; }
|
其中,calc_Interpolation 函数根据曲线段序号i,和参数 u 的不同值,计算具体一个插值点的坐标。
在paintWindow 类中编写鼠标回调函数mousePressEvent,记录通过鼠标交互选定的关键点。以及绘制函数 paintEvent,在每次update()时调用。paintWindow 类定义具体如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| class paintWindow : public QWidget { Q_OBJECT private: spline* sp; int grain; //每个曲线区间有多少个插值点(包括两端关键点) double tension; //参数,控制曲线弯曲程度
bool ifDrawInpoint=false; //是否显示插值点
int time;//时间 QTimer* timer; //计时器 QPixmap* car[5]; //储存小车位图信息
int car_index;//显示第几种小车 int pen_index=0;//使用第几种笔刷
double speed; //小车速度 double accelerate; //小车加速度
point now_point; //运动当前点 point next_point; //运动下一个点 double ratio; //旋转角度
vector<QPoint> points;//储存所有关键点
bool endflag=false; //小车是否运动到曲线末端
public: paintWindow();
void paintEvent(QPaintEvent *); //绘制函数 void mousePressEvent(QMouseEvent *e); //鼠标回调函数
void create_Spline(int _grain, double _tension); //生成并显示cubicspline曲线 void start_Move(double _speed,double _accelarate); //小车开始运动 void stop_Move(); //小车暂停运动 void continue_Move(); //小车继续运动
int numbers(); //关键点个数 double total_length(); //曲线总长度 double now_length(); //小车当前走过的路线长度 int get_spline_index(double now_len); //获取小车当前在哪一段曲线 double get_Ratio(); //获取当前曲线斜率
void change_car(); //改变小车 void change_pen(QPainter& paint); //改变笔刷 void change_DrawInPoint(); //改变是否绘制插值点的控制变量 QPixmap* now_car(); //当前小车位图指针
void clear(); private slots: void changeState(); //连接计时器,改变小车坐标,旋转角度等信息 };
|
4.
鼠标回调函数mousePressEvent,记录通过鼠标交互选定的关键点。
绘制函数 paintEvent,根据数据变化,绘制所有的关键点、曲线,已经选择是否绘制插值点。
5. 引入QTimer 类作为计时器.
每隔一段时间调用 changestate 函数,改变小车坐标及旋转角度等。其中now_point 是当前小车位置,next_point 是下一个小车位置。get_Ratio 函数计算当下曲线的斜率,以及小车旋转角度。
6. MainWindow 设计及按钮槽函数
MainWindow 窗口设计如下:
MainWindow 类设计:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| class MainWindow : public QMainWindow { Q_OBJECT private: paintWindow* p_w;
public: MainWindow(QWidget *parent = nullptr); ~MainWindow();
private slots: void on_create_clicked();//绘制曲线并显示 void on_clear_clicked();//清屏 void on_start_clicked();//开始运动按钮槽函数 void on_show_clicked();//展示插值点按钮槽函数 void on_stop_move_clicked();//停止运动按钮槽函数 void on_continue_move_clicked();//继续运动按钮槽函数
void update_numbers();//更新关键点数目 void on_change_clicked();
private: Ui::MainWindow *ui; };
|
五、 实验结果分析
选取控制点:
绘制曲线:同时显示关键点个数和路线总长度。
不同参数对曲线值的影响:
1、不同的 grain:grain=5(红色)和 grain=60(蓝色)
2、不同的tension(τ):分别为:
0(绿色),0.25(灰色),0.5(红色),0.75(蓝色),1.0(黄色)
显示插值点(白色为内部插值点):
小车开始运动:
更换小车: