這是我在做多邊形圓角功能時需要用到貝塞爾曲線實現,所以學了一下。
本博客參考資源鏈接:
http://www.srcmini.com/43305.html
https://blog.csdn.net/u013935238/article/details/50012737
https://blog.csdn.net/u011283226/article/details/115420786
https://baike.baidu.com/item/%E4%BA%8C%E9%A1%B9%E5%B1%95%E5%BC%80%E5%BC%8F/7078006?fr=aladdin
bezier曲線在編程中的難點在於求取曲線的系數,如果系數確定了那么就可以用微小的直線段畫出曲線。bezier曲線的系數也就是bernstein系數,此系數的性質可以自行百度,我們在這里是利用bernstein系數的遞推性質求取:
簡單舉例
兩個點p0,p1 為一階曲線
系數為 (1-u)p0+u*p1; 將系數存在數組中b[0] =1-u,b[1]=u
三個點 p0 p1 p2 為二階曲線
系數(1-u)(1-u)p0+2u(1-u)p1+u*u*p2 可以看出二階的系數是一屆的系數的關系 ((1-u)+u)(b[0]+b[1])
注意:通過這個公式有沒有發現,當u==0的時候這個點就是p0,當u==1的時候這個點就是p2,其他時候點被p1所吸引,也就是p1點的存在會導致(u!=0&&u!=1)的時候生成的點靠近p1
四個點 三階曲線為
((1-u)+u)((1-u)+u)(b[0]+b[1])
是不是有種似曾相識的感覺,對了,這就是高中牛頓二項式((a+b)n=Σnr=0 Crnan-rbr)展開的過程
給出二階貝塞爾曲線實現代碼:
QPointF p0(0,0); QPointF p1(1000,0); QPointF p2(1000,1000); QPainterPath path; path.moveTo(p0); QPointF pTemp; for(double t=0; t<1; t+=0.01) //2次Bezier曲線
{ pTemp =pow((1-t),2)*p0+2*t*(1-t)*p1+pow(t,2)*p2; path.lineTo(pTemp); }
沒有使用貝塞爾曲線(三個點直接相連)畫出來三角形是這樣:
使用貝塞爾曲線之后,(1000,0)這個位置的角會圓化
上圖中你會發現曲線不太圓滑,這個你可以調參數precision,主要的問題是它用了貝塞爾曲線之后都不像一個三角形了,我們只想對三角形的角進行圓化。我們可以選擇構成三角形角的兩邊上接近交點位置的兩個點,用這個兩個點和這兩邊的交點(三角形的角)生成貝塞爾曲線,效果如下:
放大一下:
我們發現他就是有很多短小的曲線構成的,所以這就是多邊形的角圓化的原理。
上面是實現的二階貝塞爾曲線,但是有時候我們可能會使用其他階數曲線,所以我們需要改一下代碼使得代碼更大眾化:
/** * @brief createNBezierCurve 生成N階貝塞爾曲線點 * @param src 源貝塞爾控制點,里面有兩個點就是一階,有三個點就是二階,依次類推 * @param dest 目的貝塞爾曲線點 * @param precision 生成精度,控制着細小直線的長度,細小直線長度越小模擬出現的圓角越圓滑(此值越小細小直線長度越小) */
static void createNBezierCurve(const QList<QPointF> &src, QList<QPointF> &dest, qreal precision=0.5) { if (src.size() <= 0) return; //清空
QList<QPointF>().swap(dest); //外側循環控制(1-u)p0+u*p1中u的值,用來生成多個點 for (qreal t = 0; t < 1.0000; t += precision) { int size = src.size(); QVector<qreal> coefficient(size, 0); coefficient[0] = 1.000; qreal u1 = 1.0 - t; //里面循環用來生成每一次u改變之后的參數值,參數就是二項展開式,然后把參數和各頂點乘起來就得到貝塞爾曲線的一個頂點 for (int j = 1; j <= size - 1; j++) { qreal saved = 0.0; for (int k = 0; k < j; k++){ qreal temp = coefficient[k]; coefficient[k] = saved + u1 * temp; saved = t * temp; } coefficient[j] = saved; } //最后的貝塞爾頂點 QPointF resultPoint; for (int i = 0; i < size; i++) { QPointF point = src.at(i); resultPoint = resultPoint + point * coefficient[i]; } dest.append(resultPoint); } }
然后我來講講代碼如何實現把三角形的角圓化的:
/* src就是保存多邊形所有頂點的集合,要有序(有序的意思就是按照點的順序可以形成一個多邊形) dest就是一個空的集合,最后生成的所有點都放在里面,然后按照這些點依次連接最后就是一個角圓化之后的多邊形 */
void GeometryViewer::centralHandler(vector<CVector2d>&src, vector<CVector2d>&dest) { vector<CVector2d>tmp; for (int i = 0; i < src.size(); ++i) { //對於每一個多邊形頂點(角),我們需要找到構成這個頂點的兩條直線上接近頂點的兩個點,用這三個點生成貝塞爾曲線 CVector2d pt1 = getLineStart(src[i],src[(src.size() + i - 1) % src.size()]); tmp.push_back(pt1); tmp.push_back(src[i]); CVector2d pt3 = getLineStart(src[i], src[(i + 1) % src.size()]); tmp.push_back(pt3); createNBezierCurve(tmp, dest); tmp.clear(); } }
CVector2d類的功能大致如下:
class CVector2d { public: double X,Y; CVector2d(double x,double y):X(x),Y(y) { X=x; Y=y; printf("%lf 00**** %lf\n",x,y); } CVector2d operator+(CVector2d y)const { return CVector2d(X+y.X,Y+y.Y); } };
getLineStart它將返回一個點, 該點是pt1頂點朝着pt2頂點離開m_uiRadius像素。變量fRat保持半徑與第i個線段長度之間的比率。還有一項檢查可以防止fRat的值超過0.5。如果fRat的值超過0.5, 則兩個連續的圓角將重疊, 這將導致較差的視覺效果。
當從點P1到點P2直線行駛並完成距離的30%時, 我們可以使用公式0.7•P1 + 0.3•P2確定位置。通常, 如果我們獲得完整距離的一小部分, 並且α= 1表示完整距離, 則當前位置為(1-α)•P1 +α•P2。
這就是GetLineStart方法確定在第(i + 1)方向上距離第i個頂點m_uiRadius像素的點的位置的方式。
CVector2d GeometryViewer::getLineStart(CVector2d pt1,CVector2d pt2,double radius=0.0) { CVector2d pt; double fRat; if(radius==0) fRat = 0.02; else fRat = radius / getDistance(pt1, pt2); if (fRat > 0.5f) fRat = 0.5f; pt.X = (1.0f - fRat)*pt1.X + fRat*pt2.X; pt.Y = (1.0f - fRat)*pt1.Y + fRat*pt2.Y; return pt; }
//歐幾里得距離
double getDistance(CVector2d pt1, CVector2d pt2) { double fD = (pt1.X - pt2.X)*(pt1.X - pt2.X) + (pt1.Y - pt2.Y) * (pt1.Y - pt2.Y); return sqrt(fD); }
補充:
Qt有內置的畫貝塞爾曲線的方法,但是只能畫一階和二階曲線
QPainter類無法繪制二次Bezier曲線。雖然很容易按照等式(1)從頭開始實現它, 但Qt庫確實提供了更好的解決方案。還有一個用於2D繪圖的強大類:QPainterPath。 QPainterPath類是直線和曲線的集合, 可以將其添加和以后與QPainter對象一起使用。有一些重載方法可將Bezier曲線添加到當前集合。特別是, 方法quadTo將添加二次Bezier曲線。曲線將從當前的QPainterPath點(P0)開始, 而P1和P2必須作為參數傳遞給quadTo。