Use a better packing algorithm for the texture atlases

- The old algorithm is something I threw together that produced decent,
  but not spectacular results since it had a tendency to waste space by
  forcing everything onto "shelves".
  The new packer is the Skyline-MinWaste-WasteMap-BestFirstFit algorithm
  described by Jukka Jylanki in his paper *A Thousand Ways to Pack the Bin - A
  Practical Approach to Two-Dimensional Rectangle Bin Packing*, which can
  currently be read at http://clb.demon.fi/files/RectangleBinPack.pdf
  This is minus the optimization to rotate rectangles to make better fits.
This commit is contained in:
Randy Heit 2016-01-08 22:07:16 -06:00
parent f330a81909
commit bf31d66d31
9 changed files with 1378 additions and 251 deletions

View file

@ -995,6 +995,8 @@ add_executable( zdoom WIN32 MACOSX_BUNDLE
wi_stuff.cpp
zstrformat.cpp
zstring.cpp
GuillotineBinPack.cpp
SkylineBinPack.cpp
g_doom/a_doommisc.cpp
g_heretic/a_hereticmisc.cpp
g_hexen/a_hexenmisc.cpp

633
src/GuillotineBinPack.cpp Normal file
View file

@ -0,0 +1,633 @@
/** @file GuillotineBinPack.cpp
@author Jukka Jylänki
@brief Implements different bin packer algorithms that use the GUILLOTINE data structure.
This work is released to Public Domain, do whatever you want with it.
*/
#include <cassert>
#include "GuillotineBinPack.h"
using namespace std;
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;
#ifdef _DEBUG
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(n);
}
void GuillotineBinPack::Insert(TArray<RectSize> &rects, TArray<Rect> &dst, bool merge,
FreeRectChoiceHeuristic rectChoice, GuillotineSplitHeuristic splitMethod)
{
dst.Clear();
// Remember variables about the best packing choice we have made so far during the iteration process.
int bestFreeRect = 0;
int bestRect = 0;
bool bestFlipped = false;
// Pack rectangles one at a time until we have cleared the rects array of all rectangles.
// rects will get destroyed in the process.
while(rects.Size() > 0)
{
// Stores the penalty score of the best rectangle placement - bigger=worse, smaller=better.
int bestScore = INT_MAX;
for(unsigned i = 0; i < freeRectangles.Size(); ++i)
{
for(unsigned j = 0; j < rects.Size(); ++j)
{
// If this rectangle is a perfect match, we pick it instantly.
if (rects[j].width == freeRectangles[i].width && rects[j].height == freeRectangles[i].height)
{
bestFreeRect = i;
bestRect = j;
bestFlipped = false;
bestScore = 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 (rects[j].height == freeRectangles[i].width && rects[j].width == freeRectangles[i].height)
{
bestFreeRect = i;
bestRect = j;
bestFlipped = true;
bestScore = 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 (rects[j].width <= freeRectangles[i].width && rects[j].height <= freeRectangles[i].height)
{
int score = ScoreByHeuristic(rects[j].width, rects[j].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 (rects[j].height <= freeRectangles[i].width && rects[j].width <= freeRectangles[i].height)
{
int score = ScoreByHeuristic(rects[j].height, rects[j].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 == INT_MAX)
return;
// 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 = rects[bestRect].width;
newNode.height = rects[bestRect].height;
if (bestFlipped)
std::swap(newNode.width, newNode.height);
// Remove the free space we lost in the bin.
SplitFreeRectByHeuristic(freeRectangles[bestFreeRect], newNode, splitMethod);
freeRectangles.Delete(bestFreeRect);
// Remove the rectangle we just packed from the input list.
rects.Delete(bestRect);
// Perform a Rectangle Merge step if desired.
if (merge)
MergeFreeList();
// Remember the new used rectangle.
usedRectangles.Push(newNode);
// Check that we're really producing correct packings here.
assert(disjointRects.Add(newNode) == true);
}
}
/// @return True if r fits inside freeRect (possibly rotated).
bool Fits(const RectSize &r, const Rect &freeRect)
{
return (r.width <= freeRect.width && r.height <= freeRect.height) ||
(r.height <= freeRect.width && r.width <= freeRect.height);
}
/// @return True if r fits perfectly inside freeRect, i.e. the leftover area is 0.
bool FitsPerfectly(const RectSize &r, const Rect &freeRect)
{
return (r.width == freeRect.width && r.height == freeRect.height) ||
(r.height == freeRect.width && r.width == freeRect.height);
}
/*
// A helper function for GUILLOTINE-MAXFITTING. Counts how many rectangles fit into the given rectangle
// after it has been split.
void CountNumFitting(const Rect &freeRect, int width, int height, const TArray<RectSize> &rects,
int usedRectIndex, bool splitHorizontal, int &score1, int &score2)
{
const int w = freeRect.width - width;
const int h = freeRect.height - height;
Rect bottom;
bottom.x = freeRect.x;
bottom.y = freeRect.y + height;
bottom.height = h;
Rect right;
right.x = freeRect.x + width;
right.y = freeRect.y;
right.width = w;
if (splitHorizontal)
{
bottom.width = freeRect.width;
right.height = height;
}
else // Split vertically
{
bottom.width = width;
right.height = freeRect.height;
}
int fitBottom = 0;
int fitRight = 0;
for(size_t i = 0; i < rects.size(); ++i)
if (i != usedRectIndex)
{
if (FitsPerfectly(rects[i], bottom))
fitBottom |= 0x10000000;
if (FitsPerfectly(rects[i], right))
fitRight |= 0x10000000;
if (Fits(rects[i], bottom))
++fitBottom;
if (Fits(rects[i], right))
++fitRight;
}
score1 = min(fitBottom, fitRight);
score2 = max(fitBottom, fitRight);
}
*/
/*
// Implements GUILLOTINE-MAXFITTING, an experimental heuristic that's really cool but didn't quite work in practice.
void GuillotineBinPack::InsertMaxFitting(TArray<RectSize> &rects, TArray<Rect> &dst, bool merge,
FreeRectChoiceHeuristic rectChoice, GuillotineSplitHeuristic splitMethod)
{
dst.clear();
int bestRect = 0;
bool bestFlipped = false;
bool bestSplitHorizontal = false;
// Pick rectangles one at a time and pack the one that leaves the most choices still open.
while(rects.size() > 0 && freeRectangles.size() > 0)
{
int bestScore1 = -1;
int bestScore2 = -1;
///\todo Different sort predicates.
clb::sort::QuickSort(&freeRectangles[0], freeRectangles.size(), CompareRectShortSide);
Rect &freeRect = freeRectangles[0];
for(size_t j = 0; j < rects.size(); ++j)
{
int score1;
int score2;
if (rects[j].width == freeRect.width && rects[j].height == freeRect.height)
{
bestRect = j;
bestFlipped = false;
bestScore1 = bestScore2 = std::numeric_limits<int>::max();
break;
}
else if (rects[j].width <= freeRect.width && rects[j].height <= freeRect.height)
{
CountNumFitting(freeRect, rects[j].width, rects[j].height, rects, j, false, score1, score2);
if (score1 > bestScore1 || (score1 == bestScore1 && score2 > bestScore2))
{
bestRect = j;
bestScore1 = score1;
bestScore2 = score2;
bestFlipped = false;
bestSplitHorizontal = false;
}
CountNumFitting(freeRect, rects[j].width, rects[j].height, rects, j, true, score1, score2);
if (score1 > bestScore1 || (score1 == bestScore1 && score2 > bestScore2))
{
bestRect = j;
bestScore1 = score1;
bestScore2 = score2;
bestFlipped = false;
bestSplitHorizontal = true;
}
}
if (rects[j].height == freeRect.width && rects[j].width == freeRect.height)
{
bestRect = j;
bestFlipped = true;
bestScore1 = bestScore2 = std::numeric_limits<int>::max();
break;
}
else if (rects[j].height <= freeRect.width && rects[j].width <= freeRect.height)
{
CountNumFitting(freeRect, rects[j].height, rects[j].width, rects, j, false, score1, score2);
if (score1 > bestScore1 || (score1 == bestScore1 && score2 > bestScore2))
{
bestRect = j;
bestScore1 = score1;
bestScore2 = score2;
bestFlipped = true;
bestSplitHorizontal = false;
}
CountNumFitting(freeRect, rects[j].height, rects[j].width, rects, j, true, score1, score2);
if (score1 > bestScore1 || (score1 == bestScore1 && score2 > bestScore2))
{
bestRect = j;
bestScore1 = score1;
bestScore2 = score2;
bestFlipped = true;
bestSplitHorizontal = true;
}
}
}
if (bestScore1 >= 0)
{
Rect newNode;
newNode.x = freeRect.x;
newNode.y = freeRect.y;
newNode.width = rects[bestRect].width;
newNode.height = rects[bestRect].height;
if (bestFlipped)
std::swap(newNode.width, newNode.height);
assert(disjointRects.Disjoint(newNode));
SplitFreeRectAlongAxis(freeRect, newNode, bestSplitHorizontal);
rects.erase(rects.begin() + bestRect);
if (merge)
MergeFreeList();
usedRectangles.push_back(newNode);
#ifdef _DEBUG
disjointRects.Add(newNode);
#endif
}
freeRectangles.erase(freeRectangles.begin());
}
}
*/
Rect GuillotineBinPack::Insert(int width, int height, bool merge, FreeRectChoiceHeuristic rectChoice,
GuillotineSplitHeuristic splitMethod)
{
// Find where to put the new rectangle.
int 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.Delete(freeNodeIndex);
// Perform a Rectangle Merge step if desired.
if (merge)
MergeFreeList();
// Remember the new used rectangle.
usedRectangles.Push(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(unsigned 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 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 = min(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 = max(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);
}
Rect GuillotineBinPack::FindPositionForNewNode(int width, int height, FreeRectChoiceHeuristic rectChoice, int *nodeIndex)
{
Rect bestNode;
memset(&bestNode, 0, sizeof(Rect));
int bestScore = INT_MAX;
/// Try each free rectangle to find the best one for placement.
for(unsigned 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 = 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 = 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(bottom);
if (right.width > 0 && right.height > 0)
freeRectangles.Push(right);
assert(disjointRects.Disjoint(bottom));
assert(disjointRects.Disjoint(right));
}
void GuillotineBinPack::MergeFreeList()
{
#ifdef _DEBUG
DisjointRectCollection test;
for(unsigned 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(unsigned i = 0; i < freeRectangles.Size(); ++i)
for(unsigned 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.Delete(j);
--j;
}
else if (freeRectangles[i].y + freeRectangles[i].height == freeRectangles[j].y)
{
freeRectangles[i].height += freeRectangles[j].height;
freeRectangles.Delete(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.Delete(j);
--j;
}
else if (freeRectangles[i].x + freeRectangles[i].width == freeRectangles[j].x)
{
freeRectangles[i].width += freeRectangles[j].width;
freeRectangles.Delete(j);
--j;
}
}
}
#ifdef _DEBUG
test.Clear();
for(unsigned i = 0; i < freeRectangles.Size(); ++i)
assert(test.Add(freeRectangles[i]) == true);
#endif
}

135
src/GuillotineBinPack.h Normal file
View file

@ -0,0 +1,135 @@
/** @file GuillotineBinPack.h
@author Jukka Jylänki
@brief Implements different bin packer algorithms that use the GUILLOTINE data structure.
This work is released to Public Domain, do whatever you want with it.
*/
#pragma once
#include "tarray.h"
#include "Rect.h"
/** GuillotineBinPack implements different variants of bin packer algorithms that use the GUILLOTINE data structure
to keep track of the free space of the bin where rectangles may be placed. */
class GuillotineBinPack
{
public:
/// The initial bin size will be (0,0). Call Init to set the bin size.
GuillotineBinPack();
/// Initializes a new bin of the given size.
GuillotineBinPack(int width, int height);
/// (Re)initializes the packer to an empty bin of width x height units. Call whenever
/// you need to restart with a new bin.
void Init(int width, int height);
/// Specifies the different choice heuristics that can be used when deciding which of the free subrectangles
/// to place the to-be-packed rectangle into.
enum FreeRectChoiceHeuristic
{
RectBestAreaFit, ///< -BAF
RectBestShortSideFit, ///< -BSSF
RectBestLongSideFit, ///< -BLSF
RectWorstAreaFit, ///< -WAF
RectWorstShortSideFit, ///< -WSSF
RectWorstLongSideFit ///< -WLSF
};
/// Specifies the different choice heuristics that can be used when the packer needs to decide whether to
/// subdivide the remaining free space in horizontal or vertical direction.
enum GuillotineSplitHeuristic
{
SplitShorterLeftoverAxis, ///< -SLAS
SplitLongerLeftoverAxis, ///< -LLAS
SplitMinimizeArea, ///< -MINAS, Try to make a single big rectangle at the expense of making the other small.
SplitMaximizeArea, ///< -MAXAS, Try to make both remaining rectangles as even-sized as possible.
SplitShorterAxis, ///< -SAS
SplitLongerAxis ///< -LAS
};
/// Inserts a single rectangle into the bin. The packer might rotate the rectangle, in which case the returned
/// struct will have the width and height values swapped.
/// @param merge If true, performs free Rectangle Merge procedure after packing the new rectangle. This procedure
/// tries to defragment the list of disjoint free rectangles to improve packing performance, but also takes up
/// some extra time.
/// @param rectChoice The free rectangle choice heuristic rule to use.
/// @param splitMethod The free rectangle split heuristic rule to use.
Rect Insert(int width, int height, bool merge, FreeRectChoiceHeuristic rectChoice, GuillotineSplitHeuristic splitMethod);
/// Inserts a list of rectangles into the bin.
/// @param rects The list of rectangles to add. This list will be destroyed in the packing process.
/// @param dst The outputted list of rectangles. Note that the indices will not correspond to the input indices.
/// @param merge If true, performs Rectangle Merge operations during the packing process.
/// @param rectChoice The free rectangle choice heuristic rule to use.
/// @param splitMethod The free rectangle split heuristic rule to use.
void Insert(TArray<RectSize> &rects, TArray<Rect> &dst, bool merge,
FreeRectChoiceHeuristic rectChoice, GuillotineSplitHeuristic splitMethod);
// Implements GUILLOTINE-MAXFITTING, an experimental heuristic that's really cool but didn't quite work in practice.
// void InsertMaxFitting(TArray<RectSize> &rects, TArray<Rect> &dst, bool merge,
// FreeRectChoiceHeuristic rectChoice, GuillotineSplitHeuristic splitMethod);
/// Computes the ratio of used/total surface area. 0.00 means no space is yet used, 1.00 means the whole bin is used.
float Occupancy() const;
/// Returns the internal list of disjoint rectangles that track the free area of the bin. You may alter this vector
/// any way desired, as long as the end result still is a list of disjoint rectangles.
TArray<Rect> &GetFreeRectangles() { return freeRectangles; }
/// Returns the list of packed rectangles. You may alter this vector at will, for example, you can move a Rect from
/// this list to the Free Rectangles list to free up space on-the-fly, but notice that this causes fragmentation.
TArray<Rect> &GetUsedRectangles() { return usedRectangles; }
/// Performs a Rectangle Merge operation. This procedure looks for adjacent free rectangles and merges them if they
/// can be represented with a single rectangle. Takes up Theta(|freeRectangles|^2) time.
void MergeFreeList();
#ifdef _DEBUG
void DelDisjoint(const Rect &r) { disjointRects.Del(r); }
#endif
private:
int binWidth;
int binHeight;
/// Stores a list of all the rectangles that we have packed so far. This is used only to compute the Occupancy ratio,
/// so if you want to have the packer consume less memory, this can be removed.
TArray<Rect> usedRectangles;
/// Stores a list of rectangles that represents the free area of the bin. This rectangles in this list are disjoint.
TArray<Rect> freeRectangles;
#ifdef _DEBUG
/// Used to track that the packer produces proper packings.
DisjointRectCollection disjointRects;
#endif
/// Goes through the list of free rectangles and finds the best one to place a rectangle of given size into.
/// Running time is Theta(|freeRectangles|).
/// @param nodeIndex [out] The index of the free rectangle in the freeRectangles array into which the new
/// rect was placed.
/// @return A Rect structure that represents the placement of the new rect into the best free rectangle.
Rect FindPositionForNewNode(int width, int height, FreeRectChoiceHeuristic rectChoice, int *nodeIndex);
static int ScoreByHeuristic(int width, int height, const Rect &freeRect, FreeRectChoiceHeuristic rectChoice);
// The following functions compute (penalty) score values if a rect of the given size was placed into the
// given free rectangle. In these score values, smaller is better.
static int ScoreBestAreaFit(int width, int height, const Rect &freeRect);
static int ScoreBestShortSideFit(int width, int height, const Rect &freeRect);
static int ScoreBestLongSideFit(int width, int height, const Rect &freeRect);
static int ScoreWorstAreaFit(int width, int height, const Rect &freeRect);
static int ScoreWorstShortSideFit(int width, int height, const Rect &freeRect);
static int ScoreWorstLongSideFit(int width, int height, const Rect &freeRect);
/// Splits the given L-shaped free rectangle into two new free rectangles after placedRect has been placed into it.
/// Determines the split axis by using the given heuristic.
void SplitFreeRectByHeuristic(const Rect &freeRect, const Rect &placedRect, GuillotineSplitHeuristic method);
/// Splits the given L-shaped free rectangle into two new free rectangles along the given fixed split axis.
void SplitFreeRectAlongAxis(const Rect &freeRect, const Rect &placedRect, bool splitHorizontal);
};

94
src/Rect.h Normal file
View file

@ -0,0 +1,94 @@
/** @file Rect.h
@author Jukka Jylänki
This work is released to Public Domain, do whatever you want with it.
*/
#pragma once
#include <vector>
struct RectSize
{
int width;
int height;
};
struct Rect
{
int x;
int y;
int width;
int height;
};
/// Performs a lexicographic compare on (rect short side, rect long side).
/// @return -1 if the smaller side of a is shorter than the smaller side of b, 1 if the other way around.
/// If they are equal, the larger side length is used as a tie-breaker.
/// If the rectangles are of same size, returns 0.
int CompareRectShortSide(const Rect &a, const Rect &b);
/// Performs a lexicographic compare on (x, y, width, height).
int NodeSortCmp(const Rect &a, const Rect &b);
/// Returns true if a is contained in b.
bool IsContainedIn(const Rect &a, const Rect &b);
#ifdef _DEBUG
class DisjointRectCollection
{
public:
TArray<Rect> rects;
bool Add(const Rect &r)
{
// Degenerate rectangles are ignored.
if (r.width == 0 || r.height == 0)
return true;
if (!Disjoint(r))
return false;
rects.Push(r);
return true;
}
bool Del(const Rect &r)
{
for(unsigned i = 0; i < rects.Size(); ++i)
{
if(r.x == rects[i].x && r.y == rects[i].y && r.width == rects[i].width && r.height == rects[i].height)
{
rects.Delete(i);
return true;
}
}
return false;
}
void Clear()
{
rects.Clear();
}
bool Disjoint(const Rect &r) const
{
// Degenerate rectangles are ignored.
if (r.width == 0 || r.height == 0)
return true;
for(unsigned i = 0; i < rects.Size(); ++i)
if (!Disjoint(rects[i], r))
return false;
return true;
}
static bool Disjoint(const Rect &a, const Rect &b)
{
if (a.x + a.width <= b.x ||
b.x + b.width <= a.x ||
a.y + a.height <= b.y ||
b.y + b.height <= a.y)
return true;
return false;
}
};
#endif

329
src/SkylineBinPack.cpp Normal file
View file

@ -0,0 +1,329 @@
/** @file SkylineBinPack.cpp
@author Jukka Jylänki
@brief Implements different bin packer algorithms that use the SKYLINE data structure.
This work is released to Public Domain, do whatever you want with it.
*/
#include <cassert>
#include "SkylineBinPack.h"
using namespace std;
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_;
#ifdef _DEBUG
disjointRects.Clear();
#endif
usedSurfaceArea = 0;
skyLine.Clear();
SkylineNode node;
node.x = 0;
node.y = 0;
node.width = binWidth;
skyLine.Push(node);
if (useWasteMap)
{
wasteMap.Init(width, height);
wasteMap.GetFreeRectangles().Clear();
}
}
void SkylineBinPack::Insert(TArray<RectSize> &rects, TArray<Rect> &dst)
{
dst.Clear();
while(rects.Size() > 0)
{
Rect bestNode;
int bestScore1 = INT_MAX;
int bestScore2 = INT_MAX;
int bestSkylineIndex = -1;
int bestRectIndex = -1;
for(unsigned i = 0; i < rects.Size(); ++i)
{
Rect newNode;
int score1;
int score2;
int index;
newNode = FindPositionForNewNodeMinWaste(rects[i].width, rects[i].height, score2, score1, index);
assert(disjointRects.Disjoint(newNode));
if (newNode.height != 0)
{
if (score1 < bestScore1 || (score1 == bestScore1 && score2 < bestScore2))
{
bestNode = newNode;
bestScore1 = score1;
bestScore2 = score2;
bestSkylineIndex = index;
bestRectIndex = i;
}
}
}
if (bestRectIndex == -1)
return;
// Perform the actual packing.
assert(disjointRects.Disjoint(bestNode));
#ifdef _DEBUG
disjointRects.Add(bestNode);
#endif
AddSkylineLevel(bestSkylineIndex, bestNode);
usedSurfaceArea += rects[bestRectIndex].width * rects[bestRectIndex].height;
rects.Delete(bestRectIndex);
dst.Push(bestNode);
}
}
Rect SkylineBinPack::Insert(int width, int height)
{
// 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;
assert(disjointRects.Disjoint(newNode));
#ifdef _DEBUG
disjointRects.Add(newNode);
#endif
return newNode;
}
return InsertMinWaste(width, height);
}
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 = max(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 = min(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;
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 = min(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(waste);
}
}
void SkylineBinPack::AddWaste(const Rect &waste)
{
wasteMap.GetFreeRectangles().Push(waste);
#ifdef _DEBUG
disjointRects.Del(waste);
wasteMap.DelDisjoint(waste);
#endif
}
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(skylineNodeIndex, newNode);
assert(newNode.x + newNode.width <= binWidth);
assert(newNode.y <= binHeight);
for(unsigned 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.Delete(i);
--i;
}
else
break;
}
else
break;
}
MergeSkylines();
}
void SkylineBinPack::MergeSkylines()
{
for(unsigned i = 0; i < skyLine.Size()-1; ++i)
if (skyLine[i].y == skyLine[i+1].y)
{
skyLine[i].width += skyLine[i+1].width;
skyLine.Delete(i+1);
--i;
}
}
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;
#ifdef _DEBUG
disjointRects.Add(newNode);
#endif
}
else
memset(&newNode, 0, sizeof(newNode));
return newNode;
}
Rect SkylineBinPack::FindPositionForNewNodeMinWaste(int width, int height, int &bestHeight, int &bestWastedArea, int &bestIndex) const
{
bestHeight = INT_MAX;
bestWastedArea = INT_MAX;
bestIndex = -1;
Rect newNode;
memset(&newNode, 0, sizeof(newNode));
for(unsigned i = 0; i < 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);
}

88
src/SkylineBinPack.h Normal file
View file

@ -0,0 +1,88 @@
/** @file SkylineBinPack.h
@author Jukka Jylänki
@brief Implements different bin packer algorithms that use the SKYLINE data structure.
This work is released to Public Domain, do whatever you want with it.
*/
#pragma once
#include "tarray.h"
#include "Rect.h"
#include "GuillotineBinPack.h"
/** Implements bin packing algorithms that use the SKYLINE data structure to store the bin contents. Uses
GuillotineBinPack as the waste map. */
class SkylineBinPack
{
public:
/// Instantiates a bin of size (0,0). Call Init to create a new bin.
SkylineBinPack();
/// Instantiates a bin of the given size.
SkylineBinPack(int binWidth, int binHeight, bool useWasteMap);
/// (Re)initializes the packer to an empty bin of width x height units. Call whenever
/// you need to restart with a new bin.
void Init(int binWidth, int binHeight, bool useWasteMap);
/// Inserts the given list of rectangles in an offline/batch mode, possibly rotated.
/// @param rects The list of rectangles to insert. This vector will be destroyed in the process.
/// @param dst [out] This list will contain the packed rectangles. The indices will not correspond to that of rects.
/// @param method The rectangle placement rule to use when packing.
void Insert(TArray<RectSize> &rects, TArray<Rect> &dst);
/// Inserts a single rectangle into the bin, possibly rotated.
Rect Insert(int width, int height);
/// Adds a rectangle to the waste list. It must have been previously returned by
/// Insert or the results are undefined.
void AddWaste(const Rect &rect);
/// Computes the ratio of used surface area to the total bin area.
float Occupancy() const;
private:
int binWidth;
int binHeight;
#ifdef _DEBUG
DisjointRectCollection disjointRects;
#endif
/// Represents a single level (a horizontal line) of the skyline/horizon/envelope.
struct SkylineNode
{
/// The starting x-coordinate (leftmost).
int x;
/// The y-coordinate of the skyline level line.
int y;
/// The line width. The ending coordinate (inclusive) will be x+width-1.
int width;
};
TArray<SkylineNode> skyLine;
unsigned long usedSurfaceArea;
/// If true, we use the GuillotineBinPack structure to recover wasted areas into a waste map.
bool useWasteMap;
GuillotineBinPack wasteMap;
Rect InsertMinWaste(int width, int height);
Rect FindPositionForNewNodeMinWaste(int width, int height, int &bestHeight, int &bestWastedArea, int &bestIndex) const;
bool RectangleFits(int skylineNodeIndex, int width, int height, int &y) const;
bool RectangleFits(int skylineNodeIndex, int width, int height, int &y, int &wastedArea) const;
int ComputeWastedArea(int skylineNodeIndex, int width, int height, int y) const;
void AddWasteMapArea(int skylineNodeIndex, int width, int height, int y);
void AddSkylineLevel(int skylineNodeIndex, const Rect &rect);
/// Merges all skyline nodes that are at the same level.
void MergeSkylines();
};

View file

@ -72,6 +72,7 @@
#include "v_palette.h"
#include "w_wad.h"
#include "r_data/colormaps.h"
#include "SkylineBinPack.h"
// MACROS ------------------------------------------------------------------
@ -92,7 +93,7 @@ struct D3DFB::PackedTexture
{
D3DFB::Atlas *Owner;
PackedTexture *Next, **Prev;
PackedTexture **Prev, *Next;
// Pixels this image covers
RECT Area;
@ -109,18 +110,14 @@ struct D3DFB::Atlas
Atlas(D3DFB *fb, int width, int height, D3DFORMAT format);
~Atlas();
PackedTexture *GetBestFit(int width, int height, int &area);
void AllocateImage(PackedTexture *box, int width, int height);
PackedTexture *AllocateBox();
void AddEmptyBox(int left, int top, int right, int bottom);
PackedTexture *AllocateImage(const Rect &rect, bool padded);
void FreeBox(PackedTexture *box);
SkylineBinPack Packer;
Atlas *Next;
IDirect3DTexture9 *Tex;
D3DFORMAT Format;
PackedTexture *UsedList; // Boxes that contain images
PackedTexture *EmptyList; // Boxes that contain empty space
PackedTexture *FreeList; // Boxes that are just waiting to be used
int Width, Height;
bool OneUse;
};
@ -1871,7 +1868,8 @@ void D3DFB::DrawPackedTextures(int packnum)
continue;
}
AddColorOnlyQuad(x-1, y-1-LBOffsetI, 258, 258, D3DCOLOR_XRGB(255,255,0));
AddColorOnlyRect(x-1, y-1-LBOffsetI, 258, 258, D3DCOLOR_XRGB(255,255,0));
AddColorOnlyQuad(x, y-LBOffsetI, 256, 256, D3DCOLOR_ARGB(180,0,0,0));
CheckQuadBatch();
@ -1881,12 +1879,12 @@ void D3DFB::DrawPackedTextures(int packnum)
quad->Group1 = 0;
if (pack->Format == D3DFMT_L8/* && !tex->IsGray*/)
{
quad->Flags = BQF_WrapUV | BQF_GamePalette | BQF_DisableAlphaTest;
quad->Flags = BQF_WrapUV | BQF_GamePalette/* | BQF_DisableAlphaTest*/;
quad->ShaderNum = BQS_PalTex;
}
else
{
quad->Flags = BQF_WrapUV | BQF_DisableAlphaTest;
quad->Flags = BQF_WrapUV/* | BQF_DisableAlphaTest*/;
quad->ShaderNum = BQS_Plain;
}
quad->Palette = NULL;
@ -1946,16 +1944,6 @@ void D3DFB::DrawPackedTextures(int packnum)
VertexPos += 4;
IndexPos += 6;
// Draw entries in the empty list.
PackedTexture *box;
int emptynum;
for (box = pack->EmptyList, emptynum = 0; box != NULL; box = box->Next, emptynum++)
{
AddColorOnlyQuad(x + box->Area.left, y + box->Area.top - LBOffsetI,
box->Area.right - box->Area.left, box->Area.bottom - box->Area.top,
empty_colors[emptynum & 7]);
}
x += 256 + 8;
if (x > Width - 256)
{
@ -1981,54 +1969,43 @@ void D3DFB::DrawPackedTextures(int packnum)
D3DFB::PackedTexture *D3DFB::AllocPackedTexture(int w, int h, bool wrapping, D3DFORMAT format)
{
Atlas *bestpack;
PackedTexture *bestbox;
int area;
Atlas *pack;
Rect box;
bool padded;
// check for 254 to account for padding
if (w > 254 || h > 254 || wrapping)
{ // Create a new texture atlas.
bestpack = new Atlas(this, w, h, format);
bestpack->OneUse = true;
bestbox = bestpack->GetBestFit(w, h, area);
bestbox->Padded = false;
pack = new Atlas(this, w, h, format);
pack->OneUse = true;
box = pack->Packer.Insert(w, h);
padded = false;
}
else
{ // Try to find space in an existing texture atlas.
w += 2; // Add padding
h += 2;
int bestarea = INT_MAX;
int bestareaever = w * h;
bestpack = NULL;
bestbox = NULL;
for (Atlas *pack = Atlases; pack != NULL; pack = pack->Next)
for (pack = Atlases; pack != NULL; pack = pack->Next)
{
// Use the first atlas it fits in.
if (pack->Format == format)
{
PackedTexture *box = pack->GetBestFit(w, h, area);
if (area == bestareaever)
{ // An exact fit! Use it!
bestpack = pack;
bestbox = box;
break;
}
if (area < bestarea)
box = pack->Packer.Insert(w, h);
if (box.width != 0)
{
bestarea = area;
bestpack = pack;
bestbox = box;
break;
}
}
}
if (bestpack == NULL)
if (pack == NULL)
{ // Create a new texture atlas.
bestpack = new Atlas(this, 256, 256, format);
bestbox = bestpack->GetBestFit(w, h, bestarea);
pack = new Atlas(this, 256, 256, format);
box = pack->Packer.Insert(w, h);
}
bestbox->Padded = true;
padded = true;
}
bestpack->AllocateImage(bestbox, w, h);
return bestbox;
assert(box.width != 0 && box.height != 0);
return pack->AllocateImage(box, padded);
}
//==========================================================================
@ -2038,18 +2015,23 @@ D3DFB::PackedTexture *D3DFB::AllocPackedTexture(int w, int h, bool wrapping, D3D
//==========================================================================
D3DFB::Atlas::Atlas(D3DFB *fb, int w, int h, D3DFORMAT format)
: Packer(w, h, true)
{
Tex = NULL;
Format = format;
UsedList = NULL;
EmptyList = NULL;
FreeList = NULL;
OneUse = false;
Width = 0;
Height = 0;
Next = NULL;
Next = fb->Atlases;
fb->Atlases = this;
// Attach to the end of the atlas list
Atlas **prev = &fb->Atlases;
while (*prev != NULL)
{
prev = &((*prev)->Next);
}
*prev = this;
#if 1
if (FAILED(fb->D3DDevice->CreateTexture(w, h, 1, 0, format, D3DPOOL_MANAGED, &Tex, NULL)))
@ -2066,9 +2048,6 @@ D3DFB::Atlas::Atlas(D3DFB *fb, int w, int h, D3DFORMAT format)
}
Width = w;
Height = h;
// The whole texture is initially empty.
AddEmptyBox(0, 0, w, h);
}
//==========================================================================
@ -2087,53 +2066,6 @@ D3DFB::Atlas::~Atlas()
next = box->Next;
delete box;
}
for (box = EmptyList; box != NULL; box = next)
{
next = box->Next;
delete box;
}
for (box = FreeList; box != NULL; box = next)
{
next = box->Next;
delete box;
}
}
//==========================================================================
//
// Atlas :: GetBestFit
//
// Returns the empty box that provides the best fit for the requested
// dimensions, or NULL if none of them are large enough.
//
//==========================================================================
D3DFB::PackedTexture *D3DFB::Atlas::GetBestFit(int w, int h, int &area)
{
PackedTexture *box;
int smallestarea = INT_MAX;
PackedTexture *smallestbox = NULL;
for (box = EmptyList; box != NULL; box = box->Next)
{
int boxw = box->Area.right - box->Area.left;
int boxh = box->Area.bottom - box->Area.top;
if (boxw >= w && boxh >= h)
{
int boxarea = boxw * boxh;
if (boxarea < smallestarea)
{
smallestarea = boxarea;
smallestbox = box;
if (boxw == w && boxh == h)
{ // An exact fit! Use it!
break;
}
}
}
}
area = smallestarea;
return smallestbox;
}
//==========================================================================
@ -2148,24 +2080,22 @@ D3DFB::PackedTexture *D3DFB::Atlas::GetBestFit(int w, int h, int &area)
//
//==========================================================================
void D3DFB::Atlas::AllocateImage(D3DFB::PackedTexture *box, int w, int h)
D3DFB::PackedTexture *D3DFB::Atlas::AllocateImage(const Rect &rect, bool padded)
{
RECT start = box->Area;
PackedTexture *box = new PackedTexture;
box->Area.right = box->Area.left + w;
box->Area.bottom = box->Area.top + h;
box->Owner = this;
box->Area.left = rect.x;
box->Area.top = rect.y;
box->Area.right = rect.x + rect.width;
box->Area.bottom = rect.y + rect.height;
box->Left = float(box->Area.left + box->Padded) / Width;
box->Right = float(box->Area.right - box->Padded) / Width;
box->Top = float(box->Area.top + box->Padded) / Height;
box->Bottom = float(box->Area.bottom - box->Padded) / Height;
box->Left = float(box->Area.left + padded) / Width;
box->Right = float(box->Area.right - padded) / Width;
box->Top = float(box->Area.top + padded) / Height;
box->Bottom = float(box->Area.bottom - padded) / Height;
// Remove it from the empty list.
*(box->Prev) = box->Next;
if (box->Next != NULL)
{
box->Next->Prev = box->Prev;
}
box->Padded = padded;
// Add it to the used list.
box->Next = UsedList;
@ -2176,96 +2106,6 @@ void D3DFB::Atlas::AllocateImage(D3DFB::PackedTexture *box, int w, int h)
UsedList = box;
box->Prev = &UsedList;
// If we didn't use the whole box, split the remainder into the empty list.
if (box->Area.bottom + 7 < start.bottom && box->Area.right + 7 < start.right)
{
// Split like this:
// +---+------+
// |###| |
// +---+------+
// | |
// | |
// +----------+
if (box->Area.bottom < start.bottom)
{
AddEmptyBox(start.left, box->Area.bottom, start.right, start.bottom);
}
if (box->Area.right < start.right)
{
AddEmptyBox(box->Area.right, start.top, start.right, box->Area.bottom);
}
}
else
{
// Split like this:
// +---+------+
// |###| |
// +---+ |
// | | |
// | | |
// +---+------+
if (box->Area.bottom < start.bottom)
{
AddEmptyBox(start.left, box->Area.bottom, box->Area.right, start.bottom);
}
if (box->Area.right < start.right)
{
AddEmptyBox(box->Area.right, start.top, start.right, start.bottom);
}
}
}
//==========================================================================
//
// Atlas :: AddEmptyBox
//
// Adds a box with the specified dimensions to the empty list.
//
//==========================================================================
void D3DFB::Atlas::AddEmptyBox(int left, int top, int right, int bottom)
{
PackedTexture *box = AllocateBox();
box->Area.left = left;
box->Area.top = top;
box->Area.right = right;
box->Area.bottom = bottom;
box->Next = EmptyList;
if (box->Next != NULL)
{
box->Next->Prev = &box->Next;
}
box->Prev = &EmptyList;
EmptyList = box;
}
//==========================================================================
//
// Atlas :: AllocateBox
//
// Returns a new PackedTexture box, either by retrieving one off the free
// list or by creating a new one. The box is not linked into a list.
//
//==========================================================================
D3DFB::PackedTexture *D3DFB::Atlas::AllocateBox()
{
PackedTexture *box;
if (FreeList != NULL)
{
box = FreeList;
FreeList = box->Next;
if (box->Next != NULL)
{
box->Next->Prev = &FreeList;
}
}
else
{
box = new PackedTexture;
box->Owner = this;
}
return box;
}
@ -2273,10 +2113,9 @@ D3DFB::PackedTexture *D3DFB::Atlas::AllocateBox()
//
// Atlas :: FreeBox
//
// Removes a box from its current list and adds it to the empty list,
// updating EmptyArea. If there are no boxes left in the used list, then
// the empty list is replaced with a single box, so the texture can be
// subdivided again.
// Removes a box from the used list and deletes it. Space is returned to the
// waste list. Once all boxes for this atlas are freed, the entire bin
// packer is reinitialized for maximum efficiency.
//
//==========================================================================
@ -2287,47 +2126,16 @@ void D3DFB::Atlas::FreeBox(D3DFB::PackedTexture *box)
{
box->Next->Prev = box->Prev;
}
box->Next = EmptyList;
box->Prev = &EmptyList;
if (EmptyList != NULL)
{
EmptyList->Prev = &box->Next;
}
EmptyList = box;
Rect waste;
waste.x = box->Area.left;
waste.y = box->Area.top;
waste.width = box->Area.right - box->Area.left;
waste.height = box->Area.bottom - box->Area.top;
box->Owner->Packer.AddWaste(waste);
delete box;
if (UsedList == NULL)
{ // No more space in use! Move all but this into the free list.
if (box->Next != NULL)
{
D3DFB::PackedTexture *lastbox;
// Find the last box in the free list.
lastbox = FreeList;
if (lastbox != NULL)
{
while (lastbox->Next != NULL)
{
lastbox = lastbox->Next;
}
}
// Chain the empty list to the end of the free list.
if (lastbox != NULL)
{
lastbox->Next = box->Next;
box->Next->Prev = &lastbox->Next;
}
else
{
FreeList = box->Next;
box->Next->Prev = &FreeList;
}
box->Next = NULL;
}
// Now this is the only box in the empty list, so it should
// contain the whole texture.
box->Area.left = 0;
box->Area.top = 0;
box->Area.right = Width;
box->Area.bottom = Height;
{
Packer.Init(Width, Height, true);
}
}
@ -2412,6 +2220,7 @@ bool D3DTex::CheckWrapping(bool wrapping)
bool D3DTex::Create(D3DFB *fb, bool wrapping)
{
assert(Box == NULL);
if (Box != NULL)
{
Box->Owner->FreeBox(Box);
@ -3441,6 +3250,22 @@ void D3DFB::AddColorOnlyQuad(int left, int top, int width, int height, D3DCOLOR
IndexPos += 6;
}
//==========================================================================
//
// D3DFB :: AddColorOnlyRect
//
// Like AddColorOnlyQuad, except it's hollow.
//
//==========================================================================
void D3DFB::AddColorOnlyRect(int left, int top, int width, int height, D3DCOLOR color)
{
AddColorOnlyQuad(left, top, width - 1, 1, color); // top
AddColorOnlyQuad(left + width - 1, top, 1, height - 1, color); // right
AddColorOnlyQuad(left + 1, top + height - 1, width - 1, 1, color); // bottom
AddColorOnlyQuad(left, top + 1, 1, height - 1, color); // left
}
//==========================================================================
//
// D3DFB :: CheckQuadBatch

View file

@ -375,6 +375,7 @@ private:
static void SetColorOverlay(DWORD color, float alpha, D3DCOLOR &color0, D3DCOLOR &color1);
void DoWindowedGamma();
void AddColorOnlyQuad(int left, int top, int width, int height, D3DCOLOR color);
void AddColorOnlyRect(int left, int top, int width, int height, D3DCOLOR color);
void CheckQuadBatch(int numtris=2, int numverts=4);
void BeginQuadBatch();
void EndQuadBatch();

View file

@ -666,6 +666,10 @@
RelativePath=".\src\gitinfo.cpp"
>
</File>
<File
RelativePath=".\src\GuillotineBinPack.cpp"
>
</File>
<File
RelativePath=".\src\hu_scores.cpp"
>
@ -1026,6 +1030,10 @@
RelativePath=".\src\skins.cpp"
>
</File>
<File
RelativePath=".\src\SkylineBinPack.cpp"
>
</File>
<File
RelativePath=".\src\st_stuff.cpp"
>
@ -1311,6 +1319,10 @@
RelativePath=".\src\gstrings.h"
>
</File>
<File
RelativePath=".\src\GuillotineBinPack.h"
>
</File>
<File
RelativePath=".\src\hu_stuff.h"
>
@ -1491,6 +1503,10 @@
RelativePath=".\src\po_man.h"
>
</File>
<File
RelativePath=".\src\Rect.h"
>
</File>
<File
RelativePath=".\src\s_playlist.h"
>
@ -1515,6 +1531,10 @@
RelativePath=".\src\skins.h"
>
</File>
<File
RelativePath=".\src\SkylineBinPack.h"
>
</File>
<File
RelativePath=".\src\st_start.h"
>