111
@@ -1,381 +1,138 @@
|
||||
#include <iostream>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
<<<<<<< HEAD:src/main.cpp
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
=======
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <thread>
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
>>>>>>> 777ff96602c52c86f03d612bb4213de746f580a5:src/Apps/Demo/main.cpp
|
||||
#include "Vector2.h"
|
||||
#include "Vector3.h"
|
||||
#include "Vector4.h"
|
||||
#include "Matrix4x4.h"
|
||||
#include "MathUtil.h"
|
||||
#include "Color.h"
|
||||
#include "Triangle.h"
|
||||
#include "Camera.h"
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include "Timer.h"
|
||||
#include "Vertex.h"
|
||||
<<<<<<< HEAD:src/main.cpp
|
||||
#include "DepthBuffer.h"
|
||||
#include "SpriteAssetLoader.h"
|
||||
#include "SpriteRasterizer.h"
|
||||
#include "Image.h"
|
||||
#include "TomGameApp.h"
|
||||
#include "AudioInput.h"
|
||||
#include "AudioOutput.h"
|
||||
#include "ButtonInput.h"
|
||||
=======
|
||||
#include "DrawContext.h"
|
||||
#include "test_sprite.h"
|
||||
#include "font_atlas.h"
|
||||
>>>>>>> 777ff96602c52c86f03d612bb4213de746f580a5:src/Apps/Demo/main.cpp
|
||||
#include <cstring>
|
||||
#include <cstdio>
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
|
||||
#include "BitmapFont.h"
|
||||
#include "Color.h"
|
||||
#include "Display.h"
|
||||
#include "DrawContext.h"
|
||||
#include "Image.h"
|
||||
#include "TimeSource.h"
|
||||
#include "Timer.h"
|
||||
#include "font_atlas.h"
|
||||
#include "test_sprite.h"
|
||||
|
||||
#ifdef USE_FRAMEBUFFER
|
||||
#include "FBDisplay.h"
|
||||
#else
|
||||
#include "SDLDisplay.h"
|
||||
#endif
|
||||
|
||||
const int32_t width = 800;
|
||||
const int32_t height = 600;
|
||||
const int32_t tomWidth = 800;
|
||||
const int32_t tomHeight = 480;
|
||||
|
||||
struct ProjectedVertex
|
||||
namespace
|
||||
{
|
||||
Math::Vector3 screen;
|
||||
bool visible = false;
|
||||
};
|
||||
const int32_t DemoWidth = 800;
|
||||
const int32_t DemoHeight = 600;
|
||||
|
||||
struct CubeFace
|
||||
{
|
||||
std::array<int, 4> vertices;
|
||||
};
|
||||
|
||||
struct CubeTriangle
|
||||
{
|
||||
std::array<int, 3> vertices;
|
||||
};
|
||||
|
||||
static ProjectedVertex ProjectToScreen(
|
||||
const Math::Vector3 &vertex,
|
||||
const Math::Matrix4x4 &mvp,
|
||||
const Math::Matrix4x4 &viewport)
|
||||
{
|
||||
using namespace Math;
|
||||
|
||||
const Vector4 clip = mvp * Vector4::Point(vertex);
|
||||
if (std::abs(clip.w) < 1e-5f)
|
||||
struct ProgramOptions
|
||||
{
|
||||
return {};
|
||||
}
|
||||
uint32_t target_fps;
|
||||
bool show_help;
|
||||
|
||||
const float invW = 1.0f / clip.w;
|
||||
const float ndcX = clip.x * invW;
|
||||
const float ndcY = clip.y * invW;
|
||||
const float ndcZ = clip.z * invW;
|
||||
|
||||
if (ndcX < -1.0f || ndcX > 1.0f || ndcY < -1.0f || ndcY > 1.0f || ndcZ < -1.0f || ndcZ > 1.0f)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
const Vector4 screen = viewport * Vector4(ndcX, ndcY, ndcZ, 1.0f);
|
||||
ProjectedVertex result;
|
||||
result.screen = Vector3(screen.x, screen.y, screen.z);
|
||||
result.visible = true;
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool IsFaceVisible(const CubeFace &face, const std::array<Math::Vector3, 8> &viewSpaceVertices)
|
||||
{
|
||||
using namespace Math;
|
||||
|
||||
const Vector3 &v0 = viewSpaceVertices[face.vertices[0]];
|
||||
const Vector3 &v1 = viewSpaceVertices[face.vertices[1]];
|
||||
const Vector3 &v2 = viewSpaceVertices[face.vertices[2]];
|
||||
const Vector3 faceNormal = (v1 - v0).cross(v2 - v0);
|
||||
const Vector3 faceCenter =
|
||||
(viewSpaceVertices[face.vertices[0]] +
|
||||
viewSpaceVertices[face.vertices[1]] +
|
||||
viewSpaceVertices[face.vertices[2]] +
|
||||
viewSpaceVertices[face.vertices[3]]) /
|
||||
4.0f;
|
||||
|
||||
return faceNormal.dot(faceCenter) > 0.0f;
|
||||
}
|
||||
|
||||
static bool IsTriangleVisible(const CubeTriangle &triangle, const std::array<Math::Vector3, 8> &viewSpaceVertices)
|
||||
{
|
||||
using namespace Math;
|
||||
|
||||
const Vector3 &v0 = viewSpaceVertices[triangle.vertices[0]];
|
||||
const Vector3 &v1 = viewSpaceVertices[triangle.vertices[1]];
|
||||
const Vector3 &v2 = viewSpaceVertices[triangle.vertices[2]];
|
||||
const Vector3 faceNormal = (v1 - v0).cross(v2 - v0);
|
||||
const Vector3 faceCenter = (v0 + v1 + v2) / 3.0f;
|
||||
|
||||
return faceNormal.dot(faceCenter) > 0.0f;
|
||||
}
|
||||
|
||||
<<<<<<< HEAD:src/main.cpp
|
||||
static bool HasArg(int argc, char* argv[], const std::string& expected)
|
||||
{
|
||||
for (int i = 1; i < argc; ++i)
|
||||
{
|
||||
if (argv[i] != nullptr && expected == argv[i])
|
||||
ProgramOptions()
|
||||
: target_fps(Core::Timer::DefaultFps),
|
||||
show_help(false)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static std::string FindSpriteAssetPath(const std::string& fileName)
|
||||
{
|
||||
const char* roots[] = {
|
||||
"game/assets/sprites/",
|
||||
"../game/assets/sprites/",
|
||||
"../../game/assets/sprites/",
|
||||
"../../../game/assets/sprites/",
|
||||
"/tmp/game/assets/sprites/",
|
||||
"/usr/local/share/imx6u-game/sprites/"
|
||||
};
|
||||
|
||||
for (size_t i = 0; i < sizeof(roots) / sizeof(roots[0]); ++i)
|
||||
static Platform::IDisplay* CreateDisplay()
|
||||
{
|
||||
const std::string path = std::string(roots[i]) + fileName;
|
||||
std::ifstream file(path.c_str(), std::ios::binary);
|
||||
if (file.good())
|
||||
{
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
return std::string("game/assets/sprites/") + fileName;
|
||||
}
|
||||
|
||||
static bool LoadSpriteAsset(const std::string& fileName, RenderData::Image& image)
|
||||
{
|
||||
const std::string path = FindSpriteAssetPath(fileName);
|
||||
if (!Asset::SpriteAssetLoader::Load(path, image))
|
||||
{
|
||||
std::cerr << "Load sprite asset failed: " << path << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static int RunTomGame()
|
||||
{
|
||||
#ifdef USE_FRAMEBUFFER
|
||||
Platform::IDisplay *display = new Platform::FBDisplay();
|
||||
return new Platform::FBDisplay();
|
||||
#else
|
||||
Platform::IDisplay *display = new Platform::SDLDisplay();
|
||||
return new Platform::SDLDisplay();
|
||||
#endif
|
||||
|
||||
if (!display->init(tomWidth, tomHeight))
|
||||
{
|
||||
delete display;
|
||||
return -1;
|
||||
}
|
||||
|
||||
Core::FrameBuffer frameBuffer(tomWidth, tomHeight);
|
||||
Rasterizer::SpriteRasterizer spriteRasterizer(&frameBuffer);
|
||||
|
||||
RenderData::Image background;
|
||||
RenderData::Image tomStand;
|
||||
RenderData::Image tomListen;
|
||||
RenderData::Image tomSay1;
|
||||
RenderData::Image tomSay2;
|
||||
RenderData::Image tomSay3;
|
||||
RenderData::Image tomSay4;
|
||||
RenderData::Image button;
|
||||
|
||||
if (!LoadSpriteAsset("background.sprite", background) ||
|
||||
!LoadSpriteAsset("Tom-stand.sprite", tomStand) ||
|
||||
!LoadSpriteAsset("Tom-listhen.sprite", tomListen) ||
|
||||
!LoadSpriteAsset("Tom-say1.sprite", tomSay1) ||
|
||||
!LoadSpriteAsset("Tom-say2.sprite", tomSay2) ||
|
||||
!LoadSpriteAsset("Tom-say3.sprite", tomSay3) ||
|
||||
!LoadSpriteAsset("Tom-say4.sprite", tomSay4) ||
|
||||
!LoadSpriteAsset("ui-record.sprite", button))
|
||||
static void PrintUsage(const char* program_name)
|
||||
{
|
||||
std::cerr << "Run ConvertTomSprites before starting --tom." << std::endl;
|
||||
display->shutdown();
|
||||
delete display;
|
||||
return -1;
|
||||
std::cout
|
||||
<< "Usage: " << program_name << " [--fps 30|45|60]\n"
|
||||
<< " " << program_name << " [--fps=30|45|60]\n";
|
||||
}
|
||||
|
||||
Game::AudioInput audioInput;
|
||||
Game::AudioOutput audioOutput;
|
||||
Game::ButtonInput buttonInput;
|
||||
#if defined(__linux__)
|
||||
buttonInput.init();
|
||||
#endif
|
||||
|
||||
Game::TomGameAssets assets;
|
||||
assets.background = &background;
|
||||
assets.tomStand = &tomStand;
|
||||
assets.tomListen = &tomListen;
|
||||
assets.tomSay1 = &tomSay1;
|
||||
assets.tomSay2 = &tomSay2;
|
||||
assets.tomSay3 = &tomSay3;
|
||||
assets.tomSay4 = &tomSay4;
|
||||
assets.button = &button;
|
||||
|
||||
Game::TomGameApp app(&frameBuffer, &spriteRasterizer, &audioInput, &audioOutput, &buttonInput);
|
||||
app.set_assets(assets);
|
||||
if (!app.is_ready())
|
||||
static ProgramOptions ParseProgramOptions(int argc, char* argv[])
|
||||
{
|
||||
std::cerr << "TomGameApp assets are incomplete." << std::endl;
|
||||
display->shutdown();
|
||||
delete display;
|
||||
return -1;
|
||||
ProgramOptions options;
|
||||
|
||||
for (int i = 1; i < argc; ++i)
|
||||
{
|
||||
const char* arg = argv[i];
|
||||
const char* fps_value = nullptr;
|
||||
|
||||
if (std::strcmp(arg, "--help") == 0 || std::strcmp(arg, "-h") == 0)
|
||||
{
|
||||
options.show_help = true;
|
||||
return options;
|
||||
}
|
||||
else if (std::strcmp(arg, "--fps") == 0)
|
||||
{
|
||||
if (i + 1 < argc)
|
||||
{
|
||||
fps_value = argv[++i];
|
||||
}
|
||||
else
|
||||
{
|
||||
std::cerr << "Missing value for --fps, falling back to 30. Supported: 30, 45, 60.\n";
|
||||
}
|
||||
}
|
||||
else if (std::strncmp(arg, "--fps=", 6) == 0)
|
||||
{
|
||||
fps_value = arg + 6;
|
||||
}
|
||||
|
||||
if (fps_value != nullptr)
|
||||
{
|
||||
const uint32_t parsed_fps = static_cast<uint32_t>(std::strtoul(fps_value, nullptr, 10));
|
||||
if (Core::Timer::is_supported_fps(parsed_fps))
|
||||
{
|
||||
options.target_fps = parsed_fps;
|
||||
}
|
||||
else
|
||||
{
|
||||
std::cerr << "Unsupported FPS '" << fps_value << "', falling back to 30. Supported: 30, 45, 60.\n";
|
||||
options.target_fps = Core::Timer::DefaultFps;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
bool isRunning = true;
|
||||
uint32_t lastTime = display->get_time_ms();
|
||||
while (isRunning)
|
||||
static void SleepRemainingFrameTime(const Core::Timer& timer, const Platform::ITimeSource& time_source)
|
||||
{
|
||||
display->poll_events(isRunning);
|
||||
const uint32_t currentTime = display->get_time_ms();
|
||||
const float deltaTime = static_cast<float>(currentTime - lastTime) * 0.001f;
|
||||
lastTime = currentTime;
|
||||
|
||||
app.update(deltaTime);
|
||||
frameBuffer.clear(RenderData::Color(18, 18, 24, 255));
|
||||
app.draw();
|
||||
display->present(&frameBuffer);
|
||||
const uint32_t sleep_ms = timer.remaining_frame_ms(time_source.get_time_ms());
|
||||
if (sleep_ms > 0u)
|
||||
{
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(sleep_ms));
|
||||
}
|
||||
}
|
||||
|
||||
buttonInput.shutdown();
|
||||
audioInput.shutdown();
|
||||
audioOutput.shutdown();
|
||||
display->shutdown();
|
||||
delete display;
|
||||
return 0;
|
||||
=======
|
||||
static void PrintUsage(const char *program_name)
|
||||
{
|
||||
std::cout
|
||||
<< "Usage: " << program_name << " [--fps 30|45|60]\n"
|
||||
<< " " << program_name << " [--fps=30|45|60]\n";
|
||||
}
|
||||
|
||||
struct ProgramOptions
|
||||
int main(int argc, char* argv[])
|
||||
{
|
||||
uint32_t target_fps;
|
||||
bool show_help;
|
||||
|
||||
ProgramOptions()
|
||||
: target_fps(Core::Timer::DefaultFps),
|
||||
show_help(false)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
static ProgramOptions ParseProgramOptions(int argc, char *argv[])
|
||||
{
|
||||
ProgramOptions options;
|
||||
|
||||
for (int i = 1; i < argc; ++i)
|
||||
{
|
||||
const char *arg = argv[i];
|
||||
const char *fps_value = nullptr;
|
||||
|
||||
if (std::strcmp(arg, "--help") == 0 || std::strcmp(arg, "-h") == 0)
|
||||
{
|
||||
options.show_help = true;
|
||||
return options;
|
||||
}
|
||||
else if (std::strcmp(arg, "--fps") == 0)
|
||||
{
|
||||
if (i + 1 < argc)
|
||||
{
|
||||
fps_value = argv[++i];
|
||||
}
|
||||
else
|
||||
{
|
||||
std::cerr << "Missing value for --fps, falling back to 30. Supported: 30, 45, 60.\n";
|
||||
}
|
||||
}
|
||||
else if (std::strncmp(arg, "--fps=", 6) == 0)
|
||||
{
|
||||
fps_value = arg + 6;
|
||||
}
|
||||
|
||||
if (fps_value != nullptr)
|
||||
{
|
||||
const uint32_t parsed_fps = static_cast<uint32_t>(std::strtoul(fps_value, nullptr, 10));
|
||||
if (Core::Timer::is_supported_fps(parsed_fps))
|
||||
{
|
||||
options.target_fps = parsed_fps;
|
||||
}
|
||||
else
|
||||
{
|
||||
std::cerr << "Unsupported FPS '" << fps_value << "', falling back to 30. Supported: 30, 45, 60.\n";
|
||||
options.target_fps = Core::Timer::DefaultFps;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
static void SleepRemainingFrameTime(const Core::Timer &timer, const Platform::ITimeSource &time_source)
|
||||
{
|
||||
const uint32_t sleep_ms = timer.remaining_frame_ms(time_source.get_time_ms());
|
||||
if (sleep_ms > 0u)
|
||||
{
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(sleep_ms));
|
||||
}
|
||||
>>>>>>> 777ff96602c52c86f03d612bb4213de746f580a5:src/Apps/Demo/main.cpp
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
<<<<<<< HEAD:src/main.cpp
|
||||
if (HasArg(argc, argv, "--tom"))
|
||||
{
|
||||
return RunTomGame();
|
||||
}
|
||||
=======
|
||||
const ProgramOptions options = ParseProgramOptions(argc, argv);
|
||||
if (options.show_help)
|
||||
{
|
||||
PrintUsage(argv[0]);
|
||||
return 0;
|
||||
}
|
||||
Core::Timer timer(options.target_fps);
|
||||
Platform::SteadyTimeSource time_source;
|
||||
>>>>>>> 777ff96602c52c86f03d612bb4213de746f580a5:src/Apps/Demo/main.cpp
|
||||
|
||||
#ifdef USE_FRAMEBUFFER
|
||||
Platform::IDisplay *display = new Platform::FBDisplay();
|
||||
#else
|
||||
Platform::IDisplay *display = new Platform::SDLDisplay();
|
||||
#endif
|
||||
|
||||
if (!display->init(width, height))
|
||||
Platform::IDisplay* display = CreateDisplay();
|
||||
if (!display->init(DemoWidth, DemoHeight))
|
||||
{
|
||||
delete display;
|
||||
return -1;
|
||||
}
|
||||
|
||||
Gfx::DrawContext ctx(width, height);
|
||||
std::cout << "Target FPS: " << timer.target_fps() << std::endl;
|
||||
Core::DrawContext ctx(DemoWidth, DemoHeight);
|
||||
Core::Timer timer(options.target_fps);
|
||||
Platform::SteadyTimeSource time_source;
|
||||
|
||||
RenderData::BitmapFont font;
|
||||
font.atlas = RenderData::Image(font_atlas_pixels, font_atlas_width, font_atlas_height);
|
||||
@@ -384,164 +141,56 @@ int main(int argc, char *argv[])
|
||||
font.columns = font_columns;
|
||||
font.first_char = font_first_char;
|
||||
|
||||
RenderData::Image sprite_img(test_sprite_pixels, test_sprite_width, test_sprite_height, 0x00000000);
|
||||
RenderData::SpriteRegion sprite_region(&sprite_img, 0, 0, sprite_img.width, sprite_img.height);
|
||||
|
||||
const std::array<uint16_t, 8 * 4> tileIds = {
|
||||
0, RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile,
|
||||
RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile, 0,
|
||||
0, RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile,
|
||||
RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile, 0};
|
||||
RenderData::Tilemap testTilemap(tileIds.data(), 8, 4, &sprite_img, sprite_img.width, sprite_img.height, 1);
|
||||
|
||||
Scene::Camera camera;
|
||||
camera.transform.position = Math::Vector3(0.0f, 0.0f, 3.0f);
|
||||
camera.transform.rotation = Math::Vector3(0.0f, 3.1415926535f, 0.0f);
|
||||
|
||||
const std::array<Math::Vector3, 8> cubeVertices = {
|
||||
Math::Vector3(-0.5f, -0.5f, -0.5f),
|
||||
Math::Vector3(0.5f, -0.5f, -0.5f),
|
||||
Math::Vector3(0.5f, 0.5f, -0.5f),
|
||||
Math::Vector3(-0.5f, 0.5f, -0.5f),
|
||||
Math::Vector3(-0.5f, -0.5f, 0.5f),
|
||||
Math::Vector3(0.5f, -0.5f, 0.5f),
|
||||
Math::Vector3(0.5f, 0.5f, 0.5f),
|
||||
Math::Vector3(-0.5f, 0.5f, 0.5f)};
|
||||
|
||||
const std::array<CubeFace, 6> cubeFaces = {
|
||||
CubeFace{{0, 3, 2, 1}},
|
||||
CubeFace{{4, 5, 6, 7}},
|
||||
CubeFace{{0, 4, 7, 3}},
|
||||
CubeFace{{1, 2, 6, 5}},
|
||||
CubeFace{{0, 1, 5, 4}},
|
||||
CubeFace{{3, 7, 6, 2}}};
|
||||
const std::array<CubeTriangle, 12> cubeTriangles = {
|
||||
CubeTriangle{{0, 3, 2}}, CubeTriangle{{0, 2, 1}},
|
||||
CubeTriangle{{4, 5, 6}}, CubeTriangle{{4, 6, 7}},
|
||||
CubeTriangle{{0, 4, 7}}, CubeTriangle{{0, 7, 3}},
|
||||
CubeTriangle{{1, 2, 6}}, CubeTriangle{{1, 6, 5}},
|
||||
CubeTriangle{{0, 1, 5}}, CubeTriangle{{0, 5, 4}},
|
||||
CubeTriangle{{3, 7, 6}}, CubeTriangle{{3, 6, 2}}};
|
||||
|
||||
const RenderData::Color clearColor(18, 18, 24, 255);
|
||||
const RenderData::Color cubeColor(240, 240, 240, 255);
|
||||
const RenderData::Color fpsColor(0, 255, 80, 255);
|
||||
const RenderData::Color fpsBg(0, 0, 0, 200);
|
||||
const float aspectRatio = static_cast<float>(width) / static_cast<float>(height);
|
||||
RenderData::Image sprite(test_sprite_pixels, test_sprite_width, test_sprite_height, 0x00000000u);
|
||||
|
||||
int32_t fps = 0;
|
||||
int32_t frame_count = 0;
|
||||
uint32_t last_fps_time = 0;
|
||||
uint32_t animation_time_ms = 0;
|
||||
char fps_text[32];
|
||||
|
||||
bool isRunning = true;
|
||||
uint32_t animation_time_ms = 0;
|
||||
while (isRunning)
|
||||
std::cout << "Demo started. Target FPS: " << timer.target_fps() << std::endl;
|
||||
|
||||
bool is_running = true;
|
||||
while (is_running)
|
||||
{
|
||||
timer.begin_frame(time_source.get_time_ms());
|
||||
const uint32_t fixed_delta_ms = timer.fixed_delta_ms();
|
||||
animation_time_ms += fixed_delta_ms;
|
||||
animation_time_ms += timer.fixed_delta_ms();
|
||||
|
||||
display->poll_events(isRunning);
|
||||
|
||||
ctx.clear(clearColor);
|
||||
|
||||
const float animation_time = static_cast<float>(animation_time_ms) / 1000.0f;
|
||||
const Math::Matrix4x4 model =
|
||||
Math::MathUtil::get_rotation_matrix_y(animation_time) *
|
||||
Math::MathUtil::get_rotation_matrix_x(static_cast<float>(animation_time_ms * 6u) / 10000.0f);
|
||||
const Math::Matrix4x4 view = camera.get_view_matrix();
|
||||
const Math::Matrix4x4 modelView = view * model;
|
||||
const Math::Matrix4x4 projection = camera.get_perspective_projection_matrix(aspectRatio);
|
||||
const Math::Matrix4x4 viewport = camera.get_viewport_matrix(static_cast<float>(width), static_cast<float>(height));
|
||||
const Math::Matrix4x4 mvp = projection * modelView;
|
||||
|
||||
std::array<Math::Vector3, 8> viewSpaceVertices;
|
||||
std::array<ProjectedVertex, 8> projectedVertices;
|
||||
for (size_t i = 0; i < cubeVertices.size(); ++i)
|
||||
bool should_quit = false;
|
||||
display->poll_events(should_quit);
|
||||
if (should_quit)
|
||||
{
|
||||
viewSpaceVertices[i] = (modelView * Math::Vector4::Point(cubeVertices[i])).to_vector3();
|
||||
projectedVertices[i] = ProjectToScreen(cubeVertices[i], mvp, viewport);
|
||||
is_running = false;
|
||||
}
|
||||
|
||||
std::array<bool, 6> visibleFaces = {};
|
||||
for (size_t faceIndex = 0; faceIndex < cubeFaces.size(); ++faceIndex)
|
||||
{
|
||||
visibleFaces[faceIndex] = IsFaceVisible(cubeFaces[faceIndex], viewSpaceVertices);
|
||||
}
|
||||
ctx.clear(RenderData::Color(18, 18, 24, 255));
|
||||
ctx.fill_rect(48, 72, 704, 360, RenderData::Color(42, 48, 60, 255));
|
||||
ctx.fill_rect(64, 88, 672, 328, RenderData::Color(24, 28, 36, 255));
|
||||
|
||||
std::array<RenderData::Triangle, 12> drawTriangles;
|
||||
size_t drawCommandCount = 0;
|
||||
for (const CubeTriangle &cubeTriangle : cubeTriangles)
|
||||
{
|
||||
if (!IsTriangleVisible(cubeTriangle, viewSpaceVertices))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const int32_t travel = std::max(1, DemoWidth - sprite.width - 128);
|
||||
const int32_t sprite_x = 64 + static_cast<int32_t>((animation_time_ms / 8u) % static_cast<uint32_t>(travel));
|
||||
const int32_t sprite_y = 220;
|
||||
ctx.draw_sprite(sprite_x, sprite_y, sprite);
|
||||
ctx.draw_text(font, 64, 96, RenderData::Color(240, 240, 240, 255), "GFX DEMO");
|
||||
|
||||
const ProjectedVertex &v0 = projectedVertices[cubeTriangle.vertices[0]];
|
||||
const ProjectedVertex &v1 = projectedVertices[cubeTriangle.vertices[1]];
|
||||
const ProjectedVertex &v2 = projectedVertices[cubeTriangle.vertices[2]];
|
||||
if (!v0.visible || !v1.visible || !v2.visible)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
drawTriangles[drawCommandCount++] =
|
||||
RenderData::Triangle(
|
||||
Scene::Vertex(v0.screen),
|
||||
Scene::Vertex(v1.screen),
|
||||
Scene::Vertex(v2.screen));
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < drawCommandCount; ++i)
|
||||
{
|
||||
ctx.draw_triangle(drawTriangles[i], cubeColor);
|
||||
}
|
||||
|
||||
for (size_t faceIndex = 0; faceIndex < cubeFaces.size(); ++faceIndex)
|
||||
{
|
||||
if (!visibleFaces[faceIndex])
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const CubeFace &face = cubeFaces[faceIndex];
|
||||
for (size_t edgeOffset = 0; edgeOffset < face.vertices.size(); ++edgeOffset)
|
||||
{
|
||||
const int startIndex = face.vertices[edgeOffset];
|
||||
const int endIndex = face.vertices[(edgeOffset + 1) % face.vertices.size()];
|
||||
const ProjectedVertex &start = projectedVertices[startIndex];
|
||||
const ProjectedVertex &end = projectedVertices[endIndex];
|
||||
if (!start.visible || !end.visible)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ctx.draw_line(
|
||||
Math::Vector2(start.screen.x, start.screen.y).to_vector2Int(),
|
||||
Math::Vector2(end.screen.x, end.screen.y).to_vector2Int(),
|
||||
clearColor);
|
||||
}
|
||||
}
|
||||
|
||||
// sprite 测试
|
||||
ctx.draw_sprite(10, 10, sprite_img);
|
||||
ctx.draw_sprite_region_ex(30, 10, sprite_region, 2, false, false);
|
||||
ctx.draw_sprite_region_ex(10, 30, sprite_region, 3, true, false);
|
||||
ctx.draw_tilemap(testTilemap, 650, 500, 96, 48, static_cast<int32_t>(animation_time_ms / 20u) % 32, 0);
|
||||
|
||||
// FPS 计数
|
||||
++frame_count;
|
||||
const uint32_t now = time_source.get_time_ms();
|
||||
if (now - last_fps_time >= 1000)
|
||||
if (now - last_fps_time >= 1000u)
|
||||
{
|
||||
fps = frame_count;
|
||||
frame_count = 0;
|
||||
last_fps_time = now;
|
||||
}
|
||||
|
||||
std::snprintf(fps_text, sizeof(fps_text), "FPS: %d", fps);
|
||||
ctx.draw_text(font, 4, 4, fpsColor, fpsBg, fps_text);
|
||||
ctx.draw_text(
|
||||
font,
|
||||
4,
|
||||
4,
|
||||
RenderData::Color(0, 255, 80, 255),
|
||||
RenderData::Color(0, 0, 0, 200),
|
||||
fps_text);
|
||||
|
||||
ctx.present(display);
|
||||
SleepRemainingFrameTime(timer, time_source);
|
||||
|
||||
@@ -0,0 +1,301 @@
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#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"
|
||||
|
||||
#ifdef USE_FRAMEBUFFER
|
||||
#include "FBDisplay.h"
|
||||
#else
|
||||
#include "SDLDisplay.h"
|
||||
#endif
|
||||
|
||||
namespace
|
||||
{
|
||||
const int32_t ScreenWidth = 800;
|
||||
const int32_t ScreenHeight = 480;
|
||||
|
||||
struct ProgramOptions
|
||||
{
|
||||
uint32_t target_fps;
|
||||
bool show_help;
|
||||
|
||||
ProgramOptions()
|
||||
: target_fps(Core::Timer::DefaultFps),
|
||||
show_help(false)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
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
|
||||
return new Platform::FBDisplay();
|
||||
#else
|
||||
return new Platform::SDLDisplay();
|
||||
#endif
|
||||
}
|
||||
|
||||
static void PrintUsage(const char* program_name)
|
||||
{
|
||||
std::cout
|
||||
<< "Usage: " << program_name << " [--fps 30|45|60]\n"
|
||||
<< " " << program_name << " [--fps=30|45|60]\n";
|
||||
}
|
||||
|
||||
static ProgramOptions ParseProgramOptions(int argc, char* argv[])
|
||||
{
|
||||
ProgramOptions options;
|
||||
|
||||
for (int i = 1; i < argc; ++i)
|
||||
{
|
||||
const char* arg = argv[i];
|
||||
const char* fps_value = nullptr;
|
||||
|
||||
if (std::strcmp(arg, "--help") == 0 || std::strcmp(arg, "-h") == 0)
|
||||
{
|
||||
options.show_help = true;
|
||||
return options;
|
||||
}
|
||||
else if (std::strcmp(arg, "--fps") == 0)
|
||||
{
|
||||
if (i + 1 < argc)
|
||||
{
|
||||
fps_value = argv[++i];
|
||||
}
|
||||
else
|
||||
{
|
||||
std::cerr << "Missing value for --fps, falling back to 30. Supported: 30, 45, 60.\n";
|
||||
}
|
||||
}
|
||||
else if (std::strncmp(arg, "--fps=", 6) == 0)
|
||||
{
|
||||
fps_value = arg + 6;
|
||||
}
|
||||
|
||||
if (fps_value != nullptr)
|
||||
{
|
||||
const uint32_t parsed_fps = static_cast<uint32_t>(std::strtoul(fps_value, nullptr, 10));
|
||||
if (Core::Timer::is_supported_fps(parsed_fps))
|
||||
{
|
||||
options.target_fps = parsed_fps;
|
||||
}
|
||||
else
|
||||
{
|
||||
std::cerr << "Unsupported FPS '" << fps_value << "', falling back to 30. Supported: 30, 45, 60.\n";
|
||||
options.target_fps = Core::Timer::DefaultFps;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
static void SleepRemainingFrameTime(const Core::Timer& timer, const Platform::ITimeSource& time_source)
|
||||
{
|
||||
const uint32_t sleep_ms = timer.remaining_frame_ms(time_source.get_time_ms());
|
||||
if (sleep_ms > 0u)
|
||||
{
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(sleep_ms));
|
||||
}
|
||||
}
|
||||
|
||||
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(
|
||||
uint32_t animation_time_ms,
|
||||
const SpriteImage& stand,
|
||||
const SpriteImage& listen,
|
||||
const SpriteImage* const* speaking_frames,
|
||||
size_t speaking_frame_count)
|
||||
{
|
||||
const uint32_t phase = (animation_time_ms / 1000u) % 6u;
|
||||
if (phase < 2u)
|
||||
{
|
||||
return stand.image;
|
||||
}
|
||||
if (phase < 3u)
|
||||
{
|
||||
return listen.image;
|
||||
}
|
||||
|
||||
const size_t frame_index = (animation_time_ms / 120u) % speaking_frame_count;
|
||||
return speaking_frames[frame_index]->image;
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[])
|
||||
{
|
||||
const ProgramOptions options = ParseProgramOptions(argc, argv);
|
||||
if (options.show_help)
|
||||
{
|
||||
PrintUsage(argv[0]);
|
||||
return 0;
|
||||
}
|
||||
|
||||
Platform::IDisplay* display = CreateDisplay();
|
||||
if (!display->init(ScreenWidth, ScreenHeight))
|
||||
{
|
||||
delete display;
|
||||
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 size_t speaking_frame_count = sizeof(speaking_frames) / sizeof(speaking_frames[0]);
|
||||
|
||||
Core::DrawContext ctx(ScreenWidth, ScreenHeight);
|
||||
Core::Timer timer(options.target_fps);
|
||||
Platform::SteadyTimeSource time_source;
|
||||
|
||||
std::cout << "Tom game started. Target FPS: " << timer.target_fps() << std::endl;
|
||||
|
||||
bool is_running = true;
|
||||
uint32_t animation_time_ms = 0;
|
||||
while (is_running)
|
||||
{
|
||||
timer.begin_frame(time_source.get_time_ms());
|
||||
animation_time_ms += timer.fixed_delta_ms();
|
||||
|
||||
bool should_quit = false;
|
||||
display->poll_events(should_quit);
|
||||
if (should_quit)
|
||||
{
|
||||
is_running = false;
|
||||
}
|
||||
|
||||
ctx.clear(RenderData::Color(18, 18, 24, 255));
|
||||
ctx.draw_sprite(0, 0, background.image);
|
||||
|
||||
const RenderData::Image& 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);
|
||||
|
||||
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);
|
||||
|
||||
ctx.present(display);
|
||||
SleepRemainingFrameTime(timer, time_source);
|
||||
}
|
||||
|
||||
display->shutdown();
|
||||
delete display;
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
# Tom Game
|
||||
|
||||
`src/Apps/Game` 是 Tom 游戏应用目录。仓库根目录下的 `src/Core` 继续作为渲染、数学、光栅化和平台显示层;Game 目录只放游戏入口、游戏逻辑、素材和素材转换工具。
|
||||
|
||||
## 当前入口
|
||||
|
||||
主构建目标 `IMX6U-Game` 现在从下面的文件启动:
|
||||
|
||||
```text
|
||||
src/Apps/Game/Main.cpp
|
||||
```
|
||||
|
||||
运行后默认启动 Tom,不再需要 `--tom` 参数。可选参数只保留帧率:
|
||||
|
||||
```bash
|
||||
IMX6U-Game.exe --fps 30
|
||||
IMX6U-Game.exe --fps=60
|
||||
```
|
||||
|
||||
## 素材目录
|
||||
|
||||
```text
|
||||
src/Apps/Game/assets/raw/ PNG 源素材
|
||||
src/Apps/Game/assets/sprites/ 转换后的 .sprite 运行时素材
|
||||
src/Apps/Game/tools/ 素材转换等开发工具
|
||||
src/Apps/Game/src/ 旧版 TomGameApp、音频、动画等游戏模块
|
||||
src/Apps/Game/tests/ 手工测试或后续测试代码
|
||||
```
|
||||
|
||||
板端运行时不直接解码 PNG。开发时把 PNG 放在 `src/Apps/Game/assets/raw/`,再转换为 `src/Apps/Game/assets/sprites/*.sprite`。
|
||||
|
||||
## 素材转换
|
||||
|
||||
`.sprite` 是项目自定义的 RGBA8888 二进制格式:
|
||||
|
||||
```text
|
||||
offset size content
|
||||
0 4 magic: "SPRT"
|
||||
4 4 version: 1, little-endian uint32
|
||||
8 4 width, little-endian uint32
|
||||
12 4 height, little-endian uint32
|
||||
16 4 format: 1 means RGBA8888
|
||||
20 * width * height pixels, little-endian uint32, 0xRRGGBBAA
|
||||
```
|
||||
|
||||
常用命令:
|
||||
|
||||
```bash
|
||||
cmake --build build-win --config Release --target SpriteAssetTool
|
||||
cmake --build build-win --config Release --target ConvertTomSprites
|
||||
```
|
||||
|
||||
单张转换示例:
|
||||
|
||||
```bash
|
||||
./build-win/Release/SpriteAssetTool.exe src/Apps/Game/assets/raw/ui-record.png src/Apps/Game/assets/sprites/ui-record.sprite
|
||||
./build-win/Release/SpriteAssetTool.exe src/Apps/Game/assets/raw/Tom-stand.png src/Apps/Game/assets/sprites/Tom-stand.sprite --fit 560 360
|
||||
```
|
||||
|
||||
## 运行
|
||||
|
||||
Windows PC:
|
||||
|
||||
```bash
|
||||
./build-win/Release/IMX6U-Game.exe
|
||||
```
|
||||
|
||||
Linux framebuffer / IMX6U:
|
||||
|
||||
```bash
|
||||
./IMX6U-Game --fps 30
|
||||
```
|
||||
|
||||
部署到板端时,把可执行文件和 `src/Apps/Game/assets/sprites/` 一起拷贝,并保持相对路径;也可以把 `.sprite` 放到 `/usr/local/share/imx6u-game/sprites/`。
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 2.6 MiB |
|
After Width: | Height: | Size: 2.7 MiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 2.4 MiB |
|
After Width: | Height: | Size: 2.5 MiB |
|
After Width: | Height: | Size: 2.6 MiB |
|
After Width: | Height: | Size: 2.4 MiB |
|
After Width: | Height: | Size: 5.5 MiB |
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 92 KiB |
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,280 @@
|
||||
#include "TomGameApp.h"
|
||||
#include "../audio/VoiceEffect.h"
|
||||
#include "../hardware/AudioInput.h"
|
||||
#include "../hardware/AudioOutput.h"
|
||||
#include "../hardware/ButtonInput.h"
|
||||
#include "FrameBuffer.h"
|
||||
#include "SpriteRasterizer.h"
|
||||
#include "Image.h"
|
||||
|
||||
namespace Game
|
||||
{
|
||||
TomGameApp::TomGameApp(
|
||||
Core::FrameBuffer* frameBuffer,
|
||||
Rasterizer::SpriteRasterizer* spriteRasterizer,
|
||||
AudioInput* audioInput,
|
||||
AudioOutput* audioOutput,
|
||||
ButtonInput* buttonInput) :
|
||||
frameBuffer(frameBuffer),
|
||||
spriteRasterizer(spriteRasterizer),
|
||||
audioInput(audioInput),
|
||||
audioOutput(audioOutput),
|
||||
buttonInput(buttonInput),
|
||||
state(TomGameState::Idle),
|
||||
screenWidth(frameBuffer ? frameBuffer->get_width() : 0),
|
||||
screenHeight(frameBuffer ? frameBuffer->get_height() : 0),
|
||||
tomX(0),
|
||||
tomY(0),
|
||||
buttonX(0),
|
||||
buttonY(0),
|
||||
audioSampleRate(16000),
|
||||
audioChannels(1),
|
||||
pitchFactor(1.45f),
|
||||
outputGain(1.15f)
|
||||
{
|
||||
recorder.configure(audioSampleRate, audioChannels);
|
||||
}
|
||||
|
||||
void TomGameApp::set_assets(const TomGameAssets& assets)
|
||||
{
|
||||
this->assets = assets;
|
||||
|
||||
speakingFrames.clear();
|
||||
if (assets.tomSay1 != nullptr) speakingFrames.push_back(RenderData::Sprite(assets.tomSay1));
|
||||
if (assets.tomSay2 != nullptr) speakingFrames.push_back(RenderData::Sprite(assets.tomSay2));
|
||||
if (assets.tomSay3 != nullptr) speakingFrames.push_back(RenderData::Sprite(assets.tomSay3));
|
||||
if (assets.tomSay4 != nullptr) speakingFrames.push_back(RenderData::Sprite(assets.tomSay4));
|
||||
if (assets.tomSay3 != nullptr) speakingFrames.push_back(RenderData::Sprite(assets.tomSay3));
|
||||
if (assets.tomSay2 != nullptr) speakingFrames.push_back(RenderData::Sprite(assets.tomSay2));
|
||||
|
||||
speakingAnimator.set_frames(speakingFrames);
|
||||
speakingAnimator.set_frame_time(0.08f);
|
||||
speakingAnimator.set_loop(true);
|
||||
speakingAnimator.stop();
|
||||
|
||||
calculate_layout();
|
||||
}
|
||||
|
||||
void TomGameApp::configure_audio(uint32_t sampleRate, uint32_t channels)
|
||||
{
|
||||
if (sampleRate > 0)
|
||||
{
|
||||
audioSampleRate = sampleRate;
|
||||
}
|
||||
if (channels > 0)
|
||||
{
|
||||
audioChannels = channels;
|
||||
}
|
||||
|
||||
recorder.configure(audioSampleRate, audioChannels);
|
||||
}
|
||||
|
||||
void TomGameApp::update(float deltaTime, bool buttonPressed)
|
||||
{
|
||||
bool trigger = buttonPressed;
|
||||
if (buttonInput != nullptr)
|
||||
{
|
||||
buttonInput->update();
|
||||
trigger = trigger || buttonInput->was_pressed();
|
||||
}
|
||||
|
||||
switch (state)
|
||||
{
|
||||
case TomGameState::Idle:
|
||||
if (trigger)
|
||||
{
|
||||
start_recording();
|
||||
}
|
||||
break;
|
||||
|
||||
case TomGameState::Recording:
|
||||
if (audioInput != nullptr)
|
||||
{
|
||||
recorder.update(*audioInput, 512);
|
||||
}
|
||||
if (trigger || recorder.is_finished())
|
||||
{
|
||||
finish_recording();
|
||||
}
|
||||
break;
|
||||
|
||||
case TomGameState::Processing:
|
||||
process_voice();
|
||||
break;
|
||||
|
||||
case TomGameState::Speaking:
|
||||
speakingAnimator.update(deltaTime);
|
||||
if (audioOutput == nullptr)
|
||||
{
|
||||
back_to_idle();
|
||||
break;
|
||||
}
|
||||
|
||||
player.update(*audioOutput, 1024);
|
||||
if (player.is_finished())
|
||||
{
|
||||
back_to_idle();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void TomGameApp::draw()
|
||||
{
|
||||
if (spriteRasterizer == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (assets.background != nullptr)
|
||||
{
|
||||
spriteRasterizer->DrawImage(*assets.background, 0, 0);
|
||||
}
|
||||
|
||||
const RenderData::Sprite tomSprite = get_current_tom_sprite();
|
||||
if (tomSprite.is_valid())
|
||||
{
|
||||
spriteRasterizer->DrawSprite(tomSprite, tomX, tomY);
|
||||
}
|
||||
|
||||
if (assets.button != nullptr)
|
||||
{
|
||||
spriteRasterizer->DrawImage(*assets.button, buttonX, buttonY);
|
||||
}
|
||||
}
|
||||
|
||||
bool TomGameApp::is_ready() const
|
||||
{
|
||||
return frameBuffer != nullptr &&
|
||||
spriteRasterizer != nullptr &&
|
||||
assets.background != nullptr &&
|
||||
assets.tomStand != nullptr &&
|
||||
assets.tomListen != nullptr &&
|
||||
assets.button != nullptr &&
|
||||
speakingFrames.size() == 6;
|
||||
}
|
||||
|
||||
bool TomGameApp::is_button_hit(int32_t x, int32_t y) const
|
||||
{
|
||||
if (assets.button == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return x >= buttonX &&
|
||||
x < buttonX + assets.button->get_width() &&
|
||||
y >= buttonY &&
|
||||
y < buttonY + assets.button->get_height();
|
||||
}
|
||||
|
||||
void TomGameApp::calculate_layout()
|
||||
{
|
||||
screenWidth = frameBuffer ? frameBuffer->get_width() : 0;
|
||||
screenHeight = frameBuffer ? frameBuffer->get_height() : 0;
|
||||
|
||||
if (assets.button != nullptr)
|
||||
{
|
||||
buttonX = (screenWidth - assets.button->get_width()) / 2;
|
||||
buttonY = screenHeight - assets.button->get_height() - 16;
|
||||
}
|
||||
|
||||
const RenderData::Sprite tomSprite = get_current_tom_sprite();
|
||||
if (tomSprite.is_valid())
|
||||
{
|
||||
tomX = (screenWidth - tomSprite.width) / 2;
|
||||
tomY = screenHeight - tomSprite.height - 72;
|
||||
if (tomY < 0)
|
||||
{
|
||||
tomY = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TomGameApp::start_recording()
|
||||
{
|
||||
if (audioInput != nullptr && !audioInput->is_open())
|
||||
{
|
||||
audioInput->init("default", audioSampleRate, audioChannels);
|
||||
}
|
||||
|
||||
player.stop();
|
||||
recorder.configure(audioSampleRate, audioChannels);
|
||||
recorder.start();
|
||||
state = TomGameState::Recording;
|
||||
calculate_layout();
|
||||
}
|
||||
|
||||
void TomGameApp::finish_recording()
|
||||
{
|
||||
if (recorder.is_recording())
|
||||
{
|
||||
recorder.stop();
|
||||
}
|
||||
|
||||
state = TomGameState::Processing;
|
||||
}
|
||||
|
||||
void TomGameApp::process_voice()
|
||||
{
|
||||
std::vector<int16_t> samples = recorder.get_samples();
|
||||
samples = VoiceEffect::trim_silence(samples, 0.02f, audioChannels);
|
||||
samples = VoiceEffect::pitch_up(samples, pitchFactor, audioChannels);
|
||||
samples = VoiceEffect::amplify(samples, outputGain);
|
||||
|
||||
start_speaking(samples);
|
||||
}
|
||||
|
||||
void TomGameApp::start_speaking(const std::vector<int16_t>& samples)
|
||||
{
|
||||
if (samples.empty())
|
||||
{
|
||||
back_to_idle();
|
||||
return;
|
||||
}
|
||||
|
||||
player.set_voice(samples, audioSampleRate, audioChannels);
|
||||
player.play();
|
||||
|
||||
speakingAnimator.reset();
|
||||
speakingAnimator.play();
|
||||
state = TomGameState::Speaking;
|
||||
calculate_layout();
|
||||
}
|
||||
|
||||
void TomGameApp::back_to_idle()
|
||||
{
|
||||
player.stop();
|
||||
speakingAnimator.stop();
|
||||
state = TomGameState::Idle;
|
||||
calculate_layout();
|
||||
}
|
||||
|
||||
void TomGameApp::draw_sprite_centered(const RenderData::Sprite& sprite, int32_t centerX, int32_t y)
|
||||
{
|
||||
if (spriteRasterizer == nullptr || !sprite.is_valid())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
spriteRasterizer->DrawSprite(sprite, centerX - sprite.width / 2, y);
|
||||
}
|
||||
|
||||
const RenderData::Sprite TomGameApp::get_current_tom_sprite() const
|
||||
{
|
||||
if (state == TomGameState::Recording && assets.tomListen != nullptr)
|
||||
{
|
||||
return RenderData::Sprite(assets.tomListen);
|
||||
}
|
||||
|
||||
if (state == TomGameState::Speaking)
|
||||
{
|
||||
const RenderData::Sprite* sprite = speakingAnimator.get_current_sprite();
|
||||
if (sprite != nullptr)
|
||||
{
|
||||
return *sprite;
|
||||
}
|
||||
}
|
||||
|
||||
return RenderData::Sprite(assets.tomStand);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
#include "../audio/VoicePlayer.h"
|
||||
#include "../audio/VoiceRecorder.h"
|
||||
#include "../components/SpriteAnimator.h"
|
||||
#include "Sprite.h"
|
||||
|
||||
namespace Core
|
||||
{
|
||||
class FrameBuffer;
|
||||
}
|
||||
|
||||
namespace Rasterizer
|
||||
{
|
||||
class SpriteRasterizer;
|
||||
}
|
||||
|
||||
namespace RenderData
|
||||
{
|
||||
class Image;
|
||||
}
|
||||
|
||||
namespace Game
|
||||
{
|
||||
class AudioInput;
|
||||
class AudioOutput;
|
||||
class ButtonInput;
|
||||
|
||||
enum class TomGameState
|
||||
{
|
||||
Idle,
|
||||
Recording,
|
||||
Processing,
|
||||
Speaking
|
||||
};
|
||||
|
||||
struct TomGameAssets
|
||||
{
|
||||
const RenderData::Image* background;
|
||||
const RenderData::Image* tomStand;
|
||||
const RenderData::Image* tomListen;
|
||||
const RenderData::Image* tomSay1;
|
||||
const RenderData::Image* tomSay2;
|
||||
const RenderData::Image* tomSay3;
|
||||
const RenderData::Image* tomSay4;
|
||||
const RenderData::Image* button;
|
||||
|
||||
TomGameAssets() :
|
||||
background(nullptr),
|
||||
tomStand(nullptr),
|
||||
tomListen(nullptr),
|
||||
tomSay1(nullptr),
|
||||
tomSay2(nullptr),
|
||||
tomSay3(nullptr),
|
||||
tomSay4(nullptr),
|
||||
button(nullptr)
|
||||
{}
|
||||
};
|
||||
|
||||
class TomGameApp
|
||||
{
|
||||
private:
|
||||
Core::FrameBuffer* frameBuffer;
|
||||
Rasterizer::SpriteRasterizer* spriteRasterizer;
|
||||
AudioInput* audioInput;
|
||||
AudioOutput* audioOutput;
|
||||
ButtonInput* buttonInput;
|
||||
|
||||
TomGameAssets assets;
|
||||
TomGameState state;
|
||||
VoiceRecorder recorder;
|
||||
VoicePlayer player;
|
||||
SpriteAnimator speakingAnimator;
|
||||
std::vector<RenderData::Sprite> speakingFrames;
|
||||
|
||||
int32_t screenWidth;
|
||||
int32_t screenHeight;
|
||||
int32_t tomX;
|
||||
int32_t tomY;
|
||||
int32_t buttonX;
|
||||
int32_t buttonY;
|
||||
|
||||
uint32_t audioSampleRate;
|
||||
uint32_t audioChannels;
|
||||
float pitchFactor;
|
||||
float outputGain;
|
||||
|
||||
void calculate_layout();
|
||||
void start_recording();
|
||||
void finish_recording();
|
||||
void process_voice();
|
||||
void start_speaking(const std::vector<int16_t>& samples);
|
||||
void back_to_idle();
|
||||
void draw_sprite_centered(const RenderData::Sprite& sprite, int32_t centerX, int32_t y);
|
||||
const RenderData::Sprite get_current_tom_sprite() const;
|
||||
|
||||
public:
|
||||
TomGameApp(
|
||||
Core::FrameBuffer* frameBuffer,
|
||||
Rasterizer::SpriteRasterizer* spriteRasterizer,
|
||||
AudioInput* audioInput,
|
||||
AudioOutput* audioOutput,
|
||||
ButtonInput* buttonInput = nullptr);
|
||||
|
||||
void set_assets(const TomGameAssets& assets);
|
||||
void configure_audio(uint32_t sampleRate = 16000, uint32_t channels = 1);
|
||||
void update(float deltaTime, bool buttonPressed = false);
|
||||
void draw();
|
||||
|
||||
bool is_ready() const;
|
||||
bool is_button_hit(int32_t x, int32_t y) const;
|
||||
TomGameState get_state() const { return state; }
|
||||
float get_record_volume() const { return recorder.get_last_volume(); }
|
||||
float get_play_progress() const { return player.get_progress(); }
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
#include "VoiceEffect.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace Game
|
||||
{
|
||||
std::vector<int16_t> VoiceEffect::pitch_up(
|
||||
const std::vector<int16_t>& samples,
|
||||
float factor,
|
||||
uint32_t channels)
|
||||
{
|
||||
return change_speed(samples, factor, channels);
|
||||
}
|
||||
|
||||
std::vector<int16_t> VoiceEffect::change_speed(
|
||||
const std::vector<int16_t>& samples,
|
||||
float speed,
|
||||
uint32_t channels)
|
||||
{
|
||||
if (samples.empty() || speed <= 0.0f || channels == 0)
|
||||
{
|
||||
return samples;
|
||||
}
|
||||
|
||||
const size_t frameCount = samples.size() / channels;
|
||||
if (frameCount == 0)
|
||||
{
|
||||
return std::vector<int16_t>();
|
||||
}
|
||||
|
||||
const size_t outputFrameCount = std::max<size_t>(1, static_cast<size_t>(frameCount / speed));
|
||||
std::vector<int16_t> output(outputFrameCount * channels, 0);
|
||||
|
||||
for (size_t outFrame = 0; outFrame < outputFrameCount; ++outFrame)
|
||||
{
|
||||
const float sourceFrame = static_cast<float>(outFrame) * speed;
|
||||
const size_t source0 = std::min(static_cast<size_t>(sourceFrame), frameCount - 1);
|
||||
const size_t source1 = std::min(source0 + 1, frameCount - 1);
|
||||
const float t = sourceFrame - static_cast<float>(source0);
|
||||
|
||||
for (uint32_t channel = 0; channel < channels; ++channel)
|
||||
{
|
||||
const int16_t a = samples[source0 * channels + channel];
|
||||
const int16_t b = samples[source1 * channels + channel];
|
||||
const float mixed = static_cast<float>(a) + (static_cast<float>(b) - static_cast<float>(a)) * t;
|
||||
output[outFrame * channels + channel] = clamp_to_sample(mixed);
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
std::vector<int16_t> VoiceEffect::amplify(const std::vector<int16_t>& samples, float gain)
|
||||
{
|
||||
if (samples.empty() || gain == 1.0f)
|
||||
{
|
||||
return samples;
|
||||
}
|
||||
|
||||
std::vector<int16_t> output(samples.size(), 0);
|
||||
for (size_t i = 0; i < samples.size(); ++i)
|
||||
{
|
||||
output[i] = clamp_to_sample(static_cast<float>(samples[i]) * gain);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
std::vector<int16_t> VoiceEffect::trim_silence(
|
||||
const std::vector<int16_t>& samples,
|
||||
float threshold,
|
||||
uint32_t channels)
|
||||
{
|
||||
if (samples.empty() || channels == 0)
|
||||
{
|
||||
return samples;
|
||||
}
|
||||
|
||||
const size_t frameCount = samples.size() / channels;
|
||||
size_t startFrame = 0;
|
||||
size_t endFrame = frameCount;
|
||||
|
||||
while (startFrame < frameCount)
|
||||
{
|
||||
bool loud = false;
|
||||
for (uint32_t channel = 0; channel < channels; ++channel)
|
||||
{
|
||||
if (sample_abs(samples[startFrame * channels + channel]) >= threshold)
|
||||
{
|
||||
loud = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (loud)
|
||||
{
|
||||
break;
|
||||
}
|
||||
++startFrame;
|
||||
}
|
||||
|
||||
while (endFrame > startFrame)
|
||||
{
|
||||
bool loud = false;
|
||||
const size_t frame = endFrame - 1;
|
||||
for (uint32_t channel = 0; channel < channels; ++channel)
|
||||
{
|
||||
if (sample_abs(samples[frame * channels + channel]) >= threshold)
|
||||
{
|
||||
loud = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (loud)
|
||||
{
|
||||
break;
|
||||
}
|
||||
--endFrame;
|
||||
}
|
||||
|
||||
const size_t startSample = startFrame * channels;
|
||||
const size_t endSample = endFrame * channels;
|
||||
return std::vector<int16_t>(samples.begin() + startSample, samples.begin() + endSample);
|
||||
}
|
||||
|
||||
int16_t VoiceEffect::clamp_to_sample(float value)
|
||||
{
|
||||
if (value > 32767.0f)
|
||||
{
|
||||
return 32767;
|
||||
}
|
||||
if (value < -32768.0f)
|
||||
{
|
||||
return -32768;
|
||||
}
|
||||
|
||||
return static_cast<int16_t>(value);
|
||||
}
|
||||
|
||||
float VoiceEffect::sample_abs(int16_t sample)
|
||||
{
|
||||
return std::abs(static_cast<float>(sample) / 32768.0f);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace Game
|
||||
{
|
||||
class VoiceEffect
|
||||
{
|
||||
public:
|
||||
static std::vector<int16_t> pitch_up(
|
||||
const std::vector<int16_t>& samples,
|
||||
float factor = 1.45f,
|
||||
uint32_t channels = 1);
|
||||
|
||||
static std::vector<int16_t> change_speed(
|
||||
const std::vector<int16_t>& samples,
|
||||
float speed,
|
||||
uint32_t channels = 1);
|
||||
|
||||
static std::vector<int16_t> amplify(
|
||||
const std::vector<int16_t>& samples,
|
||||
float gain);
|
||||
|
||||
static std::vector<int16_t> trim_silence(
|
||||
const std::vector<int16_t>& samples,
|
||||
float threshold = 0.02f,
|
||||
uint32_t channels = 1);
|
||||
|
||||
private:
|
||||
static int16_t clamp_to_sample(float value);
|
||||
static float sample_abs(int16_t sample);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
#include "VoicePlayer.h"
|
||||
#include "../hardware/AudioOutput.h"
|
||||
#include <algorithm>
|
||||
|
||||
namespace Game
|
||||
{
|
||||
VoicePlayer::VoicePlayer() :
|
||||
playPosition(0),
|
||||
sampleRate(16000),
|
||||
channels(1),
|
||||
playing(false),
|
||||
finished(false)
|
||||
{}
|
||||
|
||||
void VoicePlayer::set_voice(const std::vector<int16_t>& samples, uint32_t sampleRate, uint32_t channels)
|
||||
{
|
||||
this->samples = samples;
|
||||
this->sampleRate = sampleRate > 0 ? sampleRate : 16000;
|
||||
this->channels = channels > 0 ? channels : 1;
|
||||
reset();
|
||||
}
|
||||
|
||||
void VoicePlayer::play()
|
||||
{
|
||||
if (samples.empty())
|
||||
{
|
||||
playing = false;
|
||||
finished = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (playPosition >= samples.size())
|
||||
{
|
||||
playPosition = 0;
|
||||
}
|
||||
|
||||
playing = true;
|
||||
finished = false;
|
||||
}
|
||||
|
||||
void VoicePlayer::stop()
|
||||
{
|
||||
playing = false;
|
||||
finished = true;
|
||||
}
|
||||
|
||||
void VoicePlayer::reset()
|
||||
{
|
||||
playPosition = 0;
|
||||
playing = false;
|
||||
finished = samples.empty();
|
||||
}
|
||||
|
||||
void VoicePlayer::clear()
|
||||
{
|
||||
samples.clear();
|
||||
playPosition = 0;
|
||||
playing = false;
|
||||
finished = false;
|
||||
}
|
||||
|
||||
int VoicePlayer::update(AudioOutput& audioOutput, int maxSamplesPerUpdate)
|
||||
{
|
||||
if (!playing || samples.empty())
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!audioOutput.is_open() ||
|
||||
audioOutput.get_sample_rate() != sampleRate ||
|
||||
audioOutput.get_channels() != channels)
|
||||
{
|
||||
if (!audioOutput.init(audioOutput.get_device_name(), sampleRate, channels))
|
||||
{
|
||||
stop();
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
const size_t remaining = get_remaining_samples();
|
||||
if (remaining == 0)
|
||||
{
|
||||
stop();
|
||||
return 0;
|
||||
}
|
||||
|
||||
int requestCount = get_aligned_sample_count(maxSamplesPerUpdate);
|
||||
if (requestCount <= 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
requestCount = static_cast<int>(std::min<size_t>(static_cast<size_t>(requestCount), remaining));
|
||||
requestCount = get_aligned_sample_count(requestCount);
|
||||
if (requestCount <= 0)
|
||||
{
|
||||
stop();
|
||||
return 0;
|
||||
}
|
||||
|
||||
const int written = audioOutput.write_samples(samples.data() + playPosition, requestCount);
|
||||
if (written <= 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
playPosition += static_cast<size_t>(written);
|
||||
if (playPosition >= samples.size())
|
||||
{
|
||||
stop();
|
||||
}
|
||||
|
||||
return written;
|
||||
}
|
||||
|
||||
size_t VoicePlayer::get_remaining_samples() const
|
||||
{
|
||||
if (playPosition >= samples.size())
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return samples.size() - playPosition;
|
||||
}
|
||||
|
||||
float VoicePlayer::get_duration_seconds() const
|
||||
{
|
||||
if (sampleRate == 0 || channels == 0)
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
return static_cast<float>(samples.size()) / static_cast<float>(sampleRate * channels);
|
||||
}
|
||||
|
||||
float VoicePlayer::get_progress() const
|
||||
{
|
||||
if (samples.empty())
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
return static_cast<float>(playPosition) / static_cast<float>(samples.size());
|
||||
}
|
||||
|
||||
int VoicePlayer::get_aligned_sample_count(int sampleCount) const
|
||||
{
|
||||
if (sampleCount <= 0 || channels == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return sampleCount - (sampleCount % static_cast<int>(channels));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
#pragma once
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace Game
|
||||
{
|
||||
class AudioOutput;
|
||||
|
||||
class VoicePlayer
|
||||
{
|
||||
private:
|
||||
std::vector<int16_t> samples;
|
||||
size_t playPosition;
|
||||
uint32_t sampleRate;
|
||||
uint32_t channels;
|
||||
bool playing;
|
||||
bool finished;
|
||||
|
||||
int get_aligned_sample_count(int sampleCount) const;
|
||||
|
||||
public:
|
||||
VoicePlayer();
|
||||
|
||||
void set_voice(const std::vector<int16_t>& samples, uint32_t sampleRate = 16000, uint32_t channels = 1);
|
||||
void play();
|
||||
void stop();
|
||||
void reset();
|
||||
void clear();
|
||||
int update(AudioOutput& audioOutput, int maxSamplesPerUpdate = 1024);
|
||||
|
||||
const std::vector<int16_t>& get_samples() const { return samples; }
|
||||
size_t get_sample_count() const { return samples.size(); }
|
||||
size_t get_play_position() const { return playPosition; }
|
||||
size_t get_remaining_samples() const;
|
||||
uint32_t get_sample_rate() const { return sampleRate; }
|
||||
uint32_t get_channels() const { return channels; }
|
||||
float get_duration_seconds() const;
|
||||
float get_progress() const;
|
||||
|
||||
bool is_playing() const { return playing; }
|
||||
bool is_finished() const { return finished; }
|
||||
bool has_voice() const { return !samples.empty(); }
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
#include "VoiceRecorder.h"
|
||||
#include "../hardware/AudioInput.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
|
||||
namespace Game
|
||||
{
|
||||
VoiceRecorder::VoiceRecorder() :
|
||||
sampleRate(16000),
|
||||
channels(1),
|
||||
maxDurationSeconds(5.0f),
|
||||
silenceThreshold(0.03f),
|
||||
silenceDurationSeconds(0.8f),
|
||||
minRecordSeconds(0.3f),
|
||||
lastVolume(0.0f),
|
||||
silentSampleCount(0),
|
||||
recording(false),
|
||||
finished(false),
|
||||
finishedBySilence(false)
|
||||
{}
|
||||
|
||||
void VoiceRecorder::configure(
|
||||
uint32_t sampleRate,
|
||||
uint32_t channels,
|
||||
float maxDurationSeconds,
|
||||
float silenceThreshold,
|
||||
float silenceDurationSeconds,
|
||||
float minRecordSeconds)
|
||||
{
|
||||
if (sampleRate > 0)
|
||||
{
|
||||
this->sampleRate = sampleRate;
|
||||
}
|
||||
if (channels > 0)
|
||||
{
|
||||
this->channels = channels;
|
||||
}
|
||||
if (maxDurationSeconds > 0.0f)
|
||||
{
|
||||
this->maxDurationSeconds = maxDurationSeconds;
|
||||
}
|
||||
if (silenceThreshold >= 0.0f)
|
||||
{
|
||||
this->silenceThreshold = silenceThreshold;
|
||||
}
|
||||
if (silenceDurationSeconds > 0.0f)
|
||||
{
|
||||
this->silenceDurationSeconds = silenceDurationSeconds;
|
||||
}
|
||||
if (minRecordSeconds >= 0.0f)
|
||||
{
|
||||
this->minRecordSeconds = minRecordSeconds;
|
||||
}
|
||||
}
|
||||
|
||||
void VoiceRecorder::start()
|
||||
{
|
||||
clear();
|
||||
recording = true;
|
||||
finished = false;
|
||||
finishedBySilence = false;
|
||||
}
|
||||
|
||||
void VoiceRecorder::stop()
|
||||
{
|
||||
recording = false;
|
||||
finished = true;
|
||||
}
|
||||
|
||||
void VoiceRecorder::clear()
|
||||
{
|
||||
samples.clear();
|
||||
lastVolume = 0.0f;
|
||||
silentSampleCount = 0;
|
||||
recording = false;
|
||||
finished = false;
|
||||
finishedBySilence = false;
|
||||
}
|
||||
|
||||
int VoiceRecorder::update(AudioInput& audioInput, int sampleCount)
|
||||
{
|
||||
if (!recording || sampleCount <= 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
const size_t maxSampleCount = get_max_sample_count();
|
||||
if (samples.size() >= maxSampleCount)
|
||||
{
|
||||
stop();
|
||||
return 0;
|
||||
}
|
||||
|
||||
const size_t remaining = maxSampleCount - samples.size();
|
||||
const int requestCount = static_cast<int>(std::min<size_t>(static_cast<size_t>(sampleCount), remaining));
|
||||
std::vector<int16_t> buffer(static_cast<size_t>(requestCount), 0);
|
||||
|
||||
const int readCount = audioInput.read_samples(buffer.data(), requestCount);
|
||||
if (readCount <= 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
lastVolume = calculate_volume(buffer.data(), readCount);
|
||||
samples.insert(samples.end(), buffer.begin(), buffer.begin() + readCount);
|
||||
|
||||
if (samples.size() >= get_min_sample_count() && lastVolume < silenceThreshold)
|
||||
{
|
||||
silentSampleCount += static_cast<size_t>(readCount);
|
||||
}
|
||||
else
|
||||
{
|
||||
silentSampleCount = 0;
|
||||
}
|
||||
|
||||
if (silentSampleCount >= get_silence_sample_limit())
|
||||
{
|
||||
finishedBySilence = true;
|
||||
stop();
|
||||
}
|
||||
else if (samples.size() >= maxSampleCount)
|
||||
{
|
||||
stop();
|
||||
}
|
||||
|
||||
return readCount;
|
||||
}
|
||||
|
||||
float VoiceRecorder::get_duration_seconds() const
|
||||
{
|
||||
if (sampleRate == 0 || channels == 0)
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
return static_cast<float>(samples.size()) / static_cast<float>(sampleRate * channels);
|
||||
}
|
||||
|
||||
size_t VoiceRecorder::get_max_sample_count() const
|
||||
{
|
||||
return static_cast<size_t>(sampleRate * channels * maxDurationSeconds);
|
||||
}
|
||||
|
||||
size_t VoiceRecorder::get_silence_sample_limit() const
|
||||
{
|
||||
return static_cast<size_t>(sampleRate * channels * silenceDurationSeconds);
|
||||
}
|
||||
|
||||
size_t VoiceRecorder::get_min_sample_count() const
|
||||
{
|
||||
return static_cast<size_t>(sampleRate * channels * minRecordSeconds);
|
||||
}
|
||||
|
||||
float VoiceRecorder::calculate_volume(const int16_t* buffer, int sampleCount) const
|
||||
{
|
||||
if (buffer == nullptr || sampleCount <= 0)
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
double sum = 0.0;
|
||||
for (int i = 0; i < sampleCount; ++i)
|
||||
{
|
||||
const double value = static_cast<double>(buffer[i]) / 32768.0;
|
||||
sum += value * value;
|
||||
}
|
||||
|
||||
return static_cast<float>(std::sqrt(sum / sampleCount));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
#pragma once
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace Game
|
||||
{
|
||||
class AudioInput;
|
||||
|
||||
class VoiceRecorder
|
||||
{
|
||||
private:
|
||||
std::vector<int16_t> samples;
|
||||
uint32_t sampleRate;
|
||||
uint32_t channels;
|
||||
float maxDurationSeconds;
|
||||
float silenceThreshold;
|
||||
float silenceDurationSeconds;
|
||||
float minRecordSeconds;
|
||||
float lastVolume;
|
||||
size_t silentSampleCount;
|
||||
bool recording;
|
||||
bool finished;
|
||||
bool finishedBySilence;
|
||||
|
||||
size_t get_max_sample_count() const;
|
||||
size_t get_silence_sample_limit() const;
|
||||
size_t get_min_sample_count() const;
|
||||
float calculate_volume(const int16_t* buffer, int sampleCount) const;
|
||||
|
||||
public:
|
||||
VoiceRecorder();
|
||||
|
||||
void configure(
|
||||
uint32_t sampleRate,
|
||||
uint32_t channels,
|
||||
float maxDurationSeconds = 5.0f,
|
||||
float silenceThreshold = 0.03f,
|
||||
float silenceDurationSeconds = 0.8f,
|
||||
float minRecordSeconds = 0.3f);
|
||||
|
||||
void start();
|
||||
void stop();
|
||||
void clear();
|
||||
int update(AudioInput& audioInput, int sampleCount = 512);
|
||||
|
||||
const std::vector<int16_t>& get_samples() const { return samples; }
|
||||
size_t get_sample_count() const { return samples.size(); }
|
||||
uint32_t get_sample_rate() const { return sampleRate; }
|
||||
uint32_t get_channels() const { return channels; }
|
||||
float get_duration_seconds() const;
|
||||
float get_last_volume() const { return lastVolume; }
|
||||
|
||||
bool is_recording() const { return recording; }
|
||||
bool is_finished() const { return finished; }
|
||||
bool is_finished_by_silence() const { return finishedBySilence; }
|
||||
bool has_samples() const { return !samples.empty(); }
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
#include "SpriteAnimator.h"
|
||||
|
||||
namespace Game
|
||||
{
|
||||
SpriteAnimator::SpriteAnimator() :
|
||||
currentFrame(0),
|
||||
frameTime(0.1f),
|
||||
timer(0.0f),
|
||||
loop(true),
|
||||
playing(true)
|
||||
{}
|
||||
|
||||
SpriteAnimator::SpriteAnimator(const std::vector<RenderData::Sprite>& frames, float frameTime, bool loop) :
|
||||
frames(frames),
|
||||
currentFrame(0),
|
||||
frameTime(frameTime > 0.0f ? frameTime : 0.1f),
|
||||
timer(0.0f),
|
||||
loop(loop),
|
||||
playing(!frames.empty())
|
||||
{}
|
||||
|
||||
void SpriteAnimator::set_frames(const std::vector<RenderData::Sprite>& frames)
|
||||
{
|
||||
this->frames = frames;
|
||||
reset();
|
||||
}
|
||||
|
||||
void SpriteAnimator::set_frame_time(float frameTime)
|
||||
{
|
||||
if (frameTime > 0.0f)
|
||||
{
|
||||
this->frameTime = frameTime;
|
||||
}
|
||||
}
|
||||
|
||||
void SpriteAnimator::set_loop(bool loop)
|
||||
{
|
||||
this->loop = loop;
|
||||
}
|
||||
|
||||
void SpriteAnimator::play()
|
||||
{
|
||||
playing = !frames.empty();
|
||||
}
|
||||
|
||||
void SpriteAnimator::stop()
|
||||
{
|
||||
playing = false;
|
||||
}
|
||||
|
||||
void SpriteAnimator::reset()
|
||||
{
|
||||
currentFrame = 0;
|
||||
timer = 0.0f;
|
||||
playing = !frames.empty();
|
||||
}
|
||||
|
||||
void SpriteAnimator::update(float deltaTime)
|
||||
{
|
||||
if (!playing || frames.empty() || frameTime <= 0.0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
timer += deltaTime;
|
||||
while (timer >= frameTime)
|
||||
{
|
||||
timer -= frameTime;
|
||||
|
||||
if (currentFrame + 1 < frames.size())
|
||||
{
|
||||
++currentFrame;
|
||||
}
|
||||
else if (loop)
|
||||
{
|
||||
currentFrame = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
playing = false;
|
||||
currentFrame = frames.size() - 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const RenderData::Sprite* SpriteAnimator::get_current_sprite() const
|
||||
{
|
||||
if (frames.empty() || currentFrame >= frames.size())
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return &frames[currentFrame];
|
||||
}
|
||||
|
||||
bool SpriteAnimator::is_finished() const
|
||||
{
|
||||
return !loop && !playing && !frames.empty() && currentFrame == frames.size() - 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
#include <cstddef>
|
||||
#include <vector>
|
||||
#include "Sprite.h"
|
||||
|
||||
namespace Game
|
||||
{
|
||||
class SpriteAnimator
|
||||
{
|
||||
private:
|
||||
std::vector<RenderData::Sprite> frames;
|
||||
size_t currentFrame;
|
||||
float frameTime;
|
||||
float timer;
|
||||
bool loop;
|
||||
bool playing;
|
||||
|
||||
public:
|
||||
SpriteAnimator();
|
||||
SpriteAnimator(const std::vector<RenderData::Sprite>& frames, float frameTime, bool loop = true);
|
||||
|
||||
void set_frames(const std::vector<RenderData::Sprite>& frames);
|
||||
void set_frame_time(float frameTime);
|
||||
void set_loop(bool loop);
|
||||
|
||||
void play();
|
||||
void stop();
|
||||
void reset();
|
||||
void update(float deltaTime);
|
||||
|
||||
const RenderData::Sprite* get_current_sprite() const;
|
||||
size_t get_current_frame() const { return currentFrame; }
|
||||
size_t get_frame_count() const { return frames.size(); }
|
||||
bool is_playing() const { return playing; }
|
||||
bool is_finished() const;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
#include "AudioInput.h"
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
|
||||
#if defined(__linux__)
|
||||
#include <alsa/asoundlib.h>
|
||||
#endif
|
||||
|
||||
namespace Game
|
||||
{
|
||||
AudioInput::AudioInput() :
|
||||
deviceName("default"),
|
||||
sampleRate(16000),
|
||||
channels(1),
|
||||
opened(false)
|
||||
#if defined(__linux__)
|
||||
,
|
||||
handle(nullptr)
|
||||
#endif
|
||||
{}
|
||||
|
||||
AudioInput::~AudioInput()
|
||||
{
|
||||
shutdown();
|
||||
}
|
||||
|
||||
bool AudioInput::init(const std::string& deviceName, uint32_t sampleRate, uint32_t channels)
|
||||
{
|
||||
shutdown();
|
||||
|
||||
if (sampleRate == 0 || channels == 0)
|
||||
{
|
||||
std::cerr << "AudioInput invalid params." << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
this->deviceName = deviceName;
|
||||
this->sampleRate = sampleRate;
|
||||
this->channels = channels;
|
||||
|
||||
#if defined(__linux__)
|
||||
int result = snd_pcm_open(&handle, deviceName.c_str(), SND_PCM_STREAM_CAPTURE, 0);
|
||||
if (result < 0)
|
||||
{
|
||||
std::cerr << "AudioInput open failed: " << snd_strerror(result) << std::endl;
|
||||
handle = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
result = snd_pcm_set_params(
|
||||
handle,
|
||||
SND_PCM_FORMAT_S16_LE,
|
||||
SND_PCM_ACCESS_RW_INTERLEAVED,
|
||||
channels,
|
||||
sampleRate,
|
||||
1,
|
||||
50000);
|
||||
if (result < 0)
|
||||
{
|
||||
std::cerr << "AudioInput set params failed: " << snd_strerror(result) << std::endl;
|
||||
shutdown();
|
||||
return false;
|
||||
}
|
||||
|
||||
opened = true;
|
||||
return true;
|
||||
#else
|
||||
std::cerr << "AudioInput is only implemented on Linux ALSA." << std::endl;
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
int AudioInput::read_samples(int16_t* buffer, int sampleCount)
|
||||
{
|
||||
if (buffer == nullptr || sampleCount <= 0 || !opened)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
#if defined(__linux__)
|
||||
const snd_pcm_uframes_t frames = static_cast<snd_pcm_uframes_t>(sampleCount / channels);
|
||||
int result = snd_pcm_readi(handle, buffer, frames);
|
||||
if (result == -EPIPE)
|
||||
{
|
||||
snd_pcm_prepare(handle);
|
||||
return 0;
|
||||
}
|
||||
if (result < 0)
|
||||
{
|
||||
result = snd_pcm_recover(handle, result, 0);
|
||||
if (result < 0)
|
||||
{
|
||||
std::cerr << "AudioInput read failed: " << snd_strerror(result) << std::endl;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
return result * static_cast<int>(channels);
|
||||
#else
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
float AudioInput::read_volume(int sampleCount)
|
||||
{
|
||||
if (sampleCount <= 0)
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
std::vector<int16_t> samples(static_cast<size_t>(sampleCount), 0);
|
||||
const int count = read_samples(samples.data(), sampleCount);
|
||||
if (count <= 0)
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
double sum = 0.0;
|
||||
for (int i = 0; i < count; ++i)
|
||||
{
|
||||
const double value = static_cast<double>(samples[i]) / 32768.0;
|
||||
sum += value * value;
|
||||
}
|
||||
|
||||
return static_cast<float>(std::sqrt(sum / count));
|
||||
}
|
||||
|
||||
void AudioInput::shutdown()
|
||||
{
|
||||
#if defined(__linux__)
|
||||
if (handle != nullptr)
|
||||
{
|
||||
snd_pcm_close(handle);
|
||||
handle = nullptr;
|
||||
}
|
||||
#endif
|
||||
opened = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#if defined(__linux__)
|
||||
typedef struct _snd_pcm snd_pcm_t;
|
||||
#endif
|
||||
|
||||
namespace Game
|
||||
{
|
||||
class AudioInput
|
||||
{
|
||||
private:
|
||||
std::string deviceName;
|
||||
uint32_t sampleRate;
|
||||
uint32_t channels;
|
||||
bool opened;
|
||||
#if defined(__linux__)
|
||||
snd_pcm_t* handle;
|
||||
#endif
|
||||
|
||||
public:
|
||||
AudioInput();
|
||||
~AudioInput();
|
||||
|
||||
bool init(
|
||||
const std::string& deviceName = "default",
|
||||
uint32_t sampleRate = 16000,
|
||||
uint32_t channels = 1);
|
||||
int read_samples(int16_t* buffer, int sampleCount);
|
||||
float read_volume(int sampleCount = 512);
|
||||
void shutdown();
|
||||
|
||||
bool is_open() const { return opened; }
|
||||
uint32_t get_sample_rate() const { return sampleRate; }
|
||||
uint32_t get_channels() const { return channels; }
|
||||
const std::string& get_device_name() const { return deviceName; }
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
#include "AudioOutput.h"
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <vector>
|
||||
|
||||
#if defined(__linux__)
|
||||
#include <alsa/asoundlib.h>
|
||||
#endif
|
||||
|
||||
namespace
|
||||
{
|
||||
struct WavHeader
|
||||
{
|
||||
char riff[4];
|
||||
uint32_t fileSize;
|
||||
char wave[4];
|
||||
char fmt[4];
|
||||
uint32_t fmtSize;
|
||||
uint16_t audioFormat;
|
||||
uint16_t channels;
|
||||
uint32_t sampleRate;
|
||||
uint32_t byteRate;
|
||||
uint16_t blockAlign;
|
||||
uint16_t bitsPerSample;
|
||||
char data[4];
|
||||
uint32_t dataSize;
|
||||
};
|
||||
|
||||
static bool IsSupportedWav(const WavHeader& header)
|
||||
{
|
||||
return std::memcmp(header.riff, "RIFF", 4) == 0 &&
|
||||
std::memcmp(header.wave, "WAVE", 4) == 0 &&
|
||||
std::memcmp(header.fmt, "fmt ", 4) == 0 &&
|
||||
std::memcmp(header.data, "data", 4) == 0 &&
|
||||
header.audioFormat == 1 &&
|
||||
header.bitsPerSample == 16 &&
|
||||
header.fmtSize == 16;
|
||||
}
|
||||
}
|
||||
|
||||
namespace Game
|
||||
{
|
||||
AudioOutput::AudioOutput() :
|
||||
deviceName("default"),
|
||||
sampleRate(16000),
|
||||
channels(1),
|
||||
opened(false)
|
||||
#if defined(__linux__)
|
||||
,
|
||||
handle(nullptr)
|
||||
#endif
|
||||
{}
|
||||
|
||||
AudioOutput::~AudioOutput()
|
||||
{
|
||||
shutdown();
|
||||
}
|
||||
|
||||
bool AudioOutput::init(const std::string& deviceName, uint32_t sampleRate, uint32_t channels)
|
||||
{
|
||||
shutdown();
|
||||
|
||||
if (sampleRate == 0 || channels == 0)
|
||||
{
|
||||
std::cerr << "AudioOutput invalid params." << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
this->deviceName = deviceName;
|
||||
this->sampleRate = sampleRate;
|
||||
this->channels = channels;
|
||||
|
||||
#if defined(__linux__)
|
||||
int result = snd_pcm_open(&handle, deviceName.c_str(), SND_PCM_STREAM_PLAYBACK, 0);
|
||||
if (result < 0)
|
||||
{
|
||||
std::cerr << "AudioOutput open failed: " << snd_strerror(result) << std::endl;
|
||||
handle = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
result = snd_pcm_set_params(
|
||||
handle,
|
||||
SND_PCM_FORMAT_S16_LE,
|
||||
SND_PCM_ACCESS_RW_INTERLEAVED,
|
||||
channels,
|
||||
sampleRate,
|
||||
1,
|
||||
50000);
|
||||
if (result < 0)
|
||||
{
|
||||
std::cerr << "AudioOutput set params failed: " << snd_strerror(result) << std::endl;
|
||||
shutdown();
|
||||
return false;
|
||||
}
|
||||
|
||||
opened = true;
|
||||
return true;
|
||||
#else
|
||||
std::cerr << "AudioOutput is only implemented on Linux ALSA." << std::endl;
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
int AudioOutput::write_samples(const int16_t* samples, int sampleCount)
|
||||
{
|
||||
if (samples == nullptr || sampleCount <= 0 || !opened)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
#if defined(__linux__)
|
||||
const snd_pcm_uframes_t frames = static_cast<snd_pcm_uframes_t>(sampleCount / channels);
|
||||
int result = snd_pcm_writei(handle, samples, frames);
|
||||
if (result == -EPIPE)
|
||||
{
|
||||
snd_pcm_prepare(handle);
|
||||
return 0;
|
||||
}
|
||||
if (result < 0)
|
||||
{
|
||||
result = snd_pcm_recover(handle, result, 0);
|
||||
if (result < 0)
|
||||
{
|
||||
std::cerr << "AudioOutput write failed: " << snd_strerror(result) << std::endl;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
return result * static_cast<int>(channels);
|
||||
#else
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool AudioOutput::play_wav(const std::string& path)
|
||||
{
|
||||
std::ifstream file(path.c_str(), std::ios::binary);
|
||||
if (!file.good())
|
||||
{
|
||||
std::cerr << "Open wav failed: " << path << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
WavHeader header;
|
||||
file.read(reinterpret_cast<char*>(&header), sizeof(header));
|
||||
if (!file.good() || !IsSupportedWav(header))
|
||||
{
|
||||
std::cerr << "Unsupported wav: " << path << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!opened || sampleRate != header.sampleRate || channels != header.channels)
|
||||
{
|
||||
if (!init(deviceName, header.sampleRate, header.channels))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<int16_t> samples(header.dataSize / sizeof(int16_t), 0);
|
||||
file.read(reinterpret_cast<char*>(samples.data()), header.dataSize);
|
||||
if (!file.good())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t offset = 0;
|
||||
while (offset < samples.size())
|
||||
{
|
||||
const int count = static_cast<int>(std::min<size_t>(4096, samples.size() - offset));
|
||||
const int written = write_samples(samples.data() + offset, count);
|
||||
if (written <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
offset += static_cast<size_t>(written);
|
||||
}
|
||||
|
||||
#if defined(__linux__)
|
||||
if (handle != nullptr)
|
||||
{
|
||||
snd_pcm_drain(handle);
|
||||
}
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
void AudioOutput::shutdown()
|
||||
{
|
||||
#if defined(__linux__)
|
||||
if (handle != nullptr)
|
||||
{
|
||||
snd_pcm_close(handle);
|
||||
handle = nullptr;
|
||||
}
|
||||
#endif
|
||||
opened = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
#if defined(__linux__)
|
||||
typedef struct _snd_pcm snd_pcm_t;
|
||||
#endif
|
||||
|
||||
namespace Game
|
||||
{
|
||||
class AudioOutput
|
||||
{
|
||||
private:
|
||||
std::string deviceName;
|
||||
uint32_t sampleRate;
|
||||
uint32_t channels;
|
||||
bool opened;
|
||||
#if defined(__linux__)
|
||||
snd_pcm_t* handle;
|
||||
#endif
|
||||
|
||||
public:
|
||||
AudioOutput();
|
||||
~AudioOutput();
|
||||
|
||||
bool init(
|
||||
const std::string& deviceName = "default",
|
||||
uint32_t sampleRate = 16000,
|
||||
uint32_t channels = 1);
|
||||
int write_samples(const int16_t* samples, int sampleCount);
|
||||
bool play_wav(const std::string& path);
|
||||
void shutdown();
|
||||
|
||||
bool is_open() const { return opened; }
|
||||
uint32_t get_sample_rate() const { return sampleRate; }
|
||||
uint32_t get_channels() const { return channels; }
|
||||
const std::string& get_device_name() const { return deviceName; }
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
#include "ButtonInput.h"
|
||||
#include <iostream>
|
||||
|
||||
#if defined(__linux__)
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <linux/input.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
namespace Game
|
||||
{
|
||||
ButtonInput::ButtonInput() :
|
||||
devicePath("/dev/input/event0"),
|
||||
keyCode(28),
|
||||
opened(false),
|
||||
down(false),
|
||||
pressed(false),
|
||||
released(false)
|
||||
#if defined(__linux__)
|
||||
,
|
||||
fd(-1)
|
||||
#endif
|
||||
{}
|
||||
|
||||
ButtonInput::~ButtonInput()
|
||||
{
|
||||
shutdown();
|
||||
}
|
||||
|
||||
bool ButtonInput::init(const std::string& devicePath, int keyCode)
|
||||
{
|
||||
shutdown();
|
||||
|
||||
this->devicePath = devicePath;
|
||||
this->keyCode = keyCode;
|
||||
down = false;
|
||||
pressed = false;
|
||||
released = false;
|
||||
|
||||
#if defined(__linux__)
|
||||
fd = open(devicePath.c_str(), O_RDONLY | O_NONBLOCK);
|
||||
if (fd < 0)
|
||||
{
|
||||
std::cerr << "ButtonInput open failed: " << devicePath << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
opened = true;
|
||||
return true;
|
||||
#else
|
||||
std::cerr << "ButtonInput is only implemented on Linux evdev." << std::endl;
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void ButtonInput::update()
|
||||
{
|
||||
pressed = false;
|
||||
released = false;
|
||||
|
||||
if (!opened)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
#if defined(__linux__)
|
||||
input_event event;
|
||||
while (true)
|
||||
{
|
||||
const ssize_t bytes = read(fd, &event, sizeof(event));
|
||||
if (bytes == static_cast<ssize_t>(sizeof(event)))
|
||||
{
|
||||
if (event.type != EV_KEY || event.code != keyCode)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (event.value == 1)
|
||||
{
|
||||
if (!down)
|
||||
{
|
||||
pressed = true;
|
||||
}
|
||||
down = true;
|
||||
}
|
||||
else if (event.value == 0)
|
||||
{
|
||||
if (down)
|
||||
{
|
||||
released = true;
|
||||
}
|
||||
down = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (errno != EAGAIN && errno != EWOULDBLOCK)
|
||||
{
|
||||
std::cerr << "ButtonInput read failed." << std::endl;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void ButtonInput::shutdown()
|
||||
{
|
||||
#if defined(__linux__)
|
||||
if (fd >= 0)
|
||||
{
|
||||
close(fd);
|
||||
fd = -1;
|
||||
}
|
||||
#endif
|
||||
opened = false;
|
||||
down = false;
|
||||
pressed = false;
|
||||
released = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace Game
|
||||
{
|
||||
class ButtonInput
|
||||
{
|
||||
private:
|
||||
std::string devicePath;
|
||||
int keyCode;
|
||||
bool opened;
|
||||
bool down;
|
||||
bool pressed;
|
||||
bool released;
|
||||
#if defined(__linux__)
|
||||
int fd;
|
||||
#endif
|
||||
|
||||
public:
|
||||
ButtonInput();
|
||||
~ButtonInput();
|
||||
|
||||
bool init(const std::string& devicePath = "/dev/input/event0", int keyCode = 28);
|
||||
void update();
|
||||
void shutdown();
|
||||
|
||||
bool is_open() const { return opened; }
|
||||
bool is_down() const { return down; }
|
||||
bool was_pressed() const { return pressed; }
|
||||
bool was_released() const { return released; }
|
||||
const std::string& get_device_path() const { return devicePath; }
|
||||
int get_key_code() const { return keyCode; }
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
#include "AnimationSystem.h"
|
||||
#include "SpriteRasterizer.h"
|
||||
|
||||
namespace Game
|
||||
{
|
||||
size_t AnimationSystem::add(SpriteAnimator* animator, int32_t x, int32_t y)
|
||||
{
|
||||
objects.push_back(AnimationObject(animator, x, y));
|
||||
return objects.size() - 1;
|
||||
}
|
||||
|
||||
void AnimationSystem::remove(size_t index)
|
||||
{
|
||||
if (index >= objects.size())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
objects.erase(objects.begin() + index);
|
||||
}
|
||||
|
||||
void AnimationSystem::clear()
|
||||
{
|
||||
objects.clear();
|
||||
}
|
||||
|
||||
void AnimationSystem::update(float deltaTime)
|
||||
{
|
||||
for (size_t i = 0; i < objects.size(); ++i)
|
||||
{
|
||||
if (objects[i].animator != nullptr)
|
||||
{
|
||||
objects[i].animator->update(deltaTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AnimationSystem::draw(Rasterizer::SpriteRasterizer& spriteRasterizer) const
|
||||
{
|
||||
for (size_t i = 0; i < objects.size(); ++i)
|
||||
{
|
||||
const AnimationObject& object = objects[i];
|
||||
if (!object.visible || object.animator == nullptr)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const RenderData::Sprite* sprite = object.animator->get_current_sprite();
|
||||
if (sprite != nullptr)
|
||||
{
|
||||
spriteRasterizer.DrawSprite(*sprite, object.x, object.y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AnimationSystem::set_position(size_t index, int32_t x, int32_t y)
|
||||
{
|
||||
AnimationObject* object = get_object(index);
|
||||
if (object == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
object->x = x;
|
||||
object->y = y;
|
||||
}
|
||||
|
||||
void AnimationSystem::set_visible(size_t index, bool visible)
|
||||
{
|
||||
AnimationObject* object = get_object(index);
|
||||
if (object == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
object->visible = visible;
|
||||
}
|
||||
|
||||
AnimationObject* AnimationSystem::get_object(size_t index)
|
||||
{
|
||||
if (index >= objects.size())
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return &objects[index];
|
||||
}
|
||||
|
||||
const AnimationObject* AnimationSystem::get_object(size_t index) const
|
||||
{
|
||||
if (index >= objects.size())
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return &objects[index];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
#pragma once
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
#include "../components/SpriteAnimator.h"
|
||||
|
||||
namespace Rasterizer
|
||||
{
|
||||
class SpriteRasterizer;
|
||||
}
|
||||
|
||||
namespace Game
|
||||
{
|
||||
struct AnimationObject
|
||||
{
|
||||
SpriteAnimator* animator;
|
||||
int32_t x;
|
||||
int32_t y;
|
||||
bool visible;
|
||||
|
||||
AnimationObject() : animator(nullptr), x(0), y(0), visible(true) {}
|
||||
AnimationObject(SpriteAnimator* animator, int32_t x, int32_t y) :
|
||||
animator(animator),
|
||||
x(x),
|
||||
y(y),
|
||||
visible(true)
|
||||
{}
|
||||
};
|
||||
|
||||
class AnimationSystem
|
||||
{
|
||||
private:
|
||||
std::vector<AnimationObject> objects;
|
||||
|
||||
public:
|
||||
size_t add(SpriteAnimator* animator, int32_t x, int32_t y);
|
||||
void remove(size_t index);
|
||||
void clear();
|
||||
|
||||
void update(float deltaTime);
|
||||
void draw(Rasterizer::SpriteRasterizer& spriteRasterizer) const;
|
||||
|
||||
void set_position(size_t index, int32_t x, int32_t y);
|
||||
void set_visible(size_t index, bool visible);
|
||||
|
||||
AnimationObject* get_object(size_t index);
|
||||
const AnimationObject* get_object(size_t index) const;
|
||||
size_t count() const { return objects.size(); }
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
#include <SDL.h>
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "FrameBuffer.h"
|
||||
#include "SDLDisplay.h"
|
||||
#include "SpriteAssetLoader.h"
|
||||
#include "SpriteRasterizer.h"
|
||||
#include "Image.h"
|
||||
#include "Sprite.h"
|
||||
#include "SpriteAnimator.h"
|
||||
#include "AnimationSystem.h"
|
||||
|
||||
static std::string FindAssetPath(const std::string& fileName)
|
||||
{
|
||||
const char* roots[] = {
|
||||
"src/Apps/Game/assets/sprites/",
|
||||
"../src/Apps/Game/assets/sprites/",
|
||||
"../../src/Apps/Game/assets/sprites/",
|
||||
"../../../src/Apps/Game/assets/sprites/"
|
||||
};
|
||||
|
||||
for (size_t i = 0; i < sizeof(roots) / sizeof(roots[0]); ++i)
|
||||
{
|
||||
const std::string path = std::string(roots[i]) + fileName;
|
||||
std::ifstream file(path.c_str(), std::ios::binary);
|
||||
if (file.good())
|
||||
{
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
return std::string("src/Apps/Game/assets/sprites/") + fileName;
|
||||
}
|
||||
|
||||
static bool LoadSpriteImage(const std::string& fileName, RenderData::Image& image)
|
||||
{
|
||||
const std::string path = FindAssetPath(fileName);
|
||||
if (!Asset::SpriteAssetLoader::Load(path, image))
|
||||
{
|
||||
std::cerr << "Load sprite failed: " << path << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static RenderData::Image ResizeImageNearest(const RenderData::Image& source, int32_t width, int32_t height)
|
||||
{
|
||||
if (!source.is_valid() || width <= 0 || height <= 0)
|
||||
{
|
||||
return RenderData::Image();
|
||||
}
|
||||
|
||||
RenderData::Image result(width, height);
|
||||
for (int32_t y = 0; y < height; ++y)
|
||||
{
|
||||
const int32_t sourceY = y * source.get_height() / height;
|
||||
for (int32_t x = 0; x < width; ++x)
|
||||
{
|
||||
const int32_t sourceX = x * source.get_width() / width;
|
||||
result.set_pixel(x, y, source.get_pixel_fast(sourceX, sourceY));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static RenderData::Image ResizeImageToFit(const RenderData::Image& source, int32_t maxWidth, int32_t maxHeight)
|
||||
{
|
||||
if (!source.is_valid() || maxWidth <= 0 || maxHeight <= 0)
|
||||
{
|
||||
return RenderData::Image();
|
||||
}
|
||||
|
||||
const float scaleX = static_cast<float>(maxWidth) / source.get_width();
|
||||
const float scaleY = static_cast<float>(maxHeight) / source.get_height();
|
||||
const float scale = std::min(scaleX, scaleY);
|
||||
|
||||
const int32_t width = std::max(1, static_cast<int32_t>(source.get_width() * scale));
|
||||
const int32_t height = std::max(1, static_cast<int32_t>(source.get_height() * scale));
|
||||
return ResizeImageNearest(source, width, height);
|
||||
}
|
||||
|
||||
static bool LoadFrames(
|
||||
const std::vector<std::string>& fileNames,
|
||||
std::vector<RenderData::Image>& images,
|
||||
std::vector<RenderData::Sprite>& frames,
|
||||
int32_t maxFrameWidth,
|
||||
int32_t maxFrameHeight)
|
||||
{
|
||||
images.clear();
|
||||
frames.clear();
|
||||
images.reserve(fileNames.size());
|
||||
|
||||
for (size_t i = 0; i < fileNames.size(); ++i)
|
||||
{
|
||||
RenderData::Image image;
|
||||
if (!LoadSpriteImage(fileNames[i], image))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
images.push_back(ResizeImageToFit(image, maxFrameWidth, maxFrameHeight));
|
||||
}
|
||||
|
||||
frames.reserve(images.size());
|
||||
for (size_t i = 0; i < images.size(); ++i)
|
||||
{
|
||||
frames.push_back(RenderData::Sprite(&images[i]));
|
||||
}
|
||||
|
||||
return !frames.empty();
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[])
|
||||
{
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
|
||||
const int32_t width = 800;
|
||||
const int32_t height = 480;
|
||||
|
||||
Platform::SDLDisplay display;
|
||||
if (!display.init(width, height))
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
RenderData::Image originalBackground;
|
||||
if (!LoadSpriteImage("background.sprite", originalBackground))
|
||||
{
|
||||
display.shutdown();
|
||||
return -1;
|
||||
}
|
||||
RenderData::Image background = ResizeImageNearest(originalBackground, width, height);
|
||||
|
||||
std::vector<std::string> frameFiles;
|
||||
frameFiles.push_back("Tom-stand.sprite");
|
||||
frameFiles.push_back("Tom-listhen.sprite");
|
||||
frameFiles.push_back("Tom-openmouse1.sprite");
|
||||
frameFiles.push_back("Tom-openmouse2.sprite");
|
||||
frameFiles.push_back("Tom-say1.sprite");
|
||||
frameFiles.push_back("Tom-say2.sprite");
|
||||
frameFiles.push_back("Tom-say3.sprite");
|
||||
frameFiles.push_back("Tom-say4.sprite");
|
||||
|
||||
std::vector<RenderData::Image> frameImages;
|
||||
std::vector<RenderData::Sprite> frames;
|
||||
if (!LoadFrames(frameFiles, frameImages, frames, width * 7 / 10, height * 9 / 10))
|
||||
{
|
||||
display.shutdown();
|
||||
return -1;
|
||||
}
|
||||
|
||||
Core::FrameBuffer frameBuffer(width, height);
|
||||
Rasterizer::SpriteRasterizer spriteRasterizer(&frameBuffer);
|
||||
|
||||
Game::SpriteAnimator tomAnimator(frames, 0.12f, true);
|
||||
Game::AnimationSystem animationSystem;
|
||||
|
||||
const RenderData::Sprite* firstFrame = tomAnimator.get_current_sprite();
|
||||
const int32_t tomX = firstFrame ? (width - firstFrame->width) / 2 : 0;
|
||||
const int32_t tomY = firstFrame ? height - firstFrame->height - 20 : 0;
|
||||
animationSystem.add(&tomAnimator, tomX, tomY);
|
||||
|
||||
std::cout << "Sprite animation test" << std::endl;
|
||||
std::cout << "Space: play/pause, R: reset, Esc: quit" << std::endl;
|
||||
std::cout << "Background: " << originalBackground.get_width() << "x" << originalBackground.get_height()
|
||||
<< " -> " << background.get_width() << "x" << background.get_height() << std::endl;
|
||||
std::cout << "Tom frame 0: " << frameImages[0].get_width() << "x" << frameImages[0].get_height() << std::endl;
|
||||
|
||||
bool shouldQuit = false;
|
||||
uint32_t lastTime = display.get_time_ms();
|
||||
while (!shouldQuit)
|
||||
{
|
||||
SDL_Event event;
|
||||
while (SDL_PollEvent(&event))
|
||||
{
|
||||
if (event.type == SDL_QUIT)
|
||||
{
|
||||
shouldQuit = true;
|
||||
}
|
||||
else if (event.type == SDL_KEYDOWN)
|
||||
{
|
||||
if (event.key.repeat != 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (event.key.keysym.sym == SDLK_ESCAPE)
|
||||
{
|
||||
shouldQuit = true;
|
||||
}
|
||||
else if (event.key.keysym.sym == SDLK_SPACE)
|
||||
{
|
||||
if (tomAnimator.is_playing())
|
||||
{
|
||||
tomAnimator.stop();
|
||||
}
|
||||
else
|
||||
{
|
||||
tomAnimator.play();
|
||||
}
|
||||
}
|
||||
else if (event.key.keysym.sym == SDLK_r)
|
||||
{
|
||||
tomAnimator.reset();
|
||||
tomAnimator.play();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const uint32_t currentTime = display.get_time_ms();
|
||||
const float deltaTime = static_cast<float>(currentTime - lastTime) * 0.001f;
|
||||
lastTime = currentTime;
|
||||
|
||||
animationSystem.update(deltaTime);
|
||||
|
||||
frameBuffer.clear(RenderData::Color(18, 18, 24, 255));
|
||||
spriteRasterizer.DrawImage(background, 0, 0);
|
||||
animationSystem.draw(spriteRasterizer);
|
||||
display.present(&frameBuffer);
|
||||
|
||||
SDL_Delay(16);
|
||||
}
|
||||
|
||||
display.shutdown();
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,601 @@
|
||||
#include <SDL.h>
|
||||
#include <SDL_image.h>
|
||||
#include <algorithm>
|
||||
#include <cerrno>
|
||||
#include <cctype>
|
||||
#include <climits>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include "Image.h"
|
||||
#include "SpriteAssetLoader.h"
|
||||
|
||||
#if defined(_WIN32)
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#define NOMINMAX
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <dirent.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
#endif
|
||||
|
||||
namespace
|
||||
{
|
||||
enum class TransformMode
|
||||
{
|
||||
None,
|
||||
Resize,
|
||||
Fit,
|
||||
Tom800x480Preset
|
||||
};
|
||||
|
||||
struct TransformOptions
|
||||
{
|
||||
TransformMode mode;
|
||||
int32_t width;
|
||||
int32_t height;
|
||||
|
||||
TransformOptions() : mode(TransformMode::None), width(0), height(0) {}
|
||||
};
|
||||
|
||||
struct LoadedImage
|
||||
{
|
||||
std::vector<uint32_t> pixels;
|
||||
RenderData::Image image;
|
||||
|
||||
void assign(int32_t width, int32_t height, const std::vector<uint32_t>& sourcePixels)
|
||||
{
|
||||
pixels = sourcePixels;
|
||||
image = RenderData::Image(pixels.empty() ? nullptr : pixels.data(), width, height);
|
||||
}
|
||||
|
||||
bool is_valid() const
|
||||
{
|
||||
return image.pixels != nullptr && image.width > 0 && image.height > 0;
|
||||
}
|
||||
};
|
||||
|
||||
static bool IsPathSeparator(char value)
|
||||
{
|
||||
return value == '/' || value == '\\';
|
||||
}
|
||||
|
||||
static std::string ToLower(std::string text)
|
||||
{
|
||||
std::transform(text.begin(), text.end(), text.begin(), [](char value) {
|
||||
return static_cast<char>(std::tolower(static_cast<unsigned char>(value)));
|
||||
});
|
||||
return text;
|
||||
}
|
||||
|
||||
static std::string JoinPath(const std::string& directory, const std::string& fileName)
|
||||
{
|
||||
if (directory.empty())
|
||||
{
|
||||
return fileName;
|
||||
}
|
||||
|
||||
if (IsPathSeparator(directory[directory.size() - 1]))
|
||||
{
|
||||
return directory + fileName;
|
||||
}
|
||||
|
||||
return directory + "/" + fileName;
|
||||
}
|
||||
|
||||
static std::string GetFileName(const std::string& path)
|
||||
{
|
||||
const size_t slash = path.find_last_of("/\\");
|
||||
if (slash == std::string::npos)
|
||||
{
|
||||
return path;
|
||||
}
|
||||
|
||||
return path.substr(slash + 1);
|
||||
}
|
||||
|
||||
static std::string GetParentPath(const std::string& path)
|
||||
{
|
||||
const size_t slash = path.find_last_of("/\\");
|
||||
if (slash == std::string::npos)
|
||||
{
|
||||
return std::string();
|
||||
}
|
||||
if (slash == 0)
|
||||
{
|
||||
return path.substr(0, 1);
|
||||
}
|
||||
|
||||
return path.substr(0, slash);
|
||||
}
|
||||
|
||||
static std::string ChangeExtensionToSprite(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) + Asset::SpriteAssetLoader::GetFileExtension();
|
||||
}
|
||||
|
||||
return fileName + Asset::SpriteAssetLoader::GetFileExtension();
|
||||
}
|
||||
|
||||
static bool HasPngExtension(const std::string& path)
|
||||
{
|
||||
const std::string lower = ToLower(path);
|
||||
return lower.size() >= 4 && lower.substr(lower.size() - 4) == ".png";
|
||||
}
|
||||
|
||||
static bool ParsePositiveInt(const char* text, int32_t& value)
|
||||
{
|
||||
if (text == nullptr || text[0] == '\0')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
errno = 0;
|
||||
char* end = nullptr;
|
||||
const long parsed = std::strtol(text, &end, 10);
|
||||
if (errno != 0 || end == text || *end != '\0' || parsed <= 0 || parsed > INT_MAX)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
value = static_cast<int32_t>(parsed);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool DirectoryExists(const std::string& path)
|
||||
{
|
||||
#if defined(_WIN32)
|
||||
const DWORD attrs = GetFileAttributesA(path.c_str());
|
||||
return attrs != INVALID_FILE_ATTRIBUTES && (attrs & FILE_ATTRIBUTE_DIRECTORY) != 0;
|
||||
#else
|
||||
struct stat info;
|
||||
return stat(path.c_str(), &info) == 0 && S_ISDIR(info.st_mode);
|
||||
#endif
|
||||
}
|
||||
|
||||
static bool CreateSingleDirectory(const std::string& path)
|
||||
{
|
||||
if (path.empty() || DirectoryExists(path))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
#if defined(_WIN32)
|
||||
if (CreateDirectoryA(path.c_str(), nullptr) != 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return GetLastError() == ERROR_ALREADY_EXISTS && DirectoryExists(path);
|
||||
#else
|
||||
if (mkdir(path.c_str(), 0755) == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return errno == EEXIST && DirectoryExists(path);
|
||||
#endif
|
||||
}
|
||||
|
||||
static bool EnsureDirectoryTree(const std::string& path)
|
||||
{
|
||||
if (path.empty())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string normalized = path;
|
||||
while (!normalized.empty() && IsPathSeparator(normalized[normalized.size() - 1]))
|
||||
{
|
||||
normalized.erase(normalized.size() - 1);
|
||||
}
|
||||
|
||||
if (normalized.empty())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
size_t start = 0;
|
||||
if (normalized.size() >= 2 && normalized[1] == ':')
|
||||
{
|
||||
start = 2;
|
||||
}
|
||||
if (start < normalized.size() && IsPathSeparator(normalized[start]))
|
||||
{
|
||||
++start;
|
||||
}
|
||||
|
||||
for (size_t i = start; i <= normalized.size(); ++i)
|
||||
{
|
||||
if (i != normalized.size() && !IsPathSeparator(normalized[i]))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const std::string part = normalized.substr(0, i);
|
||||
if (part.empty() || (part.size() == 2 && part[1] == ':'))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!CreateSingleDirectory(part))
|
||||
{
|
||||
std::cerr << "Create directory failed: " << part << std::endl;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return DirectoryExists(normalized);
|
||||
}
|
||||
|
||||
static bool EnsureParentDirectory(const std::string& path)
|
||||
{
|
||||
return EnsureDirectoryTree(GetParentPath(path));
|
||||
}
|
||||
|
||||
static bool ListPngFiles(const std::string& directory, std::vector<std::string>& fileNames)
|
||||
{
|
||||
fileNames.clear();
|
||||
|
||||
#if defined(_WIN32)
|
||||
const std::string searchPath = JoinPath(directory, "*");
|
||||
WIN32_FIND_DATAA data;
|
||||
HANDLE handle = FindFirstFileA(searchPath.c_str(), &data);
|
||||
if (handle == INVALID_HANDLE_VALUE)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
do
|
||||
{
|
||||
if ((data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0 &&
|
||||
HasPngExtension(data.cFileName))
|
||||
{
|
||||
fileNames.push_back(data.cFileName);
|
||||
}
|
||||
}
|
||||
while (FindNextFileA(handle, &data) != 0);
|
||||
|
||||
FindClose(handle);
|
||||
#else
|
||||
DIR* dir = opendir(directory.c_str());
|
||||
if (dir == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
dirent* entry = nullptr;
|
||||
while ((entry = readdir(dir)) != nullptr)
|
||||
{
|
||||
const std::string name = entry->d_name;
|
||||
if (name == "." || name == ".." || !HasPngExtension(name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
fileNames.push_back(name);
|
||||
}
|
||||
|
||||
closedir(dir);
|
||||
#endif
|
||||
|
||||
std::sort(fileNames.begin(), fileNames.end(), [](const std::string& left, const std::string& right) {
|
||||
return ToLower(left) < ToLower(right);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
static bool LoadPngImage(const std::string& path, LoadedImage& image)
|
||||
{
|
||||
SDL_Surface* loadedSurface = IMG_Load(path.c_str());
|
||||
if (loadedSurface == nullptr)
|
||||
{
|
||||
std::cerr << "IMG_Load failed: " << path << " : " << IMG_GetError() << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
SDL_Surface* rgbaSurface = SDL_ConvertSurfaceFormat(loadedSurface, SDL_PIXELFORMAT_RGBA8888, 0);
|
||||
SDL_FreeSurface(loadedSurface);
|
||||
if (rgbaSurface == nullptr)
|
||||
{
|
||||
std::cerr << "SDL_ConvertSurfaceFormat failed: " << SDL_GetError() << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<uint32_t> pixels(static_cast<size_t>(rgbaSurface->w) * static_cast<size_t>(rgbaSurface->h), 0);
|
||||
if (SDL_LockSurface(rgbaSurface) != 0)
|
||||
{
|
||||
std::cerr << "SDL_LockSurface failed: " << SDL_GetError() << std::endl;
|
||||
SDL_FreeSurface(rgbaSurface);
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int32_t y = 0; y < rgbaSurface->h; ++y)
|
||||
{
|
||||
const uint8_t* srcRow = static_cast<const uint8_t*>(rgbaSurface->pixels) + y * rgbaSurface->pitch;
|
||||
const uint32_t* srcPixels = reinterpret_cast<const uint32_t*>(srcRow);
|
||||
for (int32_t x = 0; x < rgbaSurface->w; ++x)
|
||||
{
|
||||
pixels[static_cast<size_t>(y) * rgbaSurface->w + x] = srcPixels[x];
|
||||
}
|
||||
}
|
||||
SDL_UnlockSurface(rgbaSurface);
|
||||
|
||||
image.assign(rgbaSurface->w, rgbaSurface->h, pixels);
|
||||
SDL_FreeSurface(rgbaSurface);
|
||||
return image.is_valid();
|
||||
}
|
||||
|
||||
static LoadedImage ResizeImageNearest(const LoadedImage& source, int32_t width, int32_t height)
|
||||
{
|
||||
if (!source.is_valid() || width <= 0 || height <= 0)
|
||||
{
|
||||
return LoadedImage();
|
||||
}
|
||||
|
||||
std::vector<uint32_t> pixels(static_cast<size_t>(width) * static_cast<size_t>(height), 0);
|
||||
for (int32_t y = 0; y < height; ++y)
|
||||
{
|
||||
const int32_t sourceY = y * source.image.height / height;
|
||||
for (int32_t x = 0; x < width; ++x)
|
||||
{
|
||||
const int32_t sourceX = x * source.image.width / width;
|
||||
pixels[static_cast<size_t>(y) * static_cast<size_t>(width) + static_cast<size_t>(x)] =
|
||||
source.image.pixels[static_cast<size_t>(sourceY) * static_cast<size_t>(source.image.width) + static_cast<size_t>(sourceX)];
|
||||
}
|
||||
}
|
||||
|
||||
LoadedImage result;
|
||||
result.assign(width, height, pixels);
|
||||
return result;
|
||||
}
|
||||
|
||||
static LoadedImage ResizeImageToFit(const LoadedImage& source, int32_t maxWidth, int32_t maxHeight)
|
||||
{
|
||||
if (!source.is_valid() || maxWidth <= 0 || maxHeight <= 0)
|
||||
{
|
||||
return LoadedImage();
|
||||
}
|
||||
|
||||
const float scaleX = static_cast<float>(maxWidth) / source.image.width;
|
||||
const float scaleY = static_cast<float>(maxHeight) / source.image.height;
|
||||
const float scale = std::min(scaleX, scaleY);
|
||||
|
||||
const int32_t width = std::max(1, static_cast<int32_t>(source.image.width * scale));
|
||||
const int32_t height = std::max(1, static_cast<int32_t>(source.image.height * scale));
|
||||
return ResizeImageNearest(source, width, height);
|
||||
}
|
||||
|
||||
static TransformOptions ResolveTransformForInput(
|
||||
const std::string& inputPath,
|
||||
const TransformOptions& requested)
|
||||
{
|
||||
if (requested.mode != TransformMode::Tom800x480Preset)
|
||||
{
|
||||
return requested;
|
||||
}
|
||||
|
||||
TransformOptions resolved;
|
||||
const std::string fileName = ToLower(GetFileName(inputPath));
|
||||
if (fileName == "background.png")
|
||||
{
|
||||
resolved.mode = TransformMode::Resize;
|
||||
resolved.width = 800;
|
||||
resolved.height = 480;
|
||||
}
|
||||
else if (fileName.find("tom-") == 0)
|
||||
{
|
||||
resolved.mode = TransformMode::Fit;
|
||||
resolved.width = 560;
|
||||
resolved.height = 360;
|
||||
}
|
||||
else if (fileName.find("ui-") == 0)
|
||||
{
|
||||
resolved.mode = TransformMode::Fit;
|
||||
resolved.width = 72;
|
||||
resolved.height = 72;
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
static bool ApplyTransform(const std::string& inputPath, const TransformOptions& options, LoadedImage& image)
|
||||
{
|
||||
const TransformOptions resolved = ResolveTransformForInput(inputPath, options);
|
||||
switch (resolved.mode)
|
||||
{
|
||||
case TransformMode::None:
|
||||
return image.is_valid();
|
||||
|
||||
case TransformMode::Resize:
|
||||
image = ResizeImageNearest(image, resolved.width, resolved.height);
|
||||
return image.is_valid();
|
||||
|
||||
case TransformMode::Fit:
|
||||
image = ResizeImageToFit(image, resolved.width, resolved.height);
|
||||
return image.is_valid();
|
||||
|
||||
case TransformMode::Tom800x480Preset:
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool ConvertFile(
|
||||
const std::string& inputPath,
|
||||
const std::string& outputPath,
|
||||
const TransformOptions& transformOptions)
|
||||
{
|
||||
LoadedImage image;
|
||||
if (!LoadPngImage(inputPath, image))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ApplyTransform(inputPath, transformOptions, image))
|
||||
{
|
||||
std::cerr << "Image transform failed: " << inputPath << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!EnsureParentDirectory(outputPath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Asset::SpriteAssetLoader::Save(outputPath, image.image))
|
||||
{
|
||||
std::cerr << "Save failed: " << outputPath << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
std::cout << inputPath << " -> " << outputPath
|
||||
<< " (" << image.image.width << "x" << image.image.height << ")" << std::endl;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool ConvertBatch(
|
||||
const std::string& inputDirectory,
|
||||
const std::string& outputDirectory,
|
||||
const TransformOptions& transformOptions)
|
||||
{
|
||||
std::vector<std::string> files;
|
||||
if (!ListPngFiles(inputDirectory, files) || files.empty())
|
||||
{
|
||||
std::cerr << "No PNG files found: " << inputDirectory << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!EnsureDirectoryTree(outputDirectory))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t successCount = 0;
|
||||
for (size_t i = 0; i < files.size(); ++i)
|
||||
{
|
||||
const std::string inputPath = JoinPath(inputDirectory, files[i]);
|
||||
const std::string outputPath = JoinPath(outputDirectory, ChangeExtensionToSprite(files[i]));
|
||||
if (ConvertFile(inputPath, outputPath, transformOptions))
|
||||
{
|
||||
++successCount;
|
||||
}
|
||||
}
|
||||
|
||||
std::cout << "Converted " << successCount << " / " << files.size() << " PNG files." << std::endl;
|
||||
return successCount == files.size();
|
||||
}
|
||||
|
||||
static bool ParseTransformOptions(int argc, char* argv[], int startIndex, TransformOptions& options)
|
||||
{
|
||||
if (startIndex >= argc)
|
||||
{
|
||||
options = TransformOptions();
|
||||
return true;
|
||||
}
|
||||
|
||||
const std::string mode = argv[startIndex];
|
||||
if (mode == "--resize" || mode == "--fit")
|
||||
{
|
||||
if (startIndex + 2 >= argc)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int32_t width = 0;
|
||||
int32_t height = 0;
|
||||
if (!ParsePositiveInt(argv[startIndex + 1], width) ||
|
||||
!ParsePositiveInt(argv[startIndex + 2], height) ||
|
||||
startIndex + 3 != argc)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
options.mode = mode == "--resize" ? TransformMode::Resize : TransformMode::Fit;
|
||||
options.width = width;
|
||||
options.height = height;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (mode == "--preset")
|
||||
{
|
||||
if (startIndex + 2 != argc)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string presetName = argv[startIndex + 1];
|
||||
if (presetName != "tom-800x480")
|
||||
{
|
||||
std::cerr << "Unknown preset: " << presetName << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
options.mode = TransformMode::Tom800x480Preset;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static void PrintUsage()
|
||||
{
|
||||
std::cout << "Usage:" << std::endl;
|
||||
std::cout << " SpriteAssetTool input.png output.sprite [--resize width height]" << std::endl;
|
||||
std::cout << " SpriteAssetTool input.png output.sprite [--fit maxWidth maxHeight]" << 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 src/Apps/Game/assets/raw src/Apps/Game/assets/sprites --preset tom-800x480" << std::endl;
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[])
|
||||
{
|
||||
if (argc < 3)
|
||||
{
|
||||
PrintUsage();
|
||||
return 1;
|
||||
}
|
||||
|
||||
const int imageFlags = IMG_INIT_PNG;
|
||||
if ((IMG_Init(imageFlags) & imageFlags) != imageFlags)
|
||||
{
|
||||
std::cerr << "IMG_Init failed: " << IMG_GetError() << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
TransformOptions transformOptions;
|
||||
bool ok = false;
|
||||
const std::string command = argv[1];
|
||||
if (command == "--batch")
|
||||
{
|
||||
if (argc < 4 || !ParseTransformOptions(argc, argv, 4, transformOptions))
|
||||
{
|
||||
PrintUsage();
|
||||
IMG_Quit();
|
||||
return 1;
|
||||
}
|
||||
|
||||
ok = ConvertBatch(argv[2], argv[3], transformOptions);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!ParseTransformOptions(argc, argv, 3, transformOptions))
|
||||
{
|
||||
PrintUsage();
|
||||
IMG_Quit();
|
||||
return 1;
|
||||
}
|
||||
|
||||
ok = ConvertFile(argv[1], argv[2], transformOptions);
|
||||
}
|
||||
|
||||
IMG_Quit();
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
#include "SpriteAssetLoader.h"
|
||||
#include <cstdint>
|
||||
#include <fstream>
|
||||
#include <limits>
|
||||
|
||||
namespace
|
||||
{
|
||||
static const char SpriteMagic[4] = { 'S', 'P', 'R', 'T' };
|
||||
static const size_t SpriteHeaderSize = 20;
|
||||
static const uint32_t SpriteVersion = 1;
|
||||
static const uint32_t SpriteFormatRgba8888 = 1;
|
||||
static const uint64_t MaxSpritePixels = 8192ull * 8192ull;
|
||||
|
||||
static bool ReadExact(std::ifstream& file, char* data, size_t size)
|
||||
{
|
||||
file.read(data, static_cast<std::streamsize>(size));
|
||||
return static_cast<size_t>(file.gcount()) == size;
|
||||
}
|
||||
|
||||
static bool ReadU32LE(const std::vector<uint8_t>& bytes, size_t offset, uint32_t& value)
|
||||
{
|
||||
if (offset + 4 > bytes.size())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
value =
|
||||
static_cast<uint32_t>(bytes[offset]) |
|
||||
(static_cast<uint32_t>(bytes[offset + 1]) << 8) |
|
||||
(static_cast<uint32_t>(bytes[offset + 2]) << 16) |
|
||||
(static_cast<uint32_t>(bytes[offset + 3]) << 24);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void WriteU32LE(std::ofstream& file, uint32_t value)
|
||||
{
|
||||
const char bytes[4] = {
|
||||
static_cast<char>(value & 0xFF),
|
||||
static_cast<char>((value >> 8) & 0xFF),
|
||||
static_cast<char>((value >> 16) & 0xFF),
|
||||
static_cast<char>((value >> 24) & 0xFF)
|
||||
};
|
||||
file.write(bytes, sizeof(bytes));
|
||||
}
|
||||
|
||||
static bool CheckPixelCount(uint32_t width, uint32_t height, size_t& pixelCount)
|
||||
{
|
||||
if (width == 0 || height == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint64_t total = static_cast<uint64_t>(width) * static_cast<uint64_t>(height);
|
||||
if (total > MaxSpritePixels ||
|
||||
total > static_cast<uint64_t>(std::numeric_limits<size_t>::max() / sizeof(uint32_t)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
pixelCount = static_cast<size_t>(total);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool ReadPixels(std::ifstream& file, std::vector<uint32_t>& pixels)
|
||||
{
|
||||
std::vector<uint8_t> bytes(pixels.size() * sizeof(uint32_t), 0);
|
||||
if (!ReadExact(file, reinterpret_cast<char*>(bytes.data()), bytes.size()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < pixels.size(); ++i)
|
||||
{
|
||||
uint32_t value = 0;
|
||||
if (!ReadU32LE(bytes, i * sizeof(uint32_t), value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
pixels[i] = value;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
namespace Asset
|
||||
{
|
||||
bool SpriteAssetLoader::Load(
|
||||
const std::string& path,
|
||||
std::vector<uint32_t>& pixels,
|
||||
RenderData::Image& image)
|
||||
{
|
||||
std::ifstream file(path.c_str(), std::ios::binary);
|
||||
if (!file.good())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> header(SpriteHeaderSize, 0);
|
||||
if (!ReadExact(file, reinterpret_cast<char*>(header.data()), header.size()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (header[0] != SpriteMagic[0] ||
|
||||
header[1] != SpriteMagic[1] ||
|
||||
header[2] != SpriteMagic[2] ||
|
||||
header[3] != SpriteMagic[3])
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t version = 0;
|
||||
uint32_t width = 0;
|
||||
uint32_t height = 0;
|
||||
uint32_t format = 0;
|
||||
if (!ReadU32LE(header, 4, version) ||
|
||||
!ReadU32LE(header, 8, width) ||
|
||||
!ReadU32LE(header, 12, height) ||
|
||||
!ReadU32LE(header, 16, format))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t pixelCount = 0;
|
||||
if (version != SpriteVersion ||
|
||||
format != SpriteFormatRgba8888 ||
|
||||
!CheckPixelCount(width, height, pixelCount))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<uint32_t> loadedPixels(pixelCount, 0);
|
||||
if (!ReadPixels(file, loadedPixels))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
char trailingByte = 0;
|
||||
file.read(&trailingByte, 1);
|
||||
if (file.gcount() != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
pixels.swap(loadedPixels);
|
||||
image = RenderData::Image(pixels.data(), static_cast<int32_t>(width), static_cast<int32_t>(height));
|
||||
return image.pixels != nullptr && image.width > 0 && image.height > 0;
|
||||
}
|
||||
|
||||
bool SpriteAssetLoader::Save(const std::string& path, const RenderData::Image& image)
|
||||
{
|
||||
if (image.pixels == nullptr || image.width <= 0 || image.height <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t pixelCount = 0;
|
||||
if (!CheckPixelCount(static_cast<uint32_t>(image.width), static_cast<uint32_t>(image.height), pixelCount))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
std::ofstream file(path.c_str(), std::ios::binary);
|
||||
if (!file.good())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
file.write(SpriteMagic, sizeof(SpriteMagic));
|
||||
WriteU32LE(file, SpriteVersion);
|
||||
WriteU32LE(file, static_cast<uint32_t>(image.width));
|
||||
WriteU32LE(file, static_cast<uint32_t>(image.height));
|
||||
WriteU32LE(file, SpriteFormatRgba8888);
|
||||
for (size_t i = 0; i < pixelCount; ++i)
|
||||
{
|
||||
WriteU32LE(file, image.pixels[i]);
|
||||
}
|
||||
|
||||
return file.good();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include "Image.h"
|
||||
|
||||
namespace Asset
|
||||
{
|
||||
class SpriteAssetLoader
|
||||
{
|
||||
public:
|
||||
static const char* GetFileExtension() { return ".sprite"; }
|
||||
|
||||
static bool Load(const std::string& path, std::vector<uint32_t>& pixels, RenderData::Image& image);
|
||||
static bool Save(const std::string& path, const RenderData::Image& image);
|
||||
};
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
#include "Display.h"
|
||||
#include <algorithm>
|
||||
|
||||
namespace Gfx
|
||||
namespace Core
|
||||
{
|
||||
DrawContext::DrawContext(int32_t width, int32_t height)
|
||||
{
|
||||
@@ -25,7 +25,7 @@ namespace Platform
|
||||
class IDisplay;
|
||||
}
|
||||
|
||||
namespace Gfx
|
||||
namespace Core
|
||||
{
|
||||
class DrawContext
|
||||
{
|
||||