Github TinyRenderer渲染器課程實踐記錄 - Bresenham直線繪制算法


Abstract

Bresenham直線繪制算法。

Reference :


作為計算機圖形學中最基礎的畫線段,我們從淺入深地進行探索。

版本1 --- 簡單的線性插值

void line(int x0, int y0, int x1, int y1, TGAImage &image, TGAColor color) { 
    for (float t=0.; t<1.; t+=.01) { 
        int x = x0 + (x1-x0)*t; 
        int y = y0 + (y1-y0)*t; 
        image.set(x, y, color); 
    } 
}

這種算法非常簡單,分別在 \(xy\) 位移方向上進行平均采樣並畫點。采樣率越高(t的步長)直線越精確。


版本2 --- 另一種線性插值

你會發現版本1使用了用戶自定義的采樣率( \(0\)\(1\) ,步長為 \(0.1\) ),這樣有點低效。 若直接選擇 \(x\) 位移方向上的采樣作為線性插值的自變量是否比較自然呢?

void line(int x0, int y0, int x1, int y1, TGAImage &image, TGAColor color) { 
    for (int x=x0; x<=x1; x++) { 
        float t = (x-x0)/(float)(x1-x0); 
        int y = y0 + (y1 - y0)*t; 
        image.set(x, y, color); 
    } 
}

但這仍然存在大問題:當執行以下調用時,第二條線全是孔隙,而第三條根本沒被繪制:

line(13, 20, 80, 40, image, white); 
line(20, 13, 40, 80, image, red); 
line(80, 40, 13, 20, image, red);

我們檢查一下第二條直線有很多孔隙的原因。觀察第二條 line 調用你會發現兩點在 \(y\) 方向上的位移是大於 \(x\) 方向的,這導致的結果是,若執意在位移較小的 \(x\) 方向上進行采樣,采樣率(能取到點的個數)會比較小。

至於第三條線根本沒繪制的原因,是因為第一個點的 \(x\) 值一開始就大於第二個點,所以導致根本不會進入繪制循環 :p


版本3 --- 加入邊界判斷

我們通過一些邊界判斷來修復可能會出現的錯誤。

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); 
    } 
    for (int x=x0; x<=x1; x++) { 
        float t = (x-x0)/(float)(x1-x0); 
        int y = y0*(1.-t) + y1*t; 
        if (steep) { 
            image.set(y, x, color); 
        } else { 
            image.set(x, y, color); 
        } 
    } 
}
  • 第一個判斷塊修復采樣率較低的問題。判斷最大位移方向是否為y方向,若是則分別交換始終點 \(xy\) 順序。假設出現了最大位移為y方向的情況,交換xy位置后,在之后的繪制循環中的采樣方向一定是y方向。

  • 第二個判斷塊修復了始終點順序問題。

  • 繪制循環中,steep為真代表直線較陡的情況,也即最大位移方向為 \(y\) 的情形。此時畫點時需要再次調換一下采樣點 \(xy\) 順序。


中點Bresenam算法

首先我們有了直線兩端點 \(A(x_{0}, y_{0})\)\(B(x_{1}, y_{1})\),並設直線方程為

\(F(x, y) = y - kx - b\),由兩端點容易得到斜率 \(k = \frac{y_{1} - y_{0}}{x_{1} - x_{0}}\)

先提前引入一個事實:將任意一個點帶入\(F(x, y)\)后,若\(F(x, y) > 0\)說明此點在直線上方;若\(F(x, y) < 0\)說明在直線下方;若等於0則表示點剛好落在直線上。


Bresenham的思想就是,每次在最大位移方向上前進一個單位,而在另一個方向上是否前進取決於判別式。至於為什么要選擇最大位移方向,會涉及到直線的反走樣,詳見我

對DDA算法解析的文章數值微分直線生成算法(DDA)

\(0 \leq k \leq 1\)的情形

此時截距\(x_{1} - x_{0} > y_{1} - y_{0}\),也即最大位移方向為X軸正向。因此在計算下一個點\(x_{i+1}\)時,在\(X\)方向行進1,需確定下個點的\(y\)值是否加1:

如圖,我們的下一個理想繪制點其實應該是直線與\(x = x_{i} + 1\)的交點\(Q\),但是由於像素是離散的,僅有整數值,我們只能將這個交點近似地交由\(Q\)上面或下面的離散點來表

示。判斷方法很簡單,看交點\(Q\)在兩個備選點的中點\(M\)的上面還是下面,反過來判斷即為看中點\(M\)\(Q\)的上面還是下面,若在\(Q\)的上面,則下一個點的\(y\)不變;若在\(Q\)下面,

則下一個點的\(y\)移動一個步長。

判別式\(d_{i} = F(x_{M}, y_{M}) = F(x_{i} + 1, y_{i} + 0.5) = y_{i} + 0.5 - k(x_{i} + 1) - b\)

此時可將中點\(M(x_{i} + 1, y_{i} + 0.5)\)帶入\(F(x, y)\),若結果\(d_{i} > 0\),表示點\(M\)\(Q\)之上,此時下一個繪制點確定為\((x_{i} + 1, y_{i})\);反之確定為\((x_{i}+1, y_{i}+1)\);若\(d_{i} = 0\),表示

Q正好和M重合,那么我們默認下一個點取\(y\)不變的那個:

\(x_{i+1} = x_{i} + 1\)

\( \begin{equation}y_{i+1} = \begin{cases} y_{i} + 1 & (d_{i} < 0)\\ y_{i} & (d_{i} \geq 0) \end{cases} \end{equation} \)


判別式的推導

計算下一個繪制點時:

\(d_{i} < 0\)

取右上方點\(P_{u}(x_{i} + 1, y{i} + 1)\)。現在再判斷下下一個點如何計算,即:

\( \begin{equation} \begin{aligned} d_{i+1} &= F(x_{i} + 2, y_{i} + 1.5) \\ &= y_{i} + 1.5 - k(x_{i} + 1) - b - k \\ &= di + 1 - k \end{aligned} \end{equation} \)

也即當\(d_{i} < 0\)時,\(d_{i+1}\)對於\({d_{i}}\)的增量為\(1-k\)


\(d_{i} \geq 0\)

取正右方點\(P_{d}(x_{i} + 1, y{i})\)。現在再判斷下下一個點如何計算,即:

\( \begin{equation} \begin{aligned} d_{i+1} &= F(x_{i} + 2, y_{i} + 0.5) \\ &= y_{i} + 0.5 - k(x_{i} + 1) - b - k \\ &= di - k \end{aligned} \end{equation} \)

也即當\(d_{i} \geq 0\)時,\(d_{i+1}\)對於\({d_{i}}\)的增量為\(-k\)


代碼中當然需要得到\(d_{i}\)的初值。顯然直線的第一個像素\(p_{0}\)一定在直線上,因此可計算:

\( \begin{equation} \begin{aligned} d_{0} &= F(x_{0} + 1, y_{0} + 0.5) \\ &= y_{0} - kx_{0} - b - k + 0.5 \\ &= 0.5 - k \end{aligned} \end{equation} \)

注意這里的推導,由\(p_{0}\)在直線上的原因,\(y_{0} - kx_{0} - b = 0\)

看看我們推出了哪些需要的東西:

\(d_{i} < 0\)時,\(d_{i+1}\)對於\(d_{i}\)的增量\((1)1-k\),進一步寫作\(1-\frac{\Delta y}{\Delta x}\)

\(d_{i} \geq 0\)時,\(d_{i+1}\)對於\(d_{i}\)的增量\((2)-k\),進一步寫作\(-\frac{\Delta y}{\Delta x}\)

以及\(d_{0} = 0.5 - k\),進一步寫作\(0.5 - \frac{\Delta y}{\Delta x}\)

我們只需判斷判別式的正負,且希望盡量用整數來進行運算以加快速度,所以對這三個變量同時乘以\(2\)再乘以\(\Delta x\)來消除浮點運算:

(1) \(2\Delta x-2\Delta y\) (2) \(-2\Delta y\) (3) \(\Delta x - 2\Delta y\)

加入最大位移方向修正后,現在可以用代碼來表示這個算法了(注意,若另一個方向上也即 \(dy\) 方向的起點坐標大於終點坐標,那么這個方向的步長改為-1即可):

void Bresenham(int x0, int y0, int x1, int y1, TGAImage &image, TGAColor color) { 
    // 確保x為最大位移方向
    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 = std::round(fabs(x1 - x0));
    int dY = std::round(fabs(y1 - y0));
    int delta = dX - 2 * dY;

    int dStepUp = 2 * (dX - dY);
    int dStepDown = -2 * dY;

    int x = x0, y = y0;

    for(int i = x; i <= x1; i++) {
        !steep ? image.set(i, y, color)
               : image.set(y, i, color);
        if(delta < 0) {
            y += (y0<y1?1:-1);
            delta += dStepUp;
        } else {
            delta += dStepDown;
        }
    }
} 

此代碼具有較好魯棒性,無論端點順序以及線段斜率如何,都能正確繪制。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM