圖形學入門(1)——直線生成算法(DDA和Bresenham)

開一個新坑,記錄從零開始學習圖形學的過程,如今仍是個正在學習的萌新,寫的很差請見諒。ios

 

首先從最基礎的直線生成算法開始,當咱們要在屏幕上畫一條直線時,因爲屏幕由一個個像素組成,因此實際上計算機顯示的直線是由一些像素點近似組成的,直線生成算法解決的是如何選擇最佳的一組像素來顯示直線的問題。算法

對這個問題,首先想到的最暴力的方法固然是從直線起點開始令x或y每次增長1直到終點,每次根據直線方程計算對應的函數值再四捨五入取整,便可找到一個對應的像素,但這樣作每一步都要進行浮點數乘法運算,效率極低,因此出現了DDA和Bresenham兩種直線生成算法。編程

 

數值微分法(DDA算法)

DDA算法主要是利用了增量的思想,經過同時對x和y各增長一個小增量,計算下一步的x和y值。函數

 

 

根據上式可知$\bigtriangleup x$=1時,x每遞增1,y就遞增k,因此只須要對x和y不斷遞增就能夠獲得下一點的函數值,這樣避免了對每個像素都使用直線方程來計算,消除了浮點數乘法運算。學習

代碼實現:ui

 

#include<Windows.h>
#include<iostream>
#include<cmath>
using namespace std;

const int ScreenWidth = 500;
const int ScreenHeight = 500;

LRESULT CALLBACK WinProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message) {
    case WM_CLOSE:
        DestroyWindow(hWnd);
        break;
    case WM_DESTROY:
        PostQuitMessage(0);
        break;
    default:
        return DefWindowProc(hWnd, message, wParam, lParam);
        break;
    }
    return 0;
}

void DDALine(int x0,int y0,int x1,int y1,HDC hdc)
{
    int i=1;
    float dx, dy, length, x, y;
    if (fabs(x1 - x0) >= fabs(y1 - y0))
        length = fabs(x1 - x0);
    else
        length = fabs(y1 - y0);
    dx = (x1 - x0) / length;
    dy = (y1 - y0) / length;
    x = x0;
    y = y0;
    while (i<=length)
    {
        SetPixel(hdc,int(x + 0.5), ScreenHeight-40-int(y + 0.5), RGB(0, 0, 0));
        x = x + dx;
        y = y + dy;
        i++;
    }
}
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int nShowCmd)
{
    WNDCLASS wcs;
    wcs.cbClsExtra = 0;                                         // 窗口類附加參數  
    wcs.cbWndExtra = 0;                                         // 窗口附加參數  
    wcs.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);    // 窗口DC背景  
    wcs.hCursor = LoadCursor(hInstance, IDC_CROSS);             // 鼠標樣式  
    wcs.hIcon = LoadIcon(NULL, IDI_WINLOGO);                    // 窗口icon  
    wcs.hInstance = hInstance;                                  // 應用程序實例  
    wcs.lpfnWndProc = (WNDPROC)WinProc;
    wcs.lpszClassName = "CG";
    wcs.lpszMenuName = NULL;
    wcs.style = CS_VREDRAW | CS_HREDRAW;
    RegisterClass(&wcs);
    HWND hWnd;
    hWnd = CreateWindow("CG","DrawLine", WS_OVERLAPPEDWINDOW, 200, 200, ScreenWidth, ScreenHeight, NULL, NULL, hInstance, NULL);
    ShowWindow(hWnd, nShowCmd);
    UpdateWindow(hWnd);
    MSG msg;

    // hdc init
    HDC hdc = GetDC(hWnd);

    // 繪圖,畫一條從點(0,0)到(100,350)的直線
    DDALine(0, 0, 100, 350, hdc);// 消息循環  
    while (GetMessage(&msg, 0, NULL, NULL)) {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    // release
    ReleaseDC(hWnd, hdc);
    return 0;
}

 

 

以上是DDA算法的實現,WinMain和WinProc函數是Windows API編程特有的,咱們只須要關注DDALine這個繪圖函數,該函數傳入兩個點的座標畫出一條直線。spa

首先判斷起點和終點間x軸和y軸哪一個軸向的跨度更大(斜率範圍),爲了防止丟失像素,應讓跨度更大的軸向每次自增1,這樣能得到更精確的結果。3d

接下來就沒什麼好說的,依次讓x和y加上增量而後四捨五入就好了,浮點數四捨五入能夠直接用int(x+0.5)計算,setPixel函數用於設置像素的顏色值(須要傳入窗口的hdc句柄),因爲Windows窗口座標的原點在左上角,因此拿窗口高度減去y值就能夠轉換成日常習慣的左下角座標系了。code

運行結果:blog

 

 

 

 

  

Bresenham算法

DDA算法儘管消除了浮點數乘法運算,但仍存在浮點數加法和取整操做,效率仍有待提升,1965年Bresenham提出了更好的直線生成算法,成爲了時至今日圖形學領域使用最普遍的直線生成算法,該算法採用增量計算,藉助一個偏差量的符號肯定下一個像素點的位置,該算法中不存在浮點數,只有整數運算,大大提升了運行效率。

 咱們先只考慮斜率在0-1之間的狀況,從線段左端點開始處理,並逐步處理每一個後續列,每肯定當前列的像素座標$(x_{i},y_{i})$後,那麼下一步須要在列$x_{i+1}$上肯定y的值,此時y值要麼不變,要麼增長1,這是由於斜率在0-1之間,x增加比y快,因此x每增長1,y的增量是小於1的。

對於左端點默認爲其像素座標,下一列要麼是右方的像素,要麼是右上方的像素,設右上方像素到直線的距離爲d2,右方像素到直線的距離爲d1,顯然只須要判斷直線離哪一個像素點更近也就是d1-d2的符號便可找到最佳像素。

 

 因此能夠推出如下式子:

 

 其中$\bigtriangleup x$起點到終點x軸上距離,$\bigtriangleup y$爲y軸上距離,k=$\bigtriangleup y$/$\bigtriangleup x$,c是常量,與像素位置無關。

令$e_{i}$=$\bigtriangleup x$(d1-d2),則$e_{i}$的計算僅包括整數運算,符號與d1-d2一致,稱爲偏差量參數,當它小於0時,直線更接近右方像素,大於0時直線更接近右上方像素。

可利用遞增整數運算獲得後繼偏差量參數,計算以下:

 

 因此選擇右上方像素時($y_{i+1}$-$y_{i}$=1):

 

 選擇右方像素時($y_{i+1}$-$y_{i}$=0):

 

 初始時,將k=$\bigtriangleup y$/$\bigtriangleup x$代入$\bigtriangleup x$(d1-d2)中可獲得起始像素的第一個參數:

 

斜率在0-1之間的Bresenham算法代碼實現(替換上面程序中DDALine便可):

void Bresenham_Line(int x0, int y0, int x1, int y1, HDC hdc)
{
    int dx, dy, e, x=x0, y=y0;
    dx = x1 - x0; dy = y1 - y0;
    e = 2 * dy - dx;
    while (x<=x1)
    {
        SetPixel(hdc, x, ScreenHeight-40-y, RGB(0, 0, 0));
        if (e >= 0)//選右上方像素
        {
            e = e + 2 * dy - 2 * dx;
            y++;
        }
        else//選右方像素
        {
            e = e + 2 * dy;
        }
        x++;
    }
}

運行結果:

 

 

 

要實現任意方向的Bresenham算法也很容易,斜率在0-1之間意味着直線位於座標系八象限中的第一象限,若是要繪製第二象限的直線,只須要利用這兩個象限關於直線x=y對稱的性質便可,能夠先將x和y值互換先在第一象限進行計算,而後調用SetPixel時再將x和y值反過來,在第二象限中繪製,其餘象限也是相似的思路。

繪製任意方向直線的Bresenham算法代碼實現:

void Bresenham_Line(int x0, int y0, int x1, int y1, HDC hdc)
{
    int flag = 0;
    int dx = abs(x1 - x0);
    int dy = abs(y1 - y0);
    if (dx == 0 && dy == 0)   return;
    if (abs(x1 - x0) < abs(y1 - y0))
    {
        flag = 1;
        swap(x0, y0);
        swap(x1, y1);
        swap(dx, dy);
    }
    int tx = (x1 - x0) > 0 ? 1 : -1;
    int ty = (y1 - y0) > 0 ? 1 : -1;
    int x = x0;    
    int y = y0;
    int dS = 2 * dy;   int dT = 2 * (dy - dx);
    int e = dS - dx;
    SetPixel(hdc, x0, y0, RGB(0,0,0));
    while (x != x1)
    {
        if (e < 0)
            e += dS;
        else
        {
            y += ty;  e += dT;
        }
        x += tx;
        if (flag)
            SetPixel(hdc, y, ScreenHeight - 40 - x, RGB(0, 0, 0));
        else
            SetPixel(hdc, x, ScreenHeight - 40 - y, RGB(0, 0, 0));
    }
}

 

 

 

直線生成算法就到這裏啦,接下來也要加油學習圖形學~

相關文章
相關標籤/搜索