diff --git a/SConstruct b/SConstruct index f43033a..ac7f8f1 100644 --- a/SConstruct +++ b/SConstruct @@ -5,6 +5,9 @@ config = { env = SConscript('external/scons-plus-plus/SConscript', exports = ['config']) env.Append(CPPPATH = [Dir('private'), Dir('public')]) +# texture packer +env = env.Module('private/texture_packer/SModule') + # app env = env.Module('private/sdl_gpu_test/SModule') diff --git a/assets/bitmaps/.gitignore b/assets/bitmaps/.gitignore new file mode 100644 index 0000000..5c15db0 --- /dev/null +++ b/assets/bitmaps/.gitignore @@ -0,0 +1,3 @@ +# these are auto-generated +ui.png +ui.txt diff --git a/assets/bitmaps/ui.png b/assets/fonts/symtex.png similarity index 100% rename from assets/bitmaps/ui.png rename to assets/fonts/symtex.png diff --git a/external/scons-plus-plus b/external/scons-plus-plus index b45fec7..a7c736d 160000 --- a/external/scons-plus-plus +++ b/external/scons-plus-plus @@ -1 +1 @@ -Subproject commit b45fec75610a05ecf1e01b6b27807e20be5cd94b +Subproject commit a7c736de56b438d2e47b074fc1b06d05cd7a0c35 diff --git a/private/sdl_gpu_test/SModule b/private/sdl_gpu_test/SModule index 4ac2e07..e20374b 100644 --- a/private/sdl_gpu_test/SModule +++ b/private/sdl_gpu_test/SModule @@ -11,6 +11,7 @@ src_files = Split(""" util/bitmap.cpp util/font_map.cpp util/mesh.cpp + util/texture_atlas.cpp 0_clear_swapchain/app.cpp 1_green_triangle/app.cpp @@ -48,4 +49,16 @@ for shader_file in shader_files: spv = env.SpirV(source = shader_file, target=f'{shader_file}.spv') env.Default(spv) +ui_textures = Split(""" + #assets/fonts/symtex.png + #assets/bitmaps/cube.png + #assets/bitmaps/sdl.png +""") + +pack = env.PackTextures( + target = '#assets/bitmaps/ui.png', + source = ui_textures +) +env.Default(pack) + Return('env') diff --git a/private/sdl_gpu_test/gui/ui_renderer.cpp b/private/sdl_gpu_test/gui/ui_renderer.cpp index 3df81fe..1a4ec93 100644 --- a/private/sdl_gpu_test/gui/ui_renderer.cpp +++ b/private/sdl_gpu_test/gui/ui_renderer.cpp @@ -84,14 +84,6 @@ void UIRenderer::init(Application& application) } }); - // load the font map for rendering text - const FontMap fontMap = loadFontMap(application.getFileSystem().getPath("fonts/symtext.fnt")); - mFontMap = makeUVFontMap({ - .original = &fontMap, - .textureWidth = 256, - .textureHeight = 256 - }); - // create texture and sampler sdlpp::GPUTextureCreateArgs textureArgs = { .format = sdlpp::GPUTextureFormat::R8G8B8A8_UNORM_SRGB, @@ -99,6 +91,30 @@ void UIRenderer::init(Application& application) }; mTexture = application.loadTexture("bitmaps/ui.png", textureArgs); + // load the texture atlas + const TextureAtlas textureAtlas = loadTextureAtlas(application.getFileSystem().getPath("bitmaps/ui.txt")); + mTextureAtlas = makeUVTextureAtlas({ + .original = &textureAtlas, + .textureWidth = textureArgs.width, + .textureHeight = textureArgs.height + }); + + auto itEntry = mTextureAtlas.entries.find("assets/fonts/symtex.png"); + if (itEntry == mTextureAtlas.entries.end()) + { + throw std::runtime_error("Texture atlas is missing entry for the font."); + } + + // load the font map for rendering text + const FontMap fontMap = loadFontMap(application.getFileSystem().getPath("fonts/symtext.fnt")); + mFontMap = makeUVFontMap({ + .original = &fontMap, + .textureWidth = textureArgs.width, + .textureHeight = textureArgs.height, + .textureOffsetX = itEntry->second.uvX, + .textureOffsetY = itEntry->second.uvY + }); + mSampler.create(device, {}); } diff --git a/private/sdl_gpu_test/gui/ui_renderer.hpp b/private/sdl_gpu_test/gui/ui_renderer.hpp index 1a456f0..2f56f3c 100644 --- a/private/sdl_gpu_test/gui/ui_renderer.hpp +++ b/private/sdl_gpu_test/gui/ui_renderer.hpp @@ -9,6 +9,7 @@ #include "../application.hpp" #include "../sdlpp/gpu.hpp" #include "../util/font_map.hpp" +#include "../util/texture_atlas.hpp" namespace sdl_gpu_test::inline app6 { @@ -58,6 +59,7 @@ private: std::vector mVertices; std::vector mTriangleOwners; primitive_id_t mNextOwner = 1; + UVTextureAtlas mTextureAtlas; UVFontMap mFontMap; bool mVerticesDirty = true; std::size_t mVertexBufferSize = 0; diff --git a/private/sdl_gpu_test/main.cpp b/private/sdl_gpu_test/main.cpp index fca063a..dedd622 100644 --- a/private/sdl_gpu_test/main.cpp +++ b/private/sdl_gpu_test/main.cpp @@ -72,7 +72,7 @@ int main(int argc, char* argv[]) } catch (std::runtime_error& error) { - spdlog::error("{}.", error.what()); + spdlog::error("{}", error.what()); return USER_ERROR; } diff --git a/private/sdl_gpu_test/util/font_map.cpp b/private/sdl_gpu_test/util/font_map.cpp index b9fa919..01ccda0 100644 --- a/private/sdl_gpu_test/util/font_map.cpp +++ b/private/sdl_gpu_test/util/font_map.cpp @@ -75,8 +75,8 @@ UVFontMap makeUVFontMap(const MakeUVFontMapArgs& args) { const FontMapEntry& origEntry = args.original->entries[chr]; result.entries[chr] = { - .uvX = static_cast(origEntry.x + args.textureOffsetX) / static_cast(args.textureWidth), - .uvY = static_cast(origEntry.y + args.textureOffsetX) / static_cast(args.textureHeight), + .uvX = static_cast(origEntry.x) / static_cast(args.textureWidth) + args.textureOffsetX, + .uvY = static_cast(origEntry.y) / static_cast(args.textureHeight) + args.textureOffsetY, .uvWidth = static_cast(origEntry.width) / static_cast(args.textureWidth), .uvHeight = static_cast(origEntry.height) / static_cast(args.textureHeight), .width = origEntry.width, diff --git a/private/sdl_gpu_test/util/font_map.hpp b/private/sdl_gpu_test/util/font_map.hpp index aa95120..8f5dc1d 100644 --- a/private/sdl_gpu_test/util/font_map.hpp +++ b/private/sdl_gpu_test/util/font_map.hpp @@ -53,8 +53,8 @@ struct MakeUVFontMapArgs const FontMap* original; unsigned textureWidth; unsigned textureHeight; - unsigned textureOffsetX = 0; - unsigned textureOffsetY = 0; + float textureOffsetX = 0.f; + float textureOffsetY = 0.f; }; [[nodiscard]] diff --git a/private/sdl_gpu_test/util/texture_atlas.cpp b/private/sdl_gpu_test/util/texture_atlas.cpp new file mode 100644 index 0000000..5f58973 --- /dev/null +++ b/private/sdl_gpu_test/util/texture_atlas.cpp @@ -0,0 +1,53 @@ + +#include "./texture_atlas.hpp" + +#include + +namespace sdl_gpu_test +{ +TextureAtlas loadTextureAtlas(const mijin::PathReference& path) +{ + std::unique_ptr stream; + mijin::throwOnError(path.open(mijin::FileOpenMode::READ, stream)); + + auto parseNumber = [](std::string_view value, auto& outNumber) + { + if (!mijin::toNumber(value, outNumber)) + { + throw std::runtime_error("Invalid number."); + } + }; + + TextureAtlas result; + std::string line; + while (!stream->isAtEnd()) + { + mijin::throwOnError(stream->readLine(line)); + const auto [name, x, y, width, height] = mijin::splitFixed<5>(line, " "); + TextureAtlasEntry entry; + parseNumber(x, entry.x); + parseNumber(y, entry.y); + parseNumber(width, entry.width); + parseNumber(height, entry.height); + result.entries.emplace(name, entry); + } + return result; +} + +UVTextureAtlas makeUVTextureAtlas(const MakeUVTextureAtlasArgs& args) +{ + UVTextureAtlas result; + for (const auto& [name, origEntry] : args.original->entries) + { + result.entries.emplace(name, UVTextureAtlasEntry{ + .uvX = static_cast(origEntry.x) / static_cast(args.textureWidth), + .uvY = static_cast(origEntry.y) / static_cast(args.textureHeight), + .uvWidth = static_cast(origEntry.width) / static_cast(args.textureWidth), + .uvHeight = static_cast(origEntry.height) / static_cast(args.textureHeight), + .width = origEntry.width, + .height = origEntry.height + }); + } + return result; +} +} diff --git a/private/sdl_gpu_test/util/texture_atlas.hpp b/private/sdl_gpu_test/util/texture_atlas.hpp index eaa963b..dc8719d 100644 --- a/private/sdl_gpu_test/util/texture_atlas.hpp +++ b/private/sdl_gpu_test/util/texture_atlas.hpp @@ -4,11 +4,53 @@ #if !defined(SDL_GPU_TEST_PRIVATE_SDL_GPU_TEST_UTIL_TEXTURE_ATLAS_HPP_INCLUDED) #define SDL_GPU_TEST_PRIVATE_SDL_GPU_TEST_UTIL_TEXTURE_ATLAS_HPP_INCLUDED 1 -#include +#include +#include + +#include namespace sdl_gpu_test { +struct TextureAtlasEntry +{ + unsigned x = 0; + unsigned y = 0; + unsigned width = 0; + unsigned height = 0; +}; +struct TextureAtlas +{ + std::unordered_map entries; +}; + +struct UVTextureAtlasEntry +{ + float uvX = 0.f; + float uvY = 0.f; + float uvWidth = 0.f; + float uvHeight = 0.f; + unsigned width = 0; + unsigned height = 0; +}; + +struct UVTextureAtlas +{ + std::unordered_map entries; +}; + +struct MakeUVTextureAtlasArgs +{ + const TextureAtlas* original; + unsigned textureWidth; + unsigned textureHeight; +}; + +[[nodiscard]] +TextureAtlas loadTextureAtlas(const mijin::PathReference& path); + +[[nodiscard]] +UVTextureAtlas makeUVTextureAtlas(const MakeUVTextureAtlasArgs& args); } // namespace sdl_gpu_test #endif // !defined(SDL_GPU_TEST_PRIVATE_SDL_GPU_TEST_UTIL_TEXTURE_ATLAS_HPP_INCLUDED) diff --git a/private/texture_packer/SModule b/private/texture_packer/SModule new file mode 100644 index 0000000..a8e1a59 --- /dev/null +++ b/private/texture_packer/SModule @@ -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') diff --git a/private/texture_packer/main.cpp b/private/texture_packer/main.cpp new file mode 100644 index 0000000..360b486 --- /dev/null +++ b/private/texture_packer/main.cpp @@ -0,0 +1,54 @@ + +#include +#include + +#include +#include +#include + +#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 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; +} diff --git a/private/texture_packer/packer.cpp b/private/texture_packer/packer.cpp new file mode 100644 index 0000000..8a3947a --- /dev/null +++ b/private/texture_packer/packer.cpp @@ -0,0 +1,120 @@ + +#include "./packer.hpp" + +#include +#include +#include + +#define STBI_ASSERT(x) MIJIN_ASSERT(x, #x) +#define STB_IMAGE_IMPLEMENTATION +#include +#undef STB_IMAGE_IMPLEMENTATION +#undef STBI_ASSERT + +#define STBIW_ASSERT(x) MIJIN_ASSERT(x, #x) +#define STB_IMAGE_WRITE_IMPLEMENTATION +#include +#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(reinterpret_cast(pixels)), + .width = static_cast(width), + .height = static_cast(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; + using rect_type_t = rectpack2D::output_rect_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 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( + rectangles, + rectpack2D::make_finder_input( + maxSide, discardStep, reportSuccessful, reportUnsuccessful, flippingOption + ) + ); + + std::vector 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(); +} +} diff --git a/private/texture_packer/packer.hpp b/private/texture_packer/packer.hpp new file mode 100644 index 0000000..629f5ce --- /dev/null +++ b/private/texture_packer/packer.hpp @@ -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 +#include + +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 data; + unsigned width; + unsigned height; + }; + + std::vector 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)