我遇到了类似的问题,除了需要子像素端点外,我还需要确保绘制与线相交的所有像素。
我不确定我的解决方案是否会对 OP 有所帮助,因为它已经 4 年多了,而且因为这句话“这意味着第一个和最后一个像素将保持不变......”对我来说,这实际上是一个问题(稍后会详细介绍)。希望这对其他人有帮助。
我不知道这是否可以被认为是 Bresenham 的算法,但它非常相似。我将为 (+,+) 象限解释它。假设您希望在宽度为W的像素网格上画一条从点 (P x ,P y ) 到 (Q x ,Q y ) 的线。网格宽度W > 1 允许子像素端点。
对于进入 (+,+) 象限的线,起点很容易计算,只需取 (P x ,P y ) 的底即可。正如您稍后将看到的,这仅在 Q x >= P x & Q y >= P y时有效。
现在您需要找到下一个要转到的像素。有 3 种可能性:(x+1,y), (x,y+1), & (x+1,y+1)。为了做出这个决定,我使用定义为的 2D 叉积:
- 如果该值为负,则向量b是向量a的右/顺时针方向。
- 如果该值为正,则向量b是向量a的左/逆时针方向。
- 如果该值为零,则向量b指向与向量a相同的方向。
要决定下一个像素,请比较线P - Q [下图中的红色] 与点P和右上角像素 (x+1,y+1) [蓝色在下图中]。
P和右上角像素
之间的向量可以计算为:
因此,我们将使用 2D 叉积的值:
- 如果此值为负,则下一个像素将是 (x,y+1)。
- 如果该值为正,则下一个像素将是 (x+1,y)。
- 如果该值正好为零,则下一个像素将是 (x+1,y+1)。
这适用于起始像素,但其余像素将没有位于它们内部的点。幸运的是,在初始点之后,您不需要一个点位于蓝色矢量的像素内。您可以像这样继续扩展它:
蓝色向量从线条的起点开始,并针对每个像素更新为 (x+1,y+1)。取哪个像素的规则是一样的。如您所见,红色向量在蓝色向量的右侧。因此,下一个像素将是绿色像素的右侧。
需要为每个像素更新叉积的值,具体取决于您采用的像素。
如果下一个像素是 (x+1),则添加d x ,如果像素是 (y+1),则添加d y。如果像素达到 (x+1,y+1),则将两者相加。
重复该过程,直到到达结束像素(Q x / W,Q y / 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。因为W为 4,所以有 4x4 红点,使像素尺寸为 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;
// Perform ceiling/flooring of the pixel endpoints
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 项目中稍微修改的。