This commit is contained in:
HP
2026-06-07 18:31:58 +08:00
parent 8d8c9ed9d1
commit b26a6a78fa
5 changed files with 109312 additions and 129 deletions
@@ -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))