优雅/干净(特殊情况)直线网格遍历算法?

Posted

技术标签:

【中文标题】优雅/干净(特殊情况)直线网格遍历算法?【英文标题】:Elegant/Clean (special case) Straight-line Grid Traversal Algorithm? 【发布时间】:2011-03-15 02:16:10 【问题描述】:

我正在清理我的一个旧项目。它必须做的一件事是 - 给定一个笛卡尔网格系统,以及网格上的两个正方形,找到连接这两个正方形中心的线将通过的所有正方形的列表。

这里的特殊情况是所有起点和终点都被限制在正方形/单元格的确切中心。

这里有一些示例——带有成对的示例起点和终点。阴影方块是相应函数调用应返回的方块

删除了无效的 ImageShack 链接 - 示例

起点和终点由它们所在的方格表示。在上图中,假设左下角是[1,1],那么右下角的线将被标识为[6,2][9,5] .

即从左数第六列的(中心)正方形,从底部第二行到(中心)正方形 左起第九列,倒数第五行

这看起来并不那么复杂。不过,我好像在网上找到了一些复杂的算法并实现了。

我确实记得它非常非常快。例如,针对每帧数百或数千次优化。

基本上,它沿着线(线与网格线相交的点)从正方形的边界跳到边界。它通过查看哪个交叉点更近(水平交叉点或垂直交叉点)知道下一个交叉点在哪里,然后移动到下一个交叉点。

这在概念上还可以,但实际的实现结果却不是那么漂亮,而且我担心优化级别可能对于我实际需要的东西来说太高了(我是调用这个遍历算法可能一分钟五六次)。

有没有简单易懂、透明的直线网格遍历算法?

在程序化方面:

def traverse(start_point,end_point)
  # returns a list of all squares that this line would pass through
end

其中给定的坐标标识了方块本身。

一些例子:

traverse([0,0],[0,4])
# => [0,0], [0,1], [0,2], [0,3], [0,4]
traverse([0,0],[3,2])
# => [0,0], [0,1], [1,1], [2,1], [2,2], [3,2]
traverse([0,0],[3,3])
# => [0,0], [1,1], [2,2], [3,3]

请注意,直接通过拐角的线不应包括线“翼”上的正方形。

(Good ol' Bresenham's 可能在这里工作,但它与我想要的有点倒退。据我所知,为了使用它,我基本上必须将它应用到生产线上,然后扫描网格上的每个方格以判断真假。对于大型网格来说,这是不可行的——或者至少是不优雅的)

(由于我的误解,我正在重新研究 Bresenham 和基于 Bresenham 的算法)


为了澄清,一个可能的应用是,如果我将所有对象存储在区域(网格)内的游戏中,并且我有一条射线,并且想查看射线接触到哪些对象。使用这个算法,我可以只测试给定区域内的对象,而不是地图上的每个对象。

在我的应用程序中 实际 的使用是每个图块都有与之相关的效果,并且对象每转都会通过给定的线段。在每一个转弯处,都需要检查对象经过了哪些方格,因此需要对对象应用哪些效果。

请注意,在这一点上,我当前的实现确实有效。这个问题主要是出于好奇的目的。对于这样一个简单的问题,必须有一种更简单的方法......不知何故......


我到底在寻找什么?概念上/整洁干净的东西。另外,我已经意识到,由于我确切指定的内容,所有起点和终点都将始终位于正方形/单元格的中心;所以也许利用这一点的东西也会很整洁。

【问题讨论】:

您希望线的末端在哪里 - 正方形的角(如果是,哪些角)或中心? @a_m0d 感谢您指出歧义;我添加了一张图片以进行澄清。我的意思是中心。 感谢您的澄清和图片 - 让它更清晰 【参考方案1】:

你想要的是一个超级覆盖的特殊情况,它是一个几何对象相交的所有像素。对象可能是一条线或一个三角形,并且可以推广到更高的维度。

无论如何,here is one implementation for line segments。该页面还将超级封面与 Bresenham 算法的结果进行了比较——它们是不同的。 (来源:free.fr)

我不知道你是否认为那里的算法优雅/干净,但它确实看起来很简单,可以调整代码并转移到项目的其他部分。

顺便说一句,您的问题暗示 Bresenham 的算法对于大型网格效率不高。这不是真的 - 它只生成线上的像素。您不必对网格上的每个像素都进行真/假测试。

更新 1: 我注意到图片中有两个“额外”的蓝色方块,我相信这条线没有通过。其中一个与“此算法”中的“h”相邻。我不知道这是否反映了算法或图表中的错误(但请参阅下面的@kikito 评论)。

一般来说,“困难”的情况可能是直线恰好通过一个网格点。我推测,如果您使用浮点数,那么在这些情况下,浮点错误可能会搞砸您。换句话说,算法可能应该坚持整数运算。

更新 2: Another implementation。

【讨论】:

+1 特别是对布雷森纳姆的观察。 Bresenham 是适合这项工作的工具(可能包括抗锯齿版本)。 感谢您指出有关布雷森纳姆的信息。我对它的理解是有缺陷的(我认为这是一种测试像素是否是给定行的一部分的方法)。但我现在正在研究替代算法:) 我看过了;老实说,我不相信论文中的算法与我已经拥有的算法相比有很多优雅(参见a_m0d's answer),但我会研究超级覆盖算法。 @Justin L:我同意这个算法看起来有点乱,缺乏简单的解释。但它避免了浮点(这可能会弄乱直线穿过网格点的情况)。 “我注意到图片中有两个“额外”的蓝色方块,我相信这条线没有通过”我认为这是作者在页面末尾提出的,他在那里说“这里我们假设如果线通过一个角,两个正方形都被绘制。如果你想删除它,你可以简单地删除处理角的 else 部分”。【参考方案2】:

可以在here 找到有关此主题的论文。这是关于光线追踪的,但这似乎与你所追求的非常相关,我认为你将能够使用它。

还有另一篇论文here,处理类似的事情。

这两篇论文都链接在Jakko Bikker 的优秀tutorials on raytracing 中的part 4(其中还包括他的源代码,因此您可以浏览/检查他的实现)。

【讨论】:

不幸的是,第一篇论文似乎正是我已经在使用的算法。它做的一切都很好;我只是想找一个可能更优雅一点的。我在第二篇论文中找不到任何相关内容 =/ 但感谢您的建议 @Justin - 抱歉 :( - 写出完全相同的论文的机会有多大? 哈哈可能很小。但这只意味着你和我的想法是一致的。【参考方案3】:

对于您的问题,有一个非常简单的算法可以在线性时间内运行:

    给定两个点 A 和 B,确定线 (A, B) 与位于此区间内的网格中每条垂直线的交点。 在包含 A 和 B 的单元格内从点 1 开始/结束插入两个特殊的交点。 将每两个连续的交点解释为轴对齐矩形的最小和最大向量,并标记位于该矩形内的所有网格单元(这很容易(两个轴对齐的矩形的交点),特别是考虑到矩形宽度为 1,因此只占用网格的 1 列)
示例:
+------+------+------+------+
|      |      |      |      |
|      |      | B    *      |
|      |      |/     |      |
+------+------*------+------+
|      |     /|      |      |
|      |    / |      |      |
|      |   /  |      |      |
+------+--/---+------+------+
|      | /    |      |      |
|      |/     |      |      |
|      *      |      |      |
+-----/+------+------+------+
|    / |      |      |      |
*   A  |      |      |      |
|      |      |      |      |
+------+------+------+------+ 

“A”和“B”是终止由“/”表示的线的点。 “*”标记线与网格的交点。需要两个特殊的交点来标记包含 A 和 B 的单元格并处理 A.x == B.x 等特殊情况

优化的实现需要 Θ(|B.x - A.x| + |B.y - A.y|) 行 (A, B) 的时间。此外,如果对实现者来说更容易的话,可以编写这种算法来确定与水平网格线的交点。

更新:边境案件

正如brainjam 在他的回答中正确指出的那样,当一条线完全穿过一个网格点时,困难的情况就是那些。让我们假设这种情况发生并且浮点算术运算正确地返回具有整数坐标的交点。在这种情况下,所提出的算法只标记正确的单元格(由 OP 提供的图像指定)。

但是,浮点错误迟早会发生并产生不正确的结果。在我看来,即使使用 double 也是不够的,应该切换到 Decimal 数字类型。优化的实现将对该数据类型执行 Θ(|max.x - min.x|) 加法,每个加法都需要 Θ(log max.y) 时间。这意味着在最坏的情况下(行 ((0, 0), (N, N)) 具有巨大的 N (> 106),算法降级为 O(N log N) 最坏情况即使在垂直/水平网格线交叉点检测之间切换取决于线的斜率(A,B)在最坏的情况下也无济于事,但在一般情况下确实有帮助 - 我只会考虑实现这样的如果分析器将 Decimal 操作产生为瓶颈,则切换。

最后,我可以想象一些聪明的人可能会想出一个 O(N) 的解决方案来正确处理这种边界情况。欢迎您提出所有建议!

修正

brainjam 指出,即使十进制数据类型可以表示任意精度的浮点数,它也不令人满意,因为例如 1/3 可以' t 被正确表示。因此应该使用分数数据类型,它应该能够正确处理边界情况。谢谢你的脑筋急转弯! :)

【讨论】:

+1。好的!我认为所有这些算法可能必须小心处理浮点错误,以避免交叉点恰好在网格点上时出现问题(但错误使其稍微偏离网格点)。 @Justin L. LOL,我刚刚注意到,我提出的算法是相同的,a_m0d 在他的答案中链接到(我跳过了第一个“这里”)并且它是你已经使用了。你不喜欢你的算法什么?我可以想象一个漂亮的实现——不幸的是,不是所有的东西都可以是单行的。也许一些抽象可以帮助^^或开始代码高尔夫xD。 这实际上与 a_m0d 答案中的论文在概念上有所不同;它通过正方形,注意水平 垂直轴的交叉点。但我喜欢这样的概括,即它必须与 y 交点之间的所有正方形相交。 @Dave,如果您指的是 Python Decimal 类型,我认为它不会给您带来太多好处。 1/3 无法准确表示。但是,Python Fraction 类型应该可以解决问题。 @brainjam 我指的是伪十进制数类型,它可以表示任意精度的浮点数,例如 java 的 BigDecimal。但是,是的,你是对的,分数类型更适合该任务,因为它应该能够处理边界情况。【参考方案4】:

这是一个简单的 Python 实现,使用 numpy。然而,这里使用的只是二维向量和逐个分量的操作,这是相当常见的。结果对我来说看起来很优雅(~20 loc without cmets)。

这并不通用,因为它假设图块以整数坐标为中心,而分隔线出现在每个整数加上二分之一(0.5、1.5、2.5 等)处。这允许使用舍入从世界坐标获取瓦片整数坐标(在您的特殊情况下实际上不需要)并给出幻数0.5 以确定我们何时到达最后一个瓦片。

最后,请注意,此算法不处理在交叉点处与网格完全交叉的点。

import numpy as np

def raytrace(v0, v1):
    # The equation of the ray is v = v0 + t*d
    d = v1 - v0
    inc = np.sign(d)  # Gives the quadrant in which the ray progress

    # Rounding coordinates give us the current tile
    tile = np.array(np.round(v0), dtype=int)
    tiles = [tile]
    v = v0
    endtile = np.array(np.round(v1), dtype=int)

    # Continue as long as we are not in the last tile
    while np.max(np.abs(endtile - v)) > 0.5:
        # L = (Lx, Ly) where Lx is the x coordinate of the next vertical
        # line and Ly the y coordinate of the next horizontal line
        L = tile + 0.5*inc

        # Solve the equation v + d*t == L for t, simultaneously for the next
        # horizontal line and vertical line
        t = (L - v)/d

        if t[0] < t[1]:  # The vertical line is intersected first
            tile[0] += inc[0]
            v += t[0]*d
        else:  # The horizontal line is intersected first
            tile[1] += inc[1]
            v += t[1]*d

        tiles.append(tile)

    return tiles

【讨论】:

以上是关于优雅/干净(特殊情况)直线网格遍历算法?的主要内容,如果未能解决你的问题,请参考以下文章