From a43f5a8ecac4323309954a58ba940efdc956e06c Mon Sep 17 00:00:00 2001 From: Randy Heit Date: Fri, 8 Jan 2016 20:08:06 -0600 Subject: [PATCH 01/10] Remove now unused variable from SpawnPolyobj --- src/po_man.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/po_man.cpp b/src/po_man.cpp index fac9a660b..727591bd5 100644 --- a/src/po_man.cpp +++ b/src/po_man.cpp @@ -1542,7 +1542,6 @@ static void SpawnPolyobj (int index, int tag, int type) { unsigned int ii; int i; - int j; FPolyObj *po = &polyobjs[index]; for (ii = 0; ii < KnownPolySides.Size(); ++ii) From f330a819092c58d6d3d305d99a6ca8ef7dcb38df Mon Sep 17 00:00:00 2001 From: Randy Heit Date: Fri, 8 Jan 2016 20:08:10 -0600 Subject: [PATCH 02/10] Rename packing textures to texture atlases for the D3D code - I figure since I now know the "proper" name for these things, I should call them by it. --- src/win32/fb_d3d9.cpp | 89 ++++++++++++++++++++++-------------------- src/win32/win32iface.h | 6 +-- 2 files changed, 50 insertions(+), 45 deletions(-) diff --git a/src/win32/fb_d3d9.cpp b/src/win32/fb_d3d9.cpp index e23275734..273fcde44 100644 --- a/src/win32/fb_d3d9.cpp +++ b/src/win32/fb_d3d9.cpp @@ -90,7 +90,7 @@ IMPLEMENT_CLASS(D3DFB) struct D3DFB::PackedTexture { - D3DFB::PackingTexture *Owner; + D3DFB::Atlas *Owner; PackedTexture *Next, **Prev; @@ -104,10 +104,10 @@ struct D3DFB::PackedTexture bool Padded; }; -struct D3DFB::PackingTexture +struct D3DFB::Atlas { - PackingTexture(D3DFB *fb, int width, int height, D3DFORMAT format); - ~PackingTexture(); + 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); @@ -115,7 +115,7 @@ struct D3DFB::PackingTexture void AddEmptyBox(int left, int top, int right, int bottom); void FreeBox(PackedTexture *box); - PackingTexture *Next; + Atlas *Next; IDirect3DTexture9 *Tex; D3DFORMAT Format; PackedTexture *UsedList; // Boxes that contain images @@ -283,7 +283,7 @@ D3DFB::D3DFB (UINT adapter, int width, int height, bool fullscreen) ScreenWipe = NULL; InScene = false; QuadExtra = new BufferedTris[MAX_QUAD_BATCH]; - Packs = NULL; + Atlases = NULL; PixelDoubling = 0; SkipAt = -1; CurrRenderTexture = 0; @@ -496,7 +496,7 @@ void D3DFB::FillPresentParameters (D3DPRESENT_PARAMETERS *pp, bool fullscreen, b bool D3DFB::CreateResources() { - Packs = NULL; + Atlases = NULL; if (!Windowed) { // Remove the window border in fullscreen mode @@ -624,8 +624,8 @@ void D3DFB::ReleaseResources () delete ScreenWipe; ScreenWipe = NULL; } - PackingTexture *pack, *next; - for (pack = Packs; pack != NULL; pack = next) + Atlas *pack, *next; + for (pack = Atlases; pack != NULL; pack = next) { next = pack->Next; delete pack; @@ -1826,8 +1826,9 @@ IDirect3DTexture9 *D3DFB::GetCurrentScreen(D3DPOOL pool) // // D3DFB :: DrawPackedTextures // -// DEBUG: Draws the packing textures to the screen, starting with the -// 1-based packnum. +// DEBUG: Draws the texture atlases to the screen, starting with the +// 1-based packnum. Ignores atlases that are flagged for use by one +// texture only. // //========================================================================== @@ -1838,30 +1839,34 @@ void D3DFB::DrawPackedTextures(int packnum) 0xFFFF9999, 0xFF99FF99, 0xFF9999FF, 0xFFFFFF99, 0xFFFF99FF, 0xFF99FFFF, 0xFFFFCC99, 0xFF99CCFF }; - PackingTexture *pack; + Atlas *pack; int x = 8, y = 8; if (packnum <= 0) { return; } - pack = Packs; + pack = Atlases; + // Find the first texture atlas that is an actual atlas. while (pack != NULL && pack->OneUse) - { // Skip textures that aren't used as packing containers + { // Skip textures that aren't used as atlases pack = pack->Next; } + // Skip however many atlases we would have otherwise drawn + // until we've skipped of them. while (pack != NULL && packnum != 1) { if (!pack->OneUse) - { // Skip textures that aren't used as packing containers + { // Skip textures that aren't used as atlases packnum--; } pack = pack->Next; } + // Draw atlases until we run out of room on the screen. while (pack != NULL) { if (pack->OneUse) - { // Skip textures that aren't used as packing containers + { // Skip textures that aren't used as atlases pack = pack->Next; continue; } @@ -1969,34 +1974,34 @@ void D3DFB::DrawPackedTextures(int packnum) // // D3DFB :: AllocPackedTexture // -// Finds space to pack an image inside a packing texture and returns it. +// Finds space to pack an image inside a texture atlas and returns it. // Large images and those that need to wrap always get their own textures. // //========================================================================== D3DFB::PackedTexture *D3DFB::AllocPackedTexture(int w, int h, bool wrapping, D3DFORMAT format) { - PackingTexture *bestpack; + Atlas *bestpack; PackedTexture *bestbox; int area; // check for 254 to account for padding if (w > 254 || h > 254 || wrapping) - { // Create a new packing texture. - bestpack = new PackingTexture(this, w, h, format); + { // Create a new texture atlas. + bestpack = new Atlas(this, w, h, format); bestpack->OneUse = true; bestbox = bestpack->GetBestFit(w, h, area); bestbox->Padded = false; } else - { // Try to find space in an existing packing texture. + { // 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 (PackingTexture *pack = Packs; pack != NULL; pack = pack->Next) + for (Atlas *pack = Atlases; pack != NULL; pack = pack->Next) { if (pack->Format == format) { @@ -2016,8 +2021,8 @@ D3DFB::PackedTexture *D3DFB::AllocPackedTexture(int w, int h, bool wrapping, D3D } } if (bestpack == NULL) - { // Create a new packing texture. - bestpack = new PackingTexture(this, 256, 256, format); + { // Create a new texture atlas. + bestpack = new Atlas(this, 256, 256, format); bestbox = bestpack->GetBestFit(w, h, bestarea); } bestbox->Padded = true; @@ -2028,11 +2033,11 @@ D3DFB::PackedTexture *D3DFB::AllocPackedTexture(int w, int h, bool wrapping, D3D //========================================================================== // -// PackingTexture Constructor +// Atlas Constructor // //========================================================================== -D3DFB::PackingTexture::PackingTexture(D3DFB *fb, int w, int h, D3DFORMAT format) +D3DFB::Atlas::Atlas(D3DFB *fb, int w, int h, D3DFORMAT format) { Tex = NULL; Format = format; @@ -2043,8 +2048,8 @@ D3DFB::PackingTexture::PackingTexture(D3DFB *fb, int w, int h, D3DFORMAT format) Width = 0; Height = 0; - Next = fb->Packs; - fb->Packs = this; + Next = fb->Atlases; + fb->Atlases = this; #if 1 if (FAILED(fb->D3DDevice->CreateTexture(w, h, 1, 0, format, D3DPOOL_MANAGED, &Tex, NULL))) @@ -2068,11 +2073,11 @@ D3DFB::PackingTexture::PackingTexture(D3DFB *fb, int w, int h, D3DFORMAT format) //========================================================================== // -// PackingTexture Destructor +// Atlas Destructor // //========================================================================== -D3DFB::PackingTexture::~PackingTexture() +D3DFB::Atlas::~Atlas() { PackedTexture *box, *next; @@ -2096,14 +2101,14 @@ D3DFB::PackingTexture::~PackingTexture() //========================================================================== // -// PackingTexture :: GetBestFit +// 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::PackingTexture::GetBestFit(int w, int h, int &area) +D3DFB::PackedTexture *D3DFB::Atlas::GetBestFit(int w, int h, int &area) { PackedTexture *box; int smallestarea = INT_MAX; @@ -2133,17 +2138,17 @@ D3DFB::PackedTexture *D3DFB::PackingTexture::GetBestFit(int w, int h, int &area) //========================================================================== // -// PackingTexture :: AllocateImage +// Atlas :: AllocateImage // // Moves the box from the empty list to the used list, sizing it to the // requested dimensions and adding additional boxes to the empty list if // needed. // -// The passed box *MUST* be in this packing texture's empty list. +// The passed box *MUST* be in this texture atlas's empty list. // //========================================================================== -void D3DFB::PackingTexture::AllocateImage(D3DFB::PackedTexture *box, int w, int h) +void D3DFB::Atlas::AllocateImage(D3DFB::PackedTexture *box, int w, int h) { RECT start = box->Area; @@ -2212,13 +2217,13 @@ void D3DFB::PackingTexture::AllocateImage(D3DFB::PackedTexture *box, int w, int //========================================================================== // -// PackingTexture :: AddEmptyBox +// Atlas :: AddEmptyBox // // Adds a box with the specified dimensions to the empty list. // //========================================================================== -void D3DFB::PackingTexture::AddEmptyBox(int left, int top, int right, int bottom) +void D3DFB::Atlas::AddEmptyBox(int left, int top, int right, int bottom) { PackedTexture *box = AllocateBox(); box->Area.left = left; @@ -2236,14 +2241,14 @@ void D3DFB::PackingTexture::AddEmptyBox(int left, int top, int right, int bottom //========================================================================== // -// PackingTexture :: AllocateBox +// 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::PackingTexture::AllocateBox() +D3DFB::PackedTexture *D3DFB::Atlas::AllocateBox() { PackedTexture *box; @@ -2266,7 +2271,7 @@ D3DFB::PackedTexture *D3DFB::PackingTexture::AllocateBox() //========================================================================== // -// PackingTexture :: FreeBox +// 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 @@ -2275,7 +2280,7 @@ D3DFB::PackedTexture *D3DFB::PackingTexture::AllocateBox() // //========================================================================== -void D3DFB::PackingTexture::FreeBox(D3DFB::PackedTexture *box) +void D3DFB::Atlas::FreeBox(D3DFB::PackedTexture *box) { *(box->Prev) = box->Next; if (box->Next != NULL) @@ -4004,7 +4009,7 @@ void D3DFB::SetPaletteTexture(IDirect3DTexture9 *texture, int count, D3DCOLOR bo SetTexture(1, texture); } -void D3DFB::SetPalTexBilinearConstants(PackingTexture *tex) +void D3DFB::SetPalTexBilinearConstants(Atlas *tex) { #if 0 float con[8]; diff --git a/src/win32/win32iface.h b/src/win32/win32iface.h index 934931ea0..a64b1dd82 100644 --- a/src/win32/win32iface.h +++ b/src/win32/win32iface.h @@ -279,7 +279,7 @@ private: friend class D3DPal; struct PackedTexture; - struct PackingTexture; + struct Atlas; struct FBVERTEX { @@ -392,7 +392,7 @@ private: void SetPixelShader(IDirect3DPixelShader9 *shader); void SetTexture(int tnum, IDirect3DTexture9 *texture); void SetPaletteTexture(IDirect3DTexture9 *texture, int count, D3DCOLOR border_color); - void SetPalTexBilinearConstants(PackingTexture *texture); + void SetPalTexBilinearConstants(Atlas *texture); BOOL AlphaTestEnabled; BOOL AlphaBlendEnabled; @@ -431,7 +431,7 @@ private: BYTE BlockNum; D3DPal *Palettes; D3DTex *Textures; - PackingTexture *Packs; + Atlas *Atlases; HRESULT LastHR; UINT Adapter; From bf31d66d312bbf3b557bab0662f654a1015c0af5 Mon Sep 17 00:00:00 2001 From: Randy Heit Date: Fri, 8 Jan 2016 22:07:16 -0600 Subject: [PATCH 03/10] 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. --- src/CMakeLists.txt | 2 + src/GuillotineBinPack.cpp | 633 ++++++++++++++++++++++++++++++++++++++ src/GuillotineBinPack.h | 135 ++++++++ src/Rect.h | 94 ++++++ src/SkylineBinPack.cpp | 329 ++++++++++++++++++++ src/SkylineBinPack.h | 88 ++++++ src/win32/fb_d3d9.cpp | 327 +++++--------------- src/win32/win32iface.h | 1 + zdoom.vcproj | 20 ++ 9 files changed, 1378 insertions(+), 251 deletions(-) create mode 100644 src/GuillotineBinPack.cpp create mode 100644 src/GuillotineBinPack.h create mode 100644 src/Rect.h create mode 100644 src/SkylineBinPack.cpp create mode 100644 src/SkylineBinPack.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 8503fabcf..90748f257 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -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 diff --git a/src/GuillotineBinPack.cpp b/src/GuillotineBinPack.cpp new file mode 100644 index 000000000..61ac7a4ce --- /dev/null +++ b/src/GuillotineBinPack.cpp @@ -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 + +#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 &rects, TArray &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 &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 &rects, TArray &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::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::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 +} diff --git a/src/GuillotineBinPack.h b/src/GuillotineBinPack.h new file mode 100644 index 000000000..54cd77a9e --- /dev/null +++ b/src/GuillotineBinPack.h @@ -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 &rects, TArray &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 &rects, TArray &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 &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 &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 usedRectangles; + + /// Stores a list of rectangles that represents the free area of the bin. This rectangles in this list are disjoint. + TArray 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); +}; diff --git a/src/Rect.h b/src/Rect.h new file mode 100644 index 000000000..8b7ba1e2a --- /dev/null +++ b/src/Rect.h @@ -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 + +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 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 diff --git a/src/SkylineBinPack.cpp b/src/SkylineBinPack.cpp new file mode 100644 index 000000000..578a248a5 --- /dev/null +++ b/src/SkylineBinPack.cpp @@ -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 + +#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 &rects, TArray &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); +} diff --git a/src/SkylineBinPack.h b/src/SkylineBinPack.h new file mode 100644 index 000000000..f93ddd29d --- /dev/null +++ b/src/SkylineBinPack.h @@ -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 &rects, TArray &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 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(); +}; diff --git a/src/win32/fb_d3d9.cpp b/src/win32/fb_d3d9.cpp index 273fcde44..1641df365 100644 --- a/src/win32/fb_d3d9.cpp +++ b/src/win32/fb_d3d9.cpp @@ -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 diff --git a/src/win32/win32iface.h b/src/win32/win32iface.h index a64b1dd82..8b0c88f45 100644 --- a/src/win32/win32iface.h +++ b/src/win32/win32iface.h @@ -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(); diff --git a/zdoom.vcproj b/zdoom.vcproj index a2587840f..597b56f2f 100644 --- a/zdoom.vcproj +++ b/zdoom.vcproj @@ -666,6 +666,10 @@ RelativePath=".\src\gitinfo.cpp" > + + @@ -1026,6 +1030,10 @@ RelativePath=".\src\skins.cpp" > + + @@ -1311,6 +1319,10 @@ RelativePath=".\src\gstrings.h" > + + @@ -1491,6 +1503,10 @@ RelativePath=".\src\po_man.h" > + + @@ -1515,6 +1531,10 @@ RelativePath=".\src\skins.h" > + + From 786caaf36b31d27d717a292ca6ef27fc1bde615d Mon Sep 17 00:00:00 2001 From: Randy Heit Date: Fri, 8 Jan 2016 22:41:23 -0600 Subject: [PATCH 04/10] Execute disconnect scripts immediately before the player is destroyed. - Disconnect scripts were previously run at some point after the player left. Now they are run immediately before destroying the player. Since the player hasn't actually been destroyed yet, the player also gets to be the script's activator. This gives you a chance to scrape whatever data you want from the player before they're history. Note that if you do anything to make the script wait, the script's activator will become the world, as it was before. --- src/b_game.cpp | 2 +- src/g_game.cpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/b_game.cpp b/src/b_game.cpp index e136f3f29..5f852d043 100644 --- a/src/b_game.cpp +++ b/src/b_game.cpp @@ -423,8 +423,8 @@ void FCajunMaster::RemoveAllBots (bool fromlist) } } } + FBehavior::StaticStartTypedScripts (SCRIPT_Disconnect, players[i].mo, true, i, true); ClearPlayer (i, !fromlist); - FBehavior::StaticStartTypedScripts (SCRIPT_Disconnect, NULL, true, i); } } diff --git a/src/g_game.cpp b/src/g_game.cpp index 8101ca23d..64e746cef 100644 --- a/src/g_game.cpp +++ b/src/g_game.cpp @@ -1713,6 +1713,8 @@ void G_DoPlayerPop(int playernum) // [RH] Make the player disappear FBehavior::StaticStopMyScripts(players[playernum].mo); + // [RH] Let the scripts know the player left + FBehavior::StaticStartTypedScripts(SCRIPT_Disconnect, players[playernum].mo, true, playernum, true); if (players[playernum].mo != NULL) { P_DisconnectEffect(players[playernum].mo); @@ -1726,8 +1728,6 @@ void G_DoPlayerPop(int playernum) players[playernum].mo = NULL; players[playernum].camera = NULL; } - // [RH] Let the scripts know the player left - FBehavior::StaticStartTypedScripts(SCRIPT_Disconnect, NULL, true, playernum); } void G_ScreenShot (char *filename) From 0b5e4195936776364a247bfcef323bf9290cbdfc Mon Sep 17 00:00:00 2001 From: Christoph Oelckers Date: Sat, 9 Jan 2016 10:11:20 +0100 Subject: [PATCH 05/10] - fixed friction logic for sectors. Issues with the old code: * when calculating friction for a 3D-floor - swimmable or not - the 3D floor's top texture must be used. The previous version always checked the sector's floor texture, even though the top might as well come from the sector's ceiling. * 3D floors never checked for the SECF_FRICTION flag at all. According to Boom specs, sector-based friction must be ignored if this is the case. * Terrain based friction had a higher priority than the sector's own value. Changed the rules as follows: * if the sector's SECF_FRICTION flag is set (i.e. something explicitly changed the sector's friction), this value is used regardless of terrain settings. * if this flag is not set, the terrain's friction is used, if defined, using the proper plane for 3D-floors. * otherwise the default is used. --- src/p_map.cpp | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/p_map.cpp b/src/p_map.cpp index f409dafa4..fe5959df9 100644 --- a/src/p_map.cpp +++ b/src/p_map.cpp @@ -529,16 +529,18 @@ void P_PlayerStartStomp(AActor *actor) // //========================================================================== -inline fixed_t secfriction(const sector_t *sec) +inline fixed_t secfriction(const sector_t *sec, int plane = sector_t::floor) { - fixed_t friction = Terrains[TerrainTypes[sec->GetTexture(sector_t::floor)]].Friction; - return friction != 0 ? friction : sec->friction; + if (sec->Flags & SECF_FRICTION) return sec->friction; + fixed_t friction = Terrains[TerrainTypes[sec->GetTexture(plane)]].Friction; + return friction != 0 ? friction : ORIG_FRICTION; } -inline fixed_t secmovefac(const sector_t *sec) +inline fixed_t secmovefac(const sector_t *sec, int plane = sector_t::floor) { - fixed_t movefactor = Terrains[TerrainTypes[sec->GetTexture(sector_t::floor)]].MoveFactor; - return movefactor != 0 ? movefactor : sec->movefactor; + if (sec->Flags & SECF_FRICTION) return sec->friction; + fixed_t movefactor = Terrains[TerrainTypes[sec->GetTexture(plane)]].MoveFactor; + return movefactor != 0 ? movefactor : ORIG_FRICTION_FACTOR; } //========================================================================== @@ -584,11 +586,11 @@ int P_GetFriction(const AActor *mo, int *frictionfactor) mo->z < rover->bottom.plane->ZatPoint(mo->x, mo->y)) continue; - newfriction = secfriction(rover->model); + newfriction = secfriction(rover->model, rover->top.isceiling); if (newfriction < friction || friction == ORIG_FRICTION) { friction = newfriction; - movefactor = secmovefac(rover->model) >> 1; + movefactor = secmovefac(rover->model, rover->top.isceiling) >> 1; } } } @@ -622,11 +624,11 @@ int P_GetFriction(const AActor *mo, int *frictionfactor) else continue; - newfriction = secfriction(rover->model); + newfriction = secfriction(rover->model, rover->top.isceiling); if (newfriction < friction || friction == ORIG_FRICTION) { friction = newfriction; - movefactor = secmovefac(rover->model); + movefactor = secmovefac(rover->model, rover->top.isceiling); } } From 4b3adf548ac1ea6cdb12515563655f86ff7cb4a8 Mon Sep 17 00:00:00 2001 From: Christoph Oelckers Date: Sat, 9 Jan 2016 11:44:05 +0100 Subject: [PATCH 06/10] - fixed compile errors. --- src/GuillotineBinPack.cpp | 6 +++--- src/SkylineBinPack.cpp | 7 ++++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/GuillotineBinPack.cpp b/src/GuillotineBinPack.cpp index 61ac7a4ce..2131ed5db 100644 --- a/src/GuillotineBinPack.cpp +++ b/src/GuillotineBinPack.cpp @@ -7,7 +7,7 @@ */ #include - +#include "templates.h" #include "GuillotineBinPack.h" using namespace std; @@ -395,7 +395,7 @@ int GuillotineBinPack::ScoreBestShortSideFit(int width, int height, const Rect & { int leftoverHoriz = abs(freeRect.width - width); int leftoverVert = abs(freeRect.height - height); - int leftover = min(leftoverHoriz, leftoverVert); + int leftover = MIN(leftoverHoriz, leftoverVert); return leftover; } @@ -403,7 +403,7 @@ int GuillotineBinPack::ScoreBestLongSideFit(int width, int height, const Rect &f { int leftoverHoriz = abs(freeRect.width - width); int leftoverVert = abs(freeRect.height - height); - int leftover = max(leftoverHoriz, leftoverVert); + int leftover = MAX(leftoverHoriz, leftoverVert); return leftover; } diff --git a/src/SkylineBinPack.cpp b/src/SkylineBinPack.cpp index 578a248a5..33f862043 100644 --- a/src/SkylineBinPack.cpp +++ b/src/SkylineBinPack.cpp @@ -7,6 +7,7 @@ */ #include +#include "templates.h" #include "SkylineBinPack.h" @@ -131,7 +132,7 @@ bool SkylineBinPack::RectangleFits(int skylineNodeIndex, int width, int height, y = skyLine[skylineNodeIndex].y; while(widthLeft > 0) { - y = max(y, skyLine[i].y); + y = MAX(y, skyLine[i].y); if (y + height > binHeight) return false; widthLeft -= skyLine[i].width; @@ -152,7 +153,7 @@ int SkylineBinPack::ComputeWastedArea(int skylineNodeIndex, int width, int heigh break; int leftSide = skyLine[skylineNodeIndex].x; - int rightSide = min(rectRight, leftSide + skyLine[skylineNodeIndex].width); + int rightSide = MIN(rectRight, leftSide + skyLine[skylineNodeIndex].width); assert(y >= skyLine[skylineNodeIndex].y); wastedArea += (rightSide - leftSide) * (y - skyLine[skylineNodeIndex].y); } @@ -179,7 +180,7 @@ void SkylineBinPack::AddWasteMapArea(int skylineNodeIndex, int width, int height break; int leftSide = skyLine[skylineNodeIndex].x; - int rightSide = min(rectRight, leftSide + skyLine[skylineNodeIndex].width); + int rightSide = MIN(rectRight, leftSide + skyLine[skylineNodeIndex].width); assert(y >= skyLine[skylineNodeIndex].y); Rect waste; From 71c7f2b42c115c9f9a0f47e7eeaf0d536573dea1 Mon Sep 17 00:00:00 2001 From: Christoph Oelckers Date: Sat, 9 Jan 2016 12:10:36 +0100 Subject: [PATCH 07/10] - added 'floorterrain' and 'ceilingterrain' sector properties for UDMF. These will take precedence over texture based terrain if used. 'ceilingterrain' is needed because the top of 3D-floors refers to the model sector's ceiling, so in order to give a 3D floor a terrain it must be assignable to the sector's ceiling. Note that although it is basically the same property, its actual function bears no relevance to its use in Eternity. --- specs/udmf_zdoom.txt | 2 ++ src/actor.h | 1 + src/g_level.cpp | 1 + src/namedef.h | 2 ++ src/p_3dfloors.cpp | 5 ++++- src/p_3dmidtex.cpp | 3 +++ src/p_buildmap.cpp | 1 + src/p_local.h | 2 ++ src/p_map.cpp | 18 +++++++++++++++--- src/p_maputl.cpp | 3 +++ src/p_mobj.cpp | 21 ++++++++++++++------- src/p_saveg.cpp | 4 ++++ src/p_sectors.cpp | 6 ++++++ src/p_setup.cpp | 1 + src/p_terrain.cpp | 9 ++++----- src/p_terrain.h | 2 ++ src/p_udmf.cpp | 10 ++++++++++ src/r_defs.h | 4 ++++ src/version.h | 2 +- 19 files changed, 80 insertions(+), 17 deletions(-) diff --git a/specs/udmf_zdoom.txt b/specs/udmf_zdoom.txt index 74d5fb4f7..611a667af 100644 --- a/specs/udmf_zdoom.txt +++ b/specs/udmf_zdoom.txt @@ -212,6 +212,8 @@ Note: All fields default to false unless mentioned otherwise. leakiness = ; // Probability of leaking through radiation suit (0 = never, 256 = always), default = 0. damageterraineffect = ; // Will spawn a terrain splash when damage is inflicted. Default = false. damagehazard = ; // Changes damage model to Strife's delayed damage for the given sector. Default = false. + floorterrain = ; // Sets the terrain for the sector's floor. Default = 'use the flat texture's terrain definition.' + ceilingterrain = ; // Sets the terrain for the sector's ceiling. Default = 'use the flat texture's terrain definition.' * Note about dropactors diff --git a/src/actor.h b/src/actor.h index 468dcc0c6..cb0524587 100644 --- a/src/actor.h +++ b/src/actor.h @@ -896,6 +896,7 @@ public: struct sector_t *floorsector; FTextureID floorpic; // contacted sec floorpic + int floorterrain; struct sector_t *ceilingsector; FTextureID ceilingpic; // contacted sec ceilingpic fixed_t radius, height; // for movement checking diff --git a/src/g_level.cpp b/src/g_level.cpp index aba58e810..03474a5b7 100644 --- a/src/g_level.cpp +++ b/src/g_level.cpp @@ -1232,6 +1232,7 @@ void G_FinishTravel () pawn->dropoffz = pawndup->dropoffz; pawn->floorsector = pawndup->floorsector; pawn->floorpic = pawndup->floorpic; + pawn->floorterrain = pawndup->floorterrain; pawn->ceilingsector = pawndup->ceilingsector; pawn->ceilingpic = pawndup->ceilingpic; pawn->floorclip = pawndup->floorclip; diff --git a/src/namedef.h b/src/namedef.h index 9ca8cb8ed..ae456883c 100644 --- a/src/namedef.h +++ b/src/namedef.h @@ -512,6 +512,8 @@ xx(damageinterval) xx(leakiness) xx(damageterraineffect) xx(damagehazard) +xx(floorterrain) +xx(ceilingterrain) // USDF keywords xx(Amount) diff --git a/src/p_3dfloors.cpp b/src/p_3dfloors.cpp index 3fbbfeab7..496809fac 100644 --- a/src/p_3dfloors.cpp +++ b/src/p_3dfloors.cpp @@ -346,7 +346,7 @@ void P_PlayerOnSpecial3DFloor(player_t* player) // Apply flat specials (using the ceiling!) P_PlayerOnSpecialFlat( - player, TerrainTypes[rover->model->GetTexture(rover->flags & FF_INVERTSECTOR? sector_t::floor : sector_t::ceiling)]); + player, rover->model->GetTerrain(rover->top.isceiling)); break; } @@ -762,6 +762,7 @@ void P_LineOpening_XFloors (FLineOpening &open, AActor * thing, const line_t *li linedef->frontsector->floorplane.ZatPoint(x, y), linedef->backsector->floorplane.ZatPoint(x, y) }; FTextureID highestfloorpic; + int highestfloorterrain = -1; FTextureID lowestceilingpic; highestfloorpic.SetInvalid(); @@ -792,6 +793,7 @@ void P_LineOpening_XFloors (FLineOpening &open, AActor * thing, const line_t *li { highestfloor = ff_top; highestfloorpic = *rover->top.texture; + highestfloorterrain = rover->model->GetTerrain(rover->top.isceiling); } if(ff_top > lowestfloor[j] && ff_top <= thing->z + thing->MaxStepHeight) lowestfloor[j] = ff_top; } @@ -801,6 +803,7 @@ void P_LineOpening_XFloors (FLineOpening &open, AActor * thing, const line_t *li { open.bottom = highestfloor; open.floorpic = highestfloorpic; + open.floorterrain = highestfloorterrain; } if(lowestceiling < open.top) diff --git a/src/p_3dmidtex.cpp b/src/p_3dmidtex.cpp index 9a9492e08..dbca50d10 100644 --- a/src/p_3dmidtex.cpp +++ b/src/p_3dmidtex.cpp @@ -37,6 +37,7 @@ #include "templates.h" #include "p_local.h" +#include "p_terrain.h" //============================================================================ @@ -290,6 +291,8 @@ bool P_LineOpening_3dMidtex(AActor *thing, const line_t *linedef, FLineOpening & open.bottom = tt; open.abovemidtex = true; open.floorpic = linedef->sidedef[0]->GetTexture(side_t::mid); + open.floorterrain = TerrainTypes[open.floorpic]; + } // returns true if it touches the midtexture return (abs(thing->z - tt) <= thing->MaxStepHeight); diff --git a/src/p_buildmap.cpp b/src/p_buildmap.cpp index 670f53751..d9074310d 100644 --- a/src/p_buildmap.cpp +++ b/src/p_buildmap.cpp @@ -443,6 +443,7 @@ static void LoadSectors (sectortype *bsec) sec->movefactor = ORIG_FRICTION_FACTOR; sec->ColorMap = map; sec->ZoneNumber = 0xFFFF; + sec->terrainnum[sector_t::ceiling] = sec->terrainnum[sector_t::floor] = -1; if (bsec->floorstat & 4) { diff --git a/src/p_local.h b/src/p_local.h index 63ee4c1b1..fd5064f3c 100644 --- a/src/p_local.h +++ b/src/p_local.h @@ -293,6 +293,7 @@ struct FLineOpening sector_t *topsec; FTextureID ceilingpic; FTextureID floorpic; + int floorterrain; bool touchmidtex; bool abovemidtex; }; @@ -408,6 +409,7 @@ struct FCheckPosition fixed_t ceilingz; fixed_t dropoffz; FTextureID floorpic; + int floorterrain; sector_t *floorsector; FTextureID ceilingpic; sector_t *ceilingsector; diff --git a/src/p_map.cpp b/src/p_map.cpp index fe5959df9..7aeb9d115 100644 --- a/src/p_map.cpp +++ b/src/p_map.cpp @@ -242,6 +242,7 @@ void P_GetFloorCeilingZ(FCheckPosition &tmf, int flags) tmf.floorz = tmf.dropoffz = sec->floorplane.ZatPoint(tmf.x, tmf.y); tmf.ceilingz = sec->ceilingplane.ZatPoint(tmf.x, tmf.y); tmf.floorpic = sec->GetTexture(sector_t::floor); + tmf.floorterrain = sec->GetTerrain(sector_t::floor); tmf.ceilingpic = sec->GetTexture(sector_t::ceiling); } else @@ -264,6 +265,7 @@ void P_GetFloorCeilingZ(FCheckPosition &tmf, int flags) { tmf.dropoffz = tmf.floorz = ff_top; tmf.floorpic = *rover->top.texture; + tmf.floorterrain = rover->model->GetTerrain(rover->top.isceiling); } } if (ff_bottom <= tmf.ceilingz && ff_bottom > tmf.z + tmf.thing->height) @@ -304,6 +306,7 @@ void P_FindFloorCeiling(AActor *actor, int flags) tmf.floorz = tmf.dropoffz = actor->floorz; tmf.ceilingz = actor->ceilingz; tmf.floorpic = actor->floorpic; + tmf.floorterrain = actor->floorterrain; tmf.ceilingpic = actor->ceilingpic; P_GetFloorCeilingZ(tmf, flags); } @@ -311,6 +314,7 @@ void P_FindFloorCeiling(AActor *actor, int flags) actor->dropoffz = tmf.dropoffz; actor->ceilingz = tmf.ceilingz; actor->floorpic = tmf.floorpic; + actor->floorterrain = tmf.floorterrain; actor->floorsector = tmf.floorsector; actor->ceilingpic = tmf.ceilingpic; actor->ceilingsector = tmf.ceilingsector; @@ -337,6 +341,7 @@ void P_FindFloorCeiling(AActor *actor, int flags) actor->dropoffz = tmf.dropoffz; actor->ceilingz = tmf.ceilingz; actor->floorpic = tmf.floorpic; + actor->floorterrain = tmf.floorterrain; actor->floorsector = tmf.floorsector; actor->ceilingpic = tmf.ceilingpic; actor->ceilingsector = tmf.ceilingsector; @@ -348,6 +353,7 @@ void P_FindFloorCeiling(AActor *actor, int flags) if (actor->Sector != NULL) { actor->floorpic = actor->Sector->GetTexture(sector_t::floor); + actor->floorterrain = actor->Sector->GetTerrain(sector_t::floor); actor->ceilingpic = actor->Sector->GetTexture(sector_t::ceiling); } } @@ -455,6 +461,7 @@ bool P_TeleportMove(AActor *thing, fixed_t x, fixed_t y, fixed_t z, bool telefra thing->ceilingz = tmf.ceilingz; thing->floorsector = tmf.floorsector; thing->floorpic = tmf.floorpic; + thing->floorterrain = tmf.floorterrain; thing->ceilingsector = tmf.ceilingsector; thing->ceilingpic = tmf.ceilingpic; thing->dropoffz = tmf.dropoffz; // killough 11/98 @@ -532,14 +539,14 @@ void P_PlayerStartStomp(AActor *actor) inline fixed_t secfriction(const sector_t *sec, int plane = sector_t::floor) { if (sec->Flags & SECF_FRICTION) return sec->friction; - fixed_t friction = Terrains[TerrainTypes[sec->GetTexture(plane)]].Friction; + fixed_t friction = Terrains[sec->GetTerrain(plane)].Friction; return friction != 0 ? friction : ORIG_FRICTION; } inline fixed_t secmovefac(const sector_t *sec, int plane = sector_t::floor) { if (sec->Flags & SECF_FRICTION) return sec->friction; - fixed_t movefactor = Terrains[TerrainTypes[sec->GetTexture(plane)]].MoveFactor; + fixed_t movefactor = Terrains[sec->GetTerrain(plane)].MoveFactor; return movefactor != 0 ? movefactor : ORIG_FRICTION_FACTOR; } @@ -633,7 +640,7 @@ int P_GetFriction(const AActor *mo, int *frictionfactor) } if (!(sec->Flags & SECF_FRICTION) && - Terrains[TerrainTypes[sec->GetTexture(sector_t::floor)]].Friction == 0) + Terrains[sec->GetTerrain(sector_t::floor)].Friction == 0) { continue; } @@ -891,6 +898,7 @@ bool PIT_CheckLine(line_t *ld, const FBoundingBox &box, FCheckPosition &tm) tm.floorz = open.bottom; tm.floorsector = open.bottomsec; tm.floorpic = open.floorpic; + tm.floorterrain = open.floorterrain; tm.touchmidtex = open.touchmidtex; tm.abovemidtex = open.abovemidtex; tm.thing->BlockingLine = ld; @@ -1436,6 +1444,7 @@ bool P_CheckPosition(AActor *thing, fixed_t x, fixed_t y, FCheckPosition &tm, bo tm.floorz = tm.dropoffz = newsec->floorplane.ZatPoint(x, y); tm.ceilingz = newsec->ceilingplane.ZatPoint(x, y); tm.floorpic = newsec->GetTexture(sector_t::floor); + tm.floorterrain = newsec->GetTerrain(sector_t::floor); tm.floorsector = newsec; tm.ceilingpic = newsec->GetTexture(sector_t::ceiling); tm.ceilingsector = newsec; @@ -1468,6 +1477,7 @@ bool P_CheckPosition(AActor *thing, fixed_t x, fixed_t y, FCheckPosition &tm, bo { tm.floorz = tm.dropoffz = ff_top; tm.floorpic = *rover->top.texture; + tm.floorterrain = rover->model->GetTerrain(rover->top.isceiling); } if (ff_bottom < tm.ceilingz && abs(delta1) >= abs(delta2)) { @@ -2091,6 +2101,7 @@ bool P_TryMove(AActor *thing, fixed_t x, fixed_t y, thing->ceilingz = tm.ceilingz; thing->dropoffz = tm.dropoffz; // killough 11/98: keep track of dropoffs thing->floorpic = tm.floorpic; + thing->floorterrain = tm.floorterrain; thing->floorsector = tm.floorsector; thing->ceilingpic = tm.ceilingpic; thing->ceilingsector = tm.ceilingsector; @@ -4951,6 +4962,7 @@ bool P_AdjustFloorCeil(AActor *thing, FChangePosition *cpos) thing->ceilingz = tm.ceilingz; thing->dropoffz = tm.dropoffz; // killough 11/98: remember dropoffs thing->floorpic = tm.floorpic; + thing->floorterrain = tm.floorterrain; thing->floorsector = tm.floorsector; thing->ceilingpic = tm.ceilingpic; thing->ceilingsector = tm.ceilingsector; diff --git a/src/p_maputl.cpp b/src/p_maputl.cpp index 0a39460e7..e6d5a99e6 100644 --- a/src/p_maputl.cpp +++ b/src/p_maputl.cpp @@ -194,6 +194,7 @@ void P_LineOpening (FLineOpening &open, AActor *actor, const line_t *linedef, open.bottom = ff; open.bottomsec = front; open.floorpic = front->GetTexture(sector_t::floor); + open.floorterrain = front->GetTerrain(sector_t::floor); open.lowfloor = bf; } else @@ -201,6 +202,7 @@ void P_LineOpening (FLineOpening &open, AActor *actor, const line_t *linedef, open.bottom = bf; open.bottomsec = back; open.floorpic = back->GetTexture(sector_t::floor); + open.floorterrain = back->GetTerrain(sector_t::floor); open.lowfloor = ff; } } @@ -211,6 +213,7 @@ void P_LineOpening (FLineOpening &open, AActor *actor, const line_t *linedef, open.top = FIXED_MAX; open.bottomsec = NULL; open.floorpic.SetInvalid(); + open.floorterrain = -1; open.bottom = FIXED_MIN; open.lowfloor = FIXED_MAX; } diff --git a/src/p_mobj.cpp b/src/p_mobj.cpp index 48f16ba0e..8c814738e 100644 --- a/src/p_mobj.cpp +++ b/src/p_mobj.cpp @@ -191,6 +191,10 @@ void AActor::Serialize (FArchive &arc) << tics << state << Damage; + if (SaveVersion >= 4530) + { + arc << floorterrain; + } if (SaveVersion >= 3227) { arc << projectileKickback; @@ -4035,6 +4039,7 @@ AActor *AActor::StaticSpawn (const PClass *type, fixed_t ix, fixed_t iy, fixed_t { actor->floorsector = actor->Sector; actor->floorpic = actor->floorsector->GetTexture(sector_t::floor); + actor->floorterrain = actor->floorsector->GetTerrain(sector_t::floor); actor->ceilingsector = actor->Sector; actor->ceilingpic = actor->ceilingsector->GetTexture(sector_t::ceiling); } @@ -4046,6 +4051,7 @@ AActor *AActor::StaticSpawn (const PClass *type, fixed_t ix, fixed_t iy, fixed_t else { actor->floorpic = actor->Sector->GetTexture(sector_t::floor); + actor->floorterrain = actor->Sector->GetTerrain(sector_t::floor); actor->floorsector = actor->Sector; actor->ceilingpic = actor->Sector->GetTexture(sector_t::ceiling); actor->ceilingsector = actor->Sector; @@ -4339,7 +4345,7 @@ void AActor::AdjustFloorClip () sector_t *hsec = m->m_sector->GetHeightSec(); if (hsec == NULL && m->m_sector->floorplane.ZatPoint (x, y) == z) { - fixed_t clip = Terrains[TerrainTypes[m->m_sector->GetTexture(sector_t::floor)]].FootClip; + fixed_t clip = Terrains[m->m_sector->GetTerrain(sector_t::floor)].FootClip; if (clip < shallowestclip) { shallowestclip = clip; @@ -5303,13 +5309,13 @@ void P_RipperBlood (AActor *mo, AActor *bleeder) int P_GetThingFloorType (AActor *thing) { - if (thing->floorpic.isValid()) + if (thing->floorterrain >= 0) { - return TerrainTypes[thing->floorpic]; + return thing->floorterrain; } else { - return TerrainTypes[thing->Sector->GetTexture(sector_t::floor)]; + return thing->Sector->GetTerrain(sector_t::floor); } } @@ -5362,6 +5368,7 @@ bool P_HitWater (AActor * thing, sector_t * sec, fixed_t x, fixed_t y, fixed_t z } #endif + // 'force' means, we want this sector's terrain, no matter what. if (!force) { for (unsigned int i = 0; ie->XFloor.ffloors.Size(); i++) @@ -5373,7 +5380,7 @@ bool P_HitWater (AActor * thing, sector_t * sec, fixed_t x, fixed_t y, fixed_t z { if (rover->flags & (FF_SOLID | FF_SWIMMABLE)) { - terrainnum = TerrainTypes[*rover->top.texture]; + terrainnum = rover->model->GetTerrain(rover->top.isceiling); goto foundone; } } @@ -5384,11 +5391,11 @@ bool P_HitWater (AActor * thing, sector_t * sec, fixed_t x, fixed_t y, fixed_t z hsec = sec->GetHeightSec(); if (force || hsec == NULL || !(hsec->MoreFlags & SECF_CLIPFAKEPLANES)) { - terrainnum = TerrainTypes[sec->GetTexture(sector_t::floor)]; + terrainnum = sec->GetTerrain(sector_t::floor); } else { - terrainnum = TerrainTypes[hsec->GetTexture(sector_t::floor)]; + terrainnum = hsec->GetTerrain(sector_t::floor); } foundone: diff --git a/src/p_saveg.cpp b/src/p_saveg.cpp index 8e2f7e9d9..918eb7a89 100644 --- a/src/p_saveg.cpp +++ b/src/p_saveg.cpp @@ -369,6 +369,10 @@ void P_SerializeWorld (FArchive &arc) << sec->heightsec << sec->bottommap << sec->midmap << sec->topmap << sec->gravity; + if (SaveVersion >= 4530) + { + arc << sec->terrainnum[0] << sec->terrainnum[1]; + } if (SaveVersion >= 4529) { arc << sec->damageamount; diff --git a/src/p_sectors.cpp b/src/p_sectors.cpp index 321ecdd37..5a60933ba 100644 --- a/src/p_sectors.cpp +++ b/src/p_sectors.cpp @@ -26,6 +26,7 @@ #include "doomstat.h" #include "g_level.h" #include "nodebuild.h" +#include "p_terrain.h" #include "po_man.h" #include "farchive.h" #include "r_utility.h" @@ -873,6 +874,11 @@ void sector_t::TransferSpecial(sector_t *model) Flags = (Flags&~SECF_SPECIALFLAGS) | (model->Flags & SECF_SPECIALFLAGS); } +int sector_t::GetTerrain(int pos) const +{ + return terrainnum[pos] >= 0 ? terrainnum[pos] : TerrainTypes[GetTexture(pos)]; +} + FArchive &operator<< (FArchive &arc, secspecial_t &p) { if (SaveVersion < 4529) diff --git a/src/p_setup.cpp b/src/p_setup.cpp index 772679269..3749e97aa 100644 --- a/src/p_setup.cpp +++ b/src/p_setup.cpp @@ -1532,6 +1532,7 @@ void P_LoadSectors (MapData *map, FMissingTextureTracker &missingtex) ss->gravity = 1.f; // [RH] Default sector gravity of 1.0 ss->ZoneNumber = 0xFFFF; + ss->terrainnum[sector_t::ceiling] = ss->terrainnum[sector_t::floor] = -1; // [RH] Sectors default to white light with the default fade. // If they are outside (have a sky ceiling), they use the outside fog. diff --git a/src/p_terrain.cpp b/src/p_terrain.cpp index a1a9ea25b..006835ea6 100644 --- a/src/p_terrain.cpp +++ b/src/p_terrain.cpp @@ -121,7 +121,6 @@ static void ParseSplash (FScanner &sc); static void ParseTerrain (FScanner &sc); static void ParseFloor (FScanner &sc); static int FindSplash (FName name); -static int FindTerrain (FName name); static void GenericParse (FScanner &sc, FGenericParse *parser, const char **keywords, void *fields, const char *type, FName name); static void ParseDamage (FScanner &sc, int keyword, void *fields); @@ -427,7 +426,7 @@ void ParseTerrain (FScanner &sc) sc.MustGetString (); name = sc.String; - terrainnum = (int)FindTerrain (name); + terrainnum = (int)P_FindTerrain (name); if (terrainnum < 0) { FTerrainDef def; @@ -637,7 +636,7 @@ static void ParseFloor (FScanner &sc) return; } sc.MustGetString (); - terrain = FindTerrain (sc.String); + terrain = P_FindTerrain (sc.String); if (terrain == -1) { Printf ("Unknown terrain %s\n", sc.String); @@ -657,7 +656,7 @@ static void ParseDefault (FScanner &sc) int terrain; sc.MustGetString (); - terrain = FindTerrain (sc.String); + terrain = P_FindTerrain (sc.String); if (terrain == -1) { Printf ("Unknown terrain %s\n", sc.String); @@ -692,7 +691,7 @@ int FindSplash (FName name) // //========================================================================== -int FindTerrain (FName name) +int P_FindTerrain (FName name) { unsigned int i; diff --git a/src/p_terrain.h b/src/p_terrain.h index 482c66b00..85059a945 100644 --- a/src/p_terrain.h +++ b/src/p_terrain.h @@ -122,4 +122,6 @@ struct FTerrainDef extern TArray Splashes; extern TArray Terrains; +int P_FindTerrain(FName name); + #endif //__P_TERRAIN_H__ diff --git a/src/p_udmf.cpp b/src/p_udmf.cpp index e95193860..9cfcbc0c2 100644 --- a/src/p_udmf.cpp +++ b/src/p_udmf.cpp @@ -47,6 +47,7 @@ #include "r_data/colormaps.h" #include "w_wad.h" #include "p_tags.h" +#include "p_terrain.h" //=========================================================================== // @@ -1300,6 +1301,7 @@ public: sec->heightsec = NULL; // sector used to get floor and ceiling height sec->sectornum = index; sec->damageinterval = 32; + sec->terrainnum[sector_t::ceiling] = sec->terrainnum[sector_t::floor] = -1; if (floordrop) sec->Flags = SECF_FLOORDROP; // killough 3/7/98: end changes @@ -1550,6 +1552,14 @@ public: Flag(sec->Flags, SECF_HAZARD, key); break; + case NAME_floorterrain: + sec->terrainnum[sector_t::floor] = P_FindTerrain(CheckString(key)); + break; + + case NAME_ceilingterrain: + sec->terrainnum[sector_t::ceiling] = P_FindTerrain(CheckString(key)); + break; + case NAME_MoreIds: // delay parsing of the tag string until parsing of the sector is complete // This ensures that the ID is always the first tag in the list. diff --git a/src/r_defs.h b/src/r_defs.h index 536376b63..a7e37f369 100644 --- a/src/r_defs.h +++ b/src/r_defs.h @@ -699,6 +699,8 @@ struct sector_t Flags &= ~SECF_SPECIALFLAGS; } + int GetTerrain(int pos) const; + void TransferSpecial(sector_t *model); void GetSpecial(secspecial_t *spec); void SetSpecial(const secspecial_t *spec); @@ -734,6 +736,8 @@ struct sector_t // when processed as mobj properties. Fix is to make them sector properties. fixed_t friction, movefactor; + int terrainnum[2]; + // thinker_t for reversable actions TObjPtr floordata; // jff 2/22/98 make thinkers on TObjPtr ceilingdata; // floors, ceilings, lighting, diff --git a/src/version.h b/src/version.h index b10074f41..6a0eb9c4b 100644 --- a/src/version.h +++ b/src/version.h @@ -76,7 +76,7 @@ const char *GetVersionString(); // Use 4500 as the base git save version, since it's higher than the // SVN revision ever got. -#define SAVEVER 4529 +#define SAVEVER 4530 #define SAVEVERSTRINGIFY2(x) #x #define SAVEVERSTRINGIFY(x) SAVEVERSTRINGIFY2(x) From 7f454358b9f99ae3ff565c313bb8f8dc4f02aa8b Mon Sep 17 00:00:00 2001 From: Christoph Oelckers Date: Sat, 9 Jan 2016 12:16:41 +0100 Subject: [PATCH 08/10] - added ACS SetSectorTerrain function. --- src/p_acs.cpp | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/p_acs.cpp b/src/p_acs.cpp index aacd5cc4a..c40ba6b94 100644 --- a/src/p_acs.cpp +++ b/src/p_acs.cpp @@ -75,6 +75,7 @@ #include "actorptrselect.h" #include "farchive.h" #include "decallib.h" +#include "p_terrain.h" #include "version.h" #include "g_shared/a_pickups.h" @@ -4459,6 +4460,7 @@ enum EACSFunctions ACSF_Warp, // 92 ACSF_GetMaxInventory, ACSF_SetSectorDamage, + ACSF_SetSectorTerrain, /* Zandronum's - these must be skipped when we reach 99! -100:ResetMap(0), @@ -5968,6 +5970,18 @@ doplaysound: if (funcIndex == ACSF_PlayActorSound) } break; + case ACSF_SetSectorTerrain: + if (argCount >= 3) + { + int terrain = P_FindTerrain(FBehavior::StaticLookupString(args[2])); + FSectorTagIterator it(args[0]); + int s; + while ((s = it.Next()) >= 0) + { + sectors[s].terrainnum[args[1]] = terrain; + } + } + default: break; } From 23cfd29dbb5a837ae04d4522b623ac21836dc396 Mon Sep 17 00:00:00 2001 From: Christoph Oelckers Date: Sat, 9 Jan 2016 12:28:42 +0100 Subject: [PATCH 09/10] - let's save the terrain properties as names so that they survive a change in the definition files. --- src/p_mobj.cpp | 2 +- src/p_saveg.cpp | 4 +++- src/p_terrain.cpp | 24 ++++++++++++++++++++++++ src/p_terrain.h | 2 ++ 4 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/p_mobj.cpp b/src/p_mobj.cpp index 8c814738e..3d0d8e072 100644 --- a/src/p_mobj.cpp +++ b/src/p_mobj.cpp @@ -193,7 +193,7 @@ void AActor::Serialize (FArchive &arc) << Damage; if (SaveVersion >= 4530) { - arc << floorterrain; + P_SerializeTerrain(arc, floorterrain); } if (SaveVersion >= 3227) { diff --git a/src/p_saveg.cpp b/src/p_saveg.cpp index 918eb7a89..db14aeb90 100644 --- a/src/p_saveg.cpp +++ b/src/p_saveg.cpp @@ -52,6 +52,7 @@ #include "farchive.h" #include "p_lnspec.h" #include "p_acs.h" +#include "p_terrain.h" static void CopyPlayer (player_t *dst, player_t *src, const char *name); static void ReadOnePlayer (FArchive &arc, bool skipload); @@ -371,7 +372,8 @@ void P_SerializeWorld (FArchive &arc) << sec->gravity; if (SaveVersion >= 4530) { - arc << sec->terrainnum[0] << sec->terrainnum[1]; + P_SerializeTerrain(arc, sec->terrainnum[0]); + P_SerializeTerrain(arc, sec->terrainnum[1]); } if (SaveVersion >= 4529) { diff --git a/src/p_terrain.cpp b/src/p_terrain.cpp index 006835ea6..af8de5cab 100644 --- a/src/p_terrain.cpp +++ b/src/p_terrain.cpp @@ -46,6 +46,7 @@ #include "s_sound.h" #include "p_local.h" #include "templates.h" +#include "farchive.h" // MACROS ------------------------------------------------------------------ @@ -704,3 +705,26 @@ int P_FindTerrain (FName name) } return -1; } + +void P_SerializeTerrain(FArchive &arc, int &terrainnum) +{ + FName val; + if (arc.IsStoring()) + { + if (terrainnum < 0 || terrainnum >= (int)Terrains.Size()) + { + val = NAME_Null; + } + else + { + val = Terrains[terrainnum].Name; + } + arc << val; + } + else + { + arc << val; + terrainnum = P_FindTerrain(val); + + } +} diff --git a/src/p_terrain.h b/src/p_terrain.h index 85059a945..dab4904b6 100644 --- a/src/p_terrain.h +++ b/src/p_terrain.h @@ -122,6 +122,8 @@ struct FTerrainDef extern TArray Splashes; extern TArray Terrains; +class FArchive; int P_FindTerrain(FName name); +void P_SerializeTerrain(FArchive &arc, int &terrainnum); #endif //__P_TERRAIN_H__ From b27fdcffb53bb889b9f41045ff0f6d71559b4be0 Mon Sep 17 00:00:00 2001 From: "alexey.lysiuk" Date: Sat, 9 Jan 2016 16:59:13 +0200 Subject: [PATCH 10/10] Added shortcut to toggle fullscreen on OS X Press Command+F to toggle fullscreen mode in native OS X backend Set k_allowfullscreentoggle CVAR to false to disable the shortcut --- src/posix/cocoa/i_common.h | 1 + src/posix/cocoa/i_input.mm | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/src/posix/cocoa/i_common.h b/src/posix/cocoa/i_common.h index 545540b2f..7d7f82ac5 100644 --- a/src/posix/cocoa/i_common.h +++ b/src/posix/cocoa/i_common.h @@ -79,6 +79,7 @@ typedef float CGFloat; // From HIToolbox/Events.h enum { + kVK_ANSI_F = 0x03, kVK_Return = 0x24, kVK_Tab = 0x30, kVK_Space = 0x31, diff --git a/src/posix/cocoa/i_input.mm b/src/posix/cocoa/i_input.mm index 5e0c5f1c8..3bbf42a9a 100644 --- a/src/posix/cocoa/i_input.mm +++ b/src/posix/cocoa/i_input.mm @@ -57,6 +57,8 @@ CVAR(Bool, use_mouse, true, CVAR_ARCHIVE | CVAR_GLOBALCONFIG) CVAR(Bool, m_noprescale, false, CVAR_ARCHIVE | CVAR_GLOBALCONFIG) CVAR(Bool, m_filter, false, CVAR_ARCHIVE | CVAR_GLOBALCONFIG) +CVAR(Bool, k_allowfullscreentoggle, true, CVAR_ARCHIVE | CVAR_GLOBALCONFIG) + CUSTOM_CVAR(Int, mouse_capturemode, 1, CVAR_GLOBALCONFIG | CVAR_ARCHIVE) { if (self < 0) @@ -542,6 +544,16 @@ void ProcessKeyboardEvent(NSEvent* theEvent) return; } + if (k_allowfullscreentoggle + && (kVK_ANSI_F == keyCode) + && (NSCommandKeyMask & [theEvent modifierFlags]) + && (NSKeyDown == [theEvent type]) + && ![theEvent isARepeat]) + { + ToggleFullscreen = !ToggleFullscreen; + return; + } + if (GUICapture) { ProcessKeyboardEventInMenu(theEvent);