3

Bresenham 的线条绘制算法是众所周知的,而且实现起来非常简单。

虽然有更高级的方法来绘制抗锯齿线,但我有兴趣编写一个基于浮点坐标绘制单个像素宽度的非抗锯齿线的函数。

这意味着虽然第一个和最后一个像素将保持不变,但在它们之间绘制的像素将基于两个端点的子像素位置产生偏差。

原则上这不应该那么复杂,因为我假设它可以使用子像素偏移来计算error绘制线时使用的初始值,并且算法的所有其他部分保持不变。

无子像素偏移:

X###
    ###X

假设右手点有一个靠近顶部的亚像素位置,这条线可能如下所示:

以亚像素偏移为例:

X######
       X

是否有一种经过验证的方法来绘制一条考虑到亚像素坐标的线?


笔记:

  • 这似乎是一个常见的操作,例如,我已经看到 OpenGL 驱动程序考虑了这一点 - 使用GL_LINE,尽管通过快速搜索我没有在线找到任何答案 - 可能使用了错误的搜索词?
  • 乍一看,这个问题看起来可能与以下内容重复:
    精确的亚像素线绘制算法(光栅化算法)
    然而,这是关于绘制宽线的问题,这是关于偏移单个像素线的问题。
  • 如果没有一些标准方法,我会尝试将其写下来作为答案发布。
4

3 回答 3

6

刚刚遇到同样的挑战,我可以确认这是可能的,如您所料。

首先,回到算法的最简单形式:(忽略分数;它们稍后会消失)

x = x0
y = y0
dx = x1 - x0
dy = y1 - y0
error = -0.5
while x < x1:
    if error > 0:
        y += 1
        error -= 1
    paint(x, y)
    x += 1
    error += dy/dx

这意味着对于整数坐标,我们从像素边界 ( error = -0.5) 上方的半个像素开始,并且对于我们前进的每个像素x,我们将理想的 y 坐标(因此当前的 y 坐标error)增加dy/dx

首先让我们看看如果我们停止强制x0,y0和为整数会发生什么:(这x1y1将假设坐标不是使用像素中心,而是相对于每个像素的左下角1,因为一旦您支持子像素位置您可以简单地将像素宽度的一半添加到 x 和 y 以返回以像素为中心的逻辑)

x = x0
y = y0
dx = x1 - x0
dy = y1 - y0
error = (0.5 - (x0 % 1)) * dy/dx + (y0 % 1) - 1
while x < x1:
    if error > 0:
        y += 1
        error -= 1
    paint(x, y)
    x += 1
    error += dy/dx

唯一的变化是初始误差计算。当 x 位于像素中心时,新值来自简单的 trig 计算 y 坐标。值得注意的是,您可以使用相同的想法将线条的起始位置剪辑在某个范围内,这是您在开始优化事物时可能面临的另一个挑战。

现在我们只需要将其转换为纯整数算术。我们需要一些固定的小数输入 ( scale) 的乘数,并且可以通过将它们相乘来处理除法,就像标准算法一样。

# assumes x0, y0, x1 and y1 are pre-multiplied by scale
x = x0
y = y0
dx = x1 - x0
dy = y1 - y0
error = (scale - 2 * (x0 % scale)) * dy + 2 * (y0 % scale) * dx - 2 * dx * scale
while x < x1:
    if error > 0:
        y += scale
        error -= 2 * dx * scale
    paint(x / scale, y / scale)
    x += scale
    error += 2 * dy * scale

请注意xy和保持dxdy输入变量 ( ) 相同的比例因子scale,而error具有更复杂的比例因子:2 * dx * scale。这允许它吸收原始公式中的除法和分数,但这意味着我们需要在使用它的任何地方应用相同的比例。

显然这里有很大的优化空间,但这是基本算法。如果我们假设规模是 2 的幂 ( 2^n),我们可以开始让事情变得更有效率:

dx = x1 - x0
dy = y1 - y0
mask = (1 << n) - 1
error = (2 * (y0 & mask) - (2 << n)) * dx - (2 * (x0 & mask) - (1 << n)) * dy
x = x0 >> n
y = y0 >> n
while x < (x1 >> n):
    if error > 0:
        y += 1
        error -= 2 * dx << n
    paint(x, y)
    x += 1
    error += 2 * dy << n

与原始版本一样,这只适用于 (x >= y, x > 0, y >= 0) 八分圆。通常的规则适用于将其扩展到所有情况,但请注意,由于坐标不再以像素为中心(即反射变得更加复杂),因此会出现一些额外的问题。

您还需要注意整数溢出:error输入变量的精度是输入变量的两倍,范围最多是行长的两倍。相应地规划您的输入、精度和变量类型!

1:坐标相对于最接近 0,0 的角。对于左下角的 OpenGL 样式坐标系,但根据您的特定情况,它可能是左上角。

于 2017-01-25T23:58:13.803 回答
3

我遇到了类似的问题,除了需要子像素端点外,我还需要确保绘制与线相交的所有像素。

我不确定我的解决方案是否会对 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)。取哪个像素的规则是一样的。如您所见,红色向量在蓝色向量的右侧。因此,下一个像素将是绿色像素的右侧。

需要为每个像素更新叉积的值,具体取决于您采用的像素。 dx dy

如果下一个像素是 (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 项目中稍微修改的。

于 2021-03-11T01:52:36.740 回答
2

假设您要从所有数字都是浮点像素坐标P1 = (x1, y1)的地方画一条线。P2 = (x2, y2)

  1. 计算 和 的真实像素坐标P1P2绘制它们:P* = (round(x), round(y))

  2. 如果abs(x1* - x2*) <= 1 && abs(y1* - y2*) <= 1那你就完了。

  3. 判断是横线(真)还是竖线(假)abs(x1 - x2) >= abs(y1 - y2):。

  4. 如果它是一条水平线x1 > x2或者如果它是一条垂直线,并且:与(也与)y1 > y2交换。P1P2P1*P2*

  5. 如果它是一条水平线,您可以使用以下公式获得所有 x 坐标之间的 y 坐标x1*x2*

     y(x) = round(y1 + (x - x1) / (x2 - x1) * (y2 - y1))
    

    如果您有一条垂直线,则可以使用以下公式获得所有 y 坐标的 xy1*坐标y2*

     x(y) = round(x1 + (y - y1) / (y2 - y1) * (x2 - x1))
    

这是一个你可以玩的演示,你可以在第 12 行尝试不同的点。

于 2016-12-17T12:40:40.157 回答