在我們內部開發使用的一個工具中,我們需要幾乎從 0 開始實現一個高效的二維圖像渲染引擎。比較幸運的是,我們只需要畫直線、圓以及矩形,其中比較復雜的是畫直線和圓。畫直線和圓已經有非常多的成熟的算法了,我們用的是Bresenham的算法。
計算機是如何畫直線的?簡單來說,如下圖所示,真實的直線是連續的,但我們的計算機顯示的精度有限,不可能真正顯示連續的直線,於是我們用一系列離散化后的點(像素)來近似表現這條直線。

(上圖來自於互聯網絡,《計算機圖形學的概念與方法》柳朝陽,鄭州大學數學系)
接下來的問題就是如何盡可能高效地找到這些離散的點,Bresenham直線算法就是一個非常不錯的算法。
Bresenham直線算法是用來描繪由兩點所決定的直線的算法,它會算出一條線段在 n 維光柵上最接近的點。這個算法只會用到較為快速的整數加法、減法和位元移位,常用於繪制電腦畫面中的直線。是計算機圖形學中最先發展出來的算法。
(引自wiki百科布雷森漢姆直線演算法)
這個算法的流程圖如下:

可以看到,算法其實只考慮了斜率在 0 ~ 1 之間的直線,也就是與 x 軸夾角在 0 度到 45 度的直線。只要解決了這類直線的畫法,其它角度的直線的繪制全部可以通過簡單的坐標變換來實現。
下面是一個C語言實現版本。
// 交換整數 a 、b 的值 inline void swap_int(int *a, int *b) { *a ^= *b; *b ^= *a; *a ^= *b; } // Bresenham's line algorithm void draw_line(IMAGE *img, int x1, int y1, int x2, int y2, unsigned long c) { // 參數 c 為顏色值 int dx = abs(x2 - x1), dy = abs(y2 - y1), yy = 0; if (dx < dy) { yy = 1; swap_int(&x1, &y1); swap_int(&x2, &y2); swap_int(&dx, &dy); } int ix = (x2 - x1) > 0 ? 1 : -1, iy = (y2 - y1) > 0 ? 1 : -1, cx = x1, cy = y1, n2dy = dy * 2, n2dydx = (dy - dx) * 2, d = dy * 2 - dx; if (yy) { // 如果直線與 x 軸的夾角大於 45 度 while (cx != x2) { if (d < 0) { d += n2dy; } else { cy += iy; d += n2dydx; } putpixel(img, cy, cx, c); cx += ix; } } else { // 如果直線與 x 軸的夾角小於 45 度 while (cx != x2) { if (d < 0) { d += n2dy; } else { cy += iy; d += n2dydx; } putpixel(img, cx, cy, c); cx += ix; } } }
可以看到,在畫線的循環中,這個算法只用到了整數的加法,所以可以非常的高效。
接下來,我們再來看一看Bresenham畫圓算法。
Bresenham畫圓算法又稱中點畫圓算法,與Bresenham 直線算法一樣,其基本的方法是利用判別變量來判斷選擇最近的像素點,判別變量的數值僅僅用一些加、減和移位運算就可以計算出來。為了簡便起見,考慮一個圓 心在坐標原點的圓,而且只計算八分圓周上的點,其余圓周上的點利用對稱性就可得到。
為什么只計算八分圓周上的點就可以了呢?和上面的直線算法類似,圓也有一個“八對稱性”,如下圖所示。

顯然,我們只需要知道了圓上的一個點的坐標 (x, y) ,利用八對稱性,我們馬上就能得到另外七個對稱點的坐標。
和直線算法類似,Bresenham畫圓算法也是用一系列離散的點來近似描述一個圓,如下圖。

(上圖來自於互聯網絡,《計算機圖形學的概念與方法》柳朝陽,鄭州大學數學系)
Bresenham畫圓算法的流程圖如下。

可以看到,與畫線算法相比,畫圓的循環中用到了整數的乘法,相對復雜了一些。
下面是一個C語言實現版本。
// 八對稱性 inline void _draw_circle_8(IMAGE *img, int xc, int yc, int x, int y, unsigned long c) { // 參數 c 為顏色值 putpixel(img, xc + x, yc + y, c); putpixel(img, xc - x, yc + y, c); putpixel(img, xc + x, yc - y, c); putpixel(img, xc - x, yc - y, c); putpixel(img, xc + y, yc + x, c); putpixel(img, xc - y, yc + x, c); putpixel(img, xc + y, yc - x, c); putpixel(img, xc - y, yc - x, c); } //Bresenham's circle algorithm void draw_circle(IMAGE *img, int xc, int yc, int r, int fill, unsigned long c) { // (xc, yc) 為圓心,r 為半徑 // fill 為是否填充 // c 為顏色值 // 如果圓在圖片可見區域外,直接退出 if (xc + r < 0 || xc - r >= img->w || yc + r < 0 || yc - r >= img->h) return; int x = 0, y = r, yi, d; d = 3 - 2 * r; if (fill) { // 如果填充(畫實心圓) while (x <= y) { for (yi = x; yi <= y; yi ++) _draw_circle_8(img, xc, yc, x, yi, c); if (d < 0) { d = d + 4 * x + 6; } else { d = d + 4 * (x - y) + 10; y --; } x++; } } else { // 如果不填充(畫空心圓) while (x <= y) { _draw_circle_8(img, xc, yc, x, y, c); if (d < 0) { d = d + 4 * x + 6; } else { d = d + 4 * (x - y) + 10; y --; } x ++; } } }
可以看到,Bresenham畫圓算法(中點圓算法)的實現也非常簡單。在另一個項目中,我還有一個 Python 實現的版本,不過那段代碼和其余部分結合得比較緊,有點難摳,這次就不貼出來了。
