#pragma once #include "hd2d/GL.h" #include #include #include namespace hd2d { struct Image { int width = 0, height = 0; std::vector rgba; // row 0 = bottom (GL convention) }; // Loads an RGBA8 image flipped to GL orientation. Colors of fully transparent // pixels are replaced with their nearest opaque neighbour so that filtering // never bleeds dark fringes into sprite edges. bool loadImage(const std::string& path, Image& out); // Decodes PNG bytes from memory. glTF images use flipY = false (their UV // origin is top-left, which matches GL when rows are uploaded unflipped). bool decodeImage(const unsigned char* data, size_t size, Image& out, bool flipY); // Replaces the colors of fully transparent pixels with their nearest opaque // neighbour (loadImage/decodeImage already do this). Call it on images you // compose yourself so filtering never bleeds dark fringes into edges. void bleedTransparentColors(Image& img); // sRGB pixel-art texture. Sampled with "sharp bilinear" in shaders, so it uses // linear filtering with mipmaps rather than GL_NEAREST. `nearest` = GL_NEAREST // without mips instead (crisp ImGui previews of pixel art). class Texture { public: Texture() = default; Texture(const Texture&) = delete; Texture& operator=(const Texture&) = delete; Texture(Texture&& o) noexcept { *this = std::move(o); } Texture& operator=(Texture&& o) noexcept; ~Texture(); bool load(const std::string& path, bool repeat = false, bool nearest = false); bool create(const Image& image, bool repeat = false, float anisotropy = 1.0f, bool nearest = false); // Fully transparent w x h texture (sRGB RGBA8, mipmapped, clamped) to fill with update(). bool createEmpty(int w, int h); // Uploads `img` with its bottom-left pixel at (x, y) (GL orientation, // y = 0 is the bottom row) and regenerates the mips. Does not change // opaqueBounds(), which always describes the image given to create(). void update(int x, int y, const Image& img); void bind(GLuint unit) const { glBindTextureUnit(unit, id_); } GLuint id() const { return id_; } glm::ivec2 size() const { return {width_, height_}; } // UV-space rectangle (minX, minY, maxX, maxY) of pixels with alpha >= 128. // Billboards use minY to plant the visible feet on the ground. glm::vec4 opaqueBounds() const { return opaqueBounds_; } // Grid sprite sheets authored top-down (row 0 = top row of the PNG), with // `cell` x `cell` pixel frames. cellUV is a BillboardInstance::uv rect; // cellAnchor is a BillboardInstance::anchor: the v of the bottom edge of // pixel row `feetRowFromCellTop` (0 = the cell's top row) of that cell. static glm::vec4 cellUV(glm::ivec2 texSize, glm::ivec2 cell, int col, int rowFromTop); static float cellAnchor(glm::ivec2 texSize, glm::ivec2 cell, int rowFromTop, int feetRowFromCellTop); private: GLuint id_ = 0; int width_ = 0, height_ = 0, levels_ = 0; glm::vec4 opaqueBounds_{0, 0, 1, 1}; }; // 2D array of equally sized sRGB layers (terrain materials). class TextureArray { public: TextureArray() = default; TextureArray(const TextureArray&) = delete; TextureArray& operator=(const TextureArray&) = delete; ~TextureArray(); bool load(const std::vector& paths); void bind(GLuint unit) const { glBindTextureUnit(unit, id_); } glm::ivec2 size() const { return {width_, height_}; } int layers() const { return layers_; } private: GLuint id_ = 0; int width_ = 0, height_ = 0, layers_ = 0; }; } // namespace hd2d