大三的时候上计算机图形学没认真听过课,书也不喜欢看,一直不懂Bezier曲线。这两天突然想起来,决定再看看书。
其实是挺简单的公式,每个点的位置P(u)(0<=u<=1)是通过每个控制点Pk(k是控制点的索引从0到n,总共n+1个控制点)与相应的混合函数BEZ k,n(u)的乘积累加得到的。而一般的应用都是四个控制点的三次方程,保证曲线穿过第1和第4个控制点。
既然知道了控制点确定为4个,就可以得到BEZ公式:
BEZ 0,3 = (1-u)^3
BEZ 1,3 = 3u((1-u)^2)
BEZ 2,3 = 3(u^2)(1-u)
BEZ 3,3 = u^3
大三写课设的时候给的参考程序看不懂,不明白为什么明明是描述的是四个控制点的Bezier曲线,却可以绘制无数个控制点来生成Bezier曲线。现在想想,那只是对三次Bezier曲线的拼接罢了,保证两条曲线之间的零阶连续。
这样看过后,突然想知道photoshop的钢笔是怎样实现的,思考了一会儿,发现两个锚点和它们的控制点便构成了一段三次Bezier曲线,如果没有进行角拖移,那么曲线保证一阶连续。下面便是一段三次Bezier曲线:
这样想其实挺简单的,于是决定实现之。之前看了一点代码大全和设计模式,原本想在这里小试牛刀,结果还是学艺不精。代码写着写着,又乱了。
大概的设计是这样的,定义了CtrlPoint类,只包含位置信息及相关操作,还有绘制函数,然后锚点EndPoint类和控制点PathPoint类继承自CtrlPoint,一个锚点包含两个指向控制点的指针和两个曲线ID,而一个控制点包含一个指向冒险的指针。
class CtrlPoint { public: CtrlPoint(void); ~CtrlPoint(void); void SetPosition(const Point& p); void SetPosition(Real x,Real y); Point GetPosition(){return position;} virtual void Draw(QPainter* painter)=0; protected: Point position; }; typedef CtrlPoint* CPPointer; class EndPoint: public CtrlPoint { public: EndPoint(void); ~EndPoint(void); virtual void Draw(QPainter* painter); CPPointer GetPathPoint(int index){return pathPoints[index];} int GetCurve(int index){return curveID[index];} void SetPathPoint(CPPointer p, int index); void SetCurve(int cID,int index){curveID[index] = cID;} void MovePathPoint(CPPointer p, const Point& movement); void MoveEndPoint(const Point& movement); private: int curveID[2]; CPPointer pathPoints[2]; }; class PathPoint: public CtrlPoint { public: PathPoint(void); ~PathPoint(void); virtual void Draw(QPainter* painter); void SetEndPoint(CPPointer p); CPPointer GetEndPoint(){return theEndPoint;} private: CPPointer theEndPoint; };
而曲线类包含4个控制点的指针,以及30个采样点,以及一些计算信息;绘制函数可以在采样点绘制和QtPath之间切换:
class Curve { public: Curve(void); ~Curve(void); void Draw(QPainter* painter, bool useQtPath); void SetControlPoint(CPPointer p, int index); void GenerateAllPoint(); private: static Real paraC[]; static int pointsNum; CPPointer ctrlPoints[4]; vector<Point> allPoints; };
Bezier曲线的计算:
void Curve::GenerateAllPoint() { Real u; allPoints[0] = ctrlPoints[0]->GetPosition(); allPoints[pointsNum-1] = ctrlPoints[3]->GetPosition(); for(int i=1;i<pointsNum-1; i++) { u = Real(i)/Real(pointsNum-1); allPoints[i].SetZero(); for(int j=0;j<4;j++) { Real bez = paraC[j]*pow(u,j)*pow(1-u,3-j); allPoints[i].x += ctrlPoints[j]->GetPosition().x * bez; allPoints[i].y += ctrlPoints[j]->GetPosition().y * bez; } } }
代码量不多,其实上面的很快就写好了,只是UI和操作花了一些时间。QT用的不熟,起初QPainter是类成员,结果一直没办法绘制到窗口,后来把QPainter的声明写在了paintEvent()里,就可以绘制了。至今也不明白为什么,如果有哪位大侠知道,希望能指点一二。。。
鼠标左键是添加锚点,Alt+鼠标左键拖移控制点,Ctrl+鼠标左键拖移锚点。
这是效果图,Bezier曲线是自己计算的,采样数30,虽然开了抗锯齿,还是不够平滑:
如果用QT自带的path来绘制,就平滑多了
还有很多地方需要改进,目前虽然支持拖移控制点,但锚点两端的控制点始终是对称的。删除锚点还没写,不支持曲线闭合。至于角拖移有点不想写,我不喜欢零阶连续= =|||。。如果有时间,我想试一试用锚点的权值来修改曲线的粗细。
本文原创,转载请著名出处: