mirror of
				https://github.com/ciphervance/supercell-wx.git
				synced 2025-10-31 16:20:05 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			548 lines
		
	
	
	
		
			16 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			548 lines
		
	
	
	
		
			16 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
| #include <scwx/qt/util/texture_atlas.hpp>
 | |
| #include <scwx/qt/util/streams.hpp>
 | |
| #include <scwx/network/cpr.hpp>
 | |
| #include <scwx/util/logger.hpp>
 | |
| 
 | |
| #include <execution>
 | |
| #include <shared_mutex>
 | |
| #include <unordered_map>
 | |
| 
 | |
| #if defined(_MSC_VER)
 | |
| #   pragma warning(push, 0)
 | |
| #   pragma warning(disable : 4702)
 | |
| #   pragma warning(disable : 4714)
 | |
| #endif
 | |
| 
 | |
| #include <boost/gil/extension/io/png.hpp>
 | |
| #include <boost/iostreams/stream.hpp>
 | |
| #include <boost/timer/timer.hpp>
 | |
| #include <cpr/cpr.h>
 | |
| #include <stb_image.h>
 | |
| #include <stb_rect_pack.h>
 | |
| #include <QFile>
 | |
| #include <QFileInfo>
 | |
| #include <QPainter>
 | |
| #include <QSvgRenderer>
 | |
| #include <QUrl>
 | |
| 
 | |
| #if defined(_MSC_VER)
 | |
| #   pragma warning(pop)
 | |
| #endif
 | |
| 
 | |
| #if defined(LoadImage)
 | |
| #   undef LoadImage
 | |
| #endif
 | |
| 
 | |
| namespace scwx
 | |
| {
 | |
| namespace qt
 | |
| {
 | |
| namespace util
 | |
| {
 | |
| 
 | |
| static const std::string logPrefix_ = "scwx::qt::util::texture_atlas";
 | |
| static const auto        logger_    = scwx::util::Logger::Create(logPrefix_);
 | |
| 
 | |
| class TextureAtlas::Impl
 | |
| {
 | |
| public:
 | |
|    explicit Impl() {}
 | |
|    ~Impl() {}
 | |
| 
 | |
|    static std::shared_ptr<boost::gil::rgba8_image_t>
 | |
|    LoadImage(const std::string& imagePath, double scale = 1);
 | |
| 
 | |
|    static std::shared_ptr<boost::gil::rgba8_image_t>
 | |
|    ReadPngFile(const QString& imagePath);
 | |
|    static std::shared_ptr<boost::gil::rgba8_image_t>
 | |
|    ReadSvgFile(const QString& imagePath, double scale = 1);
 | |
| 
 | |
|    std::vector<std::shared_ptr<boost::gil::rgba8_image_t>>
 | |
|                      registeredTextures_ {};
 | |
|    std::shared_mutex registeredTextureMutex_ {};
 | |
| 
 | |
|    std::shared_mutex textureCacheMutex_ {};
 | |
|    std::unordered_map<std::string, std::weak_ptr<boost::gil::rgba8_image_t>>
 | |
|       textureCache_ {};
 | |
| 
 | |
|    std::vector<boost::gil::rgba8_image_t>             atlasArray_ {};
 | |
|    std::unordered_map<std::string, TextureAttributes> atlasMap_ {};
 | |
|    std::shared_mutex                                  atlasMutex_ {};
 | |
| 
 | |
|    std::uint64_t buildCount_ {0u};
 | |
| };
 | |
| 
 | |
| TextureAtlas::TextureAtlas() : p(std::make_unique<Impl>()) {}
 | |
| TextureAtlas::~TextureAtlas() = default;
 | |
| 
 | |
| TextureAtlas::TextureAtlas(TextureAtlas&&) noexcept            = default;
 | |
| TextureAtlas& TextureAtlas::operator=(TextureAtlas&&) noexcept = default;
 | |
| 
 | |
| std::uint64_t TextureAtlas::BuildCount() const
 | |
| {
 | |
|    return p->buildCount_;
 | |
| }
 | |
| 
 | |
| void TextureAtlas::RegisterTexture(const std::string& name,
 | |
|                                    const std::string& path)
 | |
| {
 | |
|    std::unique_lock lock(p->registeredTextureMutex_);
 | |
| 
 | |
|    std::shared_ptr<boost::gil::rgba8_image_t> image = CacheTexture(name, path);
 | |
|    p->registeredTextures_.emplace_back(std::move(image));
 | |
| }
 | |
| 
 | |
| std::shared_ptr<boost::gil::rgba8_image_t> TextureAtlas::CacheTexture(
 | |
|    const std::string& name, const std::string& path, double scale)
 | |
| {
 | |
|    // Attempt to load the image
 | |
|    std::shared_ptr<boost::gil::rgba8_image_t> image =
 | |
|       TextureAtlas::Impl::LoadImage(path, scale);
 | |
| 
 | |
|    // If the image is valid
 | |
|    if (image != nullptr && image->width() > 0 && image->height() > 0)
 | |
|    {
 | |
|       // Store it in the texture cache
 | |
|       std::unique_lock lock(p->textureCacheMutex_);
 | |
| 
 | |
|       p->textureCache_.insert_or_assign(name, image);
 | |
| 
 | |
|       return image;
 | |
|    }
 | |
| 
 | |
|    return nullptr;
 | |
| }
 | |
| 
 | |
| void TextureAtlas::BuildAtlas(std::size_t width, std::size_t height)
 | |
| {
 | |
|    logger_->debug("Building {}x{} texture atlas", width, height);
 | |
| 
 | |
|    boost::timer::cpu_timer timer {};
 | |
|    timer.start();
 | |
| 
 | |
|    if (width > INT_MAX || height > INT_MAX)
 | |
|    {
 | |
|       logger_->error("Cannot build texture atlas of size {}x{}", width, height);
 | |
|       return;
 | |
|    }
 | |
| 
 | |
|    typedef std::vector<
 | |
|       std::pair<std::string, std::shared_ptr<boost::gil::rgba8_image_t>>>
 | |
|       ImageVector;
 | |
| 
 | |
|    ImageVector             images {};
 | |
|    std::vector<stbrp_rect> stbrpRects {};
 | |
| 
 | |
|    // Cached images
 | |
|    {
 | |
|       // Take a lock on the texture cache map while adding textures images to
 | |
|       // the atlas vector.
 | |
|       std::unique_lock textureCacheLock(p->textureCacheMutex_);
 | |
| 
 | |
|       // For each cached texture
 | |
|       for (auto it = p->textureCache_.begin(); it != p->textureCache_.end();)
 | |
|       {
 | |
|          auto& texture = *it;
 | |
|          auto  image   = texture.second.lock();
 | |
| 
 | |
|          if (image == nullptr)
 | |
|          {
 | |
|             logger_->trace("Removing texture from the cache: {}",
 | |
|                            texture.first);
 | |
| 
 | |
|             // If the image is no longer cached, erase the iterator and continue
 | |
|             it = p->textureCache_.erase(it);
 | |
|             continue;
 | |
|          }
 | |
|          else if (image->width() > 0u && image->height() > 0u)
 | |
|          {
 | |
|             // Store STB rectangle pack data in a vector
 | |
|             stbrpRects.push_back(
 | |
|                stbrp_rect {0,
 | |
|                            static_cast<stbrp_coord>(image->width()),
 | |
|                            static_cast<stbrp_coord>(image->height()),
 | |
|                            0,
 | |
|                            0,
 | |
|                            0});
 | |
| 
 | |
|             // Store image data in a vector
 | |
|             images.push_back({texture.first, image});
 | |
|          }
 | |
| 
 | |
|          // Increment iterator
 | |
|          ++it;
 | |
|       }
 | |
|    }
 | |
| 
 | |
|    // GL_MAX_ARRAY_TEXTURE_LAYERS is guaranteed to be at least 256 in OpenGL 3.3
 | |
|    constexpr std::size_t kMaxLayers = 256u;
 | |
| 
 | |
|    const float xStep = 1.0f / width;
 | |
|    const float yStep = 1.0f / height;
 | |
|    const float xMin  = xStep * 0.5f;
 | |
|    const float yMin  = yStep * 0.5f;
 | |
| 
 | |
|    // Optimal number of nodes = width
 | |
|    stbrp_context           stbrpContext;
 | |
|    std::vector<stbrp_node> stbrpNodes(width);
 | |
|    ImageVector             unpackedImages {};
 | |
|    std::vector<stbrp_rect> unpackedRects {};
 | |
| 
 | |
|    std::vector<boost::gil::rgba8_image_t>             newAtlasArray {};
 | |
|    std::unordered_map<std::string, TextureAttributes> newAtlasMap {};
 | |
| 
 | |
|    for (std::size_t layer = 0; layer < kMaxLayers; ++layer)
 | |
|    {
 | |
|       logger_->trace("Processing layer {}", layer);
 | |
| 
 | |
|       // Pack images
 | |
|       {
 | |
|          logger_->trace("Packing {} images", images.size());
 | |
| 
 | |
|          stbrp_init_target(&stbrpContext,
 | |
|                            static_cast<int>(width),
 | |
|                            static_cast<int>(height),
 | |
|                            stbrpNodes.data(),
 | |
|                            static_cast<int>(stbrpNodes.size()));
 | |
| 
 | |
|          // Pack loaded textures
 | |
|          stbrp_pack_rects(&stbrpContext,
 | |
|                           stbrpRects.data(),
 | |
|                           static_cast<int>(stbrpRects.size()));
 | |
|       }
 | |
| 
 | |
|       // Clear atlas
 | |
|       boost::gil::rgba8_image_t atlas(width, height);
 | |
|       boost::gil::rgba8_view_t  atlasView = boost::gil::view(atlas);
 | |
|       boost::gil::fill_pixels(atlasView,
 | |
|                               boost::gil::rgba8_pixel_t {255, 0, 255, 255});
 | |
| 
 | |
|       // Populate atlas
 | |
|       logger_->trace("Populating atlas");
 | |
| 
 | |
|       std::size_t numPackedImages = 0u;
 | |
| 
 | |
|       for (std::size_t i = 0; i < images.size(); ++i)
 | |
|       {
 | |
|          // If the image was packed successfully
 | |
|          if (stbrpRects[i].was_packed != 0)
 | |
|          {
 | |
|             // Populate the atlas
 | |
|             boost::gil::rgba8c_view_t imageView =
 | |
|                boost::gil::const_view(*images[i].second);
 | |
| 
 | |
|             boost::gil::rgba8_view_t atlasSubView =
 | |
|                boost::gil::subimage_view(atlasView,
 | |
|                                          stbrpRects[i].x,
 | |
|                                          stbrpRects[i].y,
 | |
|                                          imageView.width(),
 | |
|                                          imageView.height());
 | |
| 
 | |
|             boost::gil::copy_pixels(imageView, atlasSubView);
 | |
| 
 | |
|             // Add texture image to the index
 | |
|             const stbrp_coord x = stbrpRects[i].x;
 | |
|             const stbrp_coord y = stbrpRects[i].y;
 | |
| 
 | |
|             const float sLeft = x * xStep + xMin;
 | |
|             const float sRight =
 | |
|                sLeft + static_cast<float>(imageView.width() - 1) / width;
 | |
|             const float tTop = y * yStep + yMin;
 | |
|             const float tBottom =
 | |
|                tTop + static_cast<float>(imageView.height() - 1) / height;
 | |
| 
 | |
|             newAtlasMap.emplace(
 | |
|                std::piecewise_construct,
 | |
|                std::forward_as_tuple(images[i].first),
 | |
|                std::forward_as_tuple(
 | |
|                   layer,
 | |
|                   boost::gil::point_t {x, y},
 | |
|                   boost::gil::point_t {imageView.width(), imageView.height()},
 | |
|                   sLeft,
 | |
|                   sRight,
 | |
|                   tTop,
 | |
|                   tBottom));
 | |
| 
 | |
|             numPackedImages++;
 | |
|          }
 | |
|          else
 | |
|          {
 | |
|             unpackedImages.push_back(std::move(images[i]));
 | |
|             unpackedRects.push_back(stbrpRects[i]);
 | |
|          }
 | |
|       }
 | |
| 
 | |
|       if (numPackedImages > 0u)
 | |
|       {
 | |
|          // The new atlas layer has images that were able to be packed
 | |
|          newAtlasArray.emplace_back(std::move(atlas));
 | |
|       }
 | |
| 
 | |
|       if (unpackedImages.empty())
 | |
|       {
 | |
|          // All images have been packed into the texture atlas
 | |
|          break;
 | |
|       }
 | |
|       else if (layer == kMaxLayers - 1u || numPackedImages == 0u)
 | |
|       {
 | |
|          // Some images were unable to be packed into the texture atlas
 | |
|          for (auto& image : unpackedImages)
 | |
|          {
 | |
|             logger_->warn("Unable to pack texture: {}", image.first);
 | |
|          }
 | |
|       }
 | |
|       else
 | |
|       {
 | |
|          // Swap in unpacked images for processing the next atlas layer
 | |
|          images.swap(unpackedImages);
 | |
|          stbrpRects.swap(unpackedRects);
 | |
|          unpackedImages.clear();
 | |
|          unpackedRects.clear();
 | |
|       }
 | |
|    }
 | |
| 
 | |
|    // Lock atlas
 | |
|    std::unique_lock lock(p->atlasMutex_);
 | |
| 
 | |
|    p->atlasArray_.swap(newAtlasArray);
 | |
|    p->atlasMap_.swap(newAtlasMap);
 | |
| 
 | |
|    // Mark the need to buffer the atlas
 | |
|    ++p->buildCount_;
 | |
| 
 | |
|    timer.stop();
 | |
|    logger_->debug("Texture atlas built in {}", timer.format(6, "%ws"));
 | |
| }
 | |
| 
 | |
| void TextureAtlas::BufferAtlas(GLuint texture)
 | |
| {
 | |
|    std::shared_lock lock(p->atlasMutex_);
 | |
| 
 | |
|    if (p->atlasArray_.size() > 0u && p->atlasArray_[0].width() > 0 &&
 | |
|        p->atlasArray_[0].height() > 0)
 | |
|    {
 | |
|       const std::size_t numLayers = p->atlasArray_.size();
 | |
|       const std::size_t width     = p->atlasArray_[0].width();
 | |
|       const std::size_t height    = p->atlasArray_[0].height();
 | |
|       const std::size_t layerSize = width * height;
 | |
| 
 | |
|       std::vector<boost::gil::rgba8_pixel_t> pixelData {layerSize * numLayers};
 | |
| 
 | |
|       for (std::size_t i = 0; i < numLayers; ++i)
 | |
|       {
 | |
|          boost::gil::rgba8_view_t view = boost::gil::view(p->atlasArray_[i]);
 | |
| 
 | |
|          boost::gil::copy_pixels(
 | |
|             view,
 | |
|             boost::gil::interleaved_view(view.width(),
 | |
|                                          view.height(),
 | |
|                                          pixelData.data() + (i * layerSize),
 | |
|                                          view.width() *
 | |
|                                             sizeof(boost::gil::rgba8_pixel_t)));
 | |
|       }
 | |
| 
 | |
|       lock.unlock();
 | |
| 
 | |
|       glBindTexture(GL_TEXTURE_2D_ARRAY, texture);
 | |
| 
 | |
|       glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
 | |
|       glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
 | |
|       glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
 | |
|       glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
 | |
| 
 | |
|       glTexImage3D(GL_TEXTURE_2D_ARRAY,
 | |
|                    0,
 | |
|                    GL_RGBA,
 | |
|                    static_cast<GLsizei>(width),
 | |
|                    static_cast<GLsizei>(height),
 | |
|                    static_cast<GLsizei>(numLayers),
 | |
|                    0,
 | |
|                    GL_RGBA,
 | |
|                    GL_UNSIGNED_BYTE,
 | |
|                    pixelData.data());
 | |
|    }
 | |
| }
 | |
| 
 | |
| TextureAttributes TextureAtlas::GetTextureAttributes(const std::string& name)
 | |
| {
 | |
|    TextureAttributes attr {};
 | |
|    std::shared_lock  lock(p->atlasMutex_);
 | |
| 
 | |
|    const auto& it = p->atlasMap_.find(name);
 | |
|    if (it != p->atlasMap_.cend())
 | |
|    {
 | |
|       attr = it->second;
 | |
|    }
 | |
| 
 | |
|    return attr;
 | |
| }
 | |
| 
 | |
| std::shared_ptr<boost::gil::rgba8_image_t>
 | |
| TextureAtlas::Impl::LoadImage(const std::string& imagePath, double scale)
 | |
| {
 | |
|    logger_->debug("Loading image: {}", imagePath);
 | |
| 
 | |
|    std::shared_ptr<boost::gil::rgba8_image_t> image = nullptr;
 | |
| 
 | |
|    QString qImagePath = QString::fromStdString(imagePath);
 | |
| 
 | |
|    QUrl url = QUrl::fromUserInput(qImagePath);
 | |
| 
 | |
|    if (url.isLocalFile())
 | |
|    {
 | |
|       const QString suffix          = QFileInfo(qImagePath).suffix().toLower();
 | |
|       const QString qLocalImagePath = url.toString(QUrl::PreferLocalFile);
 | |
| 
 | |
|       if (suffix == "svg")
 | |
|       {
 | |
|          image = ReadSvgFile(qLocalImagePath, scale);
 | |
|       }
 | |
|       else
 | |
|       {
 | |
|          image = ReadPngFile(qLocalImagePath);
 | |
|       }
 | |
|    }
 | |
|    else
 | |
|    {
 | |
|       auto response = cpr::Get(cpr::Url {imagePath}, network::cpr::GetHeader());
 | |
| 
 | |
|       if (cpr::status::is_success(response.status_code))
 | |
|       {
 | |
|          // Use stbi, since we can only guess the image format
 | |
|          static constexpr int desiredChannels = 4;
 | |
| 
 | |
|          int width;
 | |
|          int height;
 | |
|          int numChannels;
 | |
| 
 | |
|          unsigned char* pixelData = stbi_load_from_memory(
 | |
|             reinterpret_cast<const unsigned char*>(response.text.data()),
 | |
|             static_cast<int>(
 | |
|                std::clamp<std::size_t>(response.text.size(), 0, INT32_MAX)),
 | |
|             &width,
 | |
|             &height,
 | |
|             &numChannels,
 | |
|             desiredChannels);
 | |
| 
 | |
|          if (pixelData == nullptr)
 | |
|          {
 | |
|             logger_->error("Error loading image: {}", stbi_failure_reason());
 | |
|             return nullptr;
 | |
|          }
 | |
| 
 | |
|          // Create a view pointing to the STB image data
 | |
|          auto stbView = boost::gil::interleaved_view(
 | |
|             width,
 | |
|             height,
 | |
|             reinterpret_cast<boost::gil::rgba8_pixel_t*>(pixelData),
 | |
|             width * desiredChannels);
 | |
| 
 | |
|          // Copy the view to the destination image
 | |
|          image      = std::make_shared<boost::gil::rgba8_image_t>();
 | |
|          *image     = boost::gil::rgba8_image_t(stbView);
 | |
|          auto& view = boost::gil::view(*image);
 | |
| 
 | |
|          // If no alpha channel, replace black with transparent
 | |
|          if (numChannels == 3)
 | |
|          {
 | |
|             std::for_each(std::execution::par,
 | |
|                           view.begin(),
 | |
|                           view.end(),
 | |
|                           [](boost::gil::rgba8_pixel_t& pixel)
 | |
|                           {
 | |
|                              static const boost::gil::rgba8_pixel_t kBlack {
 | |
|                                 0, 0, 0, 255};
 | |
|                              if (pixel == kBlack)
 | |
|                              {
 | |
|                                 pixel[3] = 0;
 | |
|                              }
 | |
|                           });
 | |
|          }
 | |
| 
 | |
|          stbi_image_free(pixelData);
 | |
|       }
 | |
|       else if (response.status_code == 0)
 | |
|       {
 | |
|          logger_->error("Error loading image: {}", response.error.message);
 | |
|       }
 | |
|       else
 | |
|       {
 | |
|          logger_->error("Error loading image: {}", response.status_line);
 | |
|       }
 | |
|    }
 | |
| 
 | |
|    return image;
 | |
| }
 | |
| 
 | |
| std::shared_ptr<boost::gil::rgba8_image_t>
 | |
| TextureAtlas::Impl::ReadPngFile(const QString& imagePath)
 | |
| {
 | |
|    QFile imageFile(imagePath);
 | |
| 
 | |
|    imageFile.open(QIODevice::ReadOnly);
 | |
| 
 | |
|    if (!imageFile.isOpen())
 | |
|    {
 | |
|       logger_->error("Could not open image: {}", imagePath.toStdString());
 | |
|       return nullptr;
 | |
|    }
 | |
| 
 | |
|    boost::iostreams::stream<util::IoDeviceSource> dataStream(imageFile);
 | |
|    std::shared_ptr<boost::gil::rgba8_image_t>     image =
 | |
|       std::make_shared<boost::gil::rgba8_image_t>();
 | |
| 
 | |
|    try
 | |
|    {
 | |
|       boost::gil::read_and_convert_image(
 | |
|          dataStream, *image, boost::gil::png_tag());
 | |
|    }
 | |
|    catch (const std::exception& ex)
 | |
|    {
 | |
|       logger_->error("Error reading image: {}", ex.what());
 | |
|       return nullptr;
 | |
|    }
 | |
| 
 | |
|    return image;
 | |
| }
 | |
| 
 | |
| std::shared_ptr<boost::gil::rgba8_image_t>
 | |
| TextureAtlas::Impl::ReadSvgFile(const QString& imagePath, double scale)
 | |
| {
 | |
|    QSvgRenderer renderer {imagePath};
 | |
|    QPixmap      pixmap {renderer.defaultSize() * scale};
 | |
|    pixmap.fill(Qt::GlobalColor::transparent);
 | |
| 
 | |
|    QPainter painter {&pixmap};
 | |
|    renderer.render(&painter, pixmap.rect());
 | |
| 
 | |
|    QImage qImage = pixmap.toImage();
 | |
| 
 | |
|    std::shared_ptr<boost::gil::rgba8_image_t> image = nullptr;
 | |
| 
 | |
|    if (qImage.width() > 0 && qImage.height() > 0)
 | |
|    {
 | |
|       // Convert to ARGB32 format if not already (equivalent to bgra8_pixel_t)
 | |
|       qImage.convertTo(QImage::Format_ARGB32);
 | |
| 
 | |
|       // Create a view pointing to the underlying QImage pixel data
 | |
|       auto view = boost::gil::interleaved_view(
 | |
|          qImage.width(),
 | |
|          qImage.height(),
 | |
|          reinterpret_cast<const boost::gil::bgra8_pixel_t*>(qImage.constBits()),
 | |
|          qImage.width() * 4);
 | |
| 
 | |
|       image = std::make_shared<boost::gil::rgba8_image_t>(view);
 | |
|    }
 | |
| 
 | |
|    return image;
 | |
| }
 | |
| 
 | |
| TextureAtlas& TextureAtlas::Instance()
 | |
| {
 | |
|    static TextureAtlas instance_ {};
 | |
|    return instance_;
 | |
| }
 | |
| 
 | |
| } // namespace util
 | |
| } // namespace qt
 | |
| } // namespace scwx
 | 
