Compare commits
2 Commits
046c21d47a
...
08088c8413
| Author | SHA1 | Date |
|---|---|---|
|
|
08088c8413 | |
|
|
b26a6a78fa |
22
README.md
22
README.md
|
|
@ -67,9 +67,9 @@ cmake --build build-win --config Release --target IMX6U-Game
|
|||
cmake --build build-win --config Release --target IMX6U-Demo
|
||||
```
|
||||
|
||||
如果 Tom 的 `.sprite` 资源缺失,先运行资源转换目标:
|
||||
如果修改了 Tom 的原始 PNG 资源,先重新生成 atlas 头文件:
|
||||
```bash
|
||||
cmake --build build-win --config Release --target ConvertTomSprites
|
||||
cmake --build build-win --config Release --target GenerateTomAtlasHeader
|
||||
```
|
||||
|
||||
运行:
|
||||
|
|
@ -106,9 +106,9 @@ cmake --build build-linux --target IMX6U-Game
|
|||
cmake --build build-linux --target IMX6U-Demo
|
||||
```
|
||||
|
||||
如果 Tom 的 `.sprite` 资源缺失,先运行资源转换目标:
|
||||
如果修改了 Tom 的原始 PNG 资源,先重新生成 atlas 头文件:
|
||||
```bash
|
||||
cmake --build build-linux --target ConvertTomSprites
|
||||
cmake --build build-linux --target GenerateTomAtlasHeader
|
||||
```
|
||||
|
||||
运行:
|
||||
|
|
@ -193,6 +193,20 @@ test_sprite_pixels
|
|||
|
||||
透明像素仍保留 alpha;当前 demo 通过 `RenderData::Image(..., 0x00000000)` 把全透明像素作为 color key 跳过。
|
||||
|
||||
Tom 游戏资源使用 `SpriteAssetTool --atlas-header` 从原始 PNG 一步生成 atlas 头文件:
|
||||
|
||||
```bash
|
||||
cmake --build build-win --config Release --target GenerateTomAtlasHeader
|
||||
```
|
||||
|
||||
生成结果位于:
|
||||
|
||||
```text
|
||||
src/Apps/Game/generated/tom_atlas.h
|
||||
```
|
||||
|
||||
该头文件包含 `tom_atlas_pixels` 和每张图的 `RenderData::SpriteRegion`,因此板端运行 Tom 游戏时不需要额外部署 `.sprite` 文件。
|
||||
|
||||
### Bitmap Font 转换
|
||||
|
||||
像素字体图集使用 `tools/gen_font_atlas.py` 生成:
|
||||
|
|
|
|||
|
|
@ -2,6 +2,11 @@ set(TOM_GAME_TARGET IMX6U-Game)
|
|||
|
||||
add_executable(${TOM_GAME_TARGET}
|
||||
Main.cpp
|
||||
generated/tom_atlas.h
|
||||
)
|
||||
|
||||
target_include_directories(${TOM_GAME_TARGET} PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/generated
|
||||
)
|
||||
|
||||
imx6u_configure_app_target(${TOM_GAME_TARGET})
|
||||
|
|
@ -13,7 +18,7 @@ if(NOT USE_FRAMEBUFFER)
|
|||
)
|
||||
|
||||
if(WIN32)
|
||||
add_executable(TomSpriteAssetTool ${SPRITE_ASSET_TOOL_SOURCES})
|
||||
add_executable(TomSpriteAssetTool EXCLUDE_FROM_ALL ${SPRITE_ASSET_TOOL_SOURCES})
|
||||
set_target_properties(TomSpriteAssetTool PROPERTIES OUTPUT_NAME SpriteAssetTool)
|
||||
target_include_directories(TomSpriteAssetTool PRIVATE
|
||||
${PROJECT_SOURCE_DIR}/src/Core/Asset
|
||||
|
|
@ -38,7 +43,7 @@ if(NOT USE_FRAMEBUFFER)
|
|||
"$<TARGET_FILE_DIR:TomSpriteAssetTool>"
|
||||
)
|
||||
elseif(SDL2_image_FOUND)
|
||||
add_executable(TomSpriteAssetTool ${SPRITE_ASSET_TOOL_SOURCES})
|
||||
add_executable(TomSpriteAssetTool EXCLUDE_FROM_ALL ${SPRITE_ASSET_TOOL_SOURCES})
|
||||
set_target_properties(TomSpriteAssetTool PROPERTIES OUTPUT_NAME SpriteAssetTool)
|
||||
target_include_directories(TomSpriteAssetTool PRIVATE
|
||||
${PROJECT_SOURCE_DIR}/src/Core/Asset
|
||||
|
|
@ -64,13 +69,33 @@ if(NOT USE_FRAMEBUFFER)
|
|||
add_custom_target(ConvertTomSprites
|
||||
COMMAND $<TARGET_FILE:TomSpriteAssetTool>
|
||||
--batch
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/assets/raw"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/assets/sprites"
|
||||
"src/Apps/Game/assets/raw"
|
||||
"src/Apps/Game/assets/sprites"
|
||||
--preset tom-800x480
|
||||
WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}"
|
||||
DEPENDS TomSpriteAssetTool
|
||||
COMMENT "Converting Tom PNG assets to board-ready .sprite files"
|
||||
VERBATIM
|
||||
)
|
||||
|
||||
add_custom_target(GenerateTomAtlasHeader
|
||||
COMMAND $<TARGET_FILE:TomSpriteAssetTool>
|
||||
--atlas-header
|
||||
"src/Apps/Game/assets/raw"
|
||||
"src/Apps/Game/generated/tom_atlas.h"
|
||||
tom_atlas
|
||||
--atlas-width
|
||||
1024
|
||||
--preset
|
||||
tom-800x480
|
||||
WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}"
|
||||
DEPENDS TomSpriteAssetTool
|
||||
COMMENT "Generating Tom atlas header from PNG assets"
|
||||
VERBATIM
|
||||
)
|
||||
|
||||
if(NOT CMAKE_CROSSCOMPILING)
|
||||
add_dependencies(${TOM_GAME_TARGET} GenerateTomAtlasHeader)
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
|
|
|
|||
|
|
@ -3,19 +3,15 @@
|
|||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include "Color.h"
|
||||
#include "Display.h"
|
||||
#include "DrawContext.h"
|
||||
#include "Image.h"
|
||||
#include "SpriteAssetLoader.h"
|
||||
#include "TimeSource.h"
|
||||
#include "Timer.h"
|
||||
#include "tom_atlas.h"
|
||||
|
||||
#ifdef USE_FRAMEBUFFER
|
||||
#include "FBDisplay.h"
|
||||
|
|
@ -40,17 +36,6 @@ namespace
|
|||
}
|
||||
};
|
||||
|
||||
struct SpriteImage
|
||||
{
|
||||
std::vector<uint32_t> pixels;
|
||||
RenderData::Image image;
|
||||
|
||||
bool is_valid() const
|
||||
{
|
||||
return image.pixels != nullptr && image.width > 0 && image.height > 0;
|
||||
}
|
||||
};
|
||||
|
||||
static Platform::IDisplay* CreateDisplay()
|
||||
{
|
||||
#ifdef USE_FRAMEBUFFER
|
||||
|
|
@ -124,89 +109,28 @@ namespace
|
|||
}
|
||||
}
|
||||
|
||||
static std::string FindSpriteAssetPath(const std::string& file_name)
|
||||
{
|
||||
const char* roots[] = {
|
||||
"src/Apps/Game/assets/sprites/",
|
||||
"../src/Apps/Game/assets/sprites/",
|
||||
"../../src/Apps/Game/assets/sprites/",
|
||||
"../../../src/Apps/Game/assets/sprites/",
|
||||
"/usr/local/share/imx6u-game/sprites/"
|
||||
};
|
||||
|
||||
for (size_t i = 0; i < sizeof(roots) / sizeof(roots[0]); ++i)
|
||||
{
|
||||
const std::string path = std::string(roots[i]) + file_name;
|
||||
std::ifstream file(path.c_str(), std::ios::binary);
|
||||
if (file.good())
|
||||
{
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
return std::string("src/Apps/Game/assets/sprites/") + file_name;
|
||||
}
|
||||
|
||||
static void EnableAlphaColorKey(SpriteImage& sprite)
|
||||
{
|
||||
for (size_t i = 0; i < sprite.pixels.size(); ++i)
|
||||
{
|
||||
if ((sprite.pixels[i] & 0xFFu) == 0u)
|
||||
{
|
||||
sprite.pixels[i] = 0x00000000u;
|
||||
}
|
||||
}
|
||||
|
||||
sprite.image = RenderData::Image(
|
||||
sprite.pixels.data(),
|
||||
sprite.image.width,
|
||||
sprite.image.height,
|
||||
0x00000000u);
|
||||
}
|
||||
|
||||
static bool LoadSpriteAsset(const std::string& file_name, SpriteImage& sprite, bool use_alpha_key)
|
||||
{
|
||||
const std::string path = FindSpriteAssetPath(file_name);
|
||||
if (!Asset::SpriteAssetLoader::Load(path, sprite.pixels, sprite.image))
|
||||
{
|
||||
std::cerr << "Load sprite asset failed: " << path << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (use_alpha_key)
|
||||
{
|
||||
EnableAlphaColorKey(sprite);
|
||||
}
|
||||
|
||||
return sprite.is_valid();
|
||||
}
|
||||
|
||||
static const RenderData::Image& SelectTomFrame(
|
||||
static const RenderData::SpriteRegion& SelectTomFrame(
|
||||
uint32_t animation_time_ms,
|
||||
const SpriteImage& stand,
|
||||
const SpriteImage& listen,
|
||||
const SpriteImage* const* speaking_frames,
|
||||
const RenderData::SpriteRegion* const* speaking_frames,
|
||||
size_t speaking_frame_count)
|
||||
{
|
||||
const uint32_t phase = (animation_time_ms / 1000u) % 6u;
|
||||
if (phase < 2u)
|
||||
{
|
||||
return stand.image;
|
||||
return TomAtlas::tom_stand;
|
||||
}
|
||||
if (phase < 3u)
|
||||
{
|
||||
return listen.image;
|
||||
return TomAtlas::tom_listhen;
|
||||
}
|
||||
|
||||
const size_t frame_index = (animation_time_ms / 120u) % speaking_frame_count;
|
||||
return speaking_frames[frame_index]->image;
|
||||
return *speaking_frames[frame_index];
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[])
|
||||
{
|
||||
std::cout << "[INFO] Start make tom game." << std::endl;
|
||||
|
||||
const ProgramOptions options = ParseProgramOptions(argc, argv);
|
||||
if (options.show_help)
|
||||
{
|
||||
|
|
@ -221,37 +145,13 @@ int main(int argc, char* argv[])
|
|||
return -1;
|
||||
}
|
||||
|
||||
SpriteImage background;
|
||||
SpriteImage tom_stand;
|
||||
SpriteImage tom_listen;
|
||||
SpriteImage tom_say1;
|
||||
SpriteImage tom_say2;
|
||||
SpriteImage tom_say3;
|
||||
SpriteImage tom_say4;
|
||||
SpriteImage record_button;
|
||||
|
||||
if (!LoadSpriteAsset("background.sprite", background, false) ||
|
||||
!LoadSpriteAsset("Tom-stand.sprite", tom_stand, true) ||
|
||||
!LoadSpriteAsset("Tom-listhen.sprite", tom_listen, true) ||
|
||||
!LoadSpriteAsset("Tom-say1.sprite", tom_say1, true) ||
|
||||
!LoadSpriteAsset("Tom-say2.sprite", tom_say2, true) ||
|
||||
!LoadSpriteAsset("Tom-say3.sprite", tom_say3, true) ||
|
||||
!LoadSpriteAsset("Tom-say4.sprite", tom_say4, true) ||
|
||||
!LoadSpriteAsset("ui-record.sprite", record_button, true))
|
||||
{
|
||||
std::cerr << "Missing Tom sprite assets. Run the ConvertTomSprites target first.\n";
|
||||
display->shutdown();
|
||||
delete display;
|
||||
return -1;
|
||||
}
|
||||
|
||||
const SpriteImage* speaking_frames[] = {
|
||||
&tom_say1,
|
||||
&tom_say2,
|
||||
&tom_say3,
|
||||
&tom_say4,
|
||||
&tom_say3,
|
||||
&tom_say2
|
||||
const RenderData::SpriteRegion* speaking_frames[] = {
|
||||
&TomAtlas::tom_say1,
|
||||
&TomAtlas::tom_say2,
|
||||
&TomAtlas::tom_say3,
|
||||
&TomAtlas::tom_say4,
|
||||
&TomAtlas::tom_say3,
|
||||
&TomAtlas::tom_say2
|
||||
};
|
||||
const size_t speaking_frame_count = sizeof(speaking_frames) / sizeof(speaking_frames[0]);
|
||||
|
||||
|
|
@ -276,22 +176,20 @@ int main(int argc, char* argv[])
|
|||
}
|
||||
|
||||
ctx.clear(RenderData::Color(18, 18, 24, 255));
|
||||
ctx.draw_sprite(0, 0, background.image);
|
||||
ctx.draw_sprite_region(0, 0, TomAtlas::background);
|
||||
|
||||
const RenderData::Image& tom = SelectTomFrame(
|
||||
const RenderData::SpriteRegion& tom = SelectTomFrame(
|
||||
animation_time_ms,
|
||||
tom_stand,
|
||||
tom_listen,
|
||||
speaking_frames,
|
||||
speaking_frame_count);
|
||||
|
||||
const int32_t tom_x = (ScreenWidth - tom.width) / 2;
|
||||
const int32_t tom_y = std::max(0, ScreenHeight - tom.height - 72);
|
||||
ctx.draw_sprite(tom_x, tom_y, tom);
|
||||
ctx.draw_sprite_region(tom_x, tom_y, tom);
|
||||
|
||||
const int32_t button_x = (ScreenWidth - record_button.image.width) / 2;
|
||||
const int32_t button_y = ScreenHeight - record_button.image.height - 16;
|
||||
ctx.draw_sprite(button_x, button_y, record_button.image);
|
||||
const int32_t button_x = (ScreenWidth - TomAtlas::ui_record.width) / 2;
|
||||
const int32_t button_y = ScreenHeight - TomAtlas::ui_record.height - 16;
|
||||
ctx.draw_sprite_region(button_x, button_y, TomAtlas::ui_record);
|
||||
|
||||
ctx.present(display);
|
||||
SleepRemainingFrameTime(timer, time_source);
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -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))
|
||||
|
|
|
|||
Loading…
Reference in New Issue