屏幕是有若干像素点组成的,任何图形画到屏幕上都要先转为像素点,也就是光栅化,下面模拟直线的光栅化。
像素的最小单位为1,且为整数,所以在直线上只能取在X轴或Y轴上步进为1的最接近的整数点,至于是X轴还是Y轴取决于跨度大的轴,假设直线起点为(x1,y1),终点为(x2,y2) ,如果|x2-x1|>=|y2-y1|,即斜率位于-1和1之间,则X轴每次步进一个像素,否则Y轴。
下面的代码模拟几种光栅化直线的算法,为了方便假设x2>x1>0,y2>y1>0,且斜率位于0到1之间,win32编程中窗口的XY轴正方向分别向右向下,与平常的坐标系不太一样,这里以Windows窗口坐标为准。画直线的时候端点也有可能出现浮点数,所以输入点都使用float类型。在测试下面代码之前首先创建一个Windows桌面应用程序。
1、通过公式直接计算
下面的函数DrawLine_1在X方向步进1,Y值通过直线的斜截公式算出
void DrawLine_1(HDC hdc, float x1, float y1, float x2, float y2)
{
//转为斜截式,y=k*x+b,分求出k和b
float dx = x2 - x1;
float dy = y2 - y1;
float k = dy / dx;
float b = y1 - k * x1;
int num = dx;//获取点的个数
int x = round(x1);
int y = round(x * k + b);
for (int i = 0; i <= num; i++)
{
::SetPixel(hdc, x, y, RGB(255, 0, 0));
x += 1;
y = round(x * k + b);
}
}
x值只要在第一次取x1最接近的整数,然后使用整数加1即可,y值每次都要用round函数取整数,取整数还可以用y=x * k + b+0.5的形式,这种方式每次都要使用浮点数的乘法和加法,效率太低。因为x每次加1,y轴增加的值是固定的k,所以只要在前一个准确y值上加k就可以算出当前准确的y值,然后取整,即DDA算法。
2、DDA算法(Digital Differential Analyzer数值微分法)
DrawLine_2函数中fy为当前x对应的准确的y值,x每次加1,fy的值每次增加斜率k,最终y的值由fy取整
void DrawLine_2(HDC hdc, float x1, float y1, float x2, float y2)
{
float dx = x2 - x1;
float dy = y2 - y1;
int stepx = 1;
float stepy = dy / dx;
int num = round(dx);
int x = round(x1);
float fy = (x - x1) * dy / dx + y1;//(x-x1)/dx=(y-y1)/dy
int y = round(fy);
for (int i = 0; i <= num; i++)
{
::SetPixel(hdc, x, y, RGB(255, 0, 0));
x += stepx;
fy += stepy;
y = round(fy);
}
}
下面展示以三种方式画出的直线对比,黑线为系统函数LineTo画出,红线为上面两种方法画出
MoveToEx(hdc, 50, 50, nullptr);
LineTo(hdc, 150, 100);
DrawLine_1(hdc, 50, 55, 150, 105);
DrawLine_2(hdc, 50, 60, 150, 110);
DrawDDC函数针对任意输入值使用DDC算法画出直线,为了看得清楚还是以斜率在和不在-1~1之间分为两个分支,斜率不在-1到1内一般只要将xy互换即可, 代码如下
void DrawDDC(HDC hdc, float x1, float y1, float x2, float y2)
{
float dx = x2 - x1;
float dy = y2 - y1;
if (fabs(dx) >= fabs(dy))
{
int directx = dx > 0.0 ? 1 : -1;
int num = round(dx);
if (num < 0)
{
num = -num;
}
int x = round(x1);
float fy = (x - x1) * dy / dx + y1;
int y = round(fy);
float stepy = fabs(dy / dx);
if (dy < 0)
{
stepy = -stepy;
}
for (int i = 0; i <= num; i++)
{
::SetPixel(hdc, x, y, RGB(255, 0, 0));
x += directx;
fy += stepy;
y = round(fy);
}
}
else
{
int directy = dy > 0.0 ? 1 : -1;
int num = round(dy);
if (num < 0)
{
num = -num;
}
int y = round(y1);
float fx = (y - y1) * dx / dy + x1;
int x = round(fx);
float stepx = fabs(dx / dy);
if (dx < 0)
{
stepx = -stepx;
}
for (int i = 0; i <= num; i++)
{
::SetPixel(hdc, x, y, RGB(255, 0, 0));
y += directy;
fx += stepx;
x = round(fx);
}
}
}
下面画一个沿中心点旋转的直线测试下这个函数,DrawRotatorLine将直线(x1,y1)-(x2,y2)旋转degree后画出
void DrawRotatorLine(HDC hdc,float x1,float y1,float x2,float y2,float degree)
{
float centerx = (x1 + x2) / 2;
float centery = (y1 + y2) / 2;
float length = sqrt(pow(y1-y2,2)+pow(x1-x2,2));
float tx1 = -length / 2 * cos(degree) + centerx;
float ty1 = -length / 2 * sin(degree) + centery;
float tx2 = 2 * centerx - tx1;
float ty2 = 2 * centery - ty1;
DrawDDC(hdc, tx1, ty1, tx2, ty2);
}
g_degree为全局变量,定义一个定时器每隔100ms增加g_degree的值,然后调用InvalidateRect发送WM_PAINT消息
float g_degree = 0.0;
void CALLBACK TimerProc(HWND hwnd, UINT uMsg, UINT idEvent, DWORD dwTime)
{
g_degree += 0.05;
::InvalidateRect(hwnd,nullptr,true);
}
SetTimer(hWnd, 100, 100, (TIMERPROC)TimerProc);
在WM_PAINT消息处理分支中调用DrawRotatorLine,运行后可以看到直线沿着重点慢慢旋转,有的角度锯齿会非常明显
case WM_PAINT:
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hWnd, &ps);
DrawRotatorLine(hdc, 300, 300, 500, 300, g_degree);
EndPaint(hWnd, &ps);
}
3、Bresenham算法
Bresenham算法的具体介绍就不多说了,只简单说下算法如何取点,如图中画(x1,y1)到(x4,y3)之间的直线
(1)当x=x1时直线与Y轴的交点为y1,刚好是整数,所以y=y1
(2)当x=x2时直线与Y轴的交点为y1+2/3,大于y1和y2的中点y1+0.5,所以取y=y2,也就是(y1+2/3)-(y1+0.5)=2/3-0.5>0时取上一个y值+1,即y1+1=y2
(3)当x=x3时与Y轴的交点为y1+2/3+2/3=y1+4/3=y2+1/3,小于y2与y3的中点y2+0.5,所以取y=y2,也写成上面的形式就是(y2+1/3)-(y2+0.5)=1/3-0.5<0时y值不变y=y2
(4)当x=x4时与Y轴的交点为y2+1/3+2/3=y2+1,大于y2与y3的中点,所以取y=y3,即(y2+1)-(y2+0.5)=1-0.5>0时上一个y值加1,即y=y3
DrawLine_3函数实现这个算法,error作为误差,x每步进1,error增加dy/dx,然后减去0.5与0进行比较,如果小于0则y值不变,如果大于0则y值加1,并且error值减1,使它的值是相对于下一个y值,因为0.5是一个常数,只要在初始值上减一次,就不用每次都减0.5,error的初始值是fy - y - 0.5,
void DrawLine_3(HDC hdc, float x1, float y1, float x2, float y2)
{
float dx = x2 - x1;
float dy = y2 - y1;
int stepx = 1;
int stepy = 1;
int num = round(dx);
int x = round(x1);
float fy = (x - x1) * dy / dx + y1;
int y = round(fy);
float error = fy - y - 0.5;
int stepError = dy / dx;
for (int i = 0; i <= num; i++)
{
::SetPixel(hdc, x, y, RGB(255, 0, 0));
x += stepx;
error += stepError;
if (error > 0)
{
error += -1;
y += stepy;
}
}
}
如果上面的输入都是整数,则可以通过将error乘以2*dx(0.5要乘以2,dy / dx要乘以dx)将浮点数都转为整数,由于error是判断正负号,所以乘以一个正数符号不会变,不影响结果,error初始值也就变成了-dx,整数运算比浮点数运算快,可以提升程序性能。
void DrawLine_4(HDC hdc, int x1, int y1, int x2, int y2)
{
int dx = x2 - x1;
int dy = y2 - y1;
int stepx = 1;
int stepy = 1;
int num = dx;
int x = x1;
int y = y1;
int doubledx = 2 * dx;
int doubledy = 2 * dy;
int error = -dx;//-0.5*2*dx
int stepError = doubledy;//(dy/dx)*2dx
for (int i = 0; i <= num; i++)
{
::SetPixel(hdc, x, y, RGB(255, 0, 0));
x += stepx;
error += stepError;
if (error > 0)
{
error += -doubledx;//-1*2*dx
y += stepy;
}
}
}
用上面的几个绘制函数画线看看效果
MoveToEx(hdc, 50, 50, nullptr);
LineTo(hdc, 150, 100);
DrawLine_1(hdc, 50, 55, 150, 105);
DrawLine_2(hdc, 50, 60, 150, 110);
DrawLine_3(hdc, 50, 65, 150, 115);
DrawLine_4(hdc, 50, 70, 150, 120);
Bresenham算法针对任意斜率绘制代码如下
void DrawBresenham(HDC hdc, int x1, int y1, int x2, int y2)
{
int dx = x2 - x1;
int dy = y2 - y1;
int stepx = dx>0?1:-1;
int stepy = dy>0?1:-1;
int x = x1;
int y = y1;
int doubledx = 2 * dx;
int doubledy = 2 * dy;
if (fabs(dx) >= fabs(dy))
{
int num = fabs(dx);
int error = dy>0?-dx:dx;//-0.5*2*dx
int stepError = dy>0?fabs(doubledy):-fabs(doubledy);//(dy/dx)*2dx
int unitElapse = dy > 0 ? -fabs(doubledx) : fabs(doubledx);//-1*2*dx
for (int i = 0; i <= num; i++)
{
::SetPixel(hdc, x, y, RGB(255, 0, 0));
x += stepx;
error += stepError;
if ((dy > 0 && error > 0) || (dy < 0 && error < 0))
{
error += unitElapse;
y += stepy;
}
}
}
else
{
int num = fabs(dy);
int error = dx>0?-dy:dy;//-0.5*2*dy
int stepError = dx>0?fabs(doubledx):-fabs(doubledx);//(dx/dy)*2dy
int unitElapse = dx > 0 ? -fabs(doubledy) : fabs(doubledy);//-1*2*dy
for (int i = 0; i <= num; i++)
{
::SetPixel(hdc, x, y, RGB(255, 0, 0));
y += stepy;
error += stepError;
if ((dx > 0 && error > 0) || (dx < 0 && error < 0))
{
error += unitElapse;
x += stepx;
}
}
}
}
由于不能确保x1与x2以及y1与y2的大小关系所以步进值也要分情况判断。如果将DrawBresenham用于绘制上面的旋转曲线,会发现中点在转的过程中会有小的移动,那是因为三角函数算出的端点是浮点数,传入DrawBresenham被转为整数了。