(注:本小節不是對劃線演算法事無鉅細的證明,如果你需要更加系統的學習,請跳轉至文末的參考部分)
如果你是一名曾經學習過圖形學基礎的學生,那麼你一定對畫線演算法稔熟於心,中點劃線演算法,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的值是否需要改變。
這個值的變化由直線方程給出,在這裡我們僅使用begin
和end
給出。(具體論證請翻閱參考)
還記得我們的假設場景麼?在這個場景下:
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
那麼對於其他場景呢?
begin.x
>end.x
- slope < 0 or slope > 1
- 平行於座標軸的方向
解決方案:
- 只需要將begin和end兩個點做一下交換即可
- slope < 0 or slope > 1
- slope < 0只需要對begin和end加上符號,最後set的時候再變回來即可
- slope > 1這個更加簡單,只需要交換xy即可
- 平行於軸體的方向只需要特判一下進行處理,而且這樣效率更高
實現
實際上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
- # Lesson 1: Bresenham’s Line Drawing Algorithm
- Bresenham.pdf
- # wiki Bresenham's line algorithm