计算机图形学:直线的光栅化模拟

       屏幕是有若干像素点组成的,任何图形画到屏幕上都要先转为像素点,也就是光栅化,下面模拟直线的光栅化。
像素的最小单位为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);

计算机图形学:直线的光栅化模拟_第1张图片

       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)之间的直线

计算机图形学:直线的光栅化模拟_第2张图片
     (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);

计算机图形学:直线的光栅化模拟_第3张图片

       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被转为整数了。

你可能感兴趣的:(计算机图形,光栅化,DDC,Bresenham)