[AlgorithmStaff] Bresenham快速直線算法
操作系統:Windows8.1
顯卡:Nivida GTX965M
開發工具:Unity2017.3 | NativeC
最近在學習 Unity tilemap Brush 自定義筆刷功能時候,看到其直線筆刷 LineBrush 是采用 Bresenham 算法實現,故借此機會在這裏記錄下學習過程,並在最後給出完整實現。
Introduction
Bresenham 是光柵化的直線算法,或者說是通過像素來模擬直線。比如下圖所示像素點來模擬紅色的直線。
給定兩個起點 P1(x1, y1) | P2(x2, y2),如何繪制兩點之間的連線呢。這裏假設斜率約束在,那麽算法的過程如下:
- 繪制起點 (x1, y1)
- 繪制下一個點, X坐標加1,判斷是否到終點,如果是則算法完成。否則找下一個點,由上圖可知將要繪制的點不是右鄰點,要麽就是右上鄰接點。
- 繪制點。
- 跳回第二步驟。
- 結束。
算法具體過程就是在每次繪制點的時候選取與直線的交點y坐標的差最小的那個點,如下圖所示:
那麽問題聚焦在如何找最近的點,邏輯上每次 x 都遞增 1,y 則增加 1 或不增加。具體上圖,假設已經繪制到了 d1 點,那麽接下來 x 加 1,但是選擇 d2 還是 u 點呢,直觀上可以知道 d2 與目標直線和 x + 1 直線的交點比較近,即縱坐標之差小。換句話說 (x + 1, y + 1) 點縱坐標差大於 0.5。 所以選擇 d2
The Basic Bresenham
假設以 (x, y) 為繪制起點,一般情況下的直觀想法是先求 m = dy /dx( y 的增量),然後逐步遞增 x, 設新的點為 x1 = x + j ,則 y1 = round(y + j * m) 。可以看到,這個過程涉及大量的浮點運算,效率上是比較低的(特別是在嵌入式應用中,DSP可以一周期內完成2次乘法,一次浮點卻要上百個周期)。
下面我們來看一下 Bresenham 算法,如圖1, (x, y +ε) 的下一個點為 (x, y + ε + m), 這裏 ε 為累加誤差。可以看出,當 ε+m < 0.5 時,繪制 (x + 1, y)
ε = ε + m ,如果 (ε + m) <0.5 (或表示為 2 * (ε + m) < 1 )
ε = ε + m – 1,其他情況
將上述公式都乘以 dx ,並將 ε * dx 用新符號 ξ 表示,可得
ξ = ξ + dy,如果 2 * (ξ + dy) < dx
ξ = ξ + dy – dx,其他情況
可以看到,此時運算已經全變為整數了。以下為算法的偽代碼:
ξ ← 0,y ← y1
For x ← x1 to x2 do
Plot Point at (x, y)
If (2(ξ + dy) < dx)
ξ ←ξ + dy
Else
y ← y + 1,ξ ←ξ + dy – dx
End If
End For
Handing multiple slopes
在實際應用中,我們會發現,當 dy > dx 或出現上圖右側情況時,便得不到想要的結果,這是由於我們只考慮 dx > dy, 且 (x, y) 的增量均為正的情況所致。經過分析,需要考慮 8 種不同的分區情況,如下圖所示:
當然,如果直接在算法中對8種情況分別枚舉, 那重復代碼便會顯得十分臃腫,因此在設計算法時必須充分考慮上述各種情況的共性。比如右側 X 正負 45 度分區僅僅是互為 Y 軸鏡像的關系,在具體實現的時候設定正確增量方向即可。
Implementation
下面給出基於C語言的實現:
void draw_line(int x1, int y1, int x2, int y2) { int dx = x2 - x1; int dy = y2 - y1; int ux = ((dx > 0) << 1) - 1; int uy = ((dy > 0) << 1) - 1; int x = x1, y = y1, eps; eps = 0; dx = abs(dx); dy = abs(dy); if (dx > dy) { for (x = x1; x != x2; x += ux) { printf("x = %d y = %d\n", x, y); eps += dy; if ((eps << 1) >= dx) { y += uy; eps -= dx; } } } else { for (y = y1; y != y2; y += uy) { printf("x = %d y = %d\n", x, y); eps += dx; if ((eps << 1) >= dy) { x += ux; eps -= dy; } } } }
測試數據分別為綠色線段,起點 (1,1) 終點 (10,10) 和 藍色線段,起點 (1, 10) 終點 (10,1) 。
測試數據分別為褐色線段,起點 (2,10) 終點 (4,1) 及起點 (6,9) 終點 (10,1) 。
通過將程序運行的測試數據填充的表格後,可以很直觀的看到兩點之間線段的連接路徑。
Based on Unity
Unity 官方的例子中已經給出了基於C#實現,代碼如下:
// http://ericw.ca/notes/bresenhams-line-algorithm-in-csharp.html public static IEnumerable<Vector2Int> GetPointsOnLine(Vector2Int p1, Vector2Int p2) { int x0 = p1.x; int y0 = p1.y; int x1 = p2.x; int y1 = p2.y; bool steep = Math.Abs(y1 - y0) > Math.Abs(x1 - x0); if (steep) { int t; t = x0; // swap x0 and y0 x0 = y0; y0 = t; t = x1; // swap x1 and y1 x1 = y1; y1 = t; } if (x0 > x1) { int t; t = x0; // swap x0 and x1 x0 = x1; x1 = t; t = y0; // swap y0 and y1 y0 = y1; y1 = t; } int dx = x1 - x0; int dy = Math.Abs(y1 - y0); int error = dx / 2; int ystep = (y0 < y1) ? 1 : -1; int y = y0; for (int x = x0; x <= x1; x++) { yield return new Vector2Int((steep ? y : x), (steep ? x : y)); error = error - dy; if (error < 0) { y += ystep; error += dx; } } yield break; }
最後將之前的兩組數據帶入Unity驗證結果一致性:
可以看到前文程序算法輸出的數據結果與Unity采用的 C# 實現結果一致。
Summary
參考資料:
http://blog.csdn.net/jinbing_peng/article/details/44797993
https://www.cnblogs.com/gamesky/archive/2012/08/21/2648623.html
http://www.cnblogs.com/pheye/archive/2010/08/14/1799803.html
[AlgorithmStaff] Bresenham快速直線算法