summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorStanislaw Halik <sthalik@misaki.pl>2022-10-01 02:14:39 +0200
committerStanislaw Halik <sthalik@misaki.pl>2022-10-01 02:14:39 +0200
commit509ecb59ef4de77e8d5e53014fd52fdcf0919d88 (patch)
treea267a829a3ad928e1af0f9fac2753a9bb1286d7d
parent5ee36284ab2f2d85679f83ad7680a741bf7f6702 (diff)
.
-rw-r--r--anim-crop-tool/main.cpp280
-rw-r--r--big-atlas-tool/CMakeLists.txt10
-rw-r--r--big-atlas-tool/big-atlas.cpp70
-rw-r--r--big-atlas-tool/big-atlas.hpp35
-rw-r--r--big-atlas-tool/main.cpp34
5 files changed, 429 insertions, 0 deletions
diff --git a/anim-crop-tool/main.cpp b/anim-crop-tool/main.cpp
new file mode 100644
index 00000000..42d54baa
--- /dev/null
+++ b/anim-crop-tool/main.cpp
@@ -0,0 +1,280 @@
+#undef NDEBUG
+
+#include "defs.hpp"
+#include "atlas.hpp"
+#include "anim/serialize.hpp"
+
+#include <cassert>
+#include <cmath>
+#include <cstring>
+
+#include <algorithm>
+#include <utility>
+#include <tuple>
+
+#include <filesystem>
+
+#include <Corrade/Utility/Arguments.h>
+#include <Corrade/Utility/Debug.h>
+#include <Corrade/Utility/DebugStl.h>
+
+#include <opencv2/core/mat.hpp>
+#include <opencv2/imgcodecs/imgcodecs.hpp>
+#include <opencv2/imgproc/imgproc.hpp>
+
+using Corrade::Utility::Error;
+using Corrade::Utility::Debug;
+
+using std::filesystem::path;
+
+struct options
+{
+ double scale = 0;
+ path input_dir, input_file, output_dir;
+ int width = 0, height = 0, nframes = 0;
+};
+
+[[nodiscard]]
+static std::tuple<cv::Vec2i, cv::Vec2i, bool> find_image_bounds(const cv::Mat4b& mat) noexcept
+{
+ cv::Vec2i start{mat.cols, mat.rows}, end{0, 0};
+ for (int y = 0; y < mat.rows; y++)
+ {
+ const auto* ptr = mat.ptr<cv::Vec4b>(y);
+ for (int x = 0; x < mat.cols; x++)
+ {
+ enum {R, G, B, A};
+ if (cv::Vec4b px = ptr[x]; px[A] != 0)
+ {
+ start[0] = std::min(x, start[0]);
+ start[1] = std::min(y, start[1]);
+ end[0] = std::max(x+1, end[0]);
+ end[1] = std::max(y+1, end[1]);
+ }
+ }
+ }
+ if (start[0] < end[0] && start[1] < end[1])
+ return {start, end, true};
+ else
+ return {{}, {}, false};
+}
+
+[[nodiscard]]
+static bool load_file(anim_group& group, options& opts, anim_atlas& atlas, const path& filename) noexcept
+{
+ auto mat = progn(
+ cv::Mat mat = cv::imread(filename.string(), cv::IMREAD_UNCHANGED);
+ if (mat.empty() || mat.type() != CV_8UC4)
+ {
+ Error{} << "failed to load" << filename << "as RGBA32 image";
+ return cv::Mat4b{};
+ }
+ return cv::Mat4b(std::move(mat));
+ );
+
+ if (mat.empty())
+ return false;
+
+ auto [start, end, bounds_ok] = find_image_bounds(mat);
+
+ if (!bounds_ok)
+ {
+ Error{} << "no valid image data in" << filename;
+ return false;
+ }
+
+ cv::Size size{end - start};
+
+ if (opts.scale == 0.0)
+ {
+ assert(opts.width || opts.height);
+ if (opts.width)
+ opts.scale = (double)opts.width / size.width;
+ else
+ opts.scale = (double)opts.height / size.height;
+ assert(opts.scale > 1e-6);
+ }
+
+ const cv::Size dest_size = {
+ (int)std::round(opts.scale * size.width),
+ (int)std::round(opts.scale * size.height)
+ };
+
+ if (size.width < dest_size.width || size.height < dest_size.height)
+ {
+ Error{} << "refusing to upscale image" << filename;
+ return false;
+ }
+
+ cv::Mat4b resized{size};
+ cv::resize(mat({start, size}), resized, dest_size, 0, 0, cv::INTER_LANCZOS4);
+
+ const Magnum::Vector2i ground = {
+ (int)std::round((group.ground[0] - start[0]) * opts.scale),
+ (int)std::round((group.ground[1] - start[1]) * opts.scale),
+ };
+
+ group.frames.push_back({ground, atlas.offset(), {dest_size.width, dest_size.height}});
+ atlas.add_entry({&group.frames.back(), std::move(resized)});
+ return true;
+}
+
+[[nodiscard]]
+static bool load_directory(anim_group& group, options& opts, anim_atlas& atlas) noexcept
+{
+ const auto input_dir = opts.input_dir/group.name;
+
+ if (std::error_code ec; !std::filesystem::exists(input_dir/".", ec))
+ {
+ Error{Error::Flag::NoSpace} << "can't open directory " << input_dir << ": " << ec.message();
+ return false;
+ }
+
+ int max;
+ for (max = 1; max <= 9999; max++)
+ {
+ char filename[9];
+ sprintf(filename, "%04d.png", max);
+ if (std::error_code ec; !std::filesystem::exists(input_dir/filename, ec))
+ break;
+ }
+
+ if (max == 1)
+ {
+ Error{Error::Flag::NoSpace} << "no files in directory " << input_dir << "!";
+ return false;
+ }
+
+ if (!opts.nframes)
+ opts.nframes = max-1;
+ else if (opts.nframes != max-1)
+ {
+ Error{Error::Flag::NoSpace} << "wrong frame count for direction '"
+ << group.name << "' -- " << max-1
+ << " should be " << opts.nframes;
+ return false;
+ }
+
+ group.frames.clear();
+ // atlas stores its entries through a pointer.
+ // vector::reserve() is necessary to avoid use-after-free.
+ group.frames.reserve((std::size_t)max-1);
+
+ for (int i = 1; i < max; i++)
+ {
+ char filename[9];
+ sprintf(filename, "%04d.png", i);
+ if (!load_file(group, opts, atlas, input_dir/filename))
+ return false;
+ }
+
+ atlas.advance_row();
+
+ return true;
+}
+
+static char* fix_argv0(char* argv0) noexcept
+{
+#ifdef _WIN32
+ if (auto* c = strrchr(argv0, '\\'); c && c[1])
+ {
+ if (auto* s = strrchr(c, '.'); s && !strcmp(".exe", s))
+ *s = '\0';
+ return c+1;
+ }
+#else
+ if (auto* c = strrchr(argv[0], '/'); c && c[1])
+ return c+1;
+#endif
+ return argv0;
+}
+
+using Corrade::Utility::Arguments;
+
+static std::tuple<options, Arguments, bool> parse_cmdline(int argc, const char* const* argv) noexcept
+{
+ Corrade::Utility::Arguments args{};
+ args.addOption('o', "output")
+ .addArgument("input")
+ .addOption('W', "width", "")
+ .addOption('H', "height", "");
+ args.parse(argc, argv);
+ options opts;
+ if (int w = args.value<int>("width"); w != 0)
+ opts.width = w;
+ if (int h = args.value<int>("height"); h != 0)
+ opts.height = h;
+ opts.input_file = args.value<std::string>("input");
+ opts.input_dir = opts.input_file.parent_path();
+
+ if (opts.output_dir.empty())
+ opts.output_dir = opts.input_dir;
+
+ return { std::move(opts), std::move(args), true };
+}
+
+[[nodiscard]] static int usage(const Arguments& args) noexcept
+{
+ Error{Error::Flag::NoNewlineAtTheEnd} << args.usage();
+ return EX_USAGE;
+}
+
+[[nodiscard]] static bool check_atlas_name(const std::string& str) noexcept
+{
+ constexpr auto npos = std::string::npos;
+
+ if (str.empty())
+ return false;
+ if (str[0] == '.' || str[0] == '\\' || str[0] == '/')
+ return false;
+ if (str.find('"') != npos || str.find('\'') != npos)
+ return false;
+ if (str.find("/.") != npos || str.find("\\.") != npos)
+ return false; // NOLINT(readability-simplify-boolean-expr)
+
+ return true;
+}
+
+int main(int argc, char** argv)
+{
+ argv[0] = fix_argv0(argv[0]);
+ auto [opts, args, opts_ok] = parse_cmdline(argc, argv);
+ if (!opts_ok)
+ return usage(args);
+
+ auto [anim_info, anim_ok] = anim::from_json(opts.input_file);
+
+ if (!anim_ok)
+ return EX_DATAERR;
+
+ if (!check_atlas_name(anim_info.name))
+ {
+ Error{Error::Flag::NoSpace} << "atlas name '" << anim_info.name << "' contains invalid characters";
+ return EX_DATAERR;
+ }
+
+ if (!opts.width)
+ opts.width = anim_info.width;
+ if (!opts.height)
+ opts.height = anim_info.height;
+ opts.nframes = anim_info.nframes;
+
+ if (!(opts.width ^ opts.height) || opts.width < 0 || opts.height < 0)
+ {
+ Error{} << "exactly one of --width, --height must be specified";
+ return usage(args);
+ }
+
+ anim_atlas atlas;
+
+ for (anim_group& group : anim_info.groups)
+ if (!load_directory(group, opts, atlas))
+ return EX_DATAERR;
+
+ if (!atlas.dump(opts.output_dir/(anim_info.name + ".png")))
+ return EX_CANTCREAT;
+ if (!anim_info.to_json(opts.output_dir/(anim_info.name + ".json")))
+ return EX_CANTCREAT;
+
+ return 0;
+}
diff --git a/big-atlas-tool/CMakeLists.txt b/big-atlas-tool/CMakeLists.txt
new file mode 100644
index 00000000..15b8e73f
--- /dev/null
+++ b/big-atlas-tool/CMakeLists.txt
@@ -0,0 +1,10 @@
+find_package(OpenCV QUIET REQUIRED COMPONENTS core imgcodecs imgproc)
+set(self "${PROJECT_NAME}-big-atlas-tool")
+
+include_directories(SYSTEM PRIVATE ${OpenCV_INCLUDE_DIRS})
+link_libraries(Corrade::Utility)
+link_libraries(${PROJECT_NAME}-tile)
+
+file(GLOB sources "*.cpp" CONFIGURE_ARGS)
+add_executable(${self} ${sources})
+install(TARGETS ${self} RUNTIME DESTINATION "bin")
diff --git a/big-atlas-tool/big-atlas.cpp b/big-atlas-tool/big-atlas.cpp
new file mode 100644
index 00000000..4bfe5eea
--- /dev/null
+++ b/big-atlas-tool/big-atlas.cpp
@@ -0,0 +1,70 @@
+#undef NDEBUG
+
+#include "big-atlas.hpp"
+#include <cassert>
+#include <filesystem>
+#include <Corrade/Utility/DebugStl.h>
+#include <opencv2/imgproc/imgproc.hpp>
+#include <opencv2/imgcodecs/imgcodecs.hpp>
+
+using Corrade::Utility::Error;
+
+std::vector<big_atlas_frame> big_atlas_builder::add_atlas(const std::filesystem::path& filename)
+{
+ std::vector<big_atlas_frame> ret;
+ cv::Mat mat = cv::imread(filename.string(), cv::IMREAD_UNCHANGED);
+ if (mat.empty() || (mat.type() != CV_8UC4 && mat.type() != CV_8UC3))
+ {
+ Error{} << "failed to load" << filename << "as RGBA32 image";
+ return {};
+ }
+ if (mat.type() == CV_8UC3) {
+ cv::Mat mat2;
+ cv::cvtColor(mat, mat2, cv::COLOR_RGB2RGBA);
+ mat = mat2.clone();
+ }
+
+ Error{} << "file" << filename;
+
+ assert(mat.cols % TILE_SIZE[0] == 0 && mat.rows % TILE_SIZE[1] == 0);
+
+ for (int y = 0; y + TILE_SIZE[1] <= mat.rows; y += TILE_SIZE[1])
+ for (int x = 0; x + TILE_SIZE[0] <= mat.cols; x += TILE_SIZE[0])
+ {
+ cv::Rect roi { x, y, TILE_SIZE[0], TILE_SIZE[1] };
+ auto frame = add_frame(mat(roi));
+ ret.push_back(frame);
+ }
+
+ return ret;
+}
+
+big_atlas_frame& big_atlas_builder::add_frame(const cv::Mat4b& frame)
+{
+ auto [row, xpos, ypos] = advance();
+ row.frames.push_back({ frame, { xpos, ypos } });
+ return row.frames.back();
+}
+
+std::tuple<big_atlas_row&, int, int> big_atlas_builder::advance()
+{
+ auto& row = _rows.back();
+ const int xpos_ = row.xpos;
+ row.xpos += TILE_SIZE[0];
+
+ if (row.xpos + TILE_SIZE[0] > MAX_TEXTURE_SIZE[0])
+ {
+ ypos += TILE_SIZE[1];
+ assert(ypos < MAX_TEXTURE_SIZE[1]);
+ _rows.emplace_back();
+ auto& row = _rows.back();
+ row.ypos = ypos;
+ row.xpos = 0;
+ return { row, 0, row.ypos };
+ }
+ else {
+ maxx = std::max(maxx, row.xpos);
+ maxy = row.ypos + TILE_SIZE[1];
+ return { row, xpos_, row.ypos };
+ }
+}
diff --git a/big-atlas-tool/big-atlas.hpp b/big-atlas-tool/big-atlas.hpp
new file mode 100644
index 00000000..8d3f166a
--- /dev/null
+++ b/big-atlas-tool/big-atlas.hpp
@@ -0,0 +1,35 @@
+#pragma once
+
+#include <Magnum/Magnum.h>
+#include <Magnum/Math/Vector2.h>
+#include <opencv2/core/mat.hpp>
+
+namespace std::filesystem { class path; }
+
+struct big_atlas_frame {
+ cv::Mat4b frame;
+ Magnum::Vector2i position;
+};
+
+struct big_atlas_row {
+ std::vector<big_atlas_frame> frames;
+ int xpos = 0, ypos = 0;
+};
+
+struct big_atlas_builder {
+ [[nodiscard]] std::vector<big_atlas_frame> add_atlas(const std::filesystem::path& filename);
+ big_atlas_frame& add_frame(const cv::Mat4b& frame);
+ constexpr Magnum::Vector2i size() const { return {maxy, maxx}; }
+ const std::vector<big_atlas_row>& rows() const { return _rows; }
+
+private:
+ std::tuple<big_atlas_row&, int, int> advance();
+ std::vector<big_atlas_row> _rows = {{}};
+ int ypos = 0, maxx = 0, maxy = 0;
+
+ static constexpr Magnum::Vector2i TILE_SIZE = { 100, 100 },
+ MAX_TEXTURE_SIZE = { 500, 500 };
+
+ static_assert(!!TILE_SIZE[0] && !!TILE_SIZE[1] && !!MAX_TEXTURE_SIZE[0] && !!MAX_TEXTURE_SIZE[1]);
+ static_assert(MAX_TEXTURE_SIZE[0] >= TILE_SIZE[0] && MAX_TEXTURE_SIZE[1] >= TILE_SIZE[1]);
+};
diff --git a/big-atlas-tool/main.cpp b/big-atlas-tool/main.cpp
new file mode 100644
index 00000000..edc882ad
--- /dev/null
+++ b/big-atlas-tool/main.cpp
@@ -0,0 +1,34 @@
+#include "big-atlas.hpp"
+#include "tile/serialize.hpp"
+#include <tuple>
+#include <filesystem>
+#include <Corrade/Utility/Arguments.h>
+
+using Corrade::Utility::Arguments;
+
+struct options final {
+ std::filesystem::path input_dir, output_file;
+};
+
+static std::tuple<options, Arguments, bool> parse_cmdline(int argc, const char* const* argv) noexcept
+{
+ Corrade::Utility::Arguments args{};
+ args.addOption('o', "output")
+ .addArrayArgument("input");
+ args.parse(argc, argv);
+ options opts;
+ opts.input_dir = args.value<std::string>("input");
+
+ if (opts.input_dir.empty())
+ opts.output_file = opts.input_dir.parent_path() / "big-atlas.json";
+
+ return { std::move(opts), std::move(args), true };
+}
+
+int main(int argc, char** argv)
+{
+ big_atlas_builder builder;
+ builder.add_atlas("images/metal1.png");
+ builder.add_atlas("images/metal2.png");
+ return 0;
+}