【问题标题】:Circle covering algorithm with varying radii不同半径的圆覆盖算法
【发布时间】:2014-01-17 23:23:45
【问题描述】:

对于游戏,我正在绘制由 (x,y,r) 三元组序列定义的具有不同半径的数千个随机分布的圆的密集簇。这是一个由 14,000 个圆圈组成的示例图像:

我想到了一些动态效果,例如合并集群,但为了实现这一点,我需要在每一帧重新绘制所有圆圈。

许多(可能 80-90%)绘制的圆圈被后续绘制所覆盖。因此,我怀疑通过预处理,我可以通过消除被覆盖的圆圈来显着加快我的绘制循环。有没有一种算法可以以合理的效率识别它们?

我可以容忍相当多的假阴性(即绘制一些实际上被覆盖的圆圈),只要它不会影响绘图效率。我也可以容忍误报,只要它们几乎是肯定的(例如删除一些仅覆盖 99% 的圆圈)。我也愿意改变圆圈的分布方式,只要看起来还不错。

【问题讨论】:

    标签: algorithm math geometry


    【解决方案1】:

    根据您提供的示例图片,您的圆圈似乎具有近乎恒定的半径。如果它们的半径不能小于大量像素,则可以利用圆的简单几何来尝试图像空间方法。

    假设您将渲染表面划分为正方形网格,以便最小的渲染圆可以像这样适合网格:

    圆的半径是 sqrt(10) 网格单位,并覆盖至少 21 个方格,因此如果您将与任何圆完全重叠的方格标记为已绘制,您将消除大约 21/10pi 的圆表面部分,即大约是 2/3。

    您可以通过正方形here获得一些最佳圆形覆盖的想法

    剔除过程看起来有点像反向画家算法:

    For each circle from closest to farthest
       if all squares overlapped (even partially) by the circle are painted
           eliminate the circle
       else
           paint the squares totally overlapped by the circle
    

    您还可以通过绘制未完全被给定圆圈覆盖的网格正方形(或消除从已绘制表面略微溢出的圆圈)来“作弊”,以增加消除的圆圈数量为代价,但会产生一些误报。

    然后您可以使用 Z 缓冲区算法渲染剩余的圆圈(即让 GPU 完成其余工作)。

    基于 CPU 的方法

    这假设您将网格实现为内存位图,而无需 GPU 的帮助。

    要确定要绘制的正方形,您可以使用基于圆心相对于网格的距离(示例图像中的红叉)和实际圆半径的预计算模式。

    如果直径的相对变化足够小,您可以定义一个二维的图案表,以圆半径和中心距最近网格点的距离为索引。

    检索到正确的模式后,您可以使用简单的对称性将其应用到适当的位置。

    同样的原理可用于检查一个圆是否适合已绘制的表面。

    基于 GPU 的方法

    自从我从事计算机图形工作以来已经有很长时间了,但如果当前的技术允许,您可以让 GPU 为您绘制。

    绘制网格将通过渲染每个缩放以适应网格的圆圈来实现

    检查消除将需要读取包含圆的所有像素的值(缩放到网格尺寸)。

    效率

    网格维度应该有一些最佳点。更密集的网格将覆盖更高百分比的圆形表面,从而消除更多的圆形(更少的误报),但计算成本会增加 o(1/grid_step²)。

    当然,如果渲染的圆可以缩小到大约 1 像素直径,您也可以转储整个算法并让 GPU 完成工作。但与基于 GPU 像素的方法相比,效率随着网格步长的平方而增长。

    在我的示例中使用网格,对于完全随机的一组圆圈,您可能会预期大约 1/3 的假阴性。

    对于您的图片,它似乎定义了体积,应消除 2/3 的前景圆圈和(几乎)所有后向圆圈。剔除 80% 以上的圈子可能是值得的。

    话虽如此,在蛮力计算竞赛中击败 GPU 并不容易,所以我对你可以预期的实际性能提升只有最模糊的概念。不过,尝试一下可能会很有趣。

    【讨论】:

      【解决方案2】:

      这种剔除本质上是隐藏表面算法 (HSA) 所做的 - 尤其是称为“对象空间”的变体。在您的情况下,圆圈的排序顺序为它们提供了有效的恒定深度坐标。它是恒定的这一事实简化了问题。

      HSA 的经典参考是here。我会阅读它的想法。

      受这种想法启发的一个想法是使用“扫描线”算法来考虑每个圆圈,比如一条从上到下移动的水平线。扫描线包含它所接触的一组圆圈。通过按顶部坐标对圆的输入列表进行排序来初始化。

      扫描在“事件”中进行,即每个圆圈的顶部和底部坐标。当达到顶部时,将圆圈添加到扫描中。当它的底部出现时,将其移除(除非它已经如下所述消失)。当一个新的圆圈进入扫描时,考虑它与已经存在的圆圈。您可以将事件保存在最大(y 坐标)堆中,并根据需要延迟添加:下一个输入圆的顶部坐标加上所有扫描线圆的底部坐标。

      进入扫荡的新圈子可以做三件事中的任何一个或全部。

      1. 扫描中较深的模糊圆圈。 (由于我们要确定要绘制的圆,因此该决定的保守方面是使用新圆的最大包含轴对齐框 (BIALB) 来记录每个现有较深圆的遮蔽区域.)

      2. 被其他深度较小的圆圈遮挡。 (这里保守的做法是用彼此相关圈的BIALB来记录新圈的遮挡区域。)

      3. 有未被遮挡的区域。

      必须保持每个圆圈的模糊区域(通常会随着处理更多圆圈而增大),直到扫描线到达其底部。如果在任何时候被遮挡的区域覆盖了整个圆圈,它可以被删除并且永远不会被绘制。

      遮挡区域的记录越详细,算法的效果就越好。矩形区域的联合是一种可能性(例如,参见 Android 的区域代码)。单个矩形是另一个矩形,尽管这可能会导致许多误报。

      同样,也需要一个快速的数据结构来查找扫描线中可能被遮挡和被遮挡的圆圈。包含 BIALB 的区间树可能是好的。

      请注意,在实践中,这样的算法只有在基元数量巨大时才会产生胜利,因为快速图形硬件是如此……快。

      【讨论】:

      • 我不认为保持每个圆圈的遮蔽区域数量的想法会起作用,因为准确(甚至保守地)这样做意味着您需要避免计算新圆圈遮蔽的区域 已经被遮蔽了,这很棘手。更好的解决方案是,对于每个圆,保持一组 SIAAB(最小包括轴对齐框)完全覆盖圆的尚未被遮挡的部分,并在出现时减去覆盖圆的 BIALB。如果一个圈子的 SIAAB 集为空,则它是不可见的,可以删除。
      • 这听起来很有希望,但我不明白细节。据我了解,对于每一对连续的事件(水平线),对于当时扫过的每个圆圈,我都需要找到完全被圆圈覆盖的最大框。如果不出意外,这似乎是非常多的 sqrt 操作,不是吗?另外,Android的区域是什么?它在您链接的论文中吗? (抱歉,我才刚刚开始。)
      【解决方案3】:

      您没有提到 Z 组件,所以我假设它们在您的列表中按 Z 顺序排列并从后到前绘制(即画家算法)。

      正如之前的海报所说,这是一个遮挡剔除练习。

      除了提到的对象空间算法之外,我还会研究屏幕空间算法,例如分层 Z 缓冲区。您甚至不需要 z 值,只需要指示某物是否存在的位标志。

      见:http://www.gamasutra.com/view/feature/131801/occlusion_culling_algorithms.php?print=1

      【讨论】:

        【解决方案4】:

        如果一个圆完全在另一个圆内,那么它的中心之间的距离加上小圆的半径最多就是大圆的半径(画出来给你看!)。因此,您可以检查:

        float distanceBetweenCentres = sqrt((topCircle.centre.x - bottomCircle.centre.x) * (topCircle.centre.x - bottomCircle.centre.x) + (topCircle.centre.y - bottomCircle.centre.y) * (topCircle.centre.y - bottomCircle.centre.y));
        
        if((bottomCircle.radius + distanceBetweenCentres) <= topCircle.radius){
          // The bottom circle is covered by the top circle.
        }
        

        为了提高计算速度,你可以先看看上面的圆的半径是否比下面的圆大,如果没有,就不可能覆盖下面的圆。希望有帮助!

        【讨论】:

        • 在某种程度上,但是如果你有 10,000 个圆圈,这必须检查大约 1/2 * 10,000 * 10,000 对。这就是使这成为一个有趣的问题的原因。更不用说一个圆圈可以被其他几个圆圈所掩盖,所以成对比较没有那么有用
        • 我想在每次添加新圈子时检查一下,至少删除成对的圈子。
        • 我的第一个想法是,虽然这是一个非常简单的测试,但它只能匹配一小部分圆圈。然而,我在我的示例图像上对其进行了测试,我惊讶地发现它消除了 51% 的绿色圆圈和 20% 的紫色圆圈。所以这毕竟是值得的,至少作为第一次通过。
        • 太棒了!添加新圈子时,这绝对是一个快速检查。还有很多更强大的解决方案,这将受益于减少总数。
        【解决方案5】:

        这是一个简单的算法:

        1. N 圆圈插入四叉树(首先是底部圆圈)
        2. 对于每个像素,使用四叉树确定最顶层的圆(如果存在)
        3. 用圆的颜色填充像素

        通过添加一个圆,我的意思是将圆的中心添加到四叉树中。这为叶节点创建了 4 个子节点。将圆圈存储在该叶节点(现在不再是叶)中。因此每个非叶子节点对应一个圆。

        要确定最顶层的圆,遍历四叉树,测试沿途的每个节点,如果像素在该节点处与圆相交。最顶部的圆圈是与像素相交的树最深处的圆圈。

        这需要O(M log N) 时间(如果圆圈分布良好),其中M 是像素数,N 是圆圈数。如果树退化,更糟糕的情况仍然是O(MN)

        伪代码:

        quadtree T
        for each circle c
            add(T,c)
        for each pixel p
            draw color of top_circle(T,p)
        
        def add(quadtree T, circle c)
            if leaf(T)
                append four children to T, split along center(c)
                T.circle = c
            else
                quadtree U = child of T containing center(c)
                add(U,c)
        
        def top_circle(quadtree T, pixel p)
            if not leaf(T)
                if intersects(T.circle, p)
                    c = T.circle
                quadtree U = child of T containing p
                c = top_circle(U,p) if not null
            return c
        

        【讨论】:

        • 酷,我喜欢这个。为了清楚起见,第一个循环是O(Nd),其中d 是四叉树的深度,您假设O(M log N) 循环占主导地位,对吧?我只是想知道这是否会更快:初始化为 false 每个像素的布尔数组。对于每个圆圈,从顶部开始,为其覆盖的每个像素设置布尔值,并且仅当它设置了先前未设置的布尔值时才保留该圆圈。
        • 这就是我所说的好算法。唯一的麻烦是,您将让 CPU 完成所有工作,并且每个圆圈都将具有恒定的颜色(除非 CPU 也进行一些照明计算)。此外,树的实际效率将取决于圆的重新分区。根据作为示例给出的图像,我预计大约一半的圆圈代表背面,因此您可以预期 O(2M log N/2) 的计算时间。
        猜你喜欢
        • 1970-01-01
        • 2011-03-14
        • 2010-11-27
        • 1970-01-01
        • 2014-11-09
        • 1970-01-01
        • 1970-01-01
        • 2017-04-07
        • 2023-04-03
        相关资源
        最近更新 更多