111
This commit is contained in:
parent
8d8c9ed9d1
commit
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
|
cmake --build build-win --config Release --target IMX6U-Demo
|
||||||
```
|
```
|
||||||
|
|
||||||
如果 Tom 的 `.sprite` 资源缺失,先运行资源转换目标:
|
如果修改了 Tom 的原始 PNG 资源,先重新生成 atlas 头文件:
|
||||||
```bash
|
```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
|
cmake --build build-linux --target IMX6U-Demo
|
||||||
```
|
```
|
||||||
|
|
||||||
如果 Tom 的 `.sprite` 资源缺失,先运行资源转换目标:
|
如果修改了 Tom 的原始 PNG 资源,先重新生成 atlas 头文件:
|
||||||
```bash
|
```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 跳过。
|
透明像素仍保留 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 转换
|
### Bitmap Font 转换
|
||||||
|
|
||||||
像素字体图集使用 `tools/gen_font_atlas.py` 生成:
|
像素字体图集使用 `tools/gen_font_atlas.py` 生成:
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,11 @@ set(TOM_GAME_TARGET IMX6U-Game)
|
||||||
|
|
||||||
add_executable(${TOM_GAME_TARGET}
|
add_executable(${TOM_GAME_TARGET}
|
||||||
Main.cpp
|
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})
|
imx6u_configure_app_target(${TOM_GAME_TARGET})
|
||||||
|
|
@ -13,7 +18,7 @@ if(NOT USE_FRAMEBUFFER)
|
||||||
)
|
)
|
||||||
|
|
||||||
if(WIN32)
|
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)
|
set_target_properties(TomSpriteAssetTool PROPERTIES OUTPUT_NAME SpriteAssetTool)
|
||||||
target_include_directories(TomSpriteAssetTool PRIVATE
|
target_include_directories(TomSpriteAssetTool PRIVATE
|
||||||
${PROJECT_SOURCE_DIR}/src/Core/Asset
|
${PROJECT_SOURCE_DIR}/src/Core/Asset
|
||||||
|
|
@ -38,7 +43,7 @@ if(NOT USE_FRAMEBUFFER)
|
||||||
"$<TARGET_FILE_DIR:TomSpriteAssetTool>"
|
"$<TARGET_FILE_DIR:TomSpriteAssetTool>"
|
||||||
)
|
)
|
||||||
elseif(SDL2_image_FOUND)
|
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)
|
set_target_properties(TomSpriteAssetTool PROPERTIES OUTPUT_NAME SpriteAssetTool)
|
||||||
target_include_directories(TomSpriteAssetTool PRIVATE
|
target_include_directories(TomSpriteAssetTool PRIVATE
|
||||||
${PROJECT_SOURCE_DIR}/src/Core/Asset
|
${PROJECT_SOURCE_DIR}/src/Core/Asset
|
||||||
|
|
@ -64,13 +69,33 @@ if(NOT USE_FRAMEBUFFER)
|
||||||
add_custom_target(ConvertTomSprites
|
add_custom_target(ConvertTomSprites
|
||||||
COMMAND $<TARGET_FILE:TomSpriteAssetTool>
|
COMMAND $<TARGET_FILE:TomSpriteAssetTool>
|
||||||
--batch
|
--batch
|
||||||
"${CMAKE_CURRENT_SOURCE_DIR}/assets/raw"
|
"src/Apps/Game/assets/raw"
|
||||||
"${CMAKE_CURRENT_SOURCE_DIR}/assets/sprites"
|
"src/Apps/Game/assets/sprites"
|
||||||
--preset tom-800x480
|
--preset tom-800x480
|
||||||
WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}"
|
WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}"
|
||||||
DEPENDS TomSpriteAssetTool
|
DEPENDS TomSpriteAssetTool
|
||||||
COMMENT "Converting Tom PNG assets to board-ready .sprite files"
|
COMMENT "Converting Tom PNG assets to board-ready .sprite files"
|
||||||
VERBATIM
|
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()
|
||||||
endif()
|
endif()
|
||||||
|
|
|
||||||
|
|
@ -3,19 +3,15 @@
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <fstream>
|
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <string>
|
|
||||||
#include <thread>
|
#include <thread>
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include "Color.h"
|
#include "Color.h"
|
||||||
#include "Display.h"
|
#include "Display.h"
|
||||||
#include "DrawContext.h"
|
#include "DrawContext.h"
|
||||||
#include "Image.h"
|
|
||||||
#include "SpriteAssetLoader.h"
|
|
||||||
#include "TimeSource.h"
|
#include "TimeSource.h"
|
||||||
#include "Timer.h"
|
#include "Timer.h"
|
||||||
|
#include "tom_atlas.h"
|
||||||
|
|
||||||
#ifdef USE_FRAMEBUFFER
|
#ifdef USE_FRAMEBUFFER
|
||||||
#include "FBDisplay.h"
|
#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()
|
static Platform::IDisplay* CreateDisplay()
|
||||||
{
|
{
|
||||||
#ifdef USE_FRAMEBUFFER
|
#ifdef USE_FRAMEBUFFER
|
||||||
|
|
@ -124,89 +109,28 @@ namespace
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static std::string FindSpriteAssetPath(const std::string& file_name)
|
static const RenderData::SpriteRegion& SelectTomFrame(
|
||||||
{
|
|
||||||
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(
|
|
||||||
uint32_t animation_time_ms,
|
uint32_t animation_time_ms,
|
||||||
const SpriteImage& stand,
|
const RenderData::SpriteRegion* const* speaking_frames,
|
||||||
const SpriteImage& listen,
|
|
||||||
const SpriteImage* const* speaking_frames,
|
|
||||||
size_t speaking_frame_count)
|
size_t speaking_frame_count)
|
||||||
{
|
{
|
||||||
const uint32_t phase = (animation_time_ms / 1000u) % 6u;
|
const uint32_t phase = (animation_time_ms / 1000u) % 6u;
|
||||||
if (phase < 2u)
|
if (phase < 2u)
|
||||||
{
|
{
|
||||||
return stand.image;
|
return TomAtlas::tom_stand;
|
||||||
}
|
}
|
||||||
if (phase < 3u)
|
if (phase < 3u)
|
||||||
{
|
{
|
||||||
return listen.image;
|
return TomAtlas::tom_listhen;
|
||||||
}
|
}
|
||||||
|
|
||||||
const size_t frame_index = (animation_time_ms / 120u) % speaking_frame_count;
|
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[])
|
int main(int argc, char* argv[])
|
||||||
{
|
{
|
||||||
std::cout << "[INFO] Start make tom game." << std::endl;
|
|
||||||
|
|
||||||
const ProgramOptions options = ParseProgramOptions(argc, argv);
|
const ProgramOptions options = ParseProgramOptions(argc, argv);
|
||||||
if (options.show_help)
|
if (options.show_help)
|
||||||
{
|
{
|
||||||
|
|
@ -221,37 +145,13 @@ int main(int argc, char* argv[])
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
SpriteImage background;
|
const RenderData::SpriteRegion* speaking_frames[] = {
|
||||||
SpriteImage tom_stand;
|
&TomAtlas::tom_say1,
|
||||||
SpriteImage tom_listen;
|
&TomAtlas::tom_say2,
|
||||||
SpriteImage tom_say1;
|
&TomAtlas::tom_say3,
|
||||||
SpriteImage tom_say2;
|
&TomAtlas::tom_say4,
|
||||||
SpriteImage tom_say3;
|
&TomAtlas::tom_say3,
|
||||||
SpriteImage tom_say4;
|
&TomAtlas::tom_say2
|
||||||
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 size_t speaking_frame_count = sizeof(speaking_frames) / sizeof(speaking_frames[0]);
|
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.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,
|
animation_time_ms,
|
||||||
tom_stand,
|
|
||||||
tom_listen,
|
|
||||||
speaking_frames,
|
speaking_frames,
|
||||||
speaking_frame_count);
|
speaking_frame_count);
|
||||||
|
|
||||||
const int32_t tom_x = (ScreenWidth - tom.width) / 2;
|
const int32_t tom_x = (ScreenWidth - tom.width) / 2;
|
||||||
const int32_t tom_y = std::max(0, ScreenHeight - tom.height - 72);
|
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_x = (ScreenWidth - TomAtlas::ui_record.width) / 2;
|
||||||
const int32_t button_y = ScreenHeight - record_button.image.height - 16;
|
const int32_t button_y = ScreenHeight - TomAtlas::ui_record.height - 16;
|
||||||
ctx.draw_sprite(button_x, button_y, record_button.image);
|
ctx.draw_sprite_region(button_x, button_y, TomAtlas::ui_record);
|
||||||
|
|
||||||
ctx.present(display);
|
ctx.present(display);
|
||||||
SleepRemainingFrameTime(timer, time_source);
|
SleepRemainingFrameTime(timer, time_source);
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -6,6 +6,8 @@
|
||||||
#include <climits>
|
#include <climits>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
|
#include <fstream>
|
||||||
|
#include <iomanip>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#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)
|
static bool IsPathSeparator(char value)
|
||||||
{
|
{
|
||||||
return value == '/' || value == '\\';
|
return value == '/' || value == '\\';
|
||||||
|
|
@ -124,6 +154,42 @@ namespace
|
||||||
return fileName + Asset::SpriteAssetLoader::GetFileExtension();
|
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)
|
static bool HasPngExtension(const std::string& path)
|
||||||
{
|
{
|
||||||
const std::string lower = ToLower(path);
|
const std::string lower = ToLower(path);
|
||||||
|
|
@ -428,6 +494,19 @@ static bool ApplyTransform(const std::string& inputPath, const TransformOptions&
|
||||||
return false;
|
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(
|
static bool ConvertFile(
|
||||||
const std::string& inputPath,
|
const std::string& inputPath,
|
||||||
const std::string& outputPath,
|
const std::string& outputPath,
|
||||||
|
|
@ -493,6 +572,322 @@ static bool ConvertBatch(
|
||||||
return successCount == files.size();
|
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)
|
static bool ParseTransformOptions(int argc, char* argv[], int startIndex, TransformOptions& options)
|
||||||
{
|
{
|
||||||
if (startIndex >= argc)
|
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 [--resize width height]" << std::endl;
|
||||||
std::cout << " SpriteAssetTool --batch inputDir outputDir [--fit maxWidth maxHeight]" << 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 --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[])
|
int main(int argc, char* argv[])
|
||||||
|
|
@ -584,6 +980,25 @@ int main(int argc, char* argv[])
|
||||||
|
|
||||||
ok = ConvertBatch(argv[2], argv[3], transformOptions);
|
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
|
else
|
||||||
{
|
{
|
||||||
if (!ParseTransformOptions(argc, argv, 3, transformOptions))
|
if (!ParseTransformOptions(argc, argv, 3, transformOptions))
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue