111
This commit is contained in:
@@ -6,6 +6,8 @@
|
||||
#include <climits>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -58,6 +60,34 @@ namespace
|
||||
}
|
||||
};
|
||||
|
||||
struct AtlasEntry
|
||||
{
|
||||
std::string file_name;
|
||||
std::string identifier;
|
||||
int32_t x;
|
||||
int32_t y;
|
||||
int32_t width;
|
||||
int32_t height;
|
||||
std::vector<uint32_t> pixels;
|
||||
|
||||
AtlasEntry()
|
||||
: x(0), y(0), width(0), height(0)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
struct AtlasHeaderOptions
|
||||
{
|
||||
int32_t atlas_width;
|
||||
TransformOptions transform;
|
||||
|
||||
AtlasHeaderOptions()
|
||||
: atlas_width(1024),
|
||||
transform()
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
static bool IsPathSeparator(char value)
|
||||
{
|
||||
return value == '/' || value == '\\';
|
||||
@@ -124,6 +154,42 @@ namespace
|
||||
return fileName + Asset::SpriteAssetLoader::GetFileExtension();
|
||||
}
|
||||
|
||||
static std::string RemoveExtension(const std::string& fileName)
|
||||
{
|
||||
const size_t slash = fileName.find_last_of("/\\");
|
||||
const size_t dot = fileName.find_last_of('.');
|
||||
if (dot != std::string::npos && (slash == std::string::npos || dot > slash))
|
||||
{
|
||||
return fileName.substr(0, dot);
|
||||
}
|
||||
|
||||
return fileName;
|
||||
}
|
||||
|
||||
static std::string ToIdentifier(const std::string& text)
|
||||
{
|
||||
std::string result;
|
||||
for (size_t i = 0; i < text.size(); ++i)
|
||||
{
|
||||
const unsigned char ch = static_cast<unsigned char>(text[i]);
|
||||
if (std::isalnum(ch))
|
||||
{
|
||||
result.push_back(static_cast<char>(std::tolower(ch)));
|
||||
}
|
||||
else
|
||||
{
|
||||
result.push_back('_');
|
||||
}
|
||||
}
|
||||
|
||||
if (result.empty() || !(std::isalpha(static_cast<unsigned char>(result[0])) || result[0] == '_'))
|
||||
{
|
||||
result.insert(result.begin(), '_');
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool HasPngExtension(const std::string& path)
|
||||
{
|
||||
const std::string lower = ToLower(path);
|
||||
@@ -428,6 +494,19 @@ static bool ApplyTransform(const std::string& inputPath, const TransformOptions&
|
||||
return false;
|
||||
}
|
||||
|
||||
static void NormalizeTransparentPixels(LoadedImage& image)
|
||||
{
|
||||
for (size_t i = 0; i < image.pixels.size(); ++i)
|
||||
{
|
||||
if ((image.pixels[i] & 0xFFu) == 0u)
|
||||
{
|
||||
image.pixels[i] = 0x00000000u;
|
||||
}
|
||||
}
|
||||
|
||||
image.image = RenderData::Image(image.pixels.data(), image.image.width, image.image.height);
|
||||
}
|
||||
|
||||
static bool ConvertFile(
|
||||
const std::string& inputPath,
|
||||
const std::string& outputPath,
|
||||
@@ -493,6 +572,322 @@ static bool ConvertBatch(
|
||||
return successCount == files.size();
|
||||
}
|
||||
|
||||
static bool ContainsAtlasIdentifier(const std::vector<AtlasEntry>& entries, const std::string& identifier)
|
||||
{
|
||||
for (size_t i = 0; i < entries.size(); ++i)
|
||||
{
|
||||
if (entries[i].identifier == identifier)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool LoadAtlasEntry(
|
||||
const std::string& inputDirectory,
|
||||
const std::string& fileName,
|
||||
const TransformOptions& transformOptions,
|
||||
AtlasEntry& entry)
|
||||
{
|
||||
const std::string inputPath = JoinPath(inputDirectory, fileName);
|
||||
LoadedImage image;
|
||||
if (!LoadPngImage(inputPath, image))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ApplyTransform(inputPath, transformOptions, image))
|
||||
{
|
||||
std::cerr << "Image transform failed: " << inputPath << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
NormalizeTransparentPixels(image);
|
||||
|
||||
entry.file_name = fileName;
|
||||
entry.identifier = ToIdentifier(RemoveExtension(fileName));
|
||||
entry.width = image.image.width;
|
||||
entry.height = image.image.height;
|
||||
entry.pixels = image.pixels;
|
||||
return entry.width > 0 && entry.height > 0 && !entry.pixels.empty();
|
||||
}
|
||||
|
||||
static bool PackAtlasEntries(std::vector<AtlasEntry>& entries, int32_t atlasWidth, int32_t& atlasHeight)
|
||||
{
|
||||
if (atlasWidth <= 0 || entries.empty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const int32_t padding = 1;
|
||||
int32_t cursorX = 0;
|
||||
int32_t cursorY = 0;
|
||||
int32_t rowHeight = 0;
|
||||
|
||||
for (size_t i = 0; i < entries.size(); ++i)
|
||||
{
|
||||
AtlasEntry& entry = entries[i];
|
||||
if (entry.width > atlasWidth)
|
||||
{
|
||||
std::cerr << "Atlas width " << atlasWidth << " is smaller than " << entry.file_name
|
||||
<< " width " << entry.width << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (cursorX > 0 && cursorX + entry.width > atlasWidth)
|
||||
{
|
||||
cursorY += rowHeight + padding;
|
||||
cursorX = 0;
|
||||
rowHeight = 0;
|
||||
}
|
||||
|
||||
entry.x = cursorX;
|
||||
entry.y = cursorY;
|
||||
cursorX += entry.width + padding;
|
||||
rowHeight = std::max(rowHeight, entry.height);
|
||||
}
|
||||
|
||||
atlasHeight = cursorY + rowHeight;
|
||||
return atlasHeight > 0;
|
||||
}
|
||||
|
||||
static void WriteHexPixel(std::ofstream& file, uint32_t pixel)
|
||||
{
|
||||
file << "0x"
|
||||
<< std::uppercase
|
||||
<< std::hex
|
||||
<< std::setw(8)
|
||||
<< std::setfill('0')
|
||||
<< pixel
|
||||
<< std::dec
|
||||
<< std::nouppercase
|
||||
<< std::setfill(' ');
|
||||
}
|
||||
|
||||
static bool WriteAtlasHeader(
|
||||
const std::string& outputHeaderPath,
|
||||
const std::string& variablePrefix,
|
||||
const std::vector<AtlasEntry>& entries,
|
||||
const std::vector<uint32_t>& atlasPixels,
|
||||
int32_t atlasWidth,
|
||||
int32_t atlasHeight)
|
||||
{
|
||||
if (!EnsureParentDirectory(outputHeaderPath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
std::ofstream file(outputHeaderPath.c_str());
|
||||
if (!file.good())
|
||||
{
|
||||
std::cerr << "Open header failed: " << outputHeaderPath << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
file << "// Auto-generated by SpriteAssetTool --atlas-header\n";
|
||||
file << "#pragma once\n";
|
||||
file << "#include <cstdint>\n";
|
||||
file << "#include \"Image.h\"\n";
|
||||
file << "#include \"SpriteRegion.h\"\n\n";
|
||||
file << "namespace TomAtlas\n";
|
||||
file << "{\n";
|
||||
file << "\tstatic const int32_t " << variablePrefix << "_width = " << atlasWidth << ";\n";
|
||||
file << "\tstatic const int32_t " << variablePrefix << "_height = " << atlasHeight << ";\n\n";
|
||||
file << "\tstatic const uint32_t " << variablePrefix << "_pixels[] = {\n";
|
||||
|
||||
for (size_t i = 0; i < atlasPixels.size(); i += 12)
|
||||
{
|
||||
file << "\t\t";
|
||||
const size_t end = std::min(i + 12, atlasPixels.size());
|
||||
for (size_t j = i; j < end; ++j)
|
||||
{
|
||||
WriteHexPixel(file, atlasPixels[j]);
|
||||
file << ", ";
|
||||
}
|
||||
file << "\n";
|
||||
}
|
||||
|
||||
file << "\t};\n\n";
|
||||
file << "\tstatic const RenderData::Image image("
|
||||
<< variablePrefix << "_pixels, "
|
||||
<< variablePrefix << "_width, "
|
||||
<< variablePrefix << "_height, 0x00000000u);\n\n";
|
||||
|
||||
for (size_t i = 0; i < entries.size(); ++i)
|
||||
{
|
||||
const AtlasEntry& entry = entries[i];
|
||||
file << "\tstatic const RenderData::SpriteRegion " << entry.identifier
|
||||
<< "(&image, "
|
||||
<< entry.x << ", "
|
||||
<< entry.y << ", "
|
||||
<< entry.width << ", "
|
||||
<< entry.height << ");\n";
|
||||
}
|
||||
|
||||
file << "}\n";
|
||||
return file.good();
|
||||
}
|
||||
|
||||
static bool ConvertAtlasHeader(
|
||||
const std::string& inputDirectory,
|
||||
const std::string& outputHeaderPath,
|
||||
const std::string& variablePrefix,
|
||||
const AtlasHeaderOptions& options)
|
||||
{
|
||||
std::vector<std::string> files;
|
||||
if (!ListPngFiles(inputDirectory, files) || files.empty())
|
||||
{
|
||||
std::cerr << "No PNG files found: " << inputDirectory << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<AtlasEntry> entries;
|
||||
for (size_t i = 0; i < files.size(); ++i)
|
||||
{
|
||||
AtlasEntry entry;
|
||||
if (!LoadAtlasEntry(inputDirectory, files[i], options.transform, entry))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (ContainsAtlasIdentifier(entries, entry.identifier))
|
||||
{
|
||||
std::cerr << "Duplicate atlas identifier: " << entry.identifier << std::endl;
|
||||
return false;
|
||||
}
|
||||
entries.push_back(entry);
|
||||
}
|
||||
|
||||
int32_t atlasHeight = 0;
|
||||
if (!PackAtlasEntries(entries, options.atlas_width, atlasHeight))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<uint32_t> atlasPixels(static_cast<size_t>(options.atlas_width) * static_cast<size_t>(atlasHeight), 0);
|
||||
for (size_t i = 0; i < entries.size(); ++i)
|
||||
{
|
||||
const AtlasEntry& entry = entries[i];
|
||||
for (int32_t y = 0; y < entry.height; ++y)
|
||||
{
|
||||
const size_t dstOffset =
|
||||
static_cast<size_t>(entry.y + y) * static_cast<size_t>(options.atlas_width) +
|
||||
static_cast<size_t>(entry.x);
|
||||
const size_t srcOffset = static_cast<size_t>(y) * static_cast<size_t>(entry.width);
|
||||
std::copy(
|
||||
entry.pixels.begin() + static_cast<std::vector<uint32_t>::difference_type>(srcOffset),
|
||||
entry.pixels.begin() + static_cast<std::vector<uint32_t>::difference_type>(srcOffset + static_cast<size_t>(entry.width)),
|
||||
atlasPixels.begin() + static_cast<std::vector<uint32_t>::difference_type>(dstOffset));
|
||||
}
|
||||
}
|
||||
|
||||
if (!WriteAtlasHeader(outputHeaderPath, variablePrefix, entries, atlasPixels, options.atlas_width, atlasHeight))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
std::cout << "Generated atlas header: " << outputHeaderPath
|
||||
<< " (" << options.atlas_width << "x" << atlasHeight
|
||||
<< ", " << entries.size() << " regions)" << std::endl;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool ParseTransformModeAt(
|
||||
int argc,
|
||||
char* argv[],
|
||||
int& index,
|
||||
TransformOptions& options)
|
||||
{
|
||||
const std::string mode = argv[index];
|
||||
if (mode == "--resize" || mode == "--fit")
|
||||
{
|
||||
if (index + 2 >= argc)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int32_t width = 0;
|
||||
int32_t height = 0;
|
||||
if (!ParsePositiveInt(argv[index + 1], width) ||
|
||||
!ParsePositiveInt(argv[index + 2], height))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
options.mode = mode == "--resize" ? TransformMode::Resize : TransformMode::Fit;
|
||||
options.width = width;
|
||||
options.height = height;
|
||||
index += 3;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (mode == "--preset")
|
||||
{
|
||||
if (index + 1 >= argc)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string presetName = argv[index + 1];
|
||||
if (presetName != "tom-800x480")
|
||||
{
|
||||
std::cerr << "Unknown preset: " << presetName << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
options.mode = TransformMode::Tom800x480Preset;
|
||||
index += 2;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool ParseAtlasHeaderOptions(
|
||||
int argc,
|
||||
char* argv[],
|
||||
int startIndex,
|
||||
AtlasHeaderOptions& options)
|
||||
{
|
||||
options = AtlasHeaderOptions();
|
||||
int index = startIndex;
|
||||
bool hasTransform = false;
|
||||
|
||||
while (index < argc)
|
||||
{
|
||||
const std::string option = argv[index];
|
||||
if (option == "--atlas-width")
|
||||
{
|
||||
if (index + 1 >= argc || !ParsePositiveInt(argv[index + 1], options.atlas_width))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
index += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (option == "--resize" || option == "--fit" || option == "--preset")
|
||||
{
|
||||
if (hasTransform)
|
||||
{
|
||||
std::cerr << "Only one transform option is allowed for atlas header generation." << std::endl;
|
||||
return false;
|
||||
}
|
||||
if (!ParseTransformModeAt(argc, argv, index, options.transform))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
hasTransform = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool ParseTransformOptions(int argc, char* argv[], int startIndex, TransformOptions& options)
|
||||
{
|
||||
if (startIndex >= argc)
|
||||
@@ -553,6 +948,7 @@ static void PrintUsage()
|
||||
std::cout << " SpriteAssetTool --batch inputDir outputDir [--resize width height]" << std::endl;
|
||||
std::cout << " SpriteAssetTool --batch inputDir outputDir [--fit maxWidth maxHeight]" << std::endl;
|
||||
std::cout << " SpriteAssetTool --batch src/Apps/Game/assets/raw src/Apps/Game/assets/sprites --preset tom-800x480" << std::endl;
|
||||
std::cout << " SpriteAssetTool --atlas-header inputDir output.h var_prefix [--atlas-width width] [--preset tom-800x480]" << std::endl;
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[])
|
||||
@@ -584,6 +980,25 @@ int main(int argc, char* argv[])
|
||||
|
||||
ok = ConvertBatch(argv[2], argv[3], transformOptions);
|
||||
}
|
||||
else if (command == "--atlas-header")
|
||||
{
|
||||
if (argc < 5)
|
||||
{
|
||||
PrintUsage();
|
||||
IMG_Quit();
|
||||
return 1;
|
||||
}
|
||||
|
||||
AtlasHeaderOptions atlasOptions;
|
||||
if (!ParseAtlasHeaderOptions(argc, argv, 5, atlasOptions))
|
||||
{
|
||||
PrintUsage();
|
||||
IMG_Quit();
|
||||
return 1;
|
||||
}
|
||||
|
||||
ok = ConvertAtlasHeader(argv[2], argv[3], argv[4], atlasOptions);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!ParseTransformOptions(argc, argv, 3, transformOptions))
|
||||
|
||||
Reference in New Issue
Block a user