mirror of
https://github.com/ciphervance/supercell-wx.git
synced 2025-10-29 21:00:06 +00:00
551 lines
16 KiB
C++
551 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(gl::OpenGLFunctions& gl, 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();
|
|
|
|
gl.glBindTexture(GL_TEXTURE_2D_ARRAY, texture);
|
|
|
|
gl.glTexParameteri(
|
|
GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
|
gl.glTexParameteri(
|
|
GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
|
gl.glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
|
gl.glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
|
|
|
gl.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())
|
|
{
|
|
QString suffix = QFileInfo(qImagePath).suffix().toLower();
|
|
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_unseq,
|
|
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
|