天天看点

三阶贝塞尔曲线选点_用三阶贝塞尔曲线拟合圆

前言

由于贝塞尔曲线控制简便且具有极强的描述能力,它常被用来生成复杂的平滑曲线;圆形是一种很常用的普通图形,在计算机图形学中也有很多画圆的算法,本文想探究一下如何用三阶贝塞尔曲线拟合圆形。

在研究这个问题时,我从Stackoverflow上搜到了(4/3)tan(π/(2n))这个公式,她的几何意义如下图所示。该公式的值表达的具体意义可以描述为:由n段三阶贝塞尔曲线拟合圆形时,曲线端点到该端点最近的控制点的最佳距离是(4/3)tan(π/(2n))。

三阶贝塞尔曲线选点_用三阶贝塞尔曲线拟合圆

一开始看到这个值觉得很奇怪,想知道她是如何被推导出来的,于是花了一点功夫去调查,并自己求解证明,原来是一堆中学生就会的平面几何运算题。下面给出我的求解过程。

求解魔法数值

大家把这个用三阶贝塞尔曲线拟合圆形的数值叫做魔法数,可能是因为她的不同取值会影响到拟合的圆形的效果,这个值决定了贝塞尔曲线拟合圆形的误差。我通过在上面的几何图中添加辅助线、运用平面几何性质来求解该魔法数值。

辅助线及点位置说明

三阶贝塞尔曲线选点_用三阶贝塞尔曲线拟合圆

如上图命名各点,点O是圆心,P0、P3分别是圆弧(也是贝塞尔曲线)上的起点和终点,P1、P2是贝塞尔曲线的两个控制点, 点M2是线段P1P2的中点,C1、C2分别是线段P0P1和P2P3的中点,连接OM2并延长与P0P1的延长线交于点F1,过点P1作线段P0P3的垂线交于点F0。

点M1是线段C1M2和C2M2的中点的连线的中点,根据贝塞尔曲线上点的性质可知点M1是圆弧上的点,且是圆弧P0P3的中点,线段P0P3与OF1交于点M0.

证明及推算

一个隐含的条件(或者根据贝塞尔曲线的数学性质可证明的)是|P2P3|=|P0P1|,我们的目标就是要求出这个长度值,记为l(小写L)。

易知圆弧P0P3被射线OF1对称平分,且OF1与线段P0P3、C1C2和P1P2均垂直相交,又因为点C1、C2和M2是相关线段的中点,容易证明|M1M2| = |M0M2|/4,|P1F0| = |M0M2|,所以|M0M1| = (3/4)|M0M2| = (3/4)|P1F0|。

记|P1F0| = d,圆弧半径为r,∠P0-O-P3为θ,则∠P0-O-M0 = θ/2,|M0M1| = (3/4)d,|OM0| = |OM1| - |M0M1| = r - (3/4)d = r·cos(θ/2),整理等式为

(3/4)d = r - r·cos(θ/2) ····················· ①

因为线段P0P1与圆弧相切于点P0,OP0是圆弧的半径,容易证明∆P0-F0-P1与∆O-M0-P0相似,∠P1-P0-F0 = θ/2,则

d = l·sin(θ/2) ································ ②

由方程①②联立解得 l = (4/3)·r·(1-cos(θ/2))/sin(θ/2)

若圆弧半径为1,再由下面的三角函数二倍角公式推导

sin2α = 2·sinα·cosα

cos2α = (cosα)^2 - (sinα)^2 = 1 - 2·(sinα)^2

得到 l = (4/3)tan(θ/4),即是上面图中的值(4/3)tan(π/(2n))

用贝塞尔方程求解魔法数

上面用几何运算的方式求解了魔法数,还可以直接根据贝塞尔曲线方程代入特殊点坐标计算该数值。

用三阶贝塞尔曲线拟合圆形的问题可以简化为考虑拟合1/4圆弧,如下图圆弧P0P3即是端点为P0、P3,控制点为P1、P2的贝塞尔曲线,它们的坐标分别为P0 = (0,1), P1 = (h,1), P2 = (1,h), P3 = (1,0)

三阶贝塞尔曲线选点_用三阶贝塞尔曲线拟合圆

我们知道三阶贝塞尔曲线的一般方程如下

三阶贝塞尔曲线选点_用三阶贝塞尔曲线拟合圆

把上面的点坐标分别代入曲线方程,取t=0.5计算得到点坐标

三阶贝塞尔曲线选点_用三阶贝塞尔曲线拟合圆

另外根据贝塞尔曲线的数学性质可知曲线方程中t=0.5时的点一定在圆弧上,根据圆形方程定义,可得到下面的等式

三阶贝塞尔曲线选点_用三阶贝塞尔曲线拟合圆

这样,容易解出h的值为 h=(4/3)(sqrt(2)-1) ≈ 0.552284749831

用贝塞尔曲线画圆关键代码

……

private Paint mPaint;

private Path path = new Path();

private float[] mData = new float[8]; // 顺时针记录绘制圆形的四个数据点

private float[] mCtrl = new float[16]; // 顺时针记录绘制圆形的八个控制点

private static final float C = 0.552284749831f; // 用来计算绘制圆形贝塞尔曲线控制点的位置的常数

private void initData() {

mPaint = new Paint();

mPaint.setColor(Color.BLACK);

mPaint.setStrokeWidth(8);

mPaint.setStyle(Paint.Style.STROKE);

mPaint.setTextSize(60);

// 初始化数据点

mData[0] = 0;

float mCircleRadius = 200; //圆半径

mData[1] = mCircleRadius;

mData[2] = mCircleRadius;

mData[3] = 0;

mData[4] = 0;

mData[5] = -mCircleRadius;

mData[6] = -mCircleRadius;

mData[7] = 0;

// 初始化控制点

float mDifference = mCircleRadius * C; //圆形的控制点与数据点的差值

mCtrl[0] = mData[0]+ mDifference;

mCtrl[1] = mData[1];

mCtrl[2] = mData[2];

mCtrl[3] = mData[3]+ mDifference;

mCtrl[4] = mData[2];

mCtrl[5] = mData[3]- mDifference;

mCtrl[6] = mData[4]+ mDifference;

mCtrl[7] = mData[5];

mCtrl[8] = mData[4]- mDifference;

mCtrl[9] = mData[5];

mCtrl[10] = mData[6];

mCtrl[11] = mData[7]- mDifference;

mCtrl[12] = mData[6];

mCtrl[13] = mData[7]+ mDifference;

mCtrl[14] = mData[0]- mDifference;

mCtrl[15] = mData[1];

}

@Override

protected void onDraw(Canvas canvas) {

super.onDraw(canvas);

canvas.translate(mCenterX, mCenterY); // 将坐标系移动到画布中央

canvas.scale(1,-1); // 翻转Y轴

drawAuxiliaryLine(canvas);

// 绘制贝塞尔曲线

mPaint.setColor(Color.RED);

mPaint.setStrokeWidth(6);

path.moveTo(mData[0],mData[1]);

path.cubicTo(mCtrl[0], mCtrl[1], mCtrl[2], mCtrl[3], mData[2], mData[3]);

path.cubicTo(mCtrl[4], mCtrl[5], mCtrl[6], mCtrl[7], mData[4], mData[5]);

path.cubicTo(mCtrl[8], mCtrl[9], mCtrl[10], mCtrl[11], mData[6], mData[7]);

path.cubicTo(mCtrl[12], mCtrl[13], mCtrl[14], mCtrl[15], mData[0], mData[1]);

canvas.drawPath(path, mPaint);

}

// 绘制辅助线

private void drawAuxiliaryLine(Canvas canvas) {

// 绘制数据点和控制点

mPaint.setColor(Color.GRAY);

mPaint.setStrokeWidth(10);

for (int i=0; i<8; i+=2){

canvas.drawPoint(mData[i],mData[i+1], mPaint);

}

for (int i=0; i<16; i+=2){

canvas.drawPoint(mCtrl[i], mCtrl[i+1], mPaint);

}

// 绘制辅助线

mPaint.setStrokeWidth(4);

for (int i=2, j=2; i<8; i+=2, j+=4){

canvas.drawLine(mData[i],mData[i+1],mCtrl[j],mCtrl[j+1],mPaint);

canvas.drawLine(mData[i],mData[i+1],mCtrl[j+2],mCtrl[j+3],mPaint);

}

canvas.drawLine(mData[0],mData[1],mCtrl[0],mCtrl[1],mPaint);

canvas.drawLine(mData[0],mData[1],mCtrl[14],mCtrl[15],mPaint);

}

效果图

三阶贝塞尔曲线选点_用三阶贝塞尔曲线拟合圆

总结

这篇博文是在前一篇《贝塞尔曲线学习笔记》的基础上做的一个关于贝塞尔曲线应用的深入探索,是笔者在工作之余的一点学习收获,内容比较浅陋,主要的收获在于唤起了我的学习兴趣。关于前文主要求解的魔法数值,还应该深入讨论贝塞尔曲线拟合圆形的误差,Approximate a circle with cubic Bézier curves这篇文章中作了误差分析,并给出了一个更精确的魔法数值0.551915024494。

Thanks To