上一篇讲完了直线最简单的生成算法,这篇我们讲圆的生成算法,这些都是比较basic&easy的思路和code,并且一定能实现,因为博主我也在学习中~

除了直线的光栅化研究,还有很多曲线光栅化研究,比如说:圆、椭圆、抛物线、双曲线等圆锥曲线的光栅化。其中,Bresenham算法是一种最简单最有效的方法。我们先看下图:

bresenham绘制圆例题_bresenham绘制圆例题

怎么画圆呢?如果是一个像素一个像素的去算,是不是太不符合工科生的思维了。我们要的是,用尽量小的脑力劳动去完成一个比较大的工作量~只要算法写得好,放心交给计算机跑。

首先,我们只需要画出一个八分圆(逆时针0°-45°),也就是①的那段圆弧;然后②这段圆弧就可以通过y=x对称变换得到,这时候我们把①②合并起来就得到了一个四分圆(逆时针0°-90°);四分圆对x=0对称变换,得到了③④,把①②③④合并起来通过y=0对称变换,就得到了下半部分的半圆,上半圆和下边圆合并起来就是整个圆啦!

那三个 二阶对称变换矩阵怎么理解,这里需要读者有线性代数的基础。比如说我们有点A(2,1)在圆上,那么通过y=x这个直线得到的对称点是:

bresenham绘制圆例题_bresenham绘制圆例题_02

不知道哪位小主有更好的数学描述工具啊,一眼看上去就很拙劣的图都是鄙人用mspaint画出来的,委屈·····

好了,下面我们开始推导Bresenham算法啦,这里我们让坐标原点是圆心,那么第一个四分圆应该是顺时针画还是逆时针画呢,我们看到,如果起始点是(0,R),然后顺时针画圆的话,y是x的递减函数;如果起始点是(R,0),然后逆时针画圆的话,x是y的递减函数。顺时针比较符合人的理解方式,所以我们选择顺时针方向画圆~

bresenham绘制圆例题_顺时针_03

那么从起点开始,下一个像素该选择哪个像素呢?我们有三个方向的像素可以选择-----右方像素、右下角像素、下方像素,分别记为mH,mD,mV。如下图所示:

bresenham绘制圆例题_光栅化_04

我们要在这三个像素中,选择一个像素点。选择的依据就是这个点应该是最逼近真正的圆的。我们用差值来表达逼近的程度,也就是---这个点离圆心的距离平方-圆半径平方:

bresenham绘制圆例题_光栅化_05

圆这条线呢,肯定不会乖乖的经过这三个点,它可能会在点与点之间裸奔。于是我们用人脑穷举,发现圆与点(xi,yi)附近光栅网格的相交关系只有5种可能,也就是上面的那幅图。下面我们开始思考这5种情况了:

bresenham绘制圆例题_计算机图形学_06

如果∆i < 0 ,说明右下角像素点(xi + 1,yi - 1) 在圆内,对应情形①②,这时候取像素(xi + 1,yi),也就是mH;或者取像素(xi + 1,yi - 1),也就是mD,因为这时候只有mH或者mD最逼近圆弧了,反正看情况二选一。

        情形①,到底圆弧离mH近还是mD近,用这个方程来衡量:

bresenham绘制圆例题_顺时针_07

bresenham绘制圆例题_计算机图形学_08

               下面我们来简化这个判断语句,对于情形①来讲,mH肯定是在圆外,mD肯定是在圆内,也就是说

bresenham绘制圆例题_光栅化_09

               因此     

bresenham绘制圆例题_光栅化_10

               去括号以后,得到

bresenham绘制圆例题_顺时针_11

        情形②,只能选择mH,这时候mH和mD都位于圆内,因此

bresenham绘制圆例题_bresenham绘制圆例题_12

               这时候,

bresenham绘制圆例题_顺时针_13

               我们就可以把情形跟情形 中的其中一个分支合并啦,合并结果就是:

if delt_i < 0:
	if Q <=0 then
		select mH
	if Q > 0 then
		select mD

如果∆i > 0, 也就是右下角(xi + 1,yi -1)位于圆外,对应图中的情形③④,显然只能取mD,或者mV,下面就分情况讨论。

        情形③,判断mV和mD哪个点更逼近真正的圆

bresenham绘制圆例题_顺时针_14

bresenham绘制圆例题_计算机图形学_15

       同理,我们把公式简化

bresenham绘制圆例题_计算机图形学_16

bresenham绘制圆例题_bresenham绘制圆例题_17

        情形④,只能取下方像素mV

bresenham绘制圆例题_顺时针_18

        因此,我们可以把情形和情形中的一个分支合并啦,合并结果就是:

if delt_i > 0:
	if Q_ <= 0 then
		select mD
	if Q_ > 0 then
		select mV

如果∆i=0,

        情形⑤,圆恰巧乖乖的经过了mD,这时候,我们要看是否可以归并到上述的两大情况的其中一种:

bresenham绘制圆例题_顺时针_19

bresenham绘制圆例题_bresenham绘制圆例题_20

         也就是:

bresenham绘制圆例题_顺时针_21

bresenham绘制圆例题_计算机图形学_22

          随便归类到哪一个都行。

最后,判别准则就简化如下:

if delt_i < 0:
	if Q <=0 then
		select mH
	if Q > 0 then
		select mD
		
		
if delt_i > 0:
	if Q_ <= 0 then
		select mD
	if Q_ > 0 then
		select mV
if delt_i = 0:
	select mD

也可以这么写:

if delt_i <= 0:
	if Q <=0 then
		select mH
	if Q > 0 then
		select mD
		
		
if delt_i > 0:
	if Q_ <= 0 then
		select mD
	if Q_ > 0 then
		select mV

因此我们的Bresenham的伪代码就出来了:

#all variables are assumed integer
start
	#initialize the variables
	xi = 0
	yi = R
	delt_i = 2*(1-R)
	Limit = 0

	while yi >= Limit
		call setpixel(xi,yi)
		#determin if case 1 or 2, 3 or 4, or 5
		if delt_i < 0 then
			Q = 2*delt_i + 2*yi - 1
			#determin whether case 1 or 2
			if Q <=0 then
				call mh(xi,yi,delt_i)
			else
				call md(xi,yi,delt_i)
			end if
		else if delt_i > 0 then
			Q_ = 2*delt_i - 2*xi -1
			#determin whether case 3 or 4
			if Q_ <= 0 then
				call md(xi,yi,delt_i)
			else call mv(xi,yi,delt_i)
			end if
			
		else if delt_i = 0
                        #case 5
			call md(xi,yi,delt_i)
		end if
	end while
finish

#move horizontally
subroutine mh(xi,yi,delt_i):
	xi = xi + 1
	delt_i = delt_i + 2*xi + 1
#end sub

#move diagonally
subroutine md(xi,yi,delt_i):
	xi = xi + 1
	yi = yi -1
	delt_i = delt_i + 2*xi - 2*yi + 2
#end sub

#move vertically
subroutine mv(xi,yi,delt_i):
	yi = yi - i 
	delt_i = delt_i - 2*yi + 1 
#end sub

不知道读者有没有看懂,delt_i之所以在三个函数里都改变了,是因为我们的基准点移动啦~意思就是,当我从(xi,yi)水平移动到(xi + 1, yi)之后,我们的基准点(xi,yi)就变成了(xi + 1 ,yi)了。下面举个例子来帮助读者更加详细的认知这个过程:

例:以坐标原点为圆心,半径为8的圆在第一象限部分为例

#初始值
xi = 0,yi=8
delt_i = 2(1-8) = -14
Limit = 0
#开始loop

	#Step 1
	yi > Limit:
		setpixel(0,8)
		delt_i < 0:
			Q = 2*(-14) + 2*8 - 1 = -13 < 0
			call mh(0,8,-14):
				x = 0 + 1 = 1
				delt_i = -14 + 2*(1) + 1 = -11
	#Step2
	yi > Limit:
		setpixel(1,8)
		delt_i < 0:
			Q = 2*(-11) + 2*(8) -1 = -7
			Q < 0
			call mh(1,8,-11):
				x = 1 + 1 = 2
				delt_i = -11 + 2*(2) + 1 = -6
	#Step3
	....

#跳出loop

最后的画图结果应该是这样的:

bresenham绘制圆例题_计算机图形学_23

好了,一个四分圆搞定了,那么我们怎么通过整个四分圆得到整个圆呢?

思路就是在循环中,每次都把选中的像素散播在其他象限对应的位置。

于是我的c++代码是这样的:

void CCGPainterView::DrawCircle(CDC *pDC, CPoint ptOrigin, int iRadius, COLORREF cLineColor)
{
	//CPoint ptLeftTop(ptOrigin.x - iRadius, ptOrigin.y - iRadius);
	//CPoint ptRightBottom(ptOrigin.x + iRadius, ptOrigin.y + iRadius);
	//CRect circleRect(ptLeftTop, ptRightBottom);
	//pDC->Ellipse(circleRect);

/*************************************************************
write the circle algorithm for drawing the circle
use function: pDC->SetPixelV(point, cLineColor); to drawing a pixel
编码圆弧生成算法,调用函数pDC->SetPixelV(point, cLineColor)画像素。
*************************************************************/
	
	POINT point;
	int xi = 0; 
	int yi = iRadius;
	int delt_i = (xi+1)*(xi+1)+(yi-1)*(yi-1)-iRadius*iRadius;
	int alpha;
	int Limit = 0;

	while (yi >= Limit){
		point.x = ptOrigin.x + xi;
		point.y = ptOrigin.y + yi;
		pDC->SetPixelV(point, cLineColor);
		point.x = ptOrigin.x + xi;
		point.y = ptOrigin.y - yi;
		pDC->SetPixelV(point, cLineColor);
		point.x = ptOrigin.x - xi;
		point.y = ptOrigin.y + yi;
		pDC->SetPixelV(point, cLineColor);
		point.x = ptOrigin.x - xi;
		point.y = ptOrigin.y - yi;
		pDC->SetPixelV(point, cLineColor);
		if (delt_i < 0){
			alpha = 2 * delt_i + 2 * yi - 1;
			if (alpha <= 0){
				//mh(xi, yi, delt_i);
				xi = xi + 1;
				delt_i = delt_i + 2 * xi + 1;
			}
			else{
				//md(xi, yi, delt_i);
				xi = xi + 1;
				yi = yi - 1;
				delt_i = delt_i + 2 * xi - 2 * yi + 2;
			}
				
		}
		else if(delt_i>0){
			alpha = 2 * delt_i - 2 * xi - 1;
			if (alpha <= 0){
				//md(xi, yi, delt_i);
				xi = xi + 1;
				yi = yi - 1;
				delt_i = delt_i + 2 * xi - 2 * yi + 2;
			}
			else{
				//mv(xi, yi, delt_i);
				yi = yi - 1;
				delt_i = delt_i - 2 * yi + 1;
			}
				
		}
		else{
			//md(xi, yi, delt_i);
			xi = xi + 1;
			yi = yi - 1;
			delt_i = delt_i + 2 * xi - 2 * yi + 2;
		}
	}
}

我们的运行结果如下:

bresenham绘制圆例题_顺时针_24

另外我要注释一下:

如果只保留其中一个象限点的描点,你会发现,画出来的四分圆并不是数学上的第一象限,但是是计算机的第一象限。

point.x = ptOrigin.x + xi;
point.y = ptOrigin.y + yi;
pDC->SetPixelV(point, cLineColor);
/*
point.x = ptOrigin.x + xi;
point.y = ptOrigin.y - yi;
pDC->SetPixelV(point, cLineColor);
point.x = ptOrigin.x - xi;
point.y = ptOrigin.y + yi;
pDC->SetPixelV(point, cLineColor);
point.x = ptOrigin.x - xi;
point.y = ptOrigin.y - yi;
pDC->SetPixelV(point, cLineColor);
*/

bresenham绘制圆例题_顺时针_25

 

因为计算机的原点是从屏幕左上角开始的,往右是x轴正向,往下是y轴正向~

其实这个算法并不难,就是要计算的公式太多。。。。。没有耐心和数学功底的人是干不下去的。。。

若有理解不到之处,还请大佬指正~