给定一些AABB,找到包含它们的最小总表面积AABB?

时间:2013-11-21 22:18:09

标签: html5 algorithm math graphics html5-canvas

我有许多对象需要渲染到HTML5画布上。我的输入是轴对齐边界框的有序列表。这些盒子经常重叠,但往往会在它们之间留下大面积的空白区域。

我想尽量减少我必须创建的画布表面区域的数量,以便以正确的顺序渲染所有这些项目,同时不必在多个画布上渲染单个对象的部分(从而阻止了只需创建紧密适合所有占用空间的画布。

所以基本上,我希望在同一个画布上渲染紧密的对象组,而非重叠的对象应该在单独的画布上渲染。但并非所有重叠的对象都应该在单个画布上渲染 - 例如,一个非常高且非常宽的对象略微重叠以形成L仍然应该在两个单独的画布上渲染,因为组合它们会导致大量浪费的画布空间在L的开放部分。

维持Z顺序也会导致一些困难的情况。例如,下图代表一种可能的安排:

enter image description here

在这种情况下,您可能希望将蓝色和绿色图层组合到一个画布中,但是如果不包括红色图层,则无法以这种方式生成正确的分层,并且最终会导致大量死亡空间。

但您也不能只将图层组合限制为Z顺序中连续的项目。 Z顺序可能与上面的图像相同,但红色项目可能不会与其他图像重叠,在这种情况下,您想要组合蓝色和绿色图层。

我正在努力为这个问题提出一个好的算法。有人关心进来吗?

6 个答案:

答案 0 :(得分:1)

这个问题在3D中众所周知,用于在光线跟踪或碰撞检测中构建高性能AABB层次结构。尝试谷歌“BVH”,“表面区域启发式”和/或“SAH”。 下面的3.1节有一个很好的启发式算法;这应该很容易适应您的2D案例:http://graphics.stanford.edu/~boulos/papers/togbvh.pdf

答案 1 :(得分:0)

这是一个简单的建议,可能不会解决一些极端情况,但至少会部分地解决它,并希望建议一个更好的解决方案:

a = <a z-ordered list of the objects> ;
b = [];
bounds = null;
objects = [];
while ( a.length > 0) {
    c = a.pop();
    if( <c overlaps bounds> && <area of combined canvases> < <area of seperate canvases> || bounds === null) {
         objects.push(c);
         bounds = <union of bounds and c, or bounds of c if bounds is null>;
     } else {
          b.push(c);
     }
     if( a.length === 0) {
          a = b;
          b = [];
          <do something with the current bounds and objects list>
          bounds = null;
          objects = [];
     }
}

哪里

 < area of combined canvases> = sum( < area of each canvas> ) - sum( <interesections> )
 < area of seperate conavases> = sum( < area of each canvas> )

这不会捕获两个非交叉对象都与一个公共对象相交的情况,但是可以通过在每次迭代中回顾所有较低的z次序对象来改进这种情况。

答案 2 :(得分:0)

从z-ordered AABB数组开始,

  1. 将每个AABB添加到结果数组中,也是z-ordered

    一个。将这些待添加的AABB中的每一个与已存在于结果数组

    中的所有其他AABB进行比较
    • 查看待添加的AABB和任何其他AABB的哪个组合将产生最小的附加表面积。 (有可能没有人和未被加入的AABB不应与其他任何人合并。)

    • 当组合会导致较小的表面积时,检查交叉点问题(即与另一个AABB重叠且与待添加的AABB重叠的另一个AABB)

    • 如果不存在此类交叉问题,请记住此其他AABB并继续寻找更好的组合

    湾当最终找到最佳组合(或无组合)时,将待添加的AABB添加到结果数组

    • 根据交叉点问题的存在,待添加的AABB可以与结果数组中的其他AABB结合并插入其中

    • 否则,组合或新的AABB本身会添加到结果数组的顶部

  2. 重复下一个AABB

  3. 这不是一个漂亮的算法,也不是完美的做法。首先,当发现AABB的组合时,它不会试图找出是否也可以将第三或第四(或第五)AABB添加到混合物中以改善区域保护。

    以下是Javascript中此算法的实现:

    algorithm = function(allAABBsInSortedOrder) {
        var smallestCanvasSurfaceArea = [];
    
        goog.array.forEach(allAABBsInSortedOrder, function(aabb) {
            smallestCanvasSurfaceArea = findSmallestSurfaceArea(aabb, smallestCanvasSurfaceArea);
        })
    };
    
    findSmallestSurfaceArea = function(nextAABB, combinedAABBsInSortedOrder) {
        var nextAABBarea = areaOf(nextAABB);
    
        if (!nextAABB) {
            return combinedAABBsInSortedOrder;
        }
    
        var aabbToCombineWith = {'index': -1, 'area': nextAABBarea, 'upOrDown': 0};
    
        goog.array.forEach(combinedAABBsInSortedOrder, function(aabb, idx) {
            // Improvement - exhaustive combinations (three AABBs? Four?)
            if (areaOf(combine(aabb, nextAABB) - nextAABBarea <= aabbToCombineWith['area']) {
                var overlapLower = false;
                var overlapNext = false;
    
                goog.array.forEach(combinedAABBsInSortedOrder, function(intersectAABB, intersectIdx) {
                    if (intersectIdx > idx) {
                        if (checkForIntersect(aabb, intersectAABB)) {
                            overlapLower = true;
                        }
                        if (checkForIntersect(nextAABB, intersectAABB)) {
                            overlapNext = true;
                        }
                    }
                });
    
                if (overlapLower && !overlapNext) {
                    aabbsToCombineWith['index'] = idx;
                    aabbsToCombineWith['area'] = areaOf(aabb);
                    aabbsToCombineWith['upOrDown'] = -1;
                }
                else if (!overlapLower && overlapNext) {
                    aabbsToCombineWith['index'] = idx;
                    aabbsToCombineWith['area'] = areaOf(aabb);
                    aabbsToCombineWith['upOrDown'] = 1;
                }
                else if (!overlapLower && !overlapNext) {
                    aabbsToCombineWith['index'] = idx;
                    aabbsToCombineWith['area'] = areaOf(aabb);
                    aabbsToCombineWith['upOrDown'] = 0;
                }
            }
        });
    
        if (aabbToCombineWith['index'] != -1) {
            var combinedAABB = combine(combinedAABBsInSortedOrder[aabbToCombineWith['index']], nextAABB);
            if (aabbToCombineWith['upOrDown'] == -1) {
                combinedAABBsInSortedOrder[aabbToCombineWith['index']] = combinedAABB;
            }
            else {
                combinedAABBsInSortedOrder.push(combinedAABB);
                combinedAABBsInSortedOrder.splice(aabbToConbineWith['index'], 1);
            }
        }
        else {
            combinedAABBsInSortedOrder.push(nextAABB);
        }
    
        return combinedAABBsInSortedOrder;
    };
    

答案 3 :(得分:0)

由于这是一种优化,即使结果是最佳的,您也可能对具有大量计算要求的算法不感兴趣。你必须平衡运行时间和良好的结果。

没有Z顺序

为简单起见,首先考虑没有z顺序的问题。一个简单而有效的解决方案是贪婪地一次考虑和组合两个矩形。聪明之处在于以有效的方式考虑组合框和未组合框,以确保最后没有留下区域保存的成对组合。

function minBoxes(boxes) {
    var result = [];

    boxes.forEach(function(box1) {
        var bestCombinedBox,
            bestIndex;
        result.reduce(function(bestSavings, box2, i) {
            var x = Math.min(box1.x, box2.x),
                y = Math.min(box1.y, box2.y),
                w = Math.max(box1.x + box1.w, box2.x + box2.w) - x,
                h = Math.max(box1.y + box1.h, box2.y + box2.h) - y;
            var savings = box1.w * box1.h + box2.w * box2.h - w * h;
            if(savings > bestSavings) {
                bestCombinedBox = {x:x, y:y, w:w, h:h};
                bestIndex = i;
                return savings;
            }
            return bestSavings;
        }, 0);
        if(bestCombinedBox) {
            result[bestIndex] = bestCombinedBox; //faster than splicing
        } else {
            result.push(box1);
        }
    });

    return result;
}

minBoxes([{x:0, y:0, w:5, h:5}, {x:1, y:1, w:5, y:5}]);

O(N*N)是方框数时,它会在N中运行。

当然,即使这个更简单的问题也有非常棘手的问题。考虑以下四个矩形。

enter image description here

组合这些矩形中的任何两个是永远不会有利的。最好将所有四个结合起来,但如果你成对地检查它们,你将永远不会结合任何东西。

也就是说,这种相对快速的算法在大多数情况下(JSFiddle)运行得非常好,所以我们会保留它。

使用Z顺序

如果矩形具有z次序,那么在我们的计算过程中,组合矩形可以被认为具有一系列z次序。我会为每个矩形添加minZmaxZ属性以反映这一事实。我还将添加一个集合(实际上,一个对象,其键是集合的值),overlapping,矩形重叠的所有矩形。

我将使用minZmaxZoverlapping来确保我合并的任何矩形没有第三个矩形,它们都重叠其< em> z-order将介于它们之间。我还首先按z顺序对矩形进行排序,当有许多重叠的矩形时,这可能会更好。 (我对此有点不确定,但它不会受到伤害。)

这是在O(N*N*K)时间内运行的,其中N是方框数,K是每个方框重叠的方框数。

注意:下面我假设z-orders是唯一的。如果不是,那就不难做到。

function minBoxes(boxes) {
    boxes = boxes.sort(function(box1, box2) {
        return box1.z - box2.z;
    }).map(function(box1) {
        var overlapping = {};
        boxes.forEach(function(box2) {
            if(box1 != box2
                && box1.x + box1.w > box2.x
                && box2.x + box2.w > box1.x
                && box1.y + box1.h > box2.y
                && box2.y + box2.h > box1.y
            ) {
                overlapping[box2.z] = true;
            }
        });
        return {
            x: box1.x,
            y: box1.y,
            w: box1.w,
            h: box1.h,
            minZ: box1.z,
            maxZ: box1.z,
            overlapping: overlapping
        };
    });

    var result = [];

    boxes.forEach(function(box1) {
        var bestBox,
            bestIndex;

        function combinedBox(box2) {
            var x = Math.min(box1.x, box2.x),
                y = Math.min(box1.y, box2.y);
            return {
                x: x,
                y: y,
                w: Math.max(box1.x + box1.w, box2.x + box2.w) - x,
                h: Math.max(box1.y + box1.h, box2.y + box2.h) - y
            };
        }
        result.reduce(function(bestSavings, box2, i) {
            //check z-order
            var min = Math.max(Math.min(box1.minZ, box2.maxZ), Math.min(box1.maxZ, box2.minZ)),
                max = Math.min(Math.max(box1.minZ, box2.maxZ), Math.max(box1.maxZ, box2.minZ));
            for(var z in box1.overlapping) {
                if(min < z && z < max && z in box2.overlapping) {
                    return bestSavings;
                }
            }
            for(var z in box2.overlapping) {
                if(min < z && z < max && z in box1.overlapping) {
                    return bestSavings;
                }
            }
            //check area savings
            var combined = combinedBox(box2);
            var savings = box1.w * box1.h + box2.w * box2.h - combined.w * combined.h;
            if(savings > bestSavings) {
                bestBox = box2;
                bestIndex = i;
                return savings;
            }
            return bestSavings;
        }, 0);
        if(bestBox) {
            var combined = combinedBox(bestBox);
            combined.minZ = Math.min(box1.minZ, bestBox.minZ);
            combined.maxZ = Math.max(box1.maxZ, bestBox.maxZ);
            combined.overlapping = box1.overlapping;
            for(var z in bestBox.overlapping) {
                combined.overlapping[z] = true;
            }
            result[bestIndex] = combined; //faster than splicing
        } else {
            result.push(box1);
        }
    });

    return result.map(function(box) {
        return {
            x: box.x,
            y: box.y,
            w: box.w,
            h: box.h,
            z: (box.minZ + box.maxZ) / 2 //really, any value in this range will work
        };
    });
}

minBoxes([{x:0, y:0, w:5, h:5, z:0}, {x:1, y:1, w:5, y:5, z:1}]);

有更简单的方法可以做到这一点,比如事先按z顺序排序并希望最好,或者如果两个矩形之间只有(仅)一个重叠矩形,则不允许两个矩形组合。但这些方法涵盖的案例较少。

答案 4 :(得分:0)

你有一个有趣的问题!

需要考虑的事项......

由于画布已经针对绘制速度进行了优化,我的猜测是,简单地按z顺序绘制所有rects将是您最快的解决方案。这样,大部分绘图都从CPU卸载到GPU,而您的2个处理器都在做他们最擅长的事情。

无论如何,回到你的问题。

首次通过优化...

在它的核心,你的问题开始是一个关于碰撞理论的问题:我的哪个矩形相互交叉(哪个是碰撞的)?

所以你的第一步就是将你的作品组织成相互交叉最小的“桩”。

如果您将这些离散的“桩”中的每一个画在单独的画布上,您将有效地将您的作品分成可管理的画布。

目前的博弈论提出了一些非常好的碰撞算法。一个叫做Quadtree,它的复杂度接近O(n log(n))。这是Quadtree的链接:

http://gamedev.tutsplus.com/tutorials/implementation/quick-tip-use-quadtrees-to-detect-likely-collisions-in-2d-space/

第一次通过后......

四叉树进行空间排序 - 非常好。当Quadtree完成时,你会有不相交的一堆rects。

好的,我撒了谎。结果主要是非交叉。在这里,您可以考虑在完美性和实用性之间进行权衡。完美将耗费您一些计算能力。实用性放弃了完美,但也降低了计算能力。

“我接受实用性......”

Quadtree桩是一个很好的起点。如果你接受一些不完美的快速&amp;低成本 path是简单地将每个堆排序为z顺序并在单独的画布上绘制每个堆。然后你有一个“非常好”的解决方案。缺点:这种实用的方法可以允许一些跨越2桩的区域在2个桩中的1个上错误分层。如果你的设计可以满足于此,你就是金色的。

“我想要完美......”

Quadtree桩是一个很好的起点。

现在,将每个堆排成z顺序,你已经处理了所有rects的90 +%。

未延伸到任何相邻桩的任何矩形都已完成并且已正确排序。

通过Quadtree加上Sorts的力量完成了90%以上的作品!

剩下的是10%“非法侵入”的行为,确实延伸部分地在1堆中,部分在另一堆中。

如何正确地将这些非法侵入的法案插入两个相邻的法院?

这里有一个巧妙的伎俩!

Quadtree上的背景:如果使用单独的画布绘制每个四叉树堆,则组合的画布堆完全填充原始画布而不重叠。

将堆画布想象为完全构建完整画面的拼图碎片 - 没有重叠。

所以诀窍是在两个画布上绘制任何非法侵入的矩形!

为什么呢?超出画布边界的矩形会自动剪裁(画布不会显示在其边框之外)。 这正是我们想要的!

结果是画布#1将正确地画出一半的非法侵入矩形,而相邻的画布#2将正确地填充另一半。

结果:除了Quadtree算法外,没有额外数学的完美加上排序!

请记住,Quadtree旨在处理实时碰撞,因此速度快,效率高。

[根据提问者的附加评论进行更新]

我明白......所以记忆纯粹是关键。

然后,如果您的设计允许,则根本不应使用html画布,而是使用图像或多个图像。

画布的内存占用量大约是静态图像大小的4.5倍(哎哟!)。

GPU可以快速加入图像,因此图像可以更快地清除/恢复内存。

如果你的设计要求rects是动态的,那么只需创建多个图像并使用CSS转换它们就可以使用更少的内存。另外,附加的好处是CPU的计算量要少得多。

更好的是,如果你的作品都是“带边框的不同颜色的盒子”,你可以通过让每个rect成为div元素(背景颜色和边框)来节省大量内存。然后在div上使用CSS进行转换。巨额储蓄!

尽管如此,你原来的问题仍然是一个有趣的练习;)

答案 5 :(得分:0)

我建议您考虑为表示形状角的坐标计算convex hull。一种简单的计算方法是使用Graham scan。你得到的是最紧密的多边形,它包围你的形状的所有角落,其中的角落在多边形上或在同一个角落内。

给定此多边形,您可以轻松计算画布的最小宽度和高度 height = max(y coordinates) - min(y coordinates) width = max(x coordinates) - min(x coordinates)

Z顺序与凸包计算无关。