2023-09-23 11:21:08 +00:00
|
|
|
#define DQN_ASAN_POISON 1
|
|
|
|
#define DQN_ASAN_VET_POISON 1
|
|
|
|
#define DQN_ONLY_RECT
|
|
|
|
#define DQN_ONLY_WIN
|
|
|
|
#define DQN_ONLY_V2
|
|
|
|
#define DQN_ONLY_SLICE
|
|
|
|
#define DQN_ONLY_SARRAY
|
2023-09-24 01:34:04 +00:00
|
|
|
#define DQN_ONLY_DSMAP
|
2023-09-23 11:21:08 +00:00
|
|
|
#define DQN_ONLY_LIST
|
|
|
|
#define DQN_ONLY_FS
|
|
|
|
#define _CRT_SECURE_NO_WARNINGS
|
|
|
|
#define DQN_IMPLEMENTATION
|
|
|
|
#include "External/tely/External/dqn/dqn.h"
|
|
|
|
|
|
|
|
DQN_MSVC_WARNING_PUSH
|
|
|
|
DQN_MSVC_WARNING_DISABLE(4244) // warning C4244: 'argument': conversion from 'int' to 'short', possible loss of data
|
|
|
|
#define STB_RECT_PACK_IMPLEMENTATION
|
|
|
|
#include "External/tely/external/stb/stb_rect_pack.h"
|
|
|
|
|
|
|
|
#define STB_IMAGE_WRITE_IMPLEMENTATION
|
|
|
|
#include "External/tely/external/stb/stb_image_write.h"
|
|
|
|
|
|
|
|
#define STB_IMAGE_IMPLEMENTATION
|
|
|
|
#include "External/tely/external/stb/stb_image.h"
|
|
|
|
DQN_MSVC_WARNING_POP
|
|
|
|
|
2023-09-24 01:34:04 +00:00
|
|
|
struct SpriteSpecification
|
|
|
|
{
|
|
|
|
uint16_t frame_count;
|
|
|
|
uint16_t frames_per_second;
|
|
|
|
};
|
|
|
|
|
2023-09-29 07:42:58 +00:00
|
|
|
static Dqn_String8 SpriteAnimNameFromFilePath(Dqn_String8 path)
|
|
|
|
{
|
|
|
|
// NOTE: Enumerate the number of frames for this animation =================================
|
|
|
|
Dqn_String8 file_name = Dqn_String8_FileNameFromPath(path);
|
|
|
|
Dqn_String8 file_name_without_extension = Dqn_String8_BinarySplit(file_name, DQN_STRING8(".")).lhs;
|
|
|
|
|
|
|
|
// NOTE: If the sprite is a standalone sprite without any frame suffix (e.g. "_1.png") then
|
|
|
|
// we accept the entry as the name of the sprite as is.
|
|
|
|
Dqn_String8 result = {};
|
|
|
|
if (Dqn_Char_IsDigit(file_name_without_extension.data[file_name_without_extension.size - 1])) {
|
|
|
|
Dqn_String8BinarySplitResult split = Dqn_String8_BinarySplitReverse(file_name_without_extension, DQN_STRING8("_"));
|
|
|
|
result = split.lhs;
|
|
|
|
} else {
|
|
|
|
result = file_name_without_extension;
|
|
|
|
}
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2023-09-23 11:21:08 +00:00
|
|
|
int main(int argc, char const *argv[])
|
|
|
|
{
|
2023-09-24 02:36:43 +00:00
|
|
|
Dqn_Library_Init();
|
|
|
|
Dqn_ThreadScratch scratch = Dqn_Thread_GetScratch(nullptr);
|
|
|
|
|
|
|
|
if (argc != 4) {
|
|
|
|
Dqn_Log_InfoF("USAGE: feely_pona_sprite_packer <width>x<height> <sprite_spec.txt> <sprite directory>");
|
2023-09-23 11:21:08 +00:00
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
2023-09-24 02:36:43 +00:00
|
|
|
// NOTE: Verify some arguments =================================================================
|
|
|
|
Dqn_String8 atlas_dimensions = Dqn_String8_InitCString8(argv[1]);
|
|
|
|
Dqn_String8 sprite_spec_path = Dqn_String8_InitCString8(argv[2]);
|
|
|
|
Dqn_String8 dir = Dqn_String8_InitCString8(argv[3]);
|
2023-09-24 01:34:04 +00:00
|
|
|
|
|
|
|
if (!Dqn_Fs_Exists(sprite_spec_path)) {
|
|
|
|
Dqn_Log_ErrorF("Sprite specification file does not exist, we tried to find \"%.*s\" but it does not exist", DQN_STRING_FMT(sprite_spec_path));
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
2023-09-24 02:36:43 +00:00
|
|
|
if (!Dqn_Fs_DirExists(dir)) {
|
|
|
|
Dqn_Log_ErrorF("Directory to load sprites from does not exist, we tried to find \"%.*s\" but it does not exist", DQN_STRING_FMT(dir));
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
// NOTE: Parse the atlas size ==================================================================
|
|
|
|
Dqn_V2I atlas_size = {};
|
|
|
|
{
|
|
|
|
Dqn_String8BinarySplitResult atlas_dimensions_split = Dqn_String8_BinarySplit(atlas_dimensions, DQN_STRING8("x"));
|
|
|
|
Dqn_String8ToU64Result width = Dqn_String8_ToU64(atlas_dimensions_split.lhs, 0);
|
|
|
|
Dqn_String8ToU64Result height = Dqn_String8_ToU64(atlas_dimensions_split.rhs, 0);
|
|
|
|
|
|
|
|
if (!width.success || width.value == 0) {
|
|
|
|
Dqn_Log_ErrorF("Width for the sprite atlas was not a number > 0");
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!height.success || height.value == 0) {
|
|
|
|
Dqn_Log_ErrorF("Width for the sprite atlas was not a number > 0");
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
atlas_size.x = DQN_CAST(uint32_t)width.value;
|
|
|
|
atlas_size.y = DQN_CAST(uint32_t)height.value;
|
|
|
|
}
|
|
|
|
|
|
|
|
// NOTE: Parse the sprite specifications =======================================================
|
2023-09-24 01:34:04 +00:00
|
|
|
Dqn_DSMap<SpriteSpecification> sprite_spec_table = Dqn_DSMap_Init<SpriteSpecification>(1024);
|
|
|
|
{
|
|
|
|
Dqn_ThreadScratch inner_scratch = Dqn_Thread_GetScratch(scratch.arena);
|
|
|
|
Dqn_String8 sprite_spec_buffer = Dqn_Fs_Read(sprite_spec_path, inner_scratch.allocator);
|
|
|
|
Dqn_String8SplitAllocResult sprite_spec_lines = Dqn_String8_SplitAlloc(inner_scratch.allocator, sprite_spec_buffer, DQN_STRING8("\n"));
|
|
|
|
DQN_FOR_UINDEX(line_index, sprite_spec_lines.size) {
|
2023-09-24 02:36:43 +00:00
|
|
|
Dqn_String8 line = Dqn_String8_TrimWhitespaceAround(sprite_spec_lines.data[line_index]);
|
|
|
|
if (line.size == 0) // NOTE: Support empty lines, just ignore them
|
|
|
|
continue;
|
|
|
|
|
2023-09-24 01:34:04 +00:00
|
|
|
Dqn_String8SplitAllocResult line_parts = Dqn_String8_SplitAlloc(inner_scratch.allocator, line, DQN_STRING8(";"));
|
|
|
|
DQN_ASSERTF(line_parts.size == 2, "Line must have 2 parts in the sprite specification\n"
|
|
|
|
"\n"
|
|
|
|
"<animation name>;<frames per second>\\n\n"
|
|
|
|
"\n"
|
2023-09-24 02:36:43 +00:00
|
|
|
"Line was '%.*s' loaded from '%.*s'", DQN_STRING_FMT(line), DQN_STRING_FMT(sprite_spec_path));
|
2023-09-24 01:34:04 +00:00
|
|
|
|
|
|
|
Dqn_String8 anim_name = line_parts.data[0];
|
|
|
|
Dqn_String8ToU64Result frames_per_second = Dqn_String8_ToU64(line_parts.data[1], 0);
|
|
|
|
DQN_ASSERTF(frames_per_second.success, "Frames per second was not a convertible number, line was '%.*s'", line);
|
|
|
|
|
|
|
|
Dqn_DSMapResult<SpriteSpecification> slot = Dqn_DSMap_MakeKeyString8Copy(&sprite_spec_table, scratch.allocator, anim_name);
|
|
|
|
slot.value->frames_per_second = DQN_CAST(uint16_t)frames_per_second.value;
|
|
|
|
}
|
|
|
|
}
|
2023-09-23 11:21:08 +00:00
|
|
|
|
|
|
|
// NOTE: Get the list of files =================================================================
|
|
|
|
Dqn_List<Dqn_String8> file_list = Dqn_List_Init<Dqn_String8>(scratch.arena, 128);
|
|
|
|
for (Dqn_Win_FolderIterator it = {}; Dqn_Win_FolderIterate(dir, &it); ) {
|
|
|
|
if (Dqn_String8_EndsWithInsensitive(it.file_name, DQN_STRING8(".png"))) {
|
|
|
|
Dqn_String8 *item = Dqn_List_Make(&file_list, Dqn_ZeroMem_Yes);
|
|
|
|
*item = Dqn_FsPath_ConvertF(scratch.arena, "%.*s/%.*s", DQN_STRING_FMT(dir), DQN_STRING_FMT(it.file_name));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// NOTE: Setup the rect-pack state =============================================================
|
|
|
|
stbrp_node *nodes = Dqn_Arena_NewArray(scratch.arena, stbrp_node, file_list.count, Dqn_ZeroMem_Yes);
|
|
|
|
stbrp_context pack_context = {};
|
2023-09-24 02:36:43 +00:00
|
|
|
stbrp_init_target(&pack_context, atlas_size.w, atlas_size.h, nodes, DQN_CAST(int)file_list.count);
|
2023-09-23 11:21:08 +00:00
|
|
|
|
2023-09-24 01:34:04 +00:00
|
|
|
// NOTE: Load the sprites to determine their dimensions for rect packing =======================
|
2023-09-23 11:21:08 +00:00
|
|
|
Dqn_SArray<stbrp_rect> rects = Dqn_SArray_Init<stbrp_rect>(scratch.arena, file_list.count, Dqn_ZeroMem_Yes);
|
|
|
|
for (Dqn_ListIterator<Dqn_String8> it = {}; Dqn_List_Iterate<Dqn_String8>(&file_list, &it, 0);) {
|
|
|
|
int x = 0, y = 0, channels_in_file = 0;
|
|
|
|
stbi_uc *pixels = stbi_load(it.data->data, &x, &y, &channels_in_file, 4 /*desired_channels*/);
|
|
|
|
DQN_ASSERT(pixels);
|
|
|
|
stbi_image_free(pixels);
|
|
|
|
|
2023-09-24 01:34:04 +00:00
|
|
|
// NOTE: Add a rect to the packing context =================================================
|
2023-09-23 11:21:08 +00:00
|
|
|
stbrp_rect *rect = Dqn_SArray_Make(&rects, Dqn_ZeroMem_Yes);
|
|
|
|
rect->w = x;
|
|
|
|
rect->h = y;
|
|
|
|
|
2023-09-24 01:34:04 +00:00
|
|
|
// NOTE: Enumerate the number of frames for this animation =================================
|
2023-09-29 07:42:58 +00:00
|
|
|
Dqn_String8 anim_prefix = SpriteAnimNameFromFilePath(*it.data);
|
2023-09-24 01:34:04 +00:00
|
|
|
Dqn_DSMapResult<SpriteSpecification> slot = Dqn_DSMap_FindKeyString8(&sprite_spec_table, anim_prefix);
|
|
|
|
DQN_ASSERTF(slot.found,
|
|
|
|
"\n\nSprite frame loaded from file\n"
|
|
|
|
" '%.*s'\n"
|
|
|
|
"\n"
|
|
|
|
"however there's no corresponding entry in the specification file loaded at\n"
|
|
|
|
" '%.*s'\n"
|
|
|
|
"\n"
|
|
|
|
"Add a line in format of <animation name>;<frames_per_second> to the file, e.g.\n"
|
|
|
|
" %.*s;8\n",
|
|
|
|
DQN_STRING_FMT(*it.data),
|
|
|
|
DQN_STRING_FMT(sprite_spec_path),
|
|
|
|
DQN_STRING_FMT(anim_prefix));
|
|
|
|
|
|
|
|
slot.value->frame_count++;
|
2023-09-23 11:21:08 +00:00
|
|
|
Dqn_Log_InfoF("Packing sprite: %.*s", DQN_STRING_FMT(*it.data));
|
|
|
|
}
|
|
|
|
|
|
|
|
// NOTE: Pack the rects ========================================================================
|
|
|
|
if (stbrp_pack_rects(&pack_context, rects.data, DQN_CAST(int)rects.size) != 1) {
|
|
|
|
Dqn_Log_ErrorF("STB rect pack failed to pack font rects into rectangle [width=%d, height=%d, num_rects=%d]",
|
2023-09-24 02:36:43 +00:00
|
|
|
atlas_size.w,
|
|
|
|
atlas_size.h,
|
2023-09-23 11:21:08 +00:00
|
|
|
file_list.count);
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
// NOTE: Load the files once more and generate the final image =================================
|
|
|
|
int final_bpp = 4;
|
2023-09-24 02:36:43 +00:00
|
|
|
int final_image_stride = atlas_size.w * final_bpp;
|
|
|
|
char *final_image = Dqn_Arena_NewArray(scratch.arena, char, atlas_size.h * final_image_stride, Dqn_ZeroMem_Yes);
|
2023-09-24 01:34:04 +00:00
|
|
|
Dqn_String8 atlas_path = Dqn_String8_InitF(scratch.allocator, "%.*s.png", DQN_STRING_FMT(dir));
|
2023-09-23 11:21:08 +00:00
|
|
|
|
|
|
|
// NOTE: Generate the meta file ================================================================
|
2023-09-24 02:51:23 +00:00
|
|
|
// NOTE: Count the number of animations we loaded frames fore
|
2023-09-24 04:20:27 +00:00
|
|
|
Dqn_usize num_anims = 0;
|
|
|
|
Dqn_usize num_anim_rects = 0;
|
2023-09-24 02:51:23 +00:00
|
|
|
for (Dqn_usize index = 1 /*Sentinel*/; index < sprite_spec_table.occupied; index++) {
|
|
|
|
Dqn_DSMapSlot<SpriteSpecification> *slot = sprite_spec_table.slots + index;
|
|
|
|
if (slot->value.frame_count) {
|
2023-09-24 04:20:27 +00:00
|
|
|
num_anims++;
|
|
|
|
num_anim_rects += slot->value.frame_count;
|
2023-09-24 02:51:23 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-09-23 11:21:08 +00:00
|
|
|
Dqn_String8 meta_path = Dqn_String8_InitF(scratch.allocator, "%.*s.txt", DQN_STRING_FMT(dir));
|
|
|
|
Dqn_FsFile meta_file = Dqn_Fs_OpenFile(meta_path, Dqn_FsFileOpen_CreateAlways, Dqn_FsFileAccess_Write);
|
2023-09-24 01:34:04 +00:00
|
|
|
Dqn_Fs_WriteFileF(&meta_file,
|
|
|
|
"@file;%.*s;%d;%d\n",
|
|
|
|
DQN_STRING_FMT(Dqn_String8_FileNameFromPath(atlas_path)),
|
2023-09-24 04:20:27 +00:00
|
|
|
DQN_CAST(int) num_anim_rects,
|
|
|
|
DQN_CAST(int) num_anims);
|
2023-09-23 11:21:08 +00:00
|
|
|
|
|
|
|
Dqn_Log_InfoF("Generating meta file: %.*s", DQN_STRING_FMT(meta_path));
|
2023-09-29 07:42:58 +00:00
|
|
|
Dqn_String8 active_anim_prefix = {};
|
2023-09-23 11:21:08 +00:00
|
|
|
for (Dqn_ListIterator<Dqn_String8> it = {}; Dqn_List_Iterate<Dqn_String8>(&file_list, &it, 0);) {
|
|
|
|
int w, h, channels_in_file;
|
|
|
|
stbi_uc *loaded_image = stbi_load(it.data->data, &w, &h, &channels_in_file, 4 /*desired_channels*/);
|
|
|
|
|
|
|
|
stbrp_rect packed_rect = rects.data[it.index];
|
|
|
|
char *src = DQN_CAST(char *)loaded_image;
|
|
|
|
|
|
|
|
// NOTE: Generate the image ================================================================
|
|
|
|
for (int y = 0; y < packed_rect.h; y++) {
|
|
|
|
char *row = final_image + ((packed_rect.y + y) * final_image_stride) + (packed_rect.x * final_bpp);
|
|
|
|
for (int x = 0; x < packed_rect.w; x++) {
|
|
|
|
*row++ = *src++;
|
|
|
|
*row++ = *src++;
|
|
|
|
*row++ = *src++;
|
|
|
|
*row++ = *src++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
stbi_image_free(loaded_image);
|
|
|
|
|
2023-09-24 01:34:04 +00:00
|
|
|
// NOTE: Detect what animation we are currently processing =================================
|
2023-09-29 07:42:58 +00:00
|
|
|
Dqn_String8 anim_prefix = SpriteAnimNameFromFilePath(*it.data);
|
|
|
|
if (active_anim_prefix.size == 0 || active_anim_prefix != anim_prefix) {
|
2023-09-24 01:34:04 +00:00
|
|
|
// NOTE: Anim prefix is different, we are starting a new animation- mark it accordingly
|
2023-09-29 07:42:58 +00:00
|
|
|
active_anim_prefix = anim_prefix;
|
|
|
|
Dqn_DSMapResult<SpriteSpecification> slot = Dqn_DSMap_FindKeyString8(&sprite_spec_table, active_anim_prefix);
|
|
|
|
Dqn_Fs_WriteFileF(&meta_file, "@anim;%.*s;%u;%u\n", DQN_STRING_FMT(active_anim_prefix), slot.value->frames_per_second, slot.value->frame_count);
|
2023-09-24 01:34:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// NOTE: Write the sprite rectangles in ====================================================
|
|
|
|
Dqn_Fs_WriteFileF(&meta_file, "%d;%d;%d;%d", packed_rect.x, packed_rect.y, packed_rect.w, packed_rect.h);
|
2023-09-23 11:21:08 +00:00
|
|
|
if (it.index != (file_list.count - 1))
|
|
|
|
Dqn_Fs_WriteFileF(&meta_file, "\n");
|
|
|
|
}
|
|
|
|
|
|
|
|
Dqn_Log_InfoF("Generating atlas: %.*s", DQN_STRING_FMT(atlas_path));
|
2023-09-24 02:36:43 +00:00
|
|
|
stbi_write_png(atlas_path.data, atlas_size.w, atlas_size.h, 4, final_image, final_image_stride);
|
2023-09-23 11:21:08 +00:00
|
|
|
return 0;
|
|
|
|
}
|