Added GuillotineBinPack algorithm

Former-commit-id: 9f2fb342e7bd9e1b33937a4fd462c993ae5cec3e
This commit is contained in:
Lynix 2014-11-26 02:35:59 +01:00
parent 62dc2dbdf3
commit 4b2e3370d9
2 changed files with 460 additions and 0 deletions

View File

@ -0,0 +1,73 @@
// Copyright (C) 2014 Jérôme Leclercq
// This file is part of the "Nazara Engine - Core module"
// For conditions of distribution and use, see copyright notice in Config.hpp
// Implémentation originale de Jukka Jylänki (Merci de sa contribution au domaine public)
// http://clb.demon.fi/projects/even-more-rectangle-bin-packing
#pragma once
#ifndef NAZARA_GUILLOTINEBINPACK_HPP
#define NAZARA_GUILLOTINEBINPACK_HPP
#include <Nazara/Prerequesites.hpp>
#include <Nazara/Core/SparsePtr.hpp>
#include <Nazara/Math/Rect.hpp>
#include <vector>
class NAZARA_API NzGuillotineBinPack
{
public:
enum FreeRectChoiceHeuristic
{
RectBestAreaFit,
RectBestLongSideFit,
RectBestShortSideFit,
RectWorstAreaFit,
RectWorstLongSideFit,
RectWorstShortSideFit
};
enum GuillotineSplitHeuristic
{
SplitLongerAxis,
SplitLongerLeftoverAxis,
SplitMaximizeArea,
SplitMinimizeArea,
SplitShorterAxis,
SplitShorterLeftoverAxis
};
NzGuillotineBinPack();
NzGuillotineBinPack(unsigned int width, unsigned int height);
~NzGuillotineBinPack() = default;
void Clear();
void FreeRectangle(const NzRectui& rect);
unsigned int GetHeight() const;
float GetOccupancy() const;
NzVector2ui GetSize() const;
unsigned int GetWidth() const;
bool Insert(NzRectui* rects, bool* flipped, unsigned int count, bool merge, FreeRectChoiceHeuristic rectChoice, GuillotineSplitHeuristic splitMethod);
bool MergeFreeRectangles();
void Reset();
void Reset(unsigned int width, unsigned int height);
private:
void SplitFreeRectAlongAxis(const NzRectui& freeRect, const NzRectui& placedRect, bool splitHorizontal);
void SplitFreeRectByHeuristic(const NzRectui& freeRect, const NzRectui& placedRect, GuillotineSplitHeuristic method);
static int ScoreByHeuristic(int width, int height, const NzRectui& freeRect, FreeRectChoiceHeuristic rectChoice);
std::vector<NzRectui> m_freeRectangles;
float m_occupancy;
unsigned int m_height;
unsigned int m_width;
};
#endif // NAZARA_GUILLOTINEBINPACK_HPP

View File

@ -0,0 +1,387 @@
// Copyright (C) 2014 Jérôme Leclercq
// This file is part of the "Nazara Engine - Core module"
// For conditions of distribution and use, see copyright notice in Config.hpp
// Implémentation originale de Jukka Jylänki (Merci de sa contribution au domaine public)
// http://clb.demon.fi/projects/even-more-rectangle-bin-packing
// Je n'ai vraiment fait qu'adapter le code au moteur (Avec quelques améliorations), je n'ai aucun mérite sur le code ci-dessous
#include <Nazara/Core/GuillotineBinPack.hpp>
#include <algorithm>
#include <cmath>
#include <limits>
#include <Nazara/Core/Debug.hpp>
namespace
{
int ScoreBestAreaFit(int width, int height, const NzRectui& freeRectSize)
{
return freeRectSize.width * freeRectSize.height - width * height;
}
int ScoreBestLongSideFit(int width, int height, const NzRectui& freeRectSize)
{
int leftoverHoriz = std::abs(freeRectSize.width - width);
int leftoverVert = std::abs(freeRectSize.height - height);
int leftover = std::max(leftoverHoriz, leftoverVert);
return leftover;
}
int ScoreBestShortSideFit(int width, int height, const NzRectui& freeRectSize)
{
int leftoverHoriz = std::abs(freeRectSize.width - width);
int leftoverVert = std::abs(freeRectSize.height - height);
int leftover = std::min(leftoverHoriz, leftoverVert);
return leftover;
}
int ScoreWorstAreaFit(int width, int height, const NzRectui& freeRectSize)
{
return -ScoreBestAreaFit(width, height, freeRectSize);
}
int ScoreWorstLongSideFit(int width, int height, const NzRectui& freeRectSize)
{
return -ScoreBestLongSideFit(width, height, freeRectSize);
}
int ScoreWorstShortSideFit(int width, int height, const NzRectui& freeRectSize)
{
return -ScoreBestShortSideFit(width, height, freeRectSize);
}
}
NzGuillotineBinPack::NzGuillotineBinPack()
{
Reset();
}
NzGuillotineBinPack::NzGuillotineBinPack(unsigned int width, unsigned int height)
{
Reset(width, height);
}
void NzGuillotineBinPack::Clear()
{
m_freeRectangles.clear();
m_freeRectangles.push_back(NzRectui(0, 0, m_width, m_height));
m_occupancy = 0.f;
}
void NzGuillotineBinPack::FreeRectangle(const NzRectui& rect)
{
///DOC: Cette méthode ne devrait recevoir que des rectangles calculés par la méthode Insert et peut provoquer de la fragmentation
m_freeRectangles.push_back(rect);
m_occupancy -= static_cast<float>(rect.width * rect.height) / (m_width*m_height);
}
unsigned int NzGuillotineBinPack::GetHeight() const
{
return m_height;
}
float NzGuillotineBinPack::GetOccupancy() const
{
return m_occupancy;
}
NzVector2ui NzGuillotineBinPack::GetSize() const
{
return NzVector2ui(m_width, m_height);
}
unsigned int NzGuillotineBinPack::GetWidth() const
{
return m_width;
}
bool NzGuillotineBinPack::Insert(NzRectui* rects, bool* flipped, unsigned int count, bool merge, FreeRectChoiceHeuristic rectChoice, GuillotineSplitHeuristic splitMethod)
{
std::vector<NzRectui*> remainingRects(count); // La position du rectangle
for (unsigned int i = 0; i < count; ++i)
remainingRects[i] = &rects[i];
// Pack rectangles one at a time until we have cleared the rects array of all rectangles.
while (!remainingRects.empty())
{
// Stores the penalty score of the best rectangle placement - bigger=worse, smaller=better.
bool bestFlipped;
int bestFreeRect;
int bestRect;
int bestScore = std::numeric_limits<int>::max();
for (std::size_t i = 0; i < m_freeRectangles.size(); ++i)
{
NzRectui& freeRect = m_freeRectangles[i];
for (std::size_t j = 0; j < remainingRects.size(); ++j)
{
NzRectui& rect = *remainingRects[j];
// If this rectangle is a perfect match, we pick it instantly.
if (rect.width == freeRect.width && rect.height == freeRect.height)
{
bestFreeRect = i;
bestRect = j;
bestFlipped = false;
bestScore = std::numeric_limits<int>::min();
i = m_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 (rect.height == freeRect.width && rect.width == freeRect.height)
{
bestFreeRect = i;
bestRect = j;
bestFlipped = true;
bestScore = std::numeric_limits<int>::min();
i = m_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 (rect.width <= freeRect.width && rect.height <= freeRect.height)
{
int score = ScoreByHeuristic(rect.width, rect.height, freeRect, rectChoice);
if (score < bestScore)
{
bestFreeRect = i;
bestRect = j;
bestFlipped = false;
bestScore = score;
}
}
// If not, then perhaps flipping sideways will make it fit?
else if (rect.height <= freeRect.width && rect.width <= freeRect.height)
{
int score = ScoreByHeuristic(rect.height, rect.width, freeRect, 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())
return false;
// Otherwise, we're good to go and do the actual packing.
unsigned int position = remainingRects[bestRect] - rects;
NzRectui& rect = *remainingRects[bestRect];
rect.x = m_freeRectangles[bestFreeRect].x;
rect.y = m_freeRectangles[bestFreeRect].y;
if (bestFlipped)
std::swap(rect.width, rect.height);
if (flipped)
flipped[position] = bestFlipped;
// Remove the free space we lost in the bin.
SplitFreeRectByHeuristic(m_freeRectangles[bestFreeRect], rect, splitMethod);
m_freeRectangles.erase(m_freeRectangles.begin() + bestFreeRect);
// Remove the rectangle we just packed from the input list.
remainingRects.erase(remainingRects.begin() + bestRect);
// Perform a Rectangle Merge step if desired.
if (merge)
MergeFreeRectangles();
m_occupancy += static_cast<float>(rect.width * rect.height) / (m_width*m_height);
}
return true;
}
bool NzGuillotineBinPack::MergeFreeRectangles()
{
///DOC: Renvoie true s'il y a eu fusion (et donc si une fusion est encore possible)
std::size_t oriSize = m_freeRectangles.size();
// 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 (std::size_t i = 0; i < m_freeRectangles.size(); ++i)
{
NzRectui& firstRect = m_freeRectangles[i];
for (std::size_t j = i+1; j < m_freeRectangles.size(); ++j)
{
NzRectui& secondRect = m_freeRectangles[j];
if (firstRect.width == secondRect.width && firstRect.x == secondRect.x)
{
if (firstRect.y == secondRect.y + secondRect.height)
{
firstRect.y -= secondRect.height;
firstRect.height += secondRect.height;
m_freeRectangles.erase(m_freeRectangles.begin() + j);
--j;
}
else if (firstRect.y + firstRect.height == secondRect.y)
{
firstRect.height += secondRect.height;
m_freeRectangles.erase(m_freeRectangles.begin() + j);
--j;
}
}
else if (firstRect.height == secondRect.height && firstRect.y == secondRect.y)
{
if (firstRect.x == secondRect.x + secondRect.width)
{
firstRect.x -= secondRect.width;
firstRect.width += secondRect.width;
m_freeRectangles.erase(m_freeRectangles.begin() + j);
--j;
}
else if (firstRect.x + firstRect.width == secondRect.x)
{
firstRect.width += secondRect.width;
m_freeRectangles.erase(m_freeRectangles.begin() + j);
--j;
}
}
}
}
return m_freeRectangles.size() < oriSize;
}
void NzGuillotineBinPack::Reset()
{
m_height = 0;
m_width = 0;
Clear();
}
void NzGuillotineBinPack::Reset(unsigned int width, unsigned int height)
{
m_height = height;
m_width = width;
Clear();
}
void NzGuillotineBinPack::SplitFreeRectAlongAxis(const NzRectui& freeRect, const NzRectui& placedRect, bool splitHorizontal)
{
// Form the two new rectangles.
NzRectui bottom;
bottom.x = freeRect.x;
bottom.y = freeRect.y + placedRect.height;
bottom.height = freeRect.height - placedRect.height;
NzRectui 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)
m_freeRectangles.push_back(bottom);
if (right.width > 0 && right.height > 0)
m_freeRectangles.push_back(right);
}
void NzGuillotineBinPack::SplitFreeRectByHeuristic(const NzRectui& freeRect, const NzRectui& 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 SplitLongerAxis:
// Split along the longer total axis.
splitHorizontal = (freeRect.width > freeRect.height);
break;
case SplitLongerLeftoverAxis:
// Split along the longer leftover axis.
splitHorizontal = (w > h);
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 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 SplitShorterAxis:
// Split along the shorter total axis.
splitHorizontal = (freeRect.width <= freeRect.height);
break;
case SplitShorterLeftoverAxis:
// Split along the shorter leftover axis.
splitHorizontal = (w <= h);
break;
default:
NazaraError("Split heuristic out of enum (0x" + NzString::Number(method, 16) + ')');
splitHorizontal = true;
}
// Perform the actual split.
SplitFreeRectAlongAxis(freeRect, placedRect, splitHorizontal);
}
int NzGuillotineBinPack::ScoreByHeuristic(int width, int height, const NzRectui& freeRect, FreeRectChoiceHeuristic rectChoice)
{
switch (rectChoice)
{
case RectBestAreaFit:
return ScoreBestAreaFit(width, height, freeRect);
case RectBestLongSideFit:
return ScoreBestLongSideFit(width, height, freeRect);
case RectBestShortSideFit:
return ScoreBestShortSideFit(width, height, freeRect);
case RectWorstAreaFit:
return ScoreWorstAreaFit(width, height, freeRect);
case RectWorstLongSideFit:
return ScoreWorstLongSideFit(width, height, freeRect);
case RectWorstShortSideFit:
return ScoreWorstShortSideFit(width, height, freeRect);
}
NazaraError("Rect choice heuristic out of enum (0x" + NzString::Number(rectChoice, 16) + ')');
return std::numeric_limits<int>::max();
}