[TinyRenderer] Chapter1 p3 Line

qiyuewuyi2333發表於2024-06-13

(注:本小節不是對劃線演算法事無鉅細的證明,如果你需要更加系統的學習,請跳轉至文末的參考部分)
如果你是一名曾經學習過圖形學基礎的學生,那麼你一定對畫線演算法稔熟於心,中點劃線演算法,Bresenham演算法。其中,現代光柵化器中使用最多的就是Bresenham演算法,它以去除了除法和浮點運算而著稱。

但如果現在讓你看下面的這段程式碼,你能否把它和Bresenham演算法聯絡起來呢?

void Segment::draw(const Point2i begin, const Point2i end, const RGBPixel& color, BMPImage& image)
{
    int x0 = begin.x;
    int y0 = begin.y;
    int x1 = end.x;
    int y1 = end.y;

    int dx = abs(x1 - x0);
    int dy = abs(y1 - y0);
    int sx = (x0 < x1) ? 1 : -1;
    int sy = (y0 < y1) ? 1 : -1;
    int error = dx - dy;

    while (true)
    {
        image.set(x0, y0, color);

        if (x0 == x1 && y0 == y1)
            break;
        int e2 = 2 * error;
        if (e2 > -dy)
        {
            error -= dy;
            x0 += sx;
        }
        if (e2 < dx)
        {
            error += dx;
            y0 += sy;
        }
    }
}

如果你可以順利地說出每行程式碼的含義,那麼恭喜你,你已經完全掌握了Bresenham演算法,你可以跳過本小節,進行下一小節的學習。
但如果你還有所異或,那麼相信我,看完本小節,你必定有所收穫。

本節目標

在光柵化渲染器中加入畫線功能

分析

首先,讓我們考慮一種再簡單不過的場景,你從一個初始點begin出發,從左向右,沿著斜率在0~1的直線到達end

這是一個Bresenham演算法的基礎場景,在這個場景中,我們可以透過中點劃線演算法知道存在一個判斷依據,用於確定在每次遍歷時y的值是否需要改變。

這個值的變化由直線方程給出,在這裡我們僅使用beginend給出。(具體論證請翻閱參考)
還記得我們的假設場景麼?在這個場景下:

assert: (x0 < x1) and ((y1 – y0) < (x1 – x0))
δx = x1 – x0;
δy = y1 – y0;
incrE = 2 * δy;
incrNE = 2 * (δy - δx);
d = 2 * δy – δx;

保持x遞增的同時,判斷y是否改變。
d<0 -> d += incrE
else -> d += incrNE and ++y

現在我們得到了在斜率為0~1的時候的Bresenham演算法,且begin.x<end.x
那麼對於其他場景呢?

  1. begin.x > end.x
  2. slope < 0 or slope > 1
  3. 平行於座標軸的方向

解決方案:

  1. 只需要將begin和end兩個點做一下交換即可
  2. slope < 0 or slope > 1
    1. slope < 0只需要對begin和end加上符號,最後set的時候再變回來即可
    2. slope > 1這個更加簡單,只需要交換xy即可
  3. 平行於軸體的方向只需要特判一下進行處理,而且這樣效率更高
實現

實際上TinyRenderer的實現思路也是如此:

void line(int x0, int y0, int x1, int y1, TGAImage &image, TGAColor color) { 
    bool steep = false; 
    if (std::abs(x0-x1)<std::abs(y0-y1)) { 
        std::swap(x0, y0); 
        std::swap(x1, y1); 
        steep = true; 
    } 
    if (x0>x1) { 
        std::swap(x0, x1); 
        std::swap(y0, y1); 
    } 
    int dx = x1-x0; 
    int dy = y1-y0; 
    int derror2 = std::abs(dy)*2; 
    int error2 = 0; 
    int y = y0; 
    for (int x=x0; x<=x1; x++) { 
        if (steep) { 
            image.set(y, x, color); 
        } else { 
            image.set(x, y, color); 
        } 
        error2 += derror2; 
        if (error2 > dx) { 
            y += (y1>y0?1:-1); 
            error2 -= dx*2; 
        } 
    } 
} 

而這種實現還可以轉化成下面的這種寫法,就是上面我們提到的方法,更加優雅,不需要特判條件,使用統一變數。

實現

void Segment::draw(const Point2i begin, const Point2i end, const RGBPixel& color, BMPImage& image)
{
    int x0 = begin.x;
    int y0 = begin.y;
    int x1 = end.x;
    int y1 = end.y;

    int dx = abs(x1 - x0);
    int dy = abs(y1 - y0);
    // 決定決策會落在座標系四個方位中的哪一個
    int sx = (x0 < x1) ? 1 : -1;
    int sy = (y0 < y1) ? 1 : -1;
    // 維護error,用於決策下一個點的位置
    int error = dx - dy;

    while (true)
    {
        image.set(x0, y0, color);

        if (x0 == x1 && y0 == y1)
            break;
        int e2 = 2 * error;
        // 每次迭代會進行兩次決策,共同決定下一個點是在一個角落中的哪一個
        // 如果在x軸上的誤差較大
        if (e2 > -dy)
        {
            error -= dy;
            x0 += sx;
        }
        // 如果在y軸上的誤差較大
        if (e2 < dx)
        {
            error += dx;
            y0 += sy;
        }
    }
}

誤差判別的依據,B,C點

最後記得,反轉一下y軸,因為bmp影像中的y軸方向是向下的

結果

Reference

  1. # Lesson 1: Bresenham’s Line Drawing Algorithm
  2. Bresenham.pdf
  3. # wiki Bresenham's line algorithm

相關文章