479 lines
11 KiB
C
479 lines
11 KiB
C
/* Cram - A texture packing system in C
|
|
*
|
|
* Copyright (c) 2022 Evan Hemsley
|
|
*
|
|
* 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.
|
|
*
|
|
* Evan "cosmonaut" Hemsley <evan@moonside.games>
|
|
*
|
|
*/
|
|
|
|
#include "cram.h"
|
|
|
|
#ifdef _MSC_VER
|
|
|
|
#include <assert.h>
|
|
#include <stdlib.h>
|
|
#include <search.h>
|
|
#include <string.h>
|
|
|
|
#endif /* _MSC_VER */
|
|
|
|
/* TODO: ifndefs here */
|
|
#define Cram_assert assert
|
|
#define Cram_qsort qsort
|
|
#define Cram_malloc malloc
|
|
#define Cram_realloc realloc
|
|
#define Cram_free free
|
|
#define Cram_memcpy memcpy
|
|
#define Cram_memset memset
|
|
#define Cram_strdup strdup
|
|
#define Cram_max max
|
|
|
|
#define STBRP_ASSERT Cram_assert
|
|
#define STBRP_SORT Cram_qsort
|
|
|
|
#define STBRP_STATIC
|
|
#define STB_RECT_PACK_IMPLEMENTATION
|
|
#include "stb_rect_pack.h"
|
|
|
|
#define STBI_ASSERT Cram_assert
|
|
#define STBI_MALLOC Cram_malloc
|
|
#define STBI_REALLOC Cram_realloc
|
|
#define STBI_FREE Cram_free
|
|
|
|
#define STBI_ONLY_PNG
|
|
#define STB_IMAGE_IMPLEMENTATION
|
|
#include "stb_image.h"
|
|
|
|
#define STB_DS_IMPLEMENTATION
|
|
#include "stb_ds.h"
|
|
|
|
#define INITIAL_DATA_CAPACITY 8
|
|
|
|
#define STBDS_SIZE_T_BITS ((sizeof (size_t)) * 8)
|
|
#define STBDS_ROTATE_LEFT(val, n) (((val) << (n)) | ((val) >> (STBDS_SIZE_T_BITS - (n))))
|
|
#define STBDS_ROTATE_RIGHT(val, n) (((val) >> (n)) | ((val) << (STBDS_SIZE_T_BITS - (n))))
|
|
|
|
/* Structures */
|
|
|
|
typedef struct Rect
|
|
{
|
|
int32_t x, y;
|
|
int32_t w, h;
|
|
} Rect;
|
|
|
|
typedef struct Cram_Image
|
|
{
|
|
const char *name;
|
|
Rect rect;
|
|
uint8_t duplicate;
|
|
uint8_t *pixels; // will be NULL if duplicate!
|
|
size_t hash;
|
|
} Cram_Image;
|
|
|
|
typedef struct Cram_Internal_Context
|
|
{
|
|
const char *name;
|
|
uint32_t width;
|
|
uint32_t height;
|
|
|
|
uint32_t padding;
|
|
uint8_t trim;
|
|
|
|
uint8_t *pixels;
|
|
|
|
Cram_Image *images;
|
|
uint32_t imageCount;
|
|
uint32_t imageCapacity;
|
|
|
|
Cram_AtlasData *atlasData;
|
|
} Cram_Internal_Context;
|
|
|
|
/* API functions */
|
|
|
|
uint32_t Wellspring_LinkedVersion(void)
|
|
{
|
|
return WELLSPRING_COMPILED_VERSION;
|
|
}
|
|
|
|
Cram_Context* Cram_Init(Cram_ContextCreateInfo *createInfo)
|
|
{
|
|
Cram_Internal_Context *context = Cram_malloc(sizeof(Cram_Internal_Context));
|
|
|
|
context->name = Cram_strdup(createInfo->name);
|
|
|
|
context->width = createInfo->maxDimension;
|
|
context->height = createInfo->maxDimension;
|
|
|
|
context->padding = createInfo->padding;
|
|
context->trim = createInfo->trim;
|
|
|
|
context->images = Cram_malloc(INITIAL_DATA_CAPACITY * sizeof(Cram_Image));
|
|
context->imageCapacity = INITIAL_DATA_CAPACITY;
|
|
context->imageCount = 0;
|
|
|
|
context->atlasData = Cram_malloc(sizeof(Cram_AtlasData));
|
|
|
|
context->pixels = NULL;
|
|
|
|
return (Cram_Context*) context;
|
|
}
|
|
|
|
static uint8_t Cram_Internal_IsImageEqual(Cram_Image *a, Cram_Image *b)
|
|
{
|
|
int32_t i;
|
|
if (a->hash == b->hash && a->rect.w == b->rect.w && a->rect.h == b->rect.h)
|
|
{
|
|
for (i = 0; i < a->rect.w * a->rect.h * 4; i += 1)
|
|
{
|
|
if (a->pixels[i] != b->pixels[i])
|
|
{
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static inline uint32_t Cram_Internal_GetPixelIndex(uint32_t x, uint32_t y, uint32_t width)
|
|
{
|
|
return x + y * width;
|
|
}
|
|
|
|
static uint8_t Cram_Internal_IsRowClear(uint32_t* pixels, uint32_t rowIndex, uint32_t width)
|
|
{
|
|
int32_t i;
|
|
|
|
for (i = 0; i < width; i += 1)
|
|
{
|
|
if ((pixels[Cram_Internal_GetPixelIndex(i, rowIndex, width)] & 0xFF) > 0)
|
|
{
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
static uint8_t Cram_Internal_IsColumnClear(uint32_t* pixels, uint32_t columnIndex, uint32_t width, uint32_t height)
|
|
{
|
|
int32_t i;
|
|
|
|
for (i = 0; i < height; i += 1)
|
|
{
|
|
if ((pixels[Cram_Internal_GetPixelIndex(columnIndex, i, width)] & 0xFF) > 0)
|
|
{
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
/* width and height of source and destination rects must be the same! */
|
|
static int8_t Cram_Internal_CopyPixels(
|
|
uint32_t *dstPixels,
|
|
uint32_t dstPixelWidth,
|
|
uint32_t *srcPixels,
|
|
uint32_t srcPixelWidth,
|
|
Rect *dstRect,
|
|
Rect *srcRect
|
|
) {
|
|
int32_t i, j;
|
|
int32_t dstPixelIndex, srcPixelIndex;
|
|
|
|
if (dstRect->w != srcRect->w || dstRect->h != srcRect->h)
|
|
{
|
|
return -1;
|
|
}
|
|
|
|
for (i = 0; i < dstRect->w; i += 1)
|
|
{
|
|
for (j = 0; j < dstRect->h; j += 1)
|
|
{
|
|
dstPixelIndex = Cram_Internal_GetPixelIndex(i + dstRect->x, j + dstRect->y, dstPixelWidth);
|
|
srcPixelIndex = Cram_Internal_GetPixelIndex(i + srcRect->x, j + srcRect->y, srcPixelWidth);
|
|
dstPixels[dstPixelIndex] = srcPixels[srcPixelIndex];
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
void Cram_AddFile(Cram_Context *context, const char *path)
|
|
{
|
|
Cram_Internal_Context *internalContext = (Cram_Internal_Context*) context;
|
|
Cram_Image *image;
|
|
uint8_t *pixels;
|
|
int32_t leftTrim, topTrim, rightTrim, bottomTrim;
|
|
int32_t width, height, numChannels;
|
|
int32_t i;
|
|
|
|
if (internalContext->imageCapacity == internalContext->imageCount)
|
|
{
|
|
internalContext->imageCapacity *= 2;
|
|
internalContext->images = Cram_realloc(internalContext->images, internalContext->imageCapacity * sizeof(Cram_Image));
|
|
}
|
|
|
|
image = &internalContext->images[internalContext->imageCount];
|
|
|
|
/* image->name = Cram_Internal_GetTrimmedPath(); */
|
|
|
|
pixels = stbi_load(
|
|
path,
|
|
&width,
|
|
&height,
|
|
&numChannels,
|
|
STBI_rgb_alpha
|
|
);
|
|
|
|
/* Check for trim */
|
|
if (internalContext->trim)
|
|
{
|
|
topTrim = 0;
|
|
for (i = 0; i < height; i += 1)
|
|
{
|
|
if (!Cram_Internal_IsRowClear(pixels, i, width))
|
|
{
|
|
topTrim = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
leftTrim = 0;
|
|
for (i = 0; i < width; i += 1)
|
|
{
|
|
if (!Cram_Internal_IsColumnClear(pixels, i, width, height))
|
|
{
|
|
leftTrim = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
bottomTrim = height;
|
|
for (i = height - 1; i >= topTrim; i -= 1)
|
|
{
|
|
if (!Cram_Internal_IsRowClear(pixels, i, width))
|
|
{
|
|
bottomTrim = i + 1;
|
|
break;
|
|
}
|
|
}
|
|
|
|
rightTrim = width;
|
|
for (i = width - 1; i >= leftTrim; i -= 1)
|
|
{
|
|
if (!Cram_Internal_IsColumnClear(pixels, i, width, height))
|
|
{
|
|
rightTrim = i + 1;
|
|
break;
|
|
}
|
|
}
|
|
|
|
image->rect.x = leftTrim;
|
|
image->rect.y = topTrim;
|
|
image->rect.w = rightTrim - leftTrim;
|
|
image->rect.h = bottomTrim - topTrim;
|
|
}
|
|
else
|
|
{
|
|
image->rect.x = 0;
|
|
image->rect.y = 0;
|
|
image->rect.w = width;
|
|
image->rect.h = height;
|
|
}
|
|
|
|
/* copy and free source pixels */
|
|
image->pixels = Cram_malloc(image->rect.w * image->rect.h * 4);
|
|
|
|
Rect dstRect;
|
|
dstRect.x = 0;
|
|
dstRect.y = 0;
|
|
dstRect.w = image->rect.w;
|
|
dstRect.h = image->rect.h;
|
|
Cram_Internal_CopyPixels((uint32_t*) image->pixels, image->rect.w, (uint32_t*) pixels, width, &dstRect, &image->rect);
|
|
stbi_image_free(pixels);
|
|
|
|
/* hash */
|
|
image->hash = stbds_hash_bytes(image->pixels, image->rect.w * image->rect.h * 4, 0);
|
|
|
|
/* check if this is a duplicate */
|
|
image->duplicate = 0;
|
|
for (i = 0; i < internalContext->imageCount; i += 1)
|
|
{
|
|
if (!internalContext->images[i].duplicate)
|
|
{
|
|
if (Cram_Internal_IsImageEqual(image, &internalContext->images[i]))
|
|
{
|
|
/* this is duplicate data! */
|
|
image->duplicate = 1;
|
|
Cram_free(image->pixels);
|
|
image->pixels = NULL;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
internalContext->imageCount += 1;
|
|
}
|
|
|
|
int8_t Cram_Pack(Cram_Context *context)
|
|
{
|
|
stbrp_context rectPackContext;
|
|
Cram_Internal_Context *internalContext = (Cram_Internal_Context*) context;
|
|
uint32_t numNodes = internalContext->width;
|
|
stbrp_node *nodes = Cram_malloc(sizeof(stbrp_node) * numNodes);
|
|
stbrp_rect *rects;
|
|
uint32_t numRects = 0;
|
|
stbrp_rect *rect;
|
|
Rect dstRect, srcRect;
|
|
int32_t i;
|
|
uint32_t maxWidth = 0;
|
|
uint32_t maxHeight = 0;
|
|
|
|
/* FIXME: this numRects repetition sucks */
|
|
for (i = 0; i < internalContext->imageCount; i += 1)
|
|
{
|
|
if (!internalContext->images[i].duplicate)
|
|
{
|
|
numRects += 1;
|
|
}
|
|
}
|
|
|
|
rects = Cram_malloc(sizeof(stbrp_rect) * numRects);
|
|
|
|
stbrp_init_target(&rectPackContext, internalContext->width, internalContext->height, nodes, numNodes);
|
|
|
|
numRects = 0;
|
|
for (i = 0; i < internalContext->imageCount; i += 1)
|
|
{
|
|
if (!internalContext->images[i].duplicate)
|
|
{
|
|
rect = &rects[numRects];
|
|
|
|
rect->w = internalContext->images[i].rect.w + internalContext->padding;
|
|
rect->h = internalContext->images[i].rect.h + internalContext->padding;
|
|
|
|
numRects += 1;
|
|
}
|
|
}
|
|
|
|
stbrp_pack_rects(&rectPackContext, rects, numRects);
|
|
|
|
numRects = 0;
|
|
for (i = 0; i < internalContext->imageCount; i += 1)
|
|
{
|
|
if (!internalContext->images[i].duplicate)
|
|
{
|
|
rect = &rects[numRects];
|
|
|
|
if (rect->was_packed)
|
|
{
|
|
internalContext->images[i].rect.x = rect->x;
|
|
internalContext->images[i].rect.y = rect->y;
|
|
|
|
maxWidth = Cram_max(maxWidth, rect->x + rect->w);
|
|
maxHeight = Cram_max(maxHeight, rect->y + rect->h);
|
|
|
|
numRects += 1;
|
|
}
|
|
else
|
|
{
|
|
return -1;
|
|
}
|
|
}
|
|
}
|
|
|
|
internalContext->width = maxWidth;
|
|
internalContext->height = maxHeight;
|
|
|
|
internalContext->pixels = Cram_realloc(internalContext->pixels, internalContext->width * internalContext->height * 4);
|
|
Cram_memset(internalContext->pixels, 0, internalContext->width * internalContext->height * 4);
|
|
|
|
for (i = 0; i < internalContext->imageCount; i += 1)
|
|
{
|
|
if (!internalContext->images[i].duplicate)
|
|
{
|
|
dstRect.x = internalContext->images[i].rect.x;
|
|
dstRect.y = internalContext->images[i].rect.y;
|
|
dstRect.w = internalContext->images[i].rect.w;
|
|
dstRect.h = internalContext->images[i].rect.h;
|
|
|
|
srcRect.x = 0;
|
|
srcRect.y = 0;
|
|
srcRect.w = internalContext->images[i].rect.w;
|
|
srcRect.h = internalContext->images[i].rect.h;
|
|
|
|
Cram_Internal_CopyPixels(
|
|
(uint32_t*) internalContext->pixels,
|
|
internalContext->width,
|
|
(uint32_t*) internalContext->images[i].pixels,
|
|
internalContext->images[i].rect.w,
|
|
&dstRect,
|
|
&srcRect
|
|
);
|
|
}
|
|
}
|
|
|
|
Cram_free(nodes);
|
|
Cram_free(rects);
|
|
|
|
return 0;
|
|
}
|
|
|
|
void Cram_GetPixelData(Cram_Context *context, uint8_t **pPixels, uint32_t *pWidth, uint32_t *pHeight)
|
|
{
|
|
Cram_Internal_Context *internalContext = (Cram_Internal_Context*) context;
|
|
*pPixels = internalContext->pixels;
|
|
*pWidth = internalContext->width;
|
|
*pHeight = internalContext->height;
|
|
}
|
|
|
|
void Cram_GetAtlasData(Cram_Context *context, Cram_AtlasData **pAtlasData)
|
|
{
|
|
*pAtlasData = ((Cram_Internal_Context*) context)->atlasData;
|
|
}
|
|
|
|
void Cram_Destroy(Cram_Context *context)
|
|
{
|
|
Cram_Internal_Context *internalContext = (Cram_Internal_Context*) context;
|
|
int32_t i;
|
|
|
|
if (internalContext->pixels != NULL)
|
|
{
|
|
Cram_free(internalContext->pixels);
|
|
}
|
|
|
|
for (i = 0; i < internalContext->imageCount; i += 1)
|
|
{
|
|
if (!internalContext->images[i].duplicate)
|
|
{
|
|
Cram_free(internalContext->images[i].pixels);
|
|
}
|
|
}
|
|
|
|
Cram_free(internalContext->images);
|
|
Cram_free(internalContext->atlasData);
|
|
Cram_free(internalContext);
|
|
}
|