我遇到了类似的问题,需要添加亚像素端点,还需要确保
所有与线相交的像素都被绘制。我不确定我的解决方案是否对OP有帮助,因为已经过去了4年以上,而且还有这样一句话:“这意味着第一个和最后一个像素将保持不变……”对我来说,这实际上是个问题(稍后会详细说明)。希望这对其他人有所帮助。
我不确定这是否可以被视为Bresenham算法,但它非常相似。我将解释一下第一象限的情况。假设你希望在像素网格上从点(Px,Py)到(Qx,Qy)绘制一条直线,网格宽度为W。网格宽度W > 1允许子像素端点。
对于在第一象限中的直线,起始点很容易计算,只需取(Px,Py)的floor值。如后面所述,这仅适用于Qx >= Px & Qy >= Py。
现在你需要找到下一个要去的像素。有三种可能性:(x+1,y), (x,y+1), & (x+1,y+1)。为了做出这个决定,我使用定义如下的二维叉积:
- 如果这个值是负数,向量b在向量a的右侧/顺时针方向。
- 如果这个值是正数,向量b在向量a的左侧/逆时针方向。
- 如果这个值是零,向量b和向量a指向相同的方向。
为了决定下一个像素点,比较线段P-Q [下图中的红色]与点P和右上角像素(x+1,y+1)之间的连线(下图中的蓝色)的叉积。
P和右上角像素之间的向量可以计算如下:
因此,我们将使用二维叉积的值:
- 如果这个值是负数,下一个像素将是 (x,y+1)。
- 如果这个值是正数,下一个像素将是 (x+1,y)。
- 如果这个值恰好为零,下一个像素将是 (x+1,y+1)。
这对于起始像素来说很好,但其他像素将没有一个点在其内部。幸运的是,在初始点之后,对于蓝色向量,您不需要一个点在像素内部。您可以像这样继续扩展它:
蓝色向量从线的起始点开始,并更新为每个像素的(x+1,y+1)。选择哪个像素的规则相同。如您所见,红色向量在蓝色向量的右侧。因此,下一个像素将是绿色像素的右侧像素。
每个像素的叉积值需要根据您选择的像素进行更新。
如果下一个像素是(x+1),则添加
dx,如果像素是(y+1),则添加
dy。如果像素到达(x+1,y+1),则两者都要添加。
重复此过程,直到到达结束像素(Qx / W, Qy / W)。
综合起来,得到以下代码:
int dx = x2 - x2;
int dy = y2 - y1;
int local_x = x1 % width;
int local_y = y1 % width;
int cross_product = dx*(width-local_y) - dy*(width-local_x);
int dx_cross = -dy*width;
int dy_cross = dx*width;
int x = x1 / width;
int y = y1 / width;
int end_x = x2 / width;
int end_y = y2 / width;
while (x != end_x || y != end_y) {
SetPixel(x,y,color);
int old_cross = cross_product;
if (old_cross >= 0) {
x++;
cross_product += dx_cross;
}
if (old_cross <= 0) {
y++;
cross_product += dy_cross;
}
}
让它适用于所有象限是通过反转本地坐标和一些绝对值来实现的。以下是适用于所有象限的代码:
int dx = x2 - x1;
int dy = y2 - y1;
int dx_x = (dx >= 0) ? 1 : -1;
int dy_y = (dy >= 0) ? 1 : -1;
int local_x = x1 % square_width;
int local_y = y1 % square_width;
int x_dist = (dx >= 0) ? (square_width - local_x) : (local_x);
int y_dist = (dy >= 0) ? (square_width - local_y) : (local_y);
int cross_product = abs(dx) * abs(y_dist) - abs(dy) * abs(x_dist);
dx_cross = -abs(dy) * square_width;
dy_cross = abs(dx) * square_width;
int x = x1 / square_width;
int y = y1 / square_width;
int end_x = x2 / square_width;
int end_y = y2 / square_width;
while (x != end_x || y != end_y) {
SetPixel(x,y,color);
int old_cross = cross_product;
if (old_cross >= 0) {
x += dx_x;
cross_product += dx_cross;
}
if (old_cross <= 0) {
y += dy_y;
cross_product += dy_cross;
}
}
然而,存在一个问题!在某些情况下,这段代码不会停止。要理解为什么,您需要仔细研究哪些条件被视为线和像素之间的交点。
何时会准确地绘制像素?
我说过,我需要绘制所有与线相交的像素。但是在边缘情况下存在一些歧义。
以下是在Q
x >= P
x & Q
y >= P
y的情况下,绘制像素的所有可能交点的列表:
- A - 如果一条线完全穿过了像素,那么该像素就会被绘制。
- B - 如果一条垂直的线完全穿过了像素,那么该像素就会被绘制。
- C - 如果一条水平的线完全穿过了像素,那么该像素就会被绘制。
- D - 如果一条垂直的线恰好触及像素的左边,那么该像素就会被绘制。
- E - 如果一条水平的线恰好触及像素的底部,那么该像素就会被绘制。
- F - 如果一条线段的端点从像素内部开始沿着(+,+)方向前进,那么该像素就会被绘制。
- G - 如果一条线段的端点恰好位于像素的左侧并且朝着(+,+)方向前进,那么该像素就会被绘制。
- H - 如果一条线段的端点恰好位于像素的底部并且朝着(+,+)方向前进,那么该像素就会被绘制。
- I - 如果一条线段的端点恰好位于像素的左下角并且朝着(+,+)方向前进,那么该像素就会被绘制。
这里有一些像素与该线段不相交:
A' - 如果一条线显然不会与像素相交,那么像素不会被绘制。
B' - 如果一条垂直线显然不会与像素相交,那么像素不会被绘制。
C' - 如果一条水平线显然不会与像素相交,那么像素不会被绘制。
D' - 如果一条垂直线恰好接触像素的右侧,那么像素不会被绘制。
E' - 如果一条水平线恰好接触像素的顶部,那么像素不会被绘制。
F' - 如果一条线段的端点恰好从像素的右上角开始沿着(+,+)方向前进,那么像素不会被绘制。
G' - 如果一条线段的端点恰好从像素的顶部开始沿着(+,+)方向前进,那么像素不会被绘制。
H' - 如果一条线段的端点恰好从像素的右侧开始沿着(+,+)方向前进,那么像素不会被绘制。
I' - 如果一条线段恰好接触像素的任意一个角落,那么像素不会被绘制。这适用于所有角落。
那些规则适用于你所期望的方式(只需翻转图像)适用于其他象限。我需要强调的问题是当端点恰好位于像素边缘时。看看这个例子:
这就像上面的图G',只不过y轴被翻转了,因为Q
y < P
y。有4x4个红点,因为W等于4,使得像素尺寸为4x4。每个4个点都是线条唯一可以触及的端点。所画的线从(1.25, 1.0)到某个地方。
这说明为什么说计算像素端点为线段端点的下取整是不正确的(至少是按照我定义的像素线交点)。该端点的下取整像素坐标似乎为(1,1),但很明显,线条从未真正与该像素相交。它只是接触它,所以我不想画它。
不要将线的端点向下取整,而是需要在x和y维度上将最小端点向下取整,并将最大端点减1向上取整。
因此,这里是完成这种向下/向上取整的完整代码:
int dx = x2 - x1;
int dy = y2 - y1;
int dx_x = (dx >= 0) ? 1 : -1;
int dy_y = (dy >= 0) ? 1 : -1;
int local_x = x1 % square_width;
int local_y = y1 % square_width;
int x_dist = (dx >= 0) ? (square_width - local_x) : (local_x);
int y_dist = (dy >= 0) ? (square_width - local_y) : (local_y);
int cross_product = abs(dx) * abs(y_dist) - abs(dy) * abs(x_dist);
dx_cross = -abs(dy) * square_width;
dy_cross = abs(dx) * square_width;
int x = x1 / square_width;
int y = y1 / square_width;
int end_x = x2 / square_width;
int end_y = y2 / square_width;
if (dy < 0)
{
if ((y1 % square_width) == 0)
{
y--;
cross_product += dy_cross;
}
}
else if (dy > 0)
{
if ((y2 % square_width) == 0)
end_y--;
}
if (dx < 0)
{
if ((x1 % square_width) == 0)
{
x--;
cross_product += dx_cross;
}
}
else if (dx > 0)
{
if ((x2 % square_width) == 0)
end_x--;
}
while (x != end_x || y != end_y) {
SetPixel(x,y,color);
int old_cross = cross_product;
if (old_cross >= 0) {
x += dx_x;
cross_product += dx_cross;
}
if (old_cross <= 0) {
y += dy_y;
cross_product += dy_cross;
}
}
这段代码本身尚未经过测试,但它是从我的
GitHub项目稍作修改后得到的,在该项目中已进行了测试。
e = frac(x1) * (y2 - y1) + frac(y1) * (x2 - x1)
来进行种子填充,尽管使用定点坐标可以避免舍入误差。我的观点是,DDA内循环(基本上是加法和移位)通常比Bresenham更快,缺点是有一个小的初始化开销,除非你的线段非常短或者除法非常昂贵,否则这个开销通常是无关紧要的。 - doynax