模拟PS的钢笔路径

大三的时候上计算机图形学没认真听过课,书也不喜欢看,一直不懂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曲线:

image

这样想其实挺简单的,于是决定实现之。之前看了一点代码大全和设计模式,原本想在这里小试牛刀,结果还是学艺不精。代码写着写着,又乱了。

大概的设计是这样的,定义了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,虽然开了抗锯齿,还是不够平滑:

image

如果用QT自带的path来绘制,就平滑多了

image

还有很多地方需要改进,目前虽然支持拖移控制点,但锚点两端的控制点始终是对称的。删除锚点还没写,不支持曲线闭合。至于角拖移有点不想写,我不喜欢零阶连续= =|||。。如果有时间,我想试一试用锚点的权值来修改曲线的粗细。

 

本文原创,转载请著名出处:

http://www.cnblogs.com/luluathena/

posted @ 2010-09-16 19:09  筱夏  阅读(1081)  评论(1编辑  收藏  举报