一、目的
文本旨在提供一种画椭圆的快速算法,使之可在不带乘法器的cpu上快速生成椭圆的点。
二、定义
1. 走向:当前点的下一个点的方向
2. 主坐标:在走向上,起点到终点,变化量较大的分量坐标
3. 从坐标:在走向上,起点到终点,变化量较小的分量坐标
三、 原理
1. 已知椭圆上的一点,要推算出下一点的位置,该位置的主坐标一定是沿着走向递增1个像素单元,而从坐标就不一定需要递增;那么问题就可以简化为,由当前点,判断下一点的从坐标是否应该递增;
我们只希望处理整数,故方程写为:
亦即:
3. 设函数:,则f(x, y) > 0在椭圆外,f(x, y) = 0在椭圆上,f(x, y) < 0在椭圆内部;
4. 若x为主坐标,y为从坐标,则每次下一点的判别式应为f(x + 1, y - 0.5);反之为f(x - 0.5, y -1);
5. 用主从改写函数,设主为变量为m,从变量为s,他们前面的乘法因为分别为fm和fs,那么,方程变为:,判别式统一写为g(m + 1, s - 0.5);
6. 如果g(m + 1, s - 0.5) >0,那么(m + 1, s - 0.5)在椭圆外部,那么,椭圆的边在(m + 1, s-1)和(m+1,s-0.5)之间,换句话说,也就是(m + 1, s-1)更接近于椭圆的边,此时从变量应该由s变为s-1;反之应该保持不变;
7. g(m, s)也有变量乘法,我们要避免乘法运算,所以,我们不应该直接采用该式;又由于该式是二次方程式,我们只需作一次求导(计算机里是离散值,亦即差分)就可以将其变为一次方程;一次方程每次递增1,那么就变成了类似于求改点变化率的情况了;但是,因为当前的下一点有两种情况,即g(m + 1, s)和g(m+1, s-1),那么求下一点的下一点判别式也分两种情况,为g(m + 2, s -0.5)和g(m+2, s-1-0.5),那么差分递推式也有两种dg0(m, s)=g(m+2,s-0.5)-g(m+1,s-0.5)=fm*(2*m+3)=fm*3+2*fm*m和dg1(m, s)=g(m+2,s-1.5)-g(m+1,s-0.5)=dg0(m, s)+fs*(2-2*s)=dg0(m,s)+2*(fs-fs*s);当然,初始g(m,s)还是需要用乘法算出来的,不过只运算一次;
8. 我们看到差分递推公式还是有乘法,其中fm*3是常量,可事先计算好,乘2可以通过移位计算或者多加一次获得;可是还有fm*m和fs*s怎么办呢?它们是一次的,再求导一次(差分求变化速度),即可得fm*m可通过m+fm递推获得,当s有变化时,可通过s+fs获得;那么,循环里整个运算就变成了整数加减法和移位运算了;
9. 循环结束点,当主坐标的变化率不再大于从坐标的变化率时,循环就结束了,亦即2*fm*m >2*fs*s亦即fm*m > fs*s不成立的时候,循环就该结束了,当前fm*m可通过m+fm递推获得,从的变化率也一样;
10. 根据对称性,可用第一象限的点画出四个象限的点;第一个象限的点,有两种走向,一个是f'(x) >f'(y),另一个是f'(y)>f'(x),两个走向合并起来,刚好是一个完整的椭圆在第一象限里面的点;
四、代码实现
根据原理,我们知道有一些常量可以事先算出来,如fm、fm*3、fm*fs,我把它放在一个结构体里面,方便后面使用:
typedef struct EllipseParam
{
int r[2]; //x,y轴半径
int sq[2]; //x,y轴半径的平方
int sqX3[2]; //x,y,3乘上轴半径的平方
int sq0Xsq1; //x,y轴半径相乘
} EllipseParam_t;
#define INIT_ELLIPSE_PARAM(ep, rx, ry) { \
(ep).r[0] = (rx); \
(ep).r[1] = (ry); \
(ep).sq[0] = (rx) * (rx); \
(ep).sq[1] = (ry) * (ry); \
(ep).sqX3[0] = (ep).sq[0] + (ep).sq[0] + (ep).sq[0]; \
(ep).sqX3[1] = (ep).sq[1] + (ep).sq[1] + (ep).sq[1]; \
(ep).sq0Xsq1 = (ep).sq[0] * (ep).sq[1]; \
}
画椭圆的其中一部分代码,iPart表示主变量索引:
void LCD_DrawEllipsePart(const EllipseParam_t * param, const LCD_Point_t * pCenter, int iPart)
{
int coord[2];
int diff[2];
int nextD;
coord[iPart] = 0;
coord[!iPart] = (param->r)[!iPart];
//diff[iPart] = (param->sq)[!iPart] * coord[iPart];
diff[iPart] = 0;
diff[!iPart] = (param->sq)[iPart] * coord[!iPart];
//nextD = (param->sq)[!iPart] * (coord[iPart] + 1) * (coord[iPart] + 1) + (param->sq)[iPart] * ((coord[!iPart] - 0.5) * (coord[!iPart] - 0.5) + 0.5) - param->sq0Xsq1;
nextD = (param->sq)[!iPart] + (param->sq)[iPart] * ((param->sq)[!iPart] - (param->r)[!iPart]) - param->sq0Xsq1;
DRAW_ELLIPSE_POINT(coord[0], coord[1], *pCenter)
while(diff[iPart] < diff[!iPart])
{
int nextD2 = nextD + (param->sqX3)[!iPart] + (diff[iPart] << 1);
if (nextD > 0)
nextD2 += ((param->sq)[iPart] - diff[!iPart]) << 1;
++ coord[iPart];
diff[iPart] += (param->sq)[!iPart];
if (nextD > 0)
{
diff[!iPart] -= (param->sq)[iPart];
-- coord[!iPart];
}
DRAW_ELLIPSE_POINT(coord[0], coord[1], *pCenter)
nextD = nextD2;
}
}
画整个椭圆:
void LCD_DrawEllipse(const LCD_Point_t * pCenter, int rx, int ry, uint16_t color)
{
EllipseParam_t param;
const uint16_t colorOld = LCD_GetLastColor();
INIT_ELLIPSE_PARAM(param, rx, ry)
LCD_SetColor(color);
LCD_DrawEllipsePart(¶m, pCenter, 0);
LCD_DrawEllipsePart(¶m, pCenter, 1);
LCD_SetColor(colorOld);
}
不超过50行,这应该是循环部分只使用加减法和移位的椭圆快速算法非常简洁的实现了。