Added texture packer for automatically creating texture atlas on build.
This commit is contained in:
40
private/texture_packer/SModule
Normal file
40
private/texture_packer/SModule
Normal file
@@ -0,0 +1,40 @@
|
||||
|
||||
import os
|
||||
|
||||
Import('env')
|
||||
|
||||
def _exe_name() -> str:
|
||||
if os.name == 'nt':
|
||||
return env['BIN_DIR'] + '/texture_packer.exe'
|
||||
return env['BIN_DIR'] + '/texture_packer'
|
||||
|
||||
src_files = Split("""
|
||||
main.cpp
|
||||
|
||||
packer.cpp
|
||||
""")
|
||||
|
||||
prog_packer = env.UnityProgram(
|
||||
target = env['BIN_DIR'] + '/texture_packer',
|
||||
source = src_files,
|
||||
dependencies = {
|
||||
'argparse': {},
|
||||
'mijin': {},
|
||||
'rectpack2D': {},
|
||||
'spdlog': {},
|
||||
'stb': {}
|
||||
}
|
||||
)
|
||||
env.Default(prog_packer)
|
||||
|
||||
def _pack_textures(env, target: str, source: 'list[str]'):
|
||||
cmd = env.Command(
|
||||
target = target,
|
||||
source = source,
|
||||
action = f'{_exe_name()} $TARGET $SOURCES'
|
||||
)
|
||||
# env.Depends(cmd, prog_packer)
|
||||
return cmd
|
||||
env.AddMethod(_pack_textures, 'PackTextures')
|
||||
|
||||
Return('env')
|
||||
54
private/texture_packer/main.cpp
Normal file
54
private/texture_packer/main.cpp
Normal file
@@ -0,0 +1,54 @@
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <argparse/argparse.hpp>
|
||||
#include <spdlog/spdlog.h>
|
||||
#include <mijin/util/winundef.hpp>
|
||||
|
||||
#include "./packer.hpp"
|
||||
|
||||
namespace
|
||||
{
|
||||
inline constexpr int APP_ERROR = 1;
|
||||
inline constexpr int USER_ERROR = 2;
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[])
|
||||
{
|
||||
std::string outputFile;
|
||||
std::vector<std::string> inputFiles;
|
||||
argparse::ArgumentParser parser("texture_packer");
|
||||
parser.add_argument("output_file")
|
||||
.store_into(outputFile);
|
||||
parser.add_argument("input_file")
|
||||
.store_into(inputFiles)
|
||||
.nargs(argparse::nargs_pattern::at_least_one);
|
||||
// now parse
|
||||
try
|
||||
{
|
||||
parser.parse_args(argc, argv);
|
||||
}
|
||||
catch (std::runtime_error& error)
|
||||
{
|
||||
spdlog::error("{}", error.what());
|
||||
return USER_ERROR;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
sdl_gpu_test::Packer packer;
|
||||
for (const std::string& inputFile : inputFiles)
|
||||
{
|
||||
packer.addImage(inputFile);
|
||||
}
|
||||
packer.pack(outputFile);
|
||||
}
|
||||
catch(const std::runtime_error& error)
|
||||
{
|
||||
spdlog::error("{}", error.what());
|
||||
return USER_ERROR;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
120
private/texture_packer/packer.cpp
Normal file
120
private/texture_packer/packer.cpp
Normal file
@@ -0,0 +1,120 @@
|
||||
|
||||
#include "./packer.hpp"
|
||||
|
||||
#include <finders_interface.h>
|
||||
#include <mijin/io/stream.hpp>
|
||||
#include <mijin/util/iterators.hpp>
|
||||
|
||||
#define STBI_ASSERT(x) MIJIN_ASSERT(x, #x)
|
||||
#define STB_IMAGE_IMPLEMENTATION
|
||||
#include <stb_image.h>
|
||||
#undef STB_IMAGE_IMPLEMENTATION
|
||||
#undef STBI_ASSERT
|
||||
|
||||
#define STBIW_ASSERT(x) MIJIN_ASSERT(x, #x)
|
||||
#define STB_IMAGE_WRITE_IMPLEMENTATION
|
||||
#include <stb_image_write.h>
|
||||
#undef STB_IMAGE_WRITE_IMPLEMENTATION
|
||||
#undef STBIW_ASSERT
|
||||
|
||||
namespace sdl_gpu_test
|
||||
{
|
||||
void Packer::addImage(const fs::path& path)
|
||||
{
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
int numComponents = 0;
|
||||
stbi_uc* pixels = stbi_load(
|
||||
/* filename = */ path.generic_string().c_str(),
|
||||
/* x = */ &width,
|
||||
/* y = */ &height,
|
||||
/* channels_in_file = */ &numComponents,
|
||||
/* desired_channels = */ 4
|
||||
);
|
||||
if (pixels == nullptr)
|
||||
{
|
||||
throw std::runtime_error("Error loading image.");
|
||||
}
|
||||
|
||||
mImages.push_back({
|
||||
.name = path.generic_string(),
|
||||
.data = std::unique_ptr<Pixel[], FreeDeleter>(reinterpret_cast<Pixel*>(pixels)),
|
||||
.width = static_cast<unsigned>(width),
|
||||
.height = static_cast<unsigned>(height)
|
||||
});
|
||||
}
|
||||
|
||||
void Packer::pack(const fs::path& outputPath)
|
||||
{
|
||||
static constexpr bool allowFlip = false; // TODO: this could work?
|
||||
static constexpr rectpack2D::flipping_option flippingOption = rectpack2D::flipping_option::DISABLED;
|
||||
using spaces_type_t = rectpack2D::empty_spaces<allowFlip, rectpack2D::default_empty_spaces>;
|
||||
using rect_type_t = rectpack2D::output_rect_t<spaces_type_t>;
|
||||
|
||||
auto reportSuccessful = [&](rect_type_t&)
|
||||
{
|
||||
return rectpack2D::callback_result::CONTINUE_PACKING;
|
||||
};
|
||||
|
||||
auto reportUnsuccessful = [&](rect_type_t&)-> rectpack2D::callback_result
|
||||
{
|
||||
throw std::runtime_error("Could not fit images.");
|
||||
};
|
||||
|
||||
static constexpr int maxSide = 4096;
|
||||
static constexpr int discardStep = -4;
|
||||
|
||||
std::vector<rect_type_t> rectangles;
|
||||
rectangles.reserve(mImages.size());
|
||||
|
||||
for (const ImageData& imageData : mImages)
|
||||
{
|
||||
rectangles.emplace_back(0, 0, imageData.width, imageData.height);
|
||||
}
|
||||
|
||||
const rectpack2D::rect_wh resultSize = rectpack2D::find_best_packing<spaces_type_t>(
|
||||
rectangles,
|
||||
rectpack2D::make_finder_input(
|
||||
maxSide, discardStep, reportSuccessful, reportUnsuccessful, flippingOption
|
||||
)
|
||||
);
|
||||
|
||||
std::vector<Pixel> resultPixels;
|
||||
resultPixels.resize(resultSize.area());
|
||||
|
||||
for (const auto& [imageData, rect] : mijin::zip(mImages, rectangles))
|
||||
{
|
||||
for (unsigned row = 0; row < imageData.height; ++row)
|
||||
{
|
||||
const Pixel* src = &imageData.data[row * imageData.width];
|
||||
Pixel* dst = &resultPixels[(rect.y + row) * resultSize.w + rect.x];
|
||||
std::copy_n(src, imageData.width, dst);
|
||||
}
|
||||
}
|
||||
|
||||
const int writeResult = stbi_write_png(
|
||||
/* filename = */ outputPath.generic_string().c_str(),
|
||||
/* x = */ resultSize.w,
|
||||
/* y = */ resultSize.h,
|
||||
/* comp = */ 4,
|
||||
/* data = */ resultPixels.data(),
|
||||
/* stride_bytes = */ resultSize.w * sizeof(Pixel)
|
||||
);
|
||||
|
||||
if (!writeResult)
|
||||
{
|
||||
throw std::runtime_error("Error writing result image.");
|
||||
}
|
||||
|
||||
fs::path atlasPath(outputPath);
|
||||
atlasPath.replace_extension("txt");
|
||||
mijin::FileStream fileStream;
|
||||
mijin::throwOnError(fileStream.open(atlasPath.generic_string(), mijin::FileOpenMode::WRITE));
|
||||
for (const auto& [imageData, rect] : mijin::zip(mImages, rectangles))
|
||||
{
|
||||
mijin::throwOnError(fileStream.writeText(std::format("{} {} {} {} {}\n",
|
||||
imageData.name, rect.x, rect.y, rect.w, rect.h)));
|
||||
}
|
||||
fileStream.close();
|
||||
}
|
||||
}
|
||||
47
private/texture_packer/packer.hpp
Normal file
47
private/texture_packer/packer.hpp
Normal file
@@ -0,0 +1,47 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#if !defined(SDL_GPU_TEST_PRIVATE_TEXTURE_PACKER_PACKER_HPP_INCLUDED)
|
||||
#define SDL_GPU_TEST_PRIVATE_TEXTURE_PACKER_PACKER_HPP_INCLUDED 1
|
||||
|
||||
#include <filesystem>
|
||||
#include <vector>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace sdl_gpu_test
|
||||
{
|
||||
struct FreeDeleter
|
||||
{
|
||||
void operator()(void* ptr) noexcept
|
||||
{
|
||||
std::free(ptr);
|
||||
}
|
||||
};
|
||||
|
||||
class Packer
|
||||
{
|
||||
private:
|
||||
struct Pixel
|
||||
{
|
||||
std::uint8_t r;
|
||||
std::uint8_t g;
|
||||
std::uint8_t b;
|
||||
std::uint8_t a;
|
||||
};
|
||||
struct ImageData
|
||||
{
|
||||
std::string name;
|
||||
std::unique_ptr<Pixel[], FreeDeleter> data;
|
||||
unsigned width;
|
||||
unsigned height;
|
||||
};
|
||||
|
||||
std::vector<ImageData> mImages;
|
||||
public:
|
||||
void addImage(const fs::path& path);
|
||||
void pack(const fs::path& outputPath);
|
||||
};
|
||||
} // namespace sdl_gpu_test
|
||||
|
||||
#endif // !defined(SDL_GPU_TEST_PRIVATE_TEXTURE_PACKER_PACKER_HPP_INCLUDED)
|
||||
Reference in New Issue
Block a user