1
0
mirror of synced 2025-02-02 12:27:20 +01:00

Create LNMarker, SpriteSheet

Refactor Marker to use SpriteSheet
This commit is contained in:
Stepland 2020-05-08 19:08:31 +02:00
parent eab99cf7b1
commit 43f66c2888
7 changed files with 254 additions and 198 deletions

View File

@ -0,0 +1,40 @@
#include "LNMarker.hpp"
#include <fstream>
namespace Resources {
LNMarker::LNMarker(const fs::path& t_folder) :
folder(t_folder)
{
if (not fs::is_directory(folder)) {
throw std::invalid_argument(folder.string()+" is not a folder");
}
if (not fs::exists(folder/"marker.json")) {
throw std::invalid_argument("LNMarker folder ( "+folder.string()+" ) has no long.json file");
}
std::ifstream marker_json{folder/"long.json"};
nlohmann::json j;
marker_json >> j;
j.get_to(*this);
background.load_and_check(folder, size, fps, {16, 30});
outline.load_and_check(folder, size, fps, {16, 30});
highlight.load_and_check(folder, size, fps, {16, 30});
tail.load_and_check(folder, size, fps, {16, 30});
tip_appearance.load_and_check(folder, size, fps, {16, 30});
tip_begin_cycle.load_and_check(folder, size, fps, {8, 30});
tip_cycle.load_and_check(folder, size, fps, {16, 30});
}
void from_json(const nlohmann::json& j, LNMarker& m) {
j.at("name").get_to(m.name);
j.at("fps").get_to(m.fps);
j.at("size").get_to(m.size);
j.at("note").at("background").get_to(m.background);
j.at("note").at("outline").get_to(m.outline);
j.at("note").at("highlight").get_to(m.highlight);
j.at("tail").get_to(m.tail);
j.at("tip").at("appearance").get_to(m.tip_appearance);
j.at("tip").at("begin cycle").get_to(m.tip_begin_cycle);
j.at("tip").at("cycle").get_to(m.tip_cycle);
}
}

View File

@ -0,0 +1,28 @@
#pragma once
#include <ghc/filesystem.hpp>
#include <SFML/Graphics.hpp>
#include "SpriteSheet.hpp"
namespace fs = ghc::filesystem;
namespace Resources {
struct LNMarker {
LNMarker(const fs::path& folder);
fs::path folder;
std::string name;
std::size_t fps;
std::size_t size;
SpriteSheet background;
SpriteSheet outline;
SpriteSheet highlight;
SpriteSheet tail;
SpriteSheet tip_appearance;
SpriteSheet tip_begin_cycle;
SpriteSheet tip_cycle;
};
void from_json(const nlohmann::json& j, LNMarker& m);
}

View File

@ -11,174 +11,58 @@ namespace fs = ghc::filesystem;
namespace Resources {
void to_json(nlohmann::json& j, const MarkerAnimationMetadata& mam) {
j = nlohmann::json{
{"sprite_sheet", mam.sprite_sheet.string()},
{"count", mam.count},
{"columns", mam.columns},
{"rows", mam.rows}
};
}
void from_json(const nlohmann::json& j, MarkerAnimationMetadata& mam) {
mam.sprite_sheet = ghc::filesystem::path{j.at("sprite_sheet").get<std::string>()};
j.at("count").get_to(mam.count);
j.at("columns").get_to(mam.columns);
j.at("rows").get_to(mam.rows);
}
void to_json(nlohmann::json& j, const MarkerMetadata& mm) {
j = nlohmann::json{
{"name", mm.name},
{"size", mm.size},
{"fps", mm.fps},
{"approach", mm.approach},
{"miss", mm.miss},
{"poor", mm.poor},
{"good", mm.good},
{"great", mm.great},
{"perfect", mm.perfect}
};
}
void from_json(const nlohmann::json& j, MarkerMetadata& mm) {
j.at("name").get_to(mm.name);
j.at("size").get_to(mm.size);
j.at("fps").get_to(mm.fps);
j.at("approach").get_to(mm.approach);
j.at("miss").get_to(mm.miss);
j.at("poor").get_to(mm.poor);
j.at("good").get_to(mm.good);
j.at("great").get_to(mm.great);
j.at("perfect").get_to(mm.perfect);
void from_json(const nlohmann::json& j, Marker& m) {
j.at("name").get_to(m.name);
j.at("fps").get_to(m.fps);
j.at("size").get_to(m.size);
j.at("approach").get_to(m.approach);
j.at("miss").get_to(m.miss);
j.at("poor").get_to(m.poor);
j.at("good").get_to(m.good);
j.at("great").get_to(m.great);
j.at("perfect").get_to(m.perfect);
}
Marker::Marker(const fs::path& marker_folder) :
m_folder(marker_folder),
m_metadata(),
m_approach(),
m_miss(),
m_poor(),
m_good(),
m_great(),
m_perfect()
folder(marker_folder)
{
if (not fs::is_directory(m_folder)) {
throw std::invalid_argument(m_folder.string()+" is not a folder");
if (not fs::is_directory(folder)) {
throw std::invalid_argument(folder.string()+" is not a folder");
}
if (not fs::exists(m_folder/"marker.json")) {
throw std::invalid_argument("Marker folder ( "+m_folder.string()+" ) has no marker.json file");
if (not fs::exists(folder/"marker.json")) {
throw std::invalid_argument("Marker folder ( "+folder.string()+" ) has no marker.json file");
}
std::ifstream marker_json{m_folder/"marker.json"};
std::ifstream marker_json{folder/"marker.json"};
nlohmann::json j;
marker_json >> j;
j.get_to(m_metadata);
load_and_check(m_approach, m_metadata.approach);
load_and_check(m_miss, m_metadata.miss);
load_and_check(m_poor, m_metadata.poor);
load_and_check(m_good, m_metadata.good);
load_and_check(m_great, m_metadata.great);
load_and_check(m_perfect, m_metadata.perfect);
j.get_to(*this);
approach.load_and_check(folder, size, fps, {16, 30});
miss.load_and_check(folder, size, fps, {16, 30});
poor.load_and_check(folder, size, fps, {16, 30});
good.load_and_check(folder, size, fps, {16, 30});
great.load_and_check(folder, size, fps, {16, 30});
perfect.load_and_check(folder, size, fps, {16, 30});
}
void Marker::load_and_check(sf::Texture& sprite_sheet, const MarkerAnimationMetadata& metadata) {
// File Load & Check
if (not sprite_sheet.loadFromFile(m_folder/metadata.sprite_sheet)) {
throw std::runtime_error(
"Cannot open marker sprite sheet "
+(m_folder/metadata.sprite_sheet).string()
);
}
sprite_sheet.setSmooth(true);
// Sprite sheet size check
// throw if the texture size does not match what's announced by the metadata
auto sheet_size = sprite_sheet.getSize();
auto expected_size = sf::Vector2u(metadata.columns, metadata.rows) * static_cast<unsigned int>(m_metadata.size);
if (sheet_size != expected_size) {
std::stringstream ss;
ss << "Marker sprite sheet ";
ss << (m_folder/metadata.sprite_sheet).string();
ss << " should be " << expected_size.x << "×" << expected_size.y << " pixels";
ss << " but is " << sheet_size.x << "×" << sheet_size.y;
throw std::invalid_argument(ss.str());
}
// Sprite count check
// throw if the count calls for more sprites than possible according to the 'columns' and 'rows' fields
if (metadata.count > metadata.columns * metadata.rows) {
std::stringstream ss;
ss << "Metadata for marker sprite sheet ";
ss << (m_folder/metadata.sprite_sheet).string();
ss << " indicates that it holds " << metadata.count << " sprites";
ss << " when it can only hold a maximum of " << metadata.columns * metadata.rows;
ss << " according to the 'columns' and 'rows' fields";
throw std::invalid_argument(ss.str());
}
// Duration check
// We do not allow any marker animation to take longer than the jubeat standard of 16 frames at 30 fps
// For that we make sure that :
// count/fps <= 16/30
// Which is mathematically equivalent to checking that :
// count*30 <= 16*fps
// Which allows us to avoid having to cast to float
if (metadata.count*30 > 16*m_metadata.fps) {
std::stringstream ss;
ss << "Marker animation for sprite sheet ";
ss << (m_folder/metadata.sprite_sheet).string();
ss << " lasts " << metadata.count/static_cast<float>(m_metadata.fps)*1000.f << "ms";
ss << " (" << metadata.count << "f @ " << m_metadata.fps << "fps)";
ss << " which is more than the maximum of " << 16.f/30.f*1000.f << "ms";
ss << " (16f @ 30fps)";
throw std::invalid_argument(ss.str());
}
}
const sf::Texture& Marker::get_sprite_sheet_from_enum(const MarkerAnimation& state) const {
const SpriteSheet& Marker::get_sprite_sheet_from_enum(const MarkerAnimation& state) const {
switch (state) {
case MarkerAnimation::APPROACH:
return m_approach;
return approach;
break;
case MarkerAnimation::MISS:
return m_miss;
return miss;
break;
case MarkerAnimation::POOR:
return m_poor;
return poor;
break;
case MarkerAnimation::GOOD:
return m_good;
return good;
break;
case MarkerAnimation::GREAT:
return m_great;
return great;
break;
case MarkerAnimation::PERFECT:
return m_perfect;
break;
default:
throw std::runtime_error("wtf ?");
}
}
const MarkerAnimationMetadata& Marker::get_metadata_from_enum(const MarkerAnimation& state) const {
switch (state) {
case MarkerAnimation::APPROACH:
return m_metadata.approach;
break;
case MarkerAnimation::MISS:
return m_metadata.miss;
break;
case MarkerAnimation::POOR:
return m_metadata.poor;
break;
case MarkerAnimation::GOOD:
return m_metadata.good;
break;
case MarkerAnimation::GREAT:
return m_metadata.great;
break;
case MarkerAnimation::PERFECT:
return m_metadata.perfect;
return perfect;
break;
default:
throw std::runtime_error("wtf ?");
@ -191,7 +75,7 @@ namespace Resources {
}
std::optional<sf::Sprite> Marker::get_sprite(const MarkerAnimation& state, const float seconds) const {
auto raw_frame = static_cast<int>(std::floor(seconds*m_metadata.fps));
auto raw_frame = static_cast<int>(std::floor(seconds*fps));
if (raw_frame >= 0) {
if (state == MarkerAnimation::APPROACH) {
return get_sprite(MarkerAnimation::MISS, static_cast<std::size_t>(raw_frame));
@ -199,29 +83,27 @@ namespace Resources {
return get_sprite(state, static_cast<std::size_t>(raw_frame));
}
} else {
auto approach_frame_count = get_metadata_from_enum(MarkerAnimation::APPROACH).count;
return get_sprite(
MarkerAnimation::APPROACH,
static_cast<std::size_t>(raw_frame+static_cast<int>(approach_frame_count))
static_cast<std::size_t>(raw_frame+static_cast<int>(approach.count))
);
}
}
std::optional<sf::Sprite> Marker::get_sprite(const MarkerAnimation& state, const std::size_t frame) const {
auto& meta = get_metadata_from_enum(state);
if (frame >= meta.count) {
auto& sprite_sheet = get_sprite_sheet_from_enum(state);
if (frame >= sprite_sheet.count) {
return {};
} else {
auto& tex = get_sprite_sheet_from_enum(state);
sf::Sprite sprite{tex};
sf::Sprite sprite{sprite_sheet.tex};
sf::IntRect rect{
sf::Vector2i{
static_cast<int>(frame % meta.columns),
static_cast<int>(frame / meta.columns)
} * static_cast<int>(m_metadata.size),
static_cast<int>(frame % sprite_sheet.columns),
static_cast<int>(frame / sprite_sheet.columns)
} * static_cast<int>(size),
sf::Vector2i{
static_cast<int>(m_metadata.size),
static_cast<int>(m_metadata.size)
static_cast<int>(size),
static_cast<int>(size)
}
};
sprite.setTextureRect(rect);
@ -237,7 +119,7 @@ namespace Resources {
if (p.is_directory()) {
try {
Marker m{p.path()};
res.emplace(m.m_metadata.name, m);
res.emplace(m.name, m);
} catch (const std::exception& e) {
std::cerr << "Unable to load marker folder "
<< p.path().filename().string() << " : "

View File

@ -7,6 +7,10 @@
#include <nlohmann/json.hpp>
#include <SFML/Graphics.hpp>
#include "SpriteSheet.hpp"
namespace fs = ghc::filesystem;
namespace Resources {
enum class MarkerAnimation {
APPROACH,
@ -17,51 +21,27 @@ namespace Resources {
PERFECT
};
struct MarkerAnimationMetadata {
ghc::filesystem::path sprite_sheet;
std::size_t count; // how many sprites total on the sheet
std::size_t columns; // how many horizontally
std::size_t rows; // how many vertically
};
void to_json(nlohmann::json& j, const MarkerAnimationMetadata& mam);
void from_json(const nlohmann::json& j, MarkerAnimationMetadata& mam);
// Represents what's held in marker.json
struct MarkerMetadata {
std::string name;
std::size_t size; // the side length in pixels
std::size_t fps; // classic jubeat markers are 30 fps
MarkerAnimationMetadata approach;
MarkerAnimationMetadata miss;
MarkerAnimationMetadata poor;
MarkerAnimationMetadata good;
MarkerAnimationMetadata great;
MarkerAnimationMetadata perfect;
};
void to_json(nlohmann::json& j, const MarkerMetadata& mm);
void from_json(const nlohmann::json& j, MarkerMetadata& mm);
struct Marker {
explicit Marker(const ghc::filesystem::path& marker_folder);
explicit Marker(const fs::path& marker_folder);
std::optional<sf::Sprite> get_sprite(const MarkerAnimation& state, const sf::Time seconds) const;
std::optional<sf::Sprite> get_sprite(const MarkerAnimation& state, const float seconds) const;
std::optional<sf::Sprite> get_sprite(const MarkerAnimation& state, const std::size_t frame) const;
void load_and_check(sf::Texture& spritesheet, const MarkerAnimationMetadata& metadata);
const sf::Texture& get_sprite_sheet_from_enum(const MarkerAnimation& state) const;
const MarkerAnimationMetadata& get_metadata_from_enum(const MarkerAnimation& state) const;
const SpriteSheet& get_sprite_sheet_from_enum(const MarkerAnimation& state) const;
ghc::filesystem::path m_folder;
MarkerMetadata m_metadata;
sf::Texture m_approach;
sf::Texture m_miss;
sf::Texture m_poor;
sf::Texture m_good;
sf::Texture m_great;
sf::Texture m_perfect;
fs::path folder;
std::string name;
std::size_t size; // the side length in pixels
std::size_t fps; // classic jubeat markers are 30 fps
SpriteSheet approach;
SpriteSheet miss;
SpriteSheet poor;
SpriteSheet good;
SpriteSheet great;
SpriteSheet perfect;
};
void from_json(const nlohmann::json& j, Marker& m);
using Markers = std::map<std::string, Marker>;
Markers load_markers(const ghc::filesystem::path& jujube_path);
}

View File

@ -0,0 +1,67 @@
#include "SpriteSheet.hpp"
#include <sstream>
namespace Resources {
void from_json(const nlohmann::json& j, SpriteSheet& s) {
s.tex_path = fs::path{j.at("sprite_sheet").get<std::string>()};
j.at("count").get_to(s.count);
j.at("columns").get_to(s.columns);
j.at("rows").get_to(s.rows);
}
void SpriteSheet::load_and_check(const fs::path& folder, std::size_t size, std::size_t fps, const DurationInFrames& max_duration) {
// File Load & Check
if (not tex.loadFromFile(folder/tex_path)) {
throw std::runtime_error(
"Cannot open marker sprite sheet "
+(folder/tex_path).string()
);
}
tex.setSmooth(true);
// Sprite sheet size check
// throw if the texture size does not match what's announced by the metadata
auto sheet_size = tex.getSize();
auto expected_size = sf::Vector2u(columns, rows) * static_cast<unsigned int>(size);
if (sheet_size != expected_size) {
std::stringstream ss;
ss << "Marker sprite sheet ";
ss << (folder/tex_path).string();
ss << " should be " << expected_size.x << "×" << expected_size.y << " pixels";
ss << " but is " << sheet_size.x << "×" << sheet_size.y;
throw std::invalid_argument(ss.str());
}
// Sprite count check
// throw if the count calls for more sprites than possible according to the 'columns' and 'rows' fields
if (count > columns * rows) {
std::stringstream ss;
ss << "Metadata for marker sprite sheet ";
ss << (folder/tex_path).string();
ss << " indicates that it holds " << count << " sprites";
ss << " when it can only hold a maximum of " << columns * rows;
ss << " according to the 'columns' and 'rows' fields";
throw std::invalid_argument(ss.str());
}
// Duration check
// We do not allow any marker animation to take longer than the jubeat standard of 16 frames at 30 fps
// For that we make sure that :
// frames/fps <= max_frames/reference_fps
// Which is mathematically equivalent to checking that :
// count*reference_fps <= max_frames*fps
// Which allows us to avoid having to cast to float
if (count*max_duration.fps > max_duration.frames*fps) {
std::stringstream ss;
ss << "Marker animation for sprite sheet ";
ss << (folder/tex_path).string();
ss << " lasts " << count/static_cast<float>(fps)*1000.f << "ms";
ss << " (" << count << "f @ " << fps << "fps)";
ss << " which is more than the maximum of ";
ss << max_duration.frames/static_cast<float>(max_duration.fps)*1000.f << "ms";
ss << " (16f @ 30fps)";
throw std::invalid_argument(ss.str());
}
}
}

View File

@ -0,0 +1,32 @@
#pragma once
#include <ghc/filesystem.hpp>
#include <nlohmann/json.hpp>
#include <SFML/Graphics/Texture.hpp>
#include <SFML/Graphics/Sprite.hpp>
namespace fs = ghc::filesystem;
namespace Resources {
struct SpriteSheet {
sf::Texture tex;
fs::path tex_path;
std::size_t count;
std::size_t columns;
std::size_t rows;
void load_and_check(
const fs::path& folder,
std::size_t size,
std::size_t fps,
const DurationInFrames& max_duration
);
};
struct DurationInFrames {
std::size_t frames;
std::size_t fps;
};
void from_json(const nlohmann::json& j, SpriteSheet& s);
}

View File

@ -0,0 +1,27 @@
from math import sqrt, ceil
from PIL import Image
from path import Path
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("files", type=Path, nargs="+")
args = parser.parse_args()
images = [Image.open(file) for file in args.files]
assert len(set(image.size for image in images)) == 1
sprite_size = images[0].size
sheet_square_side = ceil(sqrt(len(images)))
sheet = Image.new(
'RGBA',
(
sheet_square_side*sprite_size[0],
sheet_square_side*sprite_size[1]
),
(0,0,0,0)
)
for index, sprite in enumerate(images):
x = (index % sheet_square_side)*sprite_size[0]
y = (index // sheet_square_side)*sprite_size[1]
sheet.paste(sprite, (x,y))
sheet.save("sheet.png")