/* Copyright (c) 2021 Alex Diener This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. 3. This notice may not be removed or altered from any source distribution. Alex Diener alex@ludobloom.com */ #include "3dmodelio/TexturePacker.h" #include #include #include #define stemobject_implementation TexturePacker stemobject_vtable_begin(); stemobject_vtable_entry(dispose); stemobject_vtable_end(); TexturePacker * TexturePacker_create(enum BitmapPixelFormat pixelFormat) { stemobject_create_implementation(init, pixelFormat) } bool TexturePacker_init(TexturePacker * self, enum BitmapPixelFormat pixelFormat) { call_super(init, self); self->pixelFormat = pixelFormat; self->entryCount = 0; self->entries = NULL; self->packedWidth = 0; self->packedHeight = 0; return true; } void TexturePacker_dispose(TexturePacker * self) { for (unsigned int entryIndex = 0; entryIndex < self->entryCount; entryIndex++) { if (self->entries[entryIndex].image != NULL && self->entries[entryIndex].imageOwned) { BitmapImage_dispose(self->entries[entryIndex].image); } free(self->entries[entryIndex].name); } free(self->entries); call_super(dispose, self); } static unsigned int roundUpToPowerOfTwo(unsigned int v) { // http://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2 v--; v |= v >> 1; v |= v >> 2; v |= v >> 4; v |= v >> 8; v |= v >> 16; v++; return v; } // TODO: After placing a tall element, subdivide available space to the right and recurse into it for better packing if one element is significantly taller than the rest // TODO: Could alternate top/bottom placement preference to theoretically open up even more space on the right sides of rows void TexturePacker_pack(TexturePacker * self, bool powersOfTwoOnly) { unsigned int entryCount = self->entryCount; struct TexturePacker_atlasEntry * entries = self->entries; unsigned int width = 0, height = 0; unsigned int minWidth = 0, maxWidth = 0, bestWidth = UINT_MAX, bestHeight = UINT_MAX; unsigned int localMinWidth; if (entryCount == 0) { return; } // Measure all elements to find max and min width and min height, sorting from tallest to shortest for (unsigned int entryIndex = 0; entryIndex < entryCount; entryIndex++) { for (unsigned int entryIndex2 = entryCount - 1; entryIndex2 > entryIndex; entryIndex2--) { if (entries[entryIndex2].height + entries[entryIndex2].margin > entries[entryIndex].height + entries[entryIndex].margin) { struct TexturePacker_atlasEntry swap = entries[entryIndex2]; entries[entryIndex2] = entries[entryIndex]; entries[entryIndex] = swap; } } maxWidth += entries[entryIndex].width + entries[entryIndex].margin; if (entries[entryIndex].width + entries[entryIndex].margin > minWidth) { minWidth = entries[entryIndex].width + entries[entryIndex].margin; } } width = maxWidth; if (powersOfTwoOnly) { width = roundUpToPowerOfTwo(width); } struct TexturePacker_atlasEntry * bestConfiguration = malloc(entryCount * sizeof(*bestConfiguration)); struct { unsigned int width; unsigned int height; unsigned int x; unsigned int y; bool closed; } * rows; unsigned int rowAllocatedCount = 128; rows = malloc(rowAllocatedCount * sizeof(*rows)); do { height = 0; unsigned int rowCount = 0; unsigned int entryIndex = 0; unsigned int nextRowY = 0; while (entryIndex < entryCount) { bool homeFound = false; for (unsigned int rowIndex = 0; rowIndex < rowCount; rowIndex++) { if (!rows[rowIndex].closed && rows[rowIndex].x + rows[rowIndex].width + entries[entryIndex].width + entries[entryIndex].margin <= width) { entries[entryIndex].x = rows[rowIndex].x + rows[rowIndex].width; entries[entryIndex].y = rows[rowIndex].y; homeFound = true; if (rows[rowIndex].height / 2 >= entries[entryIndex].height + entries[entryIndex].margin) { // Split row down the middle if (rowCount + 1 >= rowAllocatedCount) { rowAllocatedCount *= 2; rows = realloc(rows, rowAllocatedCount * sizeof(*rows)); } rows[rowCount].width = entries[entryIndex].width + entries[entryIndex].margin; rows[rowCount].height = rows[rowIndex].height / 2; rows[rowCount].x = rows[rowIndex].x + rows[rowIndex].width; rows[rowCount].y = rows[rowIndex].y; rows[rowCount].closed = false; rowCount++; rows[rowCount].width = 0; rows[rowCount].height = rows[rowIndex].height - rows[rowCount - 1].height; rows[rowCount].x = rows[rowCount - 1].x; rows[rowCount].y = rows[rowIndex].y + rows[rowCount - 1].height; rows[rowCount].closed = false; rowCount++; rows[rowIndex].closed = true; } else { rows[rowIndex].width += entries[entryIndex].width + entries[entryIndex].margin; } break; } } if (!homeFound) { if (rowCount >= rowAllocatedCount) { rowAllocatedCount *= 2; rows = realloc(rows, rowAllocatedCount * sizeof(*rows)); } rows[rowCount].width = entries[entryIndex].width + entries[entryIndex].margin; rows[rowCount].height = entries[entryIndex].height + entries[entryIndex].margin; rows[rowCount].x = 0; rows[rowCount].y = nextRowY; rows[rowCount].closed = false; nextRowY = rows[rowCount].y + rows[rowCount].height; entries[entryIndex].x = 0; entries[entryIndex].y = rows[rowCount].y; height += rows[rowCount].height; rowCount++; } entryIndex++; } localMinWidth = rows[0].x + rows[0].width; for (unsigned int rowIndex = 1; rowIndex < rowCount; rowIndex++) { if (rows[rowIndex].x + rows[rowIndex].width > localMinWidth) { localMinWidth = rows[rowIndex].x + rows[rowIndex].width; } } width = localMinWidth; if (powersOfTwoOnly) { width = roundUpToPowerOfTwo(width); height = roundUpToPowerOfTwo(height); if (width >= (1u << 31) || height >= (1u << 31)) { break; } } if (bestWidth == UINT_MAX || width * height < bestWidth * bestHeight || (width * height == bestWidth * bestHeight && width + height < bestWidth + bestHeight)) { bestWidth = width; bestHeight = height; memcpy(bestConfiguration, entries, entryCount * sizeof(*bestConfiguration)); } if (powersOfTwoOnly) { width = roundUpToPowerOfTwo(localMinWidth) / 2; } else if (localMinWidth > 0) { width = localMinWidth - 1; } } while (width > minWidth); free(rows); self->packedWidth = bestWidth; self->packedHeight = bestHeight; memcpy(entries, bestConfiguration, entryCount * sizeof(*bestConfiguration)); free(bestConfiguration); } BitmapImage * TexturePacker_rasterize(TexturePacker * self, bool flip) { if (self->packedWidth == 0 || self->packedHeight == 0) { return NULL; } unsigned int atlasWidth = self->packedWidth; unsigned int atlasHeight = self->packedHeight; BitmapImage * outputImage = BitmapImage_create(self->pixelFormat, self->packedWidth, self->packedHeight); memset(outputImage->pixels, 0, atlasWidth * atlasHeight * BitmapImage_pixelFormatBytes(self->pixelFormat)); for (unsigned entryIndex = 0; entryIndex < self->entryCount; entryIndex++) { BitmapImage_copyPixels(outputImage, self->entries[entryIndex].image, 0, 0, self->entries[entryIndex].x, self->entries[entryIndex].y, self->entries[entryIndex].width, self->entries[entryIndex].height); } if (flip) { BitmapImage_flipVertical(outputImage); } return outputImage; } static Rect4f getEntryBounds(struct TexturePacker_atlasEntry entry) { Rect4f bounds; bounds.xMin = entry.x + entry.internalMargin; bounds.xMax = entry.x + entry.width - entry.internalMargin; bounds.yMin = entry.y + entry.internalMargin; bounds.yMax = entry.y + entry.height - entry.internalMargin; return bounds; } TextureAtlasData * TexturePacker_createTextureAtlasData(TexturePacker * self, const char * textureName) { unsigned int entryCount = self->entryCount; struct TexturePacker_atlasEntry * entries = self->entries; struct TextureAtlasData_entry * atlasDataEntries = malloc(entryCount * sizeof(*atlasDataEntries)); unsigned int atlasDataEntryCount = 0; for (unsigned int entryIndex = 0; entryIndex < entryCount; entryIndex++) { if (entries[entryIndex].name != NULL) { struct TextureAtlasData_entry atlasDataEntry; atlasDataEntry.name = entries[entryIndex].name; atlasDataEntry.bounds = getEntryBounds(entries[entryIndex]); atlasDataEntries[atlasDataEntryCount++] = atlasDataEntry; } } return TextureAtlasData_create(textureName, atlasDataEntryCount, atlasDataEntries); } TextureAtlas * TexturePacker_createTextureAtlas(TexturePacker * self) { unsigned int entryCount = self->entryCount; struct TexturePacker_atlasEntry * entries = self->entries; TextureAtlas * textureAtlas = TextureAtlas_create(self->packedWidth, self->packedHeight); for (unsigned int entryIndex = 0; entryIndex < entryCount; entryIndex++) { HashTable_key key; if (entries[entryIndex].name != NULL) { key = HashTable_stringKey(entries[entryIndex].name); } else { key = HashTable_uint32Key(entries[entryIndex].identifier); } Rect4f entryBounds = getEntryBounds(entries[entryIndex]); TextureAtlas_setEntry(textureAtlas, key, TextureAtlasData_convertToTextureAtlasCoodinates(entryBounds, self->packedWidth, self->packedHeight)); } return textureAtlas; } bool TexturePacker_addImage(TexturePacker * self, BitmapImage * image, bool ownImage, const char * name, bool flipped, unsigned int margin, int internalMargin) { assert(image->pixelFormat == self->pixelFormat); if (flipped) { if (!ownImage) { image = BitmapImage_copy(image); ownImage = true; } BitmapImage_flipVertical(image); } struct TexturePacker_atlasEntry newEntry = {.image = image, .imageOwned = ownImage, .identifier = UINT32_MAX, .width = image->width, .height = image->height, .margin = margin, .internalMargin = internalMargin, .x = 0, .y = 0}; for (unsigned int entryIndex = 0; entryIndex < self->entryCount; entryIndex++) { if (!strcmp(self->entries[entryIndex].name, name)) { newEntry.name = self->entries[entryIndex].name; newEntry.x = self->entries[entryIndex].x; newEntry.y = self->entries[entryIndex].y; if (self->entries[entryIndex].image != NULL && self->entries[entryIndex].imageOwned) { BitmapImage_dispose(self->entries[entryIndex].image); } self->entries[entryIndex] = newEntry; return true; } } self->entries = realloc(self->entries, (self->entryCount + 1) * sizeof(*self->entries)); newEntry.name = strdup(name); self->entries[self->entryCount] = newEntry; self->entryCount++; return false; } bool TexturePacker_addRectangle(TexturePacker * self, uint32_t identifier, unsigned int width, unsigned int height, unsigned int margin, int internalMargin) { assert(identifier != UINT32_MAX); struct TexturePacker_atlasEntry newEntry = {.image = NULL, .imageOwned = false, .name = NULL, .identifier = identifier, .width = width, .height = height, .margin = margin, .internalMargin = internalMargin, .x = 0, .y = 0}; for (unsigned int entryIndex = 0; entryIndex < self->entryCount; entryIndex++) { if (self->entries[entryIndex].identifier == identifier) { newEntry.x = self->entries[entryIndex].x; newEntry.y = self->entries[entryIndex].y; self->entries[entryIndex] = newEntry; return true; } } self->entries = realloc(self->entries, (self->entryCount + 1) * sizeof(*self->entries)); self->entries[self->entryCount] = newEntry; self->entryCount++; return false; } BitmapImage * TexturePacker_extractEntryImage(Rect4f bounds, BitmapImage * atlasImage, bool inputFlipped, bool outputFlipped, bool coordinatesFlipped, bool coordinatesRelative) { assert(bounds.xMin >= 0); assert(bounds.xMax >= 0); assert(bounds.yMin >= 0); assert(bounds.yMax >= 0); unsigned int componentCount = BitmapImage_pixelFormatBytes(atlasImage->pixelFormat); if (coordinatesRelative) { bounds.xMin *= atlasImage->width; bounds.xMax *= atlasImage->width; bounds.yMin *= atlasImage->height; bounds.yMax *= atlasImage->height; } unsigned int pixelLeft, pixelRight, pixelTop, pixelBottom; pixelLeft = bounds.xMin; pixelRight = bounds.xMax; if (inputFlipped) { if (coordinatesFlipped) { pixelTop = bounds.yMin; pixelBottom = bounds.yMax; } else { pixelTop = atlasImage->height - bounds.yMax; pixelBottom = atlasImage->height - bounds.yMin; } } else { if (coordinatesFlipped) { pixelTop = atlasImage->height - bounds.yMax; pixelBottom = atlasImage->height - bounds.yMin; } else { pixelTop = bounds.yMin; pixelBottom = bounds.yMax; } } assert(pixelRight >= pixelLeft); assert(pixelBottom >= pixelTop); BitmapImage * entryImage = BitmapImage_create(atlasImage->pixelFormat, pixelRight - pixelLeft, pixelBottom - pixelTop); unsigned char * inPixels = atlasImage->pixels; unsigned char * outPixels = entryImage->pixels; unsigned int inWidth = atlasImage->width; unsigned int outWidth = entryImage->width; if (inputFlipped) { for (unsigned int rowIndex = pixelTop; rowIndex < pixelBottom; rowIndex++) { for (unsigned int columnIndex = pixelLeft; columnIndex < pixelRight; columnIndex++) { for (unsigned int componentIndex = 0; componentIndex < componentCount; componentIndex++) { outPixels[(pixelBottom - rowIndex - 1) * outWidth * componentCount + (columnIndex - pixelLeft) * componentCount + componentIndex] = inPixels[rowIndex * inWidth * componentCount + columnIndex * componentCount + componentIndex]; } } } } else { for (unsigned int rowIndex = pixelTop; rowIndex < pixelBottom; rowIndex++) { for (unsigned int columnIndex = pixelLeft; columnIndex < pixelRight; columnIndex++) { for (unsigned int componentIndex = 0; componentIndex < componentCount; componentIndex++) { outPixels[(rowIndex - pixelTop) * outWidth * componentCount + (columnIndex - pixelLeft) * componentCount + componentIndex] = inPixels[rowIndex * inWidth * componentCount + columnIndex * componentCount + componentIndex]; } } } } if (outputFlipped) { BitmapImage_flipVertical(entryImage); } return entryImage; } bool TexturePacker_addAllEntriesFromAtlas(TexturePacker * self, TextureAtlasData * atlasData, BitmapImage * atlasImage, bool imageFlipped, unsigned int margin) { assert(atlasImage->pixelFormat == self->pixelFormat); bool collision = false; for (size_t entryIndex = 0; entryIndex < atlasData->entryCount; entryIndex++) { collision |= TexturePacker_addImage(self, TexturePacker_extractEntryImage(atlasData->entries[entryIndex].bounds, atlasImage, imageFlipped, false, false, false), true, atlasData->entries[entryIndex].name, false, margin, 0); } return collision; } typedef struct rect4u { unsigned int xMin; unsigned int yMin; unsigned int xMax; unsigned int yMax; } rect4u; static inline rect4u getEntryRect(struct TexturePacker_atlasEntry entry) { return (rect4u) {entry.x, entry.y, entry.x + entry.width, entry.y + entry.height}; } float TexturePacker_getDensity(TexturePacker * self) { unsigned int area = 0; unsigned int maxWidth = 0, maxHeight = 0; rect4u entryRect; for (unsigned int entryIndex = 0; entryIndex < self->entryCount; entryIndex++) { entryRect = getEntryRect(self->entries[entryIndex]); if (entryRect.xMax > maxWidth) { maxWidth = entryRect.xMax; } if (entryRect.yMax > maxHeight) { maxHeight = entryRect.yMax; } area += (entryRect.xMax - entryRect.xMin) * (entryRect.yMax - entryRect.yMin); } return (float) area / (maxWidth * maxHeight); }