Bioblaze / DOGEngine

0 stars 1 forks source link

Texture Atlas Generation #13

Open Bioblaze opened 2 months ago

Bioblaze commented 2 months ago

Generating a texture Atlas for a series of Textures.

#include <Magick++.h>
#include <iostream>
#include <fstream>
#include <vector>
#include <string>
#include <filesystem>
#include <optional>
#include <exception>

// Helper function to check if the file has a .png extension
bool isPng(const std::string& filename) {
    return std::filesystem::path(filename).extension() == ".png";
}

// Function to create a texture atlas from a set of images
std::optional<std::string> createTextureAtlas(const std::vector<std::string>& filenames, const std::string& outputFilename) {
    if (filenames.empty()) {
        std::cerr << "No texture files provided." << std::endl;
        return std::nullopt;
    }

    size_t rows = 2; // Modify as necessary
    size_t cols = (filenames.size() + rows - 1) / rows; // Ceil of division

    try {
        Magick::Image atlas;
        size_t width = 0, height = 0;

        // Load images and calculate the total size of the atlas
        std::vector<Magick::Image> images;
        for (const auto& file : filenames) {
            if (!std::filesystem::exists(file)) {
                std::cerr << "File not found: " << file << std::endl;
                return std::nullopt;
            }
            if (!isPng(file)) {
                throw std::runtime_error("File format not supported, PNG required: " + file);
            }
            Magick::Image img;
            img.read(file);
            width = std::max(width, img.columns());
            height = std::max(height, img.rows());
            images.push_back(std::move(img));
        }

        atlas.size(Magick::Geometry(cols * width, rows * height));
        atlas.read("xc:transparent"); // Set background to transparent

        // Composite images onto the atlas
        size_t idx = 0;
        for (size_t y = 0; y < rows; ++y) {
            for (size_t x = 0; x < cols; ++x) {
                if (idx >= images.size()) break;
                atlas.composite(images[idx++], x * width, y * height, Magick::OverCompositeOp);
            }
        }

        atlas.write(outputFilename);
        return outputFilename;
    } catch (const std::exception& e) {
        std::cerr << "Error creating texture atlas: " << e.what() << std::endl;
        return std::nullopt;
    }
}

// Function to convert the texture atlas to a header file
bool atlasToHeader(const std::string& atlasFilename, const std::string& headerFilename) {
    if (!std::filesystem::exists(atlasFilename)) {
        std::cerr << "Atlas file not found: " << atlasFilename << std::endl;
        return false;
    }

    try {
        Magick::Image atlas;
        atlas.read(atlasFilename);
        std::ofstream headerFile(headerFilename, std::ios::binary);

        if (!headerFile.is_open()) {
            std::cerr << "Unable to open header file: " << headerFilename << std::endl;
            return false;
        }

        headerFile << "#ifndef ATLAS_H\n#define ATLAS_H\n\n";
        headerFile << "unsigned char textureAtlas[] = {";

        // Write image data as a C array
        Magick::Blob blob;
        atlas.write(&blob, "RGBA");
        auto data = static_cast<const unsigned char*>(blob.data());
        for (size_t i = 0; i < blob.length(); ++i) {
            headerFile << static_cast<unsigned int>(data[i]);
            if (i < blob.length() - 1) {
                headerFile << ", ";
                if ((i + 1) % 12 == 0) headerFile << "\n";  // Break line every 12 elements for readability
            }
        }

        headerFile << "};\n";
        headerFile << "unsigned int textureAtlasSize = " << blob.length() << ";\n\n";
        headerFile << "#endif // ATLAS_H\n";

        headerFile.close();
        return true;
    } catch (const std::exception& e) {
        std::cerr << "Error writing to header file: " << e.what() << std::endl;
        return false;
    }
}
Bioblaze commented 2 months ago

Made a realization that I don't know where the texture is... within the atlas... fun fun fun.

StillGreen-san commented 2 months ago

in response to your discord question (yes i know, im a bit late, not sure if this is still relevant):

ordered roughly as encountered, things already mentioned are not repeated

comments: in general these should explain why and not what. what the code is doing should be obvious from the names of variables & functions. for functions and classes doc comments can provide additional detail about their behavior & requirements that cannot be expressed by their signature.

stringly typed paths: why not use filesystem::path everywhere? it clearly expresses what it represents through the type-system and comes with build-in path related functionality. (the value_type of path is OS dependent however)

isPng: its technically only checking the extension, a bit pedantic, but hasPngExtension would say what it is actually doing. the check on the extension does create 3 paths, with optimization this might not be an issues, and for this usage it does not matter as it is not performance critical, just wanted to mention it.

createTextureAtlas: !exists(file) & !isPng(file) are handled differently, which means that client code has to use multiple error handling strategies to check for success. just use one. Magick::Image::read looks like it can fail as well but that is not handled idx could move inside the for. why is a copy of outputFilename returned? the caller already has access to it, no need to return a copy, returning a bool would be enough.

atlasToHeader: can there only ever be 1 atlas header? if not then the header guard would need the be different per header. headerFile.close(); is redundant.

Bioblaze commented 2 months ago

in response to your discord question (yes i know, im a bit late, not sure if this is still relevant):

ordered roughly as encountered, things already mentioned are not repeated

comments: in general these should explain why and not what. what the code is doing should be obvious from the names of variables & functions. for functions and classes doc comments can provide additional detail about their behavior & requirements that cannot be expressed by their signature.

stringly typed paths: why not use filesystem::path everywhere? it clearly expresses what it represents through the type-system and comes with build-in path related functionality. _(the valuetype of path is OS dependent however)

isPng: its technically only checking the extension, a bit pedantic, but hasPngExtension would say what it is actually doing. the check on the extension does create 3 paths, with optimization this might not be an issues, and for this usage it does not matter as it is not performance critical, just wanted to mention it.

createTextureAtlas: !exists(file) & !isPng(file) are handled differently, which means that client code has to use multiple error handling strategies to check for success. just use one. Magick::Image::read looks like it can fail as well but that is not handled idx could move inside the for. why is a copy of outputFilename returned? the caller already has access to it, no need to return a copy, returning a bool would be enough.

atlasToHeader: can there only ever be 1 atlas header? if not then the header guard would need the be different per header. headerFile.close(); is redundant.

Thank you very much! I'll push v3 of the script later today, and i'll make sure I also take into account your suggestions ^_^