You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

1334 lines
43 KiB

/*
* RectsBinPack.cpp
*
* Copyright (c) 2014-2015 SEACAVE
*
* Author(s):
*
* cDc <cdc.seacave@gmail.com>
*
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*
* Additional Terms:
*
* You are required to preserve legal notices and author attributions in
* that material or in the Appropriate Legal Notices displayed by works
* containing it.
*/
#include "Common.h"
#include "RectsBinPack.h"
using namespace MVS;
// D E F I N E S ///////////////////////////////////////////////////
// uncomment to enable multi-threading based on OpenMP
#ifdef _USE_OPENMP
#define RECTPACK_USE_OPENMP
#endif
// S T R U C T S ///////////////////////////////////////////////////
MaxRectsBinPack::MaxRectsBinPack()
:
binWidth(0),
binHeight(0)
{
}
MaxRectsBinPack::MaxRectsBinPack(int width, int height)
{
Init(width, height);
}
void MaxRectsBinPack::Init(int width, int height)
{
binWidth = width;
binHeight = height;
usedRectangles.Empty();
freeRectangles.Empty();
freeRectangles.Insert(Rect(0,0, width,height));
}
MaxRectsBinPack::Rect MaxRectsBinPack::Insert(int width, int height, FreeRectChoiceHeuristic method)
{
Rect newNode;
// Unused in this function. We don't need to know the score after finding the position.
int score1, score2;
switch (method) {
case RectBestShortSideFit: newNode = FindPositionForNewNodeBestShortSideFit(width, height, score1, score2); break;
case RectBestLongSideFit: newNode = FindPositionForNewNodeBestLongSideFit(width, height, score2, score1); break;
case RectBestAreaFit: newNode = FindPositionForNewNodeBestAreaFit(width, height, score1, score2); break;
case RectBottomLeftRule: newNode = FindPositionForNewNodeBottomLeft(width, height, score1, score2); break;
case RectContactPointRule: newNode = FindPositionForNewNodeContactPoint(width, height, score1); break;
}
if (newNode.height == 0)
return newNode;
PlaceRect(newNode);
return newNode;
}
MaxRectsBinPack::RectWIdxArr MaxRectsBinPack::Insert(RectWIdxArr& unplacedRects, FreeRectChoiceHeuristic method)
{
RectWIdxArr placedRects;
while (!unplacedRects.IsEmpty()) {
int bestScore1 = std::numeric_limits<int>::max();
int bestScore2 = std::numeric_limits<int>::max();
IDX bestRectIndex = NO_IDX;
Rect bestNode;
// find the best place to store this rectangle
#ifdef RECTPACK_USE_OPENMP
#pragma omp parallel
{
int privBestScore1 = std::numeric_limits<int>::max();
int privBestScore2 = std::numeric_limits<int>::max();
IDX privBestRectIndex = NO_IDX;
Rect privBestNode;
#pragma omp for nowait
for (int_t i=0; i<(int_t)unplacedRects.size(); ++i) {
int score1, score2;
Rect newNode(ScoreRect(unplacedRects[i].rect.width, unplacedRects[i].rect.height, method, score1, score2));
if (score1 < privBestScore1 || (score1 == privBestScore1 && score2 < privBestScore2)) {
privBestScore1 = score1;
privBestScore2 = score2;
privBestNode = newNode;
privBestRectIndex = i;
}
}
#pragma omp critical
{
if (privBestScore1 < bestScore1 || (privBestScore1 == bestScore1 && privBestScore2 < bestScore2)) {
bestScore1 = privBestScore1;
bestScore2 = privBestScore2;
bestNode = privBestNode;
bestRectIndex = privBestRectIndex;
}
}
}
#else
FOREACH(i, unplacedRects) {
int score1, score2;
Rect newNode(ScoreRect(unplacedRects[i].rect.width, unplacedRects[i].rect.height, method, score1, score2));
if (score1 < bestScore1 || (score1 == bestScore1 && score2 < bestScore2)) {
bestScore1 = score1;
bestScore2 = score2;
bestNode = newNode;
bestRectIndex = i;
}
}
#endif
// if no place found, return the placed rectangles list
if (bestRectIndex == NO_IDX) {
break;
}
// store rectangle
PlaceRect(bestNode);
placedRects.Insert(RectWIdx{bestNode, unplacedRects[bestRectIndex].patchIdx});
unplacedRects.RemoveAt(bestRectIndex);
}
return placedRects;
}
void MaxRectsBinPack::PlaceRect(const Rect &node)
{
for (size_t i = 0; i < freeRectangles.GetSize(); ++i) {
if (SplitFreeNode(freeRectangles[i], node))
freeRectangles.RemoveAtMove(i--);
}
PruneFreeList();
usedRectangles.Insert(node);
}
MaxRectsBinPack::Rect MaxRectsBinPack::ScoreRect(int width, int height, FreeRectChoiceHeuristic method, int &score1, int &score2) const
{
switch (method) {
case RectBestShortSideFit: return FindPositionForNewNodeBestShortSideFit(width, height, score1, score2);
case RectBestLongSideFit: return FindPositionForNewNodeBestLongSideFit(width, height, score2, score1);
case RectBestAreaFit: return FindPositionForNewNodeBestAreaFit(width, height, score1, score2);
case RectBottomLeftRule: return FindPositionForNewNodeBottomLeft(width, height, score1, score2);
case RectContactPointRule: { Rect newNode = FindPositionForNewNodeContactPoint(width, height, score1);
score1 = -score1; // Reverse since we are minimizing, but for contact point score bigger is better.
return newNode; }
default: ASSERT("unknown method" == NULL); return Rect();
}
}
/// Computes the ratio of used surface area.
float MaxRectsBinPack::Occupancy() const
{
unsigned long usedSurfaceArea = 0;
for (size_t i = 0; i < usedRectangles.GetSize(); ++i)
usedSurfaceArea += usedRectangles[i].width * usedRectangles[i].height;
return (float)usedSurfaceArea / (binWidth * binHeight);
}
MaxRectsBinPack::Rect MaxRectsBinPack::FindPositionForNewNodeBottomLeft(int width, int height, int &bestY, int &bestX) const
{
Rect bestNode;
bestY = std::numeric_limits<int>::max();
bestX = std::numeric_limits<int>::max();
for (size_t i = 0; i < freeRectangles.GetSize(); ++i) {
// Try to place the rectangle in upright (non-flipped) orientation.
if (freeRectangles[i].width >= width && freeRectangles[i].height >= height) {
int topSideY = freeRectangles[i].y + height;
if (topSideY < bestY || (topSideY == bestY && freeRectangles[i].x < bestX)) {
bestNode.x = freeRectangles[i].x;
bestNode.y = freeRectangles[i].y;
bestNode.width = width;
bestNode.height = height;
bestY = topSideY;
bestX = freeRectangles[i].x;
}
}
if (freeRectangles[i].width >= height && freeRectangles[i].height >= width) {
int topSideY = freeRectangles[i].y + width;
if (topSideY < bestY || (topSideY == bestY && freeRectangles[i].x < bestX)) {
bestNode.x = freeRectangles[i].x;
bestNode.y = freeRectangles[i].y;
bestNode.width = height;
bestNode.height = width;
bestY = topSideY;
bestX = freeRectangles[i].x;
}
}
}
return bestNode;
}
MaxRectsBinPack::Rect MaxRectsBinPack::FindPositionForNewNodeBestShortSideFit(int width, int height, int &bestShortSideFit, int &bestLongSideFit) const
{
Rect bestNode;
bestShortSideFit = std::numeric_limits<int>::max();
bestLongSideFit = std::numeric_limits<int>::max();
for (size_t i = 0; i < freeRectangles.GetSize(); ++i) {
// Try to place the rectangle in upright (non-flipped) orientation.
if (freeRectangles[i].width >= width && freeRectangles[i].height >= height) {
int leftoverHoriz = ABS(freeRectangles[i].width - width);
int leftoverVert = ABS(freeRectangles[i].height - height);
int shortSideFit = MINF(leftoverHoriz, leftoverVert);
int longSideFit = MAXF(leftoverHoriz, leftoverVert);
if (shortSideFit < bestShortSideFit || (shortSideFit == bestShortSideFit && longSideFit < bestLongSideFit)) {
bestNode.x = freeRectangles[i].x;
bestNode.y = freeRectangles[i].y;
bestNode.width = width;
bestNode.height = height;
bestShortSideFit = shortSideFit;
bestLongSideFit = longSideFit;
}
}
if (freeRectangles[i].width >= height && freeRectangles[i].height >= width) {
int flippedLeftoverHoriz = ABS(freeRectangles[i].width - height);
int flippedLeftoverVert = ABS(freeRectangles[i].height - width);
int flippedShortSideFit = MINF(flippedLeftoverHoriz, flippedLeftoverVert);
int flippedLongSideFit = MAXF(flippedLeftoverHoriz, flippedLeftoverVert);
if (flippedShortSideFit < bestShortSideFit || (flippedShortSideFit == bestShortSideFit && flippedLongSideFit < bestLongSideFit)) {
bestNode.x = freeRectangles[i].x;
bestNode.y = freeRectangles[i].y;
bestNode.width = height;
bestNode.height = width;
bestShortSideFit = flippedShortSideFit;
bestLongSideFit = flippedLongSideFit;
}
}
}
return bestNode;
}
MaxRectsBinPack::Rect MaxRectsBinPack::FindPositionForNewNodeBestLongSideFit(int width, int height, int &bestShortSideFit, int &bestLongSideFit) const
{
Rect bestNode;
bestShortSideFit = std::numeric_limits<int>::max();
bestLongSideFit = std::numeric_limits<int>::max();
for (size_t i = 0; i < freeRectangles.GetSize(); ++i) {
// Try to place the rectangle in upright (non-flipped) orientation.
if (freeRectangles[i].width >= width && freeRectangles[i].height >= height) {
int leftoverHoriz = ABS(freeRectangles[i].width - width);
int leftoverVert = ABS(freeRectangles[i].height - height);
int shortSideFit = MINF(leftoverHoriz, leftoverVert);
int longSideFit = MAXF(leftoverHoriz, leftoverVert);
if (longSideFit < bestLongSideFit || (longSideFit == bestLongSideFit && shortSideFit < bestShortSideFit)) {
bestNode.x = freeRectangles[i].x;
bestNode.y = freeRectangles[i].y;
bestNode.width = width;
bestNode.height = height;
bestShortSideFit = shortSideFit;
bestLongSideFit = longSideFit;
}
}
if (freeRectangles[i].width >= height && freeRectangles[i].height >= width) {
int leftoverHoriz = ABS(freeRectangles[i].width - height);
int leftoverVert = ABS(freeRectangles[i].height - width);
int shortSideFit = MINF(leftoverHoriz, leftoverVert);
int longSideFit = MAXF(leftoverHoriz, leftoverVert);
if (longSideFit < bestLongSideFit || (longSideFit == bestLongSideFit && shortSideFit < bestShortSideFit)) {
bestNode.x = freeRectangles[i].x;
bestNode.y = freeRectangles[i].y;
bestNode.width = height;
bestNode.height = width;
bestShortSideFit = shortSideFit;
bestLongSideFit = longSideFit;
}
}
}
return bestNode;
}
MaxRectsBinPack::Rect MaxRectsBinPack::FindPositionForNewNodeBestAreaFit(int width, int height, int &bestAreaFit, int &bestShortSideFit) const
{
Rect bestNode;
bestAreaFit = std::numeric_limits<int>::max();
bestShortSideFit = std::numeric_limits<int>::max();
for (size_t i = 0; i < freeRectangles.GetSize(); ++i) {
int areaFit = freeRectangles[i].width * freeRectangles[i].height - width * height;
// Try to place the rectangle in upright (non-flipped) orientation.
if (freeRectangles[i].width >= width && freeRectangles[i].height >= height) {
int leftoverHoriz = ABS(freeRectangles[i].width - width);
int leftoverVert = ABS(freeRectangles[i].height - height);
int shortSideFit = MINF(leftoverHoriz, leftoverVert);
if (areaFit < bestAreaFit || (areaFit == bestAreaFit && shortSideFit < bestShortSideFit)) {
bestNode.x = freeRectangles[i].x;
bestNode.y = freeRectangles[i].y;
bestNode.width = width;
bestNode.height = height;
bestShortSideFit = shortSideFit;
bestAreaFit = areaFit;
}
}
if (freeRectangles[i].width >= height && freeRectangles[i].height >= width) {
int leftoverHoriz = ABS(freeRectangles[i].width - height);
int leftoverVert = ABS(freeRectangles[i].height - width);
int shortSideFit = MINF(leftoverHoriz, leftoverVert);
if (areaFit < bestAreaFit || (areaFit == bestAreaFit && shortSideFit < bestShortSideFit)) {
bestNode.x = freeRectangles[i].x;
bestNode.y = freeRectangles[i].y;
bestNode.width = height;
bestNode.height = width;
bestShortSideFit = shortSideFit;
bestAreaFit = areaFit;
}
}
}
return bestNode;
}
/// Returns 0 if the two intervals i1 and i2 are disjoint, or the length of their overlap otherwise.
int CommonIntervalLength(int i1start, int i1end, int i2start, int i2end)
{
if (i1end < i2start || i2end < i1start)
return 0;
return MINF(i1end, i2end) - MAXF(i1start, i2start);
}
int MaxRectsBinPack::ContactPointScoreNode(int x, int y, int width, int height) const
{
int score = 0;
if (x == 0 || x + width == binWidth)
score += height;
if (y == 0 || y + height == binHeight)
score += width;
for (size_t i = 0; i < usedRectangles.GetSize(); ++i) {
if (usedRectangles[i].x == x + width || usedRectangles[i].x + usedRectangles[i].width == x)
score += CommonIntervalLength(usedRectangles[i].y, usedRectangles[i].y + usedRectangles[i].height, y, y + height);
if (usedRectangles[i].y == y + height || usedRectangles[i].y + usedRectangles[i].height == y)
score += CommonIntervalLength(usedRectangles[i].x, usedRectangles[i].x + usedRectangles[i].width, x, x + width);
}
return score;
}
MaxRectsBinPack::Rect MaxRectsBinPack::FindPositionForNewNodeContactPoint(int width, int height, int &bestContactScore) const
{
Rect bestNode;
bestContactScore = -1;
for (size_t i = 0; i < freeRectangles.GetSize(); ++i) {
// Try to place the rectangle in upright (non-flipped) orientation.
if (freeRectangles[i].width >= width && freeRectangles[i].height >= height) {
int score = ContactPointScoreNode(freeRectangles[i].x, freeRectangles[i].y, width, height);
if (score > bestContactScore) {
bestNode.x = freeRectangles[i].x;
bestNode.y = freeRectangles[i].y;
bestNode.width = width;
bestNode.height = height;
bestContactScore = score;
}
}
if (freeRectangles[i].width >= height && freeRectangles[i].height >= width) {
int score = ContactPointScoreNode(freeRectangles[i].x, freeRectangles[i].y, height, width);
if (score > bestContactScore) {
bestNode.x = freeRectangles[i].x;
bestNode.y = freeRectangles[i].y;
bestNode.width = height;
bestNode.height = width;
bestContactScore = score;
}
}
}
return bestNode;
}
bool MaxRectsBinPack::SplitFreeNode(Rect freeNode, const Rect &usedNode)
{
// Test with SAT if the rectangles even intersect.
if (usedNode.x >= freeNode.x + freeNode.width || usedNode.x + usedNode.width <= freeNode.x ||
usedNode.y >= freeNode.y + freeNode.height || usedNode.y + usedNode.height <= freeNode.y)
return false;
if (usedNode.x < freeNode.x + freeNode.width && usedNode.x + usedNode.width > freeNode.x) {
// New node at the top side of the used node.
if (usedNode.y > freeNode.y && usedNode.y < freeNode.y + freeNode.height) {
Rect newNode = freeNode;
newNode.height = usedNode.y - newNode.y;
freeRectangles.Insert(newNode);
}
// New node at the bottom side of the used node.
if (usedNode.y + usedNode.height < freeNode.y + freeNode.height) {
Rect newNode = freeNode;
newNode.y = usedNode.y + usedNode.height;
newNode.height = freeNode.y + freeNode.height - (usedNode.y + usedNode.height);
freeRectangles.Insert(newNode);
}
}
if (usedNode.y < freeNode.y + freeNode.height && usedNode.y + usedNode.height > freeNode.y) {
// New node at the left side of the used node.
if (usedNode.x > freeNode.x && usedNode.x < freeNode.x + freeNode.width) {
Rect newNode = freeNode;
newNode.width = usedNode.x - newNode.x;
freeRectangles.Insert(newNode);
}
// New node at the right side of the used node.
if (usedNode.x + usedNode.width < freeNode.x + freeNode.width) {
Rect newNode = freeNode;
newNode.x = usedNode.x + usedNode.width;
newNode.width = freeNode.x + freeNode.width - (usedNode.x + usedNode.width);
freeRectangles.Insert(newNode);
}
}
return true;
}
void MaxRectsBinPack::PruneFreeList()
{
/*
/// Would be nice to do something like this, to avoid a Theta(n^2) loop through each pair.
/// But unfortunately it doesn't quite cut it, since we also want to detect containment.
/// Perhaps there's another way to do this faster than Theta(n^2).
if (freeRectangles.size() > 0) {
clb::sort::QuickSort(freeRectangles.Begin(), freeRectangles.GetSize(), NodeSortCmp);
for (size_t i = 0; i < freeRectangles.GetSize()-1; ++i)
if (freeRectangles[i].x == freeRectangles[i+1].x &&
freeRectangles[i].y == freeRectangles[i+1].y &&
freeRectangles[i].width == freeRectangles[i+1].width &&
freeRectangles[i].height == freeRectangles[i+1].height)
{
freeRectangles.RemoveAtMove(i--);
}
}
*/
/// Go through each pair and remove any rectangle that is redundant.
for (size_t i = 0; i < freeRectangles.GetSize(); ++i)
for (size_t j = i+1; j < freeRectangles.GetSize(); ++j) {
if (IsContainedIn(freeRectangles[i], freeRectangles[j])) {
freeRectangles.RemoveAtMove(i--);
break;
}
if (IsContainedIn(freeRectangles[j], freeRectangles[i])) {
freeRectangles.RemoveAtMove(j--);
}
}
}
// Compute the appropriate texture atlas size
// (an approximation since the packing is a heuristic)
// (if mult > 0, the returned size is a multiple of that value, otherwise is a power of two)
int MaxRectsBinPack::ComputeTextureSize(const RectArr& rects, int mult)
{
int area(0), maxSizePatch(0);
FOREACHPTR(pRect, rects) {
const Rect& rect = *pRect;
area += rect.area();
const int sizePatch(MAXF(rect.width, rect.height));
if (maxSizePatch < sizePatch)
maxSizePatch = sizePatch;
}
// compute the approximate area
// considering the best case scenario for the packing algorithm: 0.9 fill
area = CEIL2INT((1.f/0.9f)*(float)area);
// compute texture size...
const int sizeTex(MAXF(CEIL2INT(SQRT((float)area)), maxSizePatch));
if (mult > 0) {
// ... as multiple of mult
return ((sizeTex+mult-1)/mult)*mult;
}
// ... as power of two
return POWI(2, CEIL2INT<unsigned>(LOGN((float)sizeTex) / LOGN(2.f)));
}
int MaxRectsBinPack::ComputeTextureSize(const RectWIdxArr& rectsWIdx, int mult) {
RectArr rects(rectsWIdx.GetSize());
FOREACH(i, rectsWIdx)
rects[i] = rectsWIdx[i].rect;
return ComputeTextureSize(rects, mult);
}
/*----------------------------------------------------------------*/
// S T R U C T S ///////////////////////////////////////////////////
GuillotineBinPack::GuillotineBinPack()
: binWidth(0), binHeight(0)
{
}
GuillotineBinPack::GuillotineBinPack(int width, int height)
{
Init(width, height);
}
void GuillotineBinPack::Init(int width, int height)
{
binWidth = width;
binHeight = height;
#ifndef _RELEASE
disjointRects.Clear();
#endif
// Clear any memory of previously packed rectangles.
usedRectangles.clear();
// We start with a single big free rectangle that spans the whole bin.
Rect n;
n.x = 0;
n.y = 0;
n.width = width;
n.height = height;
freeRectangles.clear();
freeRectangles.push_back(n);
}
GuillotineBinPack::RectWIdxArr GuillotineBinPack::Insert(RectWIdxArr& unplacedRects, bool merge,
FreeRectChoiceHeuristic rectChoice, GuillotineSplitHeuristic splitMethod)
{
// Remember variables about the best packing choice we have made so far during the iteration process.
size_t bestFreeRect = 0;
size_t bestRect = 0;
bool bestFlipped = false;
// Pack rectangles one at a time until we have cleared the rects array of all rectangles or there is no space.
// unplacedRects will get destroyed in the process.
RectWIdxArr placedRects;
while (!unplacedRects.IsEmpty()) {
// Stores the penalty score of the best rectangle placement - bigger=worse, smaller=better.
int bestScore = std::numeric_limits<int>::max();
for (size_t i = 0; i < freeRectangles.size(); ++i) {
for (size_t j = 0; j < unplacedRects.GetSize(); ++j) {
Rect currentRect = unplacedRects[j].rect;
// If this rectangle is a perfect match, we pick it instantly.
if (currentRect.width == freeRectangles[i].width && currentRect.height == freeRectangles[i].height) {
bestFreeRect = i;
bestRect = j;
bestFlipped = false;
bestScore = std::numeric_limits<int>::min();
i = freeRectangles.size(); // Force a jump out of the outer loop as well - we got an instant fit.
break;
}
// If flipping this rectangle is a perfect match, pick that then.
else if (currentRect.height == freeRectangles[i].width && currentRect.width == freeRectangles[i].height) {
bestFreeRect = i;
bestRect = j;
bestFlipped = true;
bestScore = std::numeric_limits<int>::min();
i = freeRectangles.size(); // Force a jump out of the outer loop as well - we got an instant fit.
break;
}
// Try if we can fit the rectangle upright.
else if (currentRect.width <= freeRectangles[i].width && currentRect.height <= freeRectangles[i].height) {
int score = ScoreByHeuristic(currentRect.width, currentRect.height, freeRectangles[i], rectChoice);
if (score < bestScore) {
bestFreeRect = i;
bestRect = j;
bestFlipped = false;
bestScore = score;
}
}
// If not, then perhaps flipping sideways will make it fit?
else if (currentRect.height <= freeRectangles[i].width && currentRect.width <= freeRectangles[i].height) {
int score = ScoreByHeuristic(currentRect.height, currentRect.width, freeRectangles[i], rectChoice);
if (score < bestScore) {
bestFreeRect = i;
bestRect = j;
bestFlipped = true;
bestScore = score;
}
}
}
}
// If we didn't manage to find any rectangle to pack, abort.
if (bestScore == std::numeric_limits<int>::max()) {
break;
}
// Otherwise, we're good to go and do the actual packing.
Rect newNode;
newNode.x = freeRectangles[bestFreeRect].x;
newNode.y = freeRectangles[bestFreeRect].y;
newNode.width = unplacedRects[bestRect].rect.width;
newNode.height = unplacedRects[bestRect].rect.height;
if (bestFlipped)
std::swap(newNode.width, newNode.height);
// Remove the free space we lost in the bin.
SplitFreeRectByHeuristic(freeRectangles[bestFreeRect], newNode, splitMethod);
freeRectangles.erase(freeRectangles.begin() + bestFreeRect);
// Remove the rectangle we just packed from the input list.
placedRects.Insert(MaxRectsBinPack::RectWIdx{newNode, unplacedRects[bestRect].patchIdx});
unplacedRects.RemoveAt(bestRect);
// Perform a Rectangle Merge step if desired.
if (merge)
MergeFreeList();
// Remember the new used rectangle.
usedRectangles.push_back(newNode);
// Check that we're really producing correct packings here.
ASSERT(disjointRects.Add(newNode) == true);
}
return placedRects;
}
GuillotineBinPack::Rect GuillotineBinPack::Insert(int width, int height, bool merge, FreeRectChoiceHeuristic rectChoice, GuillotineSplitHeuristic splitMethod)
{
// Find where to put the new rectangle.
size_t freeNodeIndex = 0;
Rect newRect = FindPositionForNewNode(width, height, rectChoice, freeNodeIndex);
// Abort if we didn't have enough space in the bin.
if (newRect.height == 0)
return newRect;
// Remove the space that was just consumed by the new rectangle.
SplitFreeRectByHeuristic(freeRectangles[freeNodeIndex], newRect, splitMethod);
freeRectangles.erase(freeRectangles.begin() + freeNodeIndex);
// Perform a Rectangle Merge step if desired.
if (merge)
MergeFreeList();
// Remember the new used rectangle.
usedRectangles.push_back(newRect);
// Check that we're really producing correct packings here.
ASSERT(disjointRects.Add(newRect) == true);
return newRect;
}
/// Computes the ratio of used surface area to the total bin area.
float GuillotineBinPack::Occupancy() const
{
///\todo The occupancy rate could be cached/tracked incrementally instead
/// of looping through the list of packed rectangles here.
unsigned long usedSurfaceArea = 0;
for (size_t i = 0; i < usedRectangles.size(); ++i)
usedSurfaceArea += usedRectangles[i].width * usedRectangles[i].height;
return (float)usedSurfaceArea / (binWidth * binHeight);
}
/// Returns the heuristic score value for placing a rectangle of size width*height into freeRect. Does not try to rotate.
int GuillotineBinPack::ScoreByHeuristic(int width, int height, const Rect &freeRect, FreeRectChoiceHeuristic rectChoice)
{
switch (rectChoice) {
case RectBestAreaFit: return ScoreBestAreaFit(width, height, freeRect);
case RectBestShortSideFit: return ScoreBestShortSideFit(width, height, freeRect);
case RectBestLongSideFit: return ScoreBestLongSideFit(width, height, freeRect);
case RectWorstAreaFit: return ScoreWorstAreaFit(width, height, freeRect);
case RectWorstShortSideFit: return ScoreWorstShortSideFit(width, height, freeRect);
case RectWorstLongSideFit: return ScoreWorstLongSideFit(width, height, freeRect);
default: ASSERT(false); return std::numeric_limits<int>::max();
}
}
int GuillotineBinPack::ScoreBestAreaFit(int width, int height, const Rect &freeRect)
{
return freeRect.width * freeRect.height - width * height;
}
int GuillotineBinPack::ScoreBestShortSideFit(int width, int height, const Rect &freeRect)
{
int leftoverHoriz = abs(freeRect.width - width);
int leftoverVert = abs(freeRect.height - height);
int leftover = MINF(leftoverHoriz, leftoverVert);
return leftover;
}
int GuillotineBinPack::ScoreBestLongSideFit(int width, int height, const Rect &freeRect)
{
int leftoverHoriz = abs(freeRect.width - width);
int leftoverVert = abs(freeRect.height - height);
int leftover = MAXF(leftoverHoriz, leftoverVert);
return leftover;
}
int GuillotineBinPack::ScoreWorstAreaFit(int width, int height, const Rect &freeRect)
{
return -ScoreBestAreaFit(width, height, freeRect);
}
int GuillotineBinPack::ScoreWorstShortSideFit(int width, int height, const Rect &freeRect)
{
return -ScoreBestShortSideFit(width, height, freeRect);
}
int GuillotineBinPack::ScoreWorstLongSideFit(int width, int height, const Rect &freeRect)
{
return -ScoreBestLongSideFit(width, height, freeRect);
}
GuillotineBinPack::Rect GuillotineBinPack::FindPositionForNewNode(int width, int height, FreeRectChoiceHeuristic rectChoice, size_t& nodeIndex)
{
Rect bestNode;
int bestScore = std::numeric_limits<int>::max();
/// Try each free rectangle to find the best one for placement.
for (size_t i = 0; i < freeRectangles.size(); ++i) {
// If this is a perfect fit upright, choose it immediately.
if (width == freeRectangles[i].width && height == freeRectangles[i].height) {
bestNode.x = freeRectangles[i].x;
bestNode.y = freeRectangles[i].y;
bestNode.width = width;
bestNode.height = height;
bestScore = std::numeric_limits<int>::min();
nodeIndex = i;
ASSERT(disjointRects.Disjoint(bestNode));
break;
}
// If this is a perfect fit sideways, choose it.
else if (height == freeRectangles[i].width && width == freeRectangles[i].height) {
bestNode.x = freeRectangles[i].x;
bestNode.y = freeRectangles[i].y;
bestNode.width = height;
bestNode.height = width;
bestScore = std::numeric_limits<int>::min();
nodeIndex = i;
ASSERT(disjointRects.Disjoint(bestNode));
break;
}
// Does the rectangle fit upright?
else if (width <= freeRectangles[i].width && height <= freeRectangles[i].height) {
int score = ScoreByHeuristic(width, height, freeRectangles[i], rectChoice);
if (score < bestScore) {
bestNode.x = freeRectangles[i].x;
bestNode.y = freeRectangles[i].y;
bestNode.width = width;
bestNode.height = height;
bestScore = score;
nodeIndex = i;
ASSERT(disjointRects.Disjoint(bestNode));
}
}
// Does the rectangle fit sideways?
else if (height <= freeRectangles[i].width && width <= freeRectangles[i].height) {
int score = ScoreByHeuristic(height, width, freeRectangles[i], rectChoice);
if (score < bestScore) {
bestNode.x = freeRectangles[i].x;
bestNode.y = freeRectangles[i].y;
bestNode.width = height;
bestNode.height = width;
bestScore = score;
nodeIndex = i;
ASSERT(disjointRects.Disjoint(bestNode));
}
}
}
return bestNode;
}
void GuillotineBinPack::SplitFreeRectByHeuristic(const Rect &freeRect, const Rect &placedRect, GuillotineSplitHeuristic method)
{
// Compute the lengths of the leftover area.
const int w = freeRect.width - placedRect.width;
const int h = freeRect.height - placedRect.height;
// Placing placedRect into freeRect results in an L-shaped free area, which must be split into
// two disjoint rectangles. This can be achieved with by splitting the L-shape using a single line.
// We have two choices: horizontal or vertical.
// Use the given heuristic to decide which choice to make.
bool splitHorizontal;
switch (method) {
case SplitShorterLeftoverAxis:
// Split along the shorter leftover axis.
splitHorizontal = (w <= h);
break;
case SplitLongerLeftoverAxis:
// Split along the longer leftover axis.
splitHorizontal = (w > h);
break;
case SplitMinimizeArea:
// Maximize the larger area == minimize the smaller area.
// Tries to make the single bigger rectangle.
splitHorizontal = (placedRect.width * h > w * placedRect.height);
break;
case SplitMaximizeArea:
// Maximize the smaller area == minimize the larger area.
// Tries to make the rectangles more even-sized.
splitHorizontal = (placedRect.width * h <= w * placedRect.height);
break;
case SplitShorterAxis:
// Split along the shorter total axis.
splitHorizontal = (freeRect.width <= freeRect.height);
break;
case SplitLongerAxis:
// Split along the longer total axis.
splitHorizontal = (freeRect.width > freeRect.height);
break;
default:
splitHorizontal = true;
ASSERT(false);
}
// Perform the actual split.
SplitFreeRectAlongAxis(freeRect, placedRect, splitHorizontal);
}
/// This function will add the two generated rectangles into the freeRectangles array. The caller is expected to
/// remove the original rectangle from the freeRectangles array after that.
void GuillotineBinPack::SplitFreeRectAlongAxis(const Rect &freeRect, const Rect &placedRect, bool splitHorizontal)
{
// Form the two new rectangles.
Rect bottom;
bottom.x = freeRect.x;
bottom.y = freeRect.y + placedRect.height;
bottom.height = freeRect.height - placedRect.height;
Rect right;
right.x = freeRect.x + placedRect.width;
right.y = freeRect.y;
right.width = freeRect.width - placedRect.width;
if (splitHorizontal) {
bottom.width = freeRect.width;
right.height = placedRect.height;
} else // Split vertically
{
bottom.width = placedRect.width;
right.height = freeRect.height;
}
// Add the new rectangles into the free rectangle pool if they weren't degenerate.
if (bottom.width > 0 && bottom.height > 0)
freeRectangles.push_back(bottom);
if (right.width > 0 && right.height > 0)
freeRectangles.push_back(right);
ASSERT(disjointRects.Disjoint(bottom));
ASSERT(disjointRects.Disjoint(right));
}
void GuillotineBinPack::MergeFreeList()
{
#ifndef _RELEASE
DisjointRectCollection test;
for (size_t i = 0; i < freeRectangles.size(); ++i)
ASSERT(test.Add(freeRectangles[i]) == true);
#endif
// Do a Theta(n^2) loop to see if any pair of free rectangles could me merged into one.
// Note that we miss any opportunities to merge three rectangles into one. (should call this function again to detect that)
for (size_t i = 0; i < freeRectangles.size(); ++i)
for (size_t j = i+1; j < freeRectangles.size(); ++j) {
if (freeRectangles[i].width == freeRectangles[j].width && freeRectangles[i].x == freeRectangles[j].x) {
if (freeRectangles[i].y == freeRectangles[j].y + freeRectangles[j].height) {
freeRectangles[i].y -= freeRectangles[j].height;
freeRectangles[i].height += freeRectangles[j].height;
freeRectangles.erase(freeRectangles.begin() + j);
--j;
} else if (freeRectangles[i].y + freeRectangles[i].height == freeRectangles[j].y) {
freeRectangles[i].height += freeRectangles[j].height;
freeRectangles.erase(freeRectangles.begin() + j);
--j;
}
} else if (freeRectangles[i].height == freeRectangles[j].height && freeRectangles[i].y == freeRectangles[j].y) {
if (freeRectangles[i].x == freeRectangles[j].x + freeRectangles[j].width) {
freeRectangles[i].x -= freeRectangles[j].width;
freeRectangles[i].width += freeRectangles[j].width;
freeRectangles.erase(freeRectangles.begin() + j);
--j;
} else if (freeRectangles[i].x + freeRectangles[i].width == freeRectangles[j].x) {
freeRectangles[i].width += freeRectangles[j].width;
freeRectangles.erase(freeRectangles.begin() + j);
--j;
}
}
}
#ifndef _RELEASE
test.Clear();
for (size_t i = 0; i < freeRectangles.size(); ++i)
ASSERT(test.Add(freeRectangles[i]) == true);
#endif
}
/*----------------------------------------------------------------*/
// S T R U C T S ///////////////////////////////////////////////////
SkylineBinPack::SkylineBinPack()
:binWidth(0),
binHeight(0)
{
}
SkylineBinPack::SkylineBinPack(int width, int height, bool useWasteMap)
{
Init(width, height, useWasteMap);
}
void SkylineBinPack::Init(int width, int height, bool useWasteMap_)
{
binWidth = width;
binHeight = height;
useWasteMap = useWasteMap_;
#ifndef _RELEASE
disjointRects.Clear();
#endif
usedSurfaceArea = 0;
skyLine.clear();
SkylineNode node;
node.x = 0;
node.y = 0;
node.width = binWidth;
skyLine.push_back(node);
if (useWasteMap) {
wasteMap.Init(width, height);
wasteMap.GetFreeRectangles().clear();
}
}
SkylineBinPack::RectWIdxArr SkylineBinPack::Insert(RectWIdxArr& unplacedRects, LevelChoiceHeuristic method)
{
RectWIdxArr placedRects;
while (!unplacedRects.IsEmpty()) {
int bestScore1 = std::numeric_limits<int>::max();
int bestScore2 = std::numeric_limits<int>::max();
int bestSkylineIndex = -1;
IDX bestRectIndex = NO_IDX;
Rect bestNode;
#ifdef RECTPACK_USE_OPENMP
#pragma omp parallel
{
int privBestScore1 = std::numeric_limits<int>::max();
int privBestScore2 = std::numeric_limits<int>::max();
int privBestSkylineIndex = -1;
IDX privBestRectIndex = NO_IDX;
Rect privBestNode;
#pragma omp for nowait
for (int_t i=0; i<(int_t)unplacedRects.GetSize(); ++i) {
int score1, score2, index;
Rect newNode(ScoreRect(unplacedRects[i].rect.width, unplacedRects[i].rect.height, method, score1, score2, index));
if (score1 < privBestScore1 || (score1 == privBestScore1 && score2 < privBestScore2)) {
privBestScore1 = score1;
privBestScore2 = score2;
privBestNode = newNode;
privBestSkylineIndex = index;
privBestRectIndex = i;
}
}
#pragma omp critical
{
if (privBestScore1 < bestScore1 || (privBestScore1 == bestScore1 && privBestScore2 < bestScore2)) {
bestScore1 = privBestScore1;
bestScore2 = privBestScore2;
bestNode = privBestNode;
bestSkylineIndex = privBestSkylineIndex;
bestRectIndex = privBestRectIndex;
}
}
}
#else
FOREACH(i, unplacedRects) {
int score1, score2, index;
Rect newNode(ScoreRect(unplacedRects[i].rect.width, unplacedRects[i].rect.height, method, score1, score2, index));
if (score1 < bestScore1 || (score1 == bestScore1 && score2 < bestScore2)) {
bestNode = newNode;
bestScore1 = score1;
bestScore2 = score2;
bestSkylineIndex = index;
bestRectIndex = i;
}
}
#endif
// if no place found, give up
if (bestRectIndex == NO_IDX) {
break;
}
// Perform the actual packing.
#ifndef _RELEASE
ASSERT(disjointRects.Disjoint(bestNode));
disjointRects.Add(bestNode);
#endif
AddSkylineLevel(bestSkylineIndex, bestNode);
usedSurfaceArea += unplacedRects[bestRectIndex].rect.area();
placedRects.Insert(MaxRectsBinPack::RectWIdx{bestNode, unplacedRects[bestRectIndex].patchIdx});
unplacedRects.RemoveAt(bestRectIndex);
}
return placedRects;
}
SkylineBinPack::Rect SkylineBinPack::Insert(int width, int height, LevelChoiceHeuristic method)
{
if (useWasteMap) {
// First try to pack this rectangle into the waste map, if it fits.
Rect node = wasteMap.Insert(width, height, true, GuillotineBinPack::RectBestShortSideFit,
GuillotineBinPack::SplitMaximizeArea);
ASSERT(disjointRects.Disjoint(node));
if (node.height != 0) {
Rect newNode;
newNode.x = node.x;
newNode.y = node.y;
newNode.width = node.width;
newNode.height = node.height;
usedSurfaceArea += width * height;
#ifndef _RELEASE
ASSERT(disjointRects.Disjoint(newNode));
disjointRects.Add(newNode);
#endif
return newNode;
}
}
switch (method) {
case LevelBottomLeft: return InsertBottomLeft(width, height);
case LevelMinWasteFit: return InsertMinWaste(width, height);
default: ASSERT(false); return Rect();
}
}
SkylineBinPack::Rect SkylineBinPack::ScoreRect(int width, int height, LevelChoiceHeuristic method, int &score1, int &score2, int &index) const
{
Rect newNode;
switch (method) {
case LevelBottomLeft: newNode = FindPositionForNewNodeBottomLeft(width, height, score1, score2, index); break;
case LevelMinWasteFit: newNode = FindPositionForNewNodeMinWaste(width, height, score2, score1, index); break;
default: ASSERT(false);
}
ASSERT(disjointRects.Disjoint(newNode));
return newNode;
}
bool SkylineBinPack::RectangleFits(int skylineNodeIndex, int width, int height, int &y) const
{
int x = skyLine[skylineNodeIndex].x;
if (x + width > binWidth)
return false;
int widthLeft = width;
int i = skylineNodeIndex;
y = skyLine[skylineNodeIndex].y;
while (widthLeft > 0) {
y = MAXF(y, skyLine[i].y);
if (y + height > binHeight)
return false;
widthLeft -= skyLine[i].width;
++i;
ASSERT(i < (int)skyLine.size() || widthLeft <= 0);
}
return true;
}
int SkylineBinPack::ComputeWastedArea(int skylineNodeIndex, int width, int height, int y) const
{
int wastedArea = 0;
const int rectLeft = skyLine[skylineNodeIndex].x;
const int rectRight = rectLeft + width;
for (; skylineNodeIndex < (int)skyLine.size() && skyLine[skylineNodeIndex].x < rectRight; ++skylineNodeIndex) {
if (skyLine[skylineNodeIndex].x >= rectRight || skyLine[skylineNodeIndex].x + skyLine[skylineNodeIndex].width <= rectLeft)
break;
int leftSide = skyLine[skylineNodeIndex].x;
int rightSide = MINF(rectRight, leftSide + skyLine[skylineNodeIndex].width);
ASSERT(y >= skyLine[skylineNodeIndex].y);
wastedArea += (rightSide - leftSide) * (y - skyLine[skylineNodeIndex].y);
}
return wastedArea;
}
bool SkylineBinPack::RectangleFits(int skylineNodeIndex, int width, int height, int &y, int &wastedArea) const
{
bool fits = RectangleFits(skylineNodeIndex, width, height, y);
if (fits)
wastedArea = ComputeWastedArea(skylineNodeIndex, width, height, y);
return fits;
}
void SkylineBinPack::AddWasteMapArea(int skylineNodeIndex, int width, int height, int y)
{
// int wastedArea = 0; // unused
const int rectLeft = skyLine[skylineNodeIndex].x;
const int rectRight = rectLeft + width;
for (; skylineNodeIndex < (int)skyLine.size() && skyLine[skylineNodeIndex].x < rectRight; ++skylineNodeIndex) {
if (skyLine[skylineNodeIndex].x >= rectRight || skyLine[skylineNodeIndex].x + skyLine[skylineNodeIndex].width <= rectLeft)
break;
int leftSide = skyLine[skylineNodeIndex].x;
int rightSide = MINF(rectRight, leftSide + skyLine[skylineNodeIndex].width);
ASSERT(y >= skyLine[skylineNodeIndex].y);
Rect waste;
waste.x = leftSide;
waste.y = skyLine[skylineNodeIndex].y;
waste.width = rightSide - leftSide;
waste.height = y - skyLine[skylineNodeIndex].y;
ASSERT(disjointRects.Disjoint(waste));
wasteMap.GetFreeRectangles().push_back(waste);
}
}
void SkylineBinPack::AddSkylineLevel(int skylineNodeIndex, const Rect &rect)
{
// First track all wasted areas and mark them into the waste map if we're using one.
if (useWasteMap)
AddWasteMapArea(skylineNodeIndex, rect.width, rect.height, rect.y);
SkylineNode newNode;
newNode.x = rect.x;
newNode.y = rect.y + rect.height;
newNode.width = rect.width;
skyLine.insert(skyLine.begin() + skylineNodeIndex, newNode);
ASSERT(newNode.x + newNode.width <= binWidth);
ASSERT(newNode.y <= binHeight);
for (size_t i = skylineNodeIndex+1; i < skyLine.size(); ++i) {
ASSERT(skyLine[i-1].x <= skyLine[i].x);
if (skyLine[i].x < skyLine[i-1].x + skyLine[i-1].width) {
int shrink = skyLine[i-1].x + skyLine[i-1].width - skyLine[i].x;
skyLine[i].x += shrink;
skyLine[i].width -= shrink;
if (skyLine[i].width <= 0) {
skyLine.erase(skyLine.begin() + i);
--i;
} else
break;
} else
break;
}
MergeSkylines();
}
void SkylineBinPack::MergeSkylines()
{
for (size_t i = 0; i < skyLine.size()-1; ++i)
if (skyLine[i].y == skyLine[i+1].y) {
skyLine[i].width += skyLine[i+1].width;
skyLine.erase(skyLine.begin() + (i+1));
--i;
}
}
SkylineBinPack::Rect SkylineBinPack::InsertBottomLeft(int width, int height)
{
int bestHeight;
int bestWidth;
int bestIndex;
Rect newNode = FindPositionForNewNodeBottomLeft(width, height, bestHeight, bestWidth, bestIndex);
if (bestIndex != -1) {
ASSERT(disjointRects.Disjoint(newNode));
// Perform the actual packing.
AddSkylineLevel(bestIndex, newNode);
usedSurfaceArea += width * height;
#ifndef _RELEASE
disjointRects.Add(newNode);
#endif
}
return newNode;
}
SkylineBinPack::Rect SkylineBinPack::FindPositionForNewNodeBottomLeft(int width, int height, int &bestHeight, int &bestWidth, int &bestIndex) const
{
bestHeight = std::numeric_limits<int>::max();
bestIndex = -1;
// Used to break ties if there are nodes at the same level. Then pick the narrowest one.
bestWidth = std::numeric_limits<int>::max();
Rect newNode;
for (int i = 0; i < (int)skyLine.size(); ++i) {
int y;
if (RectangleFits(i, width, height, y)) {
if (y + height < bestHeight || (y + height == bestHeight && skyLine[i].width < bestWidth)) {
bestHeight = y + height;
bestIndex = i;
bestWidth = skyLine[i].width;
newNode.x = skyLine[i].x;
newNode.y = y;
newNode.width = width;
newNode.height = height;
ASSERT(disjointRects.Disjoint(newNode));
}
}
if (RectangleFits(i, height, width, y)) {
if (y + width < bestHeight || (y + width == bestHeight && skyLine[i].width < bestWidth)) {
bestHeight = y + width;
bestIndex = i;
bestWidth = skyLine[i].width;
newNode.x = skyLine[i].x;
newNode.y = y;
newNode.width = height;
newNode.height = width;
ASSERT(disjointRects.Disjoint(newNode));
}
}
}
return newNode;
}
SkylineBinPack::Rect SkylineBinPack::InsertMinWaste(int width, int height)
{
int bestHeight;
int bestWastedArea;
int bestIndex;
Rect newNode = FindPositionForNewNodeMinWaste(width, height, bestHeight, bestWastedArea, bestIndex);
if (bestIndex != -1) {
ASSERT(disjointRects.Disjoint(newNode));
// Perform the actual packing.
AddSkylineLevel(bestIndex, newNode);
usedSurfaceArea += width * height;
#ifndef _RELEASE
disjointRects.Add(newNode);
#endif
}
return newNode;
}
SkylineBinPack::Rect SkylineBinPack::FindPositionForNewNodeMinWaste(int width, int height, int &bestHeight, int &bestWastedArea, int &bestIndex) const
{
bestHeight = std::numeric_limits<int>::max();
bestWastedArea = std::numeric_limits<int>::max();
bestIndex = -1;
Rect newNode;
for (int i = 0; i < (int)skyLine.size(); ++i) {
int y;
int wastedArea;
if (RectangleFits(i, width, height, y, wastedArea)) {
if (wastedArea < bestWastedArea || (wastedArea == bestWastedArea && y + height < bestHeight)) {
bestHeight = y + height;
bestWastedArea = wastedArea;
bestIndex = i;
newNode.x = skyLine[i].x;
newNode.y = y;
newNode.width = width;
newNode.height = height;
ASSERT(disjointRects.Disjoint(newNode));
}
}
if (RectangleFits(i, height, width, y, wastedArea)) {
if (wastedArea < bestWastedArea || (wastedArea == bestWastedArea && y + width < bestHeight)) {
bestHeight = y + width;
bestWastedArea = wastedArea;
bestIndex = i;
newNode.x = skyLine[i].x;
newNode.y = y;
newNode.width = height;
newNode.height = width;
ASSERT(disjointRects.Disjoint(newNode));
}
}
}
return newNode;
}
/// Computes the ratio of used surface area.
float SkylineBinPack::Occupancy() const
{
return (float)usedSurfaceArea / (binWidth * binHeight);
}
/*----------------------------------------------------------------*/