diff --git a/dang-gl/CMakeLists.txt b/dang-gl/CMakeLists.txt index 1f221c06..99220029 100644 --- a/dang-gl/CMakeLists.txt +++ b/dang-gl/CMakeLists.txt @@ -13,8 +13,11 @@ add_library( src/Context/StateTypes.cpp src/General/GLConstants.cpp src/Image/BorderedImage.cpp + src/Image/BorderedImageMipmapper.cpp src/Image/Image.cpp src/Image/ImageBorder.cpp + src/Image/ImageMipmapper.cpp + src/Image/MipmapLevels.cpp src/Image/Pixel.cpp src/Image/PixelFormat.cpp src/Image/PixelInternalFormat.cpp diff --git a/dang-gl/include/dang-gl/Image/BorderedImageMipmapper.h b/dang-gl/include/dang-gl/Image/BorderedImageMipmapper.h new file mode 100644 index 00000000..4df4952a --- /dev/null +++ b/dang-gl/include/dang-gl/Image/BorderedImageMipmapper.h @@ -0,0 +1,26 @@ +#pragma once + +#include "dang-gl/Image/BorderedImage.h" +#include "dang-gl/Image/ImageMipmapper.h" +#include "dang-gl/Image/PixelFormat.h" +#include "dang-gl/Image/PixelType.h" +#include "dang-math/vector.h" + +namespace dang::gl { + +/// @brief Mipmaps a bordered image into a new bordered image (with border set to "none"). +template +struct BorderedImageMipmapper { + template + auto operator()(const BorderedImage& bordered_image) const + { + // Mipmaps entire image including border, but sets border of result to "none". + using BorderedImage = BorderedImage; + return BorderedImage(image_mipmapper(bordered_image.image())); + } +}; + +template +inline constexpr BorderedImageMipmapper bordered_image_mipmapper; + +} // namespace dang::gl diff --git a/dang-gl/include/dang-gl/Image/ImageMipmapper.h b/dang-gl/include/dang-gl/Image/ImageMipmapper.h new file mode 100644 index 00000000..60be4bc0 --- /dev/null +++ b/dang-gl/include/dang-gl/Image/ImageMipmapper.h @@ -0,0 +1,70 @@ +#pragma once + +#include "dang-gl/Image/Image.h" +#include "dang-gl/Image/PixelFormat.h" +#include "dang-gl/Image/PixelType.h" +#include "dang-math/vector.h" + +namespace dang::gl { + +/// @brief Mipmaps a regular image using a 2x2 box filter. +/// @remark Odd pixels are copied over, which might result in bad mipmaps, if the size is odd on a lot of levels. +template +struct ImageMipmapper { + template + auto operator()(const Image& image) const + { + using Image = Image; + using Pixel = typename Image::Pixel; + using Size = typename Image::Size; + using CalcPixel = dmath::Vector; + + // It is not possible to change box_size at will. + // To get that working more changes would be necessary. + constexpr std::size_t box_size = 2; + constexpr auto box_pixels = box_size * box_size; + + auto floor_size = image.size() / box_size; + auto ceil_size = (image.size() - 1) / box_size + 1; + Image result(ceil_size); + for (const auto& pos : dmath::sbounds2(floor_size)) { + CalcPixel color; + for (const auto& offset : dmath::sbounds2(box_size)) + color += static_cast(image[pos * box_size + offset]); + result[pos] = static_cast(color / box_pixels); + } + + auto odd_width = floor_size.x() != ceil_size.x(); + auto odd_height = floor_size.y() != ceil_size.y(); + + if (odd_width) { + auto x = floor_size.x(); + for (std::size_t y = 0; y < floor_size.y(); y++) { + CalcPixel color; + for (std::size_t offset = 0; offset < box_size; offset++) + color += static_cast(image[Size(x * box_size, y * box_size + offset)]); + result[Size(x, y)] = static_cast(color / box_size); + } + } + + if (odd_height) { + auto y = floor_size.y(); + for (std::size_t x = 0; x < floor_size.x(); x++) { + CalcPixel color; + for (std::size_t offset = 0; offset < box_size; offset++) + color += static_cast(image[Size(x * box_size + offset, y * box_size)]); + result[Size(x, y)] = static_cast(color / box_size); + } + } + + if (odd_width && odd_height) + result[floor_size] = image[floor_size * box_size]; + + return result; + } +}; + +template +inline constexpr ImageMipmapper image_mipmapper; + +} // namespace dang::gl diff --git a/dang-gl/include/dang-gl/Image/MipmapLevels.h b/dang-gl/include/dang-gl/Image/MipmapLevels.h new file mode 100644 index 00000000..b9f19f03 --- /dev/null +++ b/dang-gl/include/dang-gl/Image/MipmapLevels.h @@ -0,0 +1,77 @@ +#pragma once + +#include "dang-gl/global.h" +#include "dang-math/vector.h" +#include "dang-utils/utils.h" + +namespace dang::gl { + +/// @brief Calculates the number of required mipmap levels for the given texture size. +constexpr std::size_t maxMipmapLevels(std::size_t size) { return dutils::ilog2ceil(size) + 1; } + +/// @brief Returns the next mipmap level size. +/// @remarks Rounds up, which is unusual for mipmaps, but necessary when used in a texture atlas. +constexpr dmath::svec2 nextMipmapSize(const dmath::svec2& size) { return (size.maxValue() - 1) / 2 + 1; } + +/// @brief Combines several mipmap levels of the same image. +template +class MipmapLevels { +public: + using BorderedImageData = TBorderedImageData; + + /// @brief Only stores the given image without generating any additional mipmaps. + MipmapLevels(BorderedImageData full_image) + : mipmap_levels_{std::move(full_image)} + {} + + /// @brief Stores the given bordered image and all mipmap levels using the provided mipmapper. + template + MipmapLevels(BorderedImageData full_image, TMipmapper mipmapper) + : mipmap_levels_(generateMipmapLevels(std::move(full_image), mipmapper)) + {} + + /// @brief The full image with the highest resolution. + auto& fullImage() { return mipmap_levels_.front(); } + /// @brief The full image with the highest resolution. + const auto& fullImage() const { return mipmap_levels_.front(); } + + /// @brief The total number of mipmap levels, including the original, full size image. + auto count() const { return mipmap_levels_.size(); } + + /// @brief A specific mipmap level with the given index, where 0 gives the original, full size image. + auto& operator[](std::size_t index) { return mipmap_levels_[index]; } + /// @brief A specific mipmap level with the given index, where 0 gives the original, full size image. + const auto& operator[](std::size_t index) const { return mipmap_levels_[index]; } + + auto begin() { return mipmap_levels_.begin(); } + auto begin() const { return mipmap_levels_.begin(); } + auto end() { return mipmap_levels_.end(); } + auto end() const { return mipmap_levels_.end(); } + +private: + /// @brief Uses the given mipmapper to generate a vector of all mipmap levels for the given image. + template + static auto generateMipmapLevels(BorderedImageData&& full_image, TMipmapper mipmapper) + { + auto count = maxMipmapLevels(full_image.size().maxValue()); + std::vector mipmap_levels{std::move(full_image)}; + for (std::size_t index = 1; index < count; index++) { + const auto& prev = mipmap_levels.back(); + auto mipmapped = mipmapper(prev); + ensureSizeHalved(prev.size(), mipmapped.size()); + mipmap_levels.push_back(std::move(mipmapped)); + } + return mipmap_levels; + } + + /// @brief Throws an exception if "halved" isn't the correct next mipmap level size for "original". + static void ensureSizeHalved(dmath::svec2 original, dmath::svec2 halved) + { + if (halved != nextMipmapSize(original)) + throw std::invalid_argument("mipmapper did not properly half the image size"); + } + + std::vector mipmap_levels_; +}; + +} // namespace dang::gl diff --git a/dang-gl/include/dang-gl/Objects/ObjectType.h b/dang-gl/include/dang-gl/Objects/ObjectType.h index 44b6ef64..67cb6630 100644 --- a/dang-gl/include/dang-gl/Objects/ObjectType.h +++ b/dang-gl/include/dang-gl/Objects/ObjectType.h @@ -53,6 +53,7 @@ enum class TextureTarget { Texture2DMultisampleArray, Texture3D, TextureCubeMap, + TextureCubeMapArray, TextureRectangle, COUNT @@ -148,8 +149,8 @@ inline constexpr dutils::EnumArray gl_constants(), image, offset, mipmap_level); } - /// @brief Regenerates all mipmaps from the top level. + /// @brief Generates all mipmaps from the top level using OpenGL's built-in glGenerateMipmap. void generateMipmap() { this->bind(); @@ -504,7 +504,7 @@ class TextureBaseRegular : public TextureBaseTyped { {} /// @brief Initializes a new texture with the given size. - /// @remark mipmap_levels defaults to generating a full mipmap down to 1x1. + /// @remark mipmap_levels defaults to generating storage for a full mipmap down to 1x1. explicit TextureBaseRegular(svec size, std::optional mipmap_levels = std::nullopt, PixelInternalFormat internal_format = PixelInternalFormat::RGBA8) @@ -514,7 +514,7 @@ class TextureBaseRegular : public TextureBaseTyped { } /// @brief Initializes a new texture with the given image data. - /// @remark mipmap_levels defaults to generating a full mipmap down to 1x1. + /// @remark mipmap_levels defaults to generating storage for a full mipmap down to 1x1. /// @remark internal_format defaults to being chosen, based on the format of the provided image. template explicit TextureBaseRegular(const Image& image, @@ -531,7 +531,7 @@ class TextureBaseRegular : public TextureBaseTyped { TextureBaseRegular& operator=(const TextureBaseRegular&) = delete; /// @brief Generates storage for the specified size. - /// @remark mipmap_levels defaults to generating a full mipmap down to 1x1. + /// @remark mipmap_levels defaults to generating storage for a full mipmap down to 1x1. void generate(svec size, std::optional mipmap_levels = std::nullopt, PixelInternalFormat internal_format = PixelInternalFormat::RGBA8) @@ -541,7 +541,7 @@ class TextureBaseRegular : public TextureBaseTyped { } /// @brief Generates texture storage and fills it with the provided image. - /// @remark mipmap_levels defaults to generating a full mipmap down to 1x1. + /// @remark mipmap_levels defaults to generating storage for a full mipmap down to 1x1. /// @remark internal_format defaults to being chosen, based on the format of the provided image. template void generate(const Image& image, @@ -553,7 +553,6 @@ class TextureBaseRegular : public TextureBaseTyped { storage( std::make_index_sequence(), static_cast>(image.size()), mipmap_levels, internal_format); this->subImage(std::make_index_sequence(), image); - glGenerateMipmap(toGLConstant(v_target)); } protected: @@ -561,30 +560,10 @@ class TextureBaseRegular : public TextureBaseTyped { TextureBaseRegular& operator=(TextureBaseRegular&&) = default; private: - /// @brief Returns the biggest component of a given vector. - template - static GLsizei maxSize(svec size, std::index_sequence) - { - GLsizei result = 0; - ((result = std::max(result, size[v_indices])), ...); - return result; - } - - /// @brief Calculates the integer log2 plus one of the given value, which is the required mipmap count for a given - /// size. - static GLsizei mipmapCount(GLsizei value) - { - // TODO: C++20 use std::bit_width - GLsizei result = 1; - while (value >>= 1) - result++; - return result; - } - - /// @brief Returns the required count to generate a full mipmap down to 1x1 for the given size. - GLsizei maxMipmapLevelsFor(svec size) + /// @brief Calculates the required mipmap count for a given size. + static constexpr GLsizei mipmapCount(GLsizei value) { - return mipmapCount(maxSize(size, std::make_index_sequence())); + return dutils::ilog2(static_cast>(value)) + 1; } /// @brief Calls glTexStorage with the provided parameters and index sequence of the textures dimension. @@ -594,10 +573,18 @@ class TextureBaseRegular : public TextureBaseTyped { std::optional mipmap_levels = std::nullopt, PixelInternalFormat internal_format = PixelInternalFormat::RGBA8) { - glTexStorage(toGLConstant(v_target), - mipmap_levels.value_or(maxMipmapLevelsFor(size)), - toGLConstant(internal_format), - size[v_indices]...); + if (!mipmap_levels) { + if constexpr (v_target == TextureTarget::Texture1DArray) + mipmap_levels = mipmapCount(size.x().maxValue()); + else if constexpr (v_target == TextureTarget::Texture2DArray || + v_target == TextureTarget::Texture2DMultisampleArray || + v_target == TextureTarget::TextureCubeMapArray) + mipmap_levels = mipmapCount(size.xy().maxValue()); + else + mipmap_levels = mipmapCount(size.maxValue()); + } + + glTexStorage(toGLConstant(v_target), *mipmap_levels, toGLConstant(internal_format), size[v_indices]...); this->setSize(size); } }; diff --git a/dang-gl/include/dang-gl/Texturing/MultiTextureAtlas.h b/dang-gl/include/dang-gl/Texturing/MultiTextureAtlas.h index 82a12684..a953aca5 100644 --- a/dang-gl/include/dang-gl/Texturing/MultiTextureAtlas.h +++ b/dang-gl/include/dang-gl/Texturing/MultiTextureAtlas.h @@ -24,9 +24,9 @@ class TextureAtlasMultiTexture { : bordered_images_((ensureCompatible(bordered_images), std::move(bordered_images))) {} - BorderedImage& operator[](TSubTextureEnum sub_texture) { return bordered_images_[sub_texture]; } + auto& operator[](TSubTextureEnum sub_texture) { return bordered_images_[sub_texture]; } - const BorderedImage& operator[](TSubTextureEnum sub_texture) const { return bordered_images_[sub_texture]; } + const auto& operator[](TSubTextureEnum sub_texture) const { return bordered_images_[sub_texture]; } // --- BorderedImageData concept: @@ -72,22 +72,27 @@ class TextureAtlasMultiTexture { Texture2DArray& texture(TSubTextureEnum sub_texture) { return textures_[sub_texture]; } protected: - bool resize(GLsizei required_size, GLsizei layers, GLsizei mipmap_levels) + bool resize(std::size_t required_size, std::size_t layers, std::size_t mipmap_levels) { assert(textures_.front().size().x() == textures_.front().size().y()); if (required_size == textures_.front().size().x() && layers == textures_.front().size().z()) return false; // /!\ Resets all texture parameters! - for (auto& texture : textures_) - texture = Texture2DArray( - {required_size, required_size, layers}, mipmap_levels, pixel_format_internal_v); + for (auto& texture : textures_) { + texture = Texture2DArray(svec3(static_cast(required_size), + static_cast(required_size), + static_cast(layers)), + static_cast(mipmap_levels), + pixel_format_internal_v); + } return true; }; - void modify(const BorderedImageData& bordered_image_data, ivec3 offset, GLint mipmap_level) + void modify(const BorderedImageData& bordered_image_data, dmath::svec3 offset, std::size_t mipmap_level) { for (auto sub_texture : dutils::enumerate) - textures_[sub_texture].modify(bordered_image_data[sub_texture], offset, mipmap_level); + textures_[sub_texture].modify( + bordered_image_data[sub_texture], static_cast(offset), static_cast(mipmap_level)); }; private: @@ -115,8 +120,8 @@ class MultiTextureAtlas using Base = TextureAtlasBase< detail::TextureAtlasMultiTexture>; - explicit MultiTextureAtlas(std::optional max_texture_size = std::nullopt, - std::optional max_layer_count = std::nullopt) + explicit MultiTextureAtlas(std::optional max_texture_size = std::nullopt, + std::optional max_layer_count = std::nullopt) : Base(TextureAtlasUtils::checkLimits(max_texture_size, max_layer_count)) {} }; diff --git a/dang-gl/include/dang-gl/Texturing/TextureAtlas.h b/dang-gl/include/dang-gl/Texturing/TextureAtlas.h index 1eeb076f..ab26edd9 100644 --- a/dang-gl/include/dang-gl/Texturing/TextureAtlas.h +++ b/dang-gl/include/dang-gl/Texturing/TextureAtlas.h @@ -26,20 +26,23 @@ class TextureAtlasSingleTexture { Texture2DArray& texture() { return texture_; } protected: - bool resize(GLsizei required_size, GLsizei layers, GLsizei mipmap_levels) + bool resize(std::size_t required_size, std::size_t layers, std::size_t mipmap_levels) { assert(texture_.size().x() == texture_.size().y()); if (required_size == texture_.size().x() && layers == texture_.size().z()) return false; // /!\ Resets all texture parameters! - texture_ = Texture2DArray( - {required_size, required_size, layers}, mipmap_levels, pixel_format_internal_v); + texture_ = Texture2DArray(svec3(static_cast(required_size), + static_cast(required_size), + static_cast(layers)), + static_cast(mipmap_levels), + pixel_format_internal_v); return true; }; - void modify(const BorderedImageData& bordered_image_data, ivec3 offset, GLint mipmap_level) + void modify(const BorderedImageData& bordered_image_data, dmath::svec3 offset, std::size_t mipmap_level) { - texture_.modify(bordered_image_data.image(), offset, mipmap_level); + texture_.modify(bordered_image_data.image(), static_cast(offset), static_cast(mipmap_level)); }; private: @@ -56,8 +59,8 @@ class TextureAtlas public: using Base = TextureAtlasBase>; - explicit TextureAtlas(std::optional max_texture_size = std::nullopt, - std::optional max_layer_count = std::nullopt) + explicit TextureAtlas(std::optional max_texture_size = std::nullopt, + std::optional max_layer_count = std::nullopt) : Base(TextureAtlasUtils::checkLimits(max_texture_size, max_layer_count)) {} }; diff --git a/dang-gl/include/dang-gl/Texturing/TextureAtlasBase.h b/dang-gl/include/dang-gl/Texturing/TextureAtlasBase.h index 00ec6602..497d58dd 100644 --- a/dang-gl/include/dang-gl/Texturing/TextureAtlasBase.h +++ b/dang-gl/include/dang-gl/Texturing/TextureAtlasBase.h @@ -11,9 +11,9 @@ The TextureBase concept: - Move-constructible - using BorderedImageData = ...; -- bool resize(GLsizei required_size, GLsizei layers, GLsizei mipmap_levels) +- bool resize(std::size_t required_size, std::size_t layers, std::size_t mipmap_levels) -> protected, resizes the texture -- void modify(const BorderedImageData& bordered_image_data, ivec3 offset, GLint mipmap_level) +- void modify(const BorderedImageData& bordered_image_data, dmath::svec3 offset, std::size_t mipmap_level) -> protected, modifies the texture at a given spot */ @@ -24,19 +24,18 @@ class BasicFrozenTextureAtlas; template class TextureAtlasBase : public TTextureBase { public: - using BorderedImageData = typename TTextureBase::BorderedImageData; + using TextureBase = TTextureBase; + using BorderedImageData = typename TextureBase::BorderedImageData; using Tiles = TextureAtlasTiles; using TileHandle = typename Tiles::TileHandle; - using Frozen = BasicFrozenTextureAtlas; + using MipmapLevels = typename Tiles::MipmapLevels; + using Frozen = BasicFrozenTextureAtlas; TextureAtlasBase(const TextureAtlasLimits& limits) : tiles_(limits) {} - [[nodiscard]] TileHandle add(BorderedImageData bordered_image_data) - { - return tiles_.add(std::move(bordered_image_data)); - } + [[nodiscard]] TileHandle add(MipmapLevels mipmap_levels) { return tiles_.add(std::move(mipmap_levels)); } [[nodiscard]] bool contains(const TileHandle& tile_handle) const { return tiles_.contains(tile_handle); } @@ -68,17 +67,18 @@ class TextureAtlasBase : public TTextureBase { template class BasicFrozenTextureAtlas : public TTextureBase { public: - using BorderedImageData = typename TTextureBase::BorderedImageData; + using TextureBase = TTextureBase; + using BorderedImageData = typename TextureBase::BorderedImageData; using Tiles = FrozenTextureAtlasTiles; using TileHandle = typename Tiles::TileHandle; - friend class TextureAtlasBase; + friend class TextureAtlasBase; [[nodiscard]] bool contains(const TileHandle& tile_handle) const { return tiles_.contains(tile_handle); } private: - BasicFrozenTextureAtlas(Tiles&& tiles, TTextureBase&& texture) - : TTextureBase(std::move(texture)) + BasicFrozenTextureAtlas(Tiles&& tiles, TextureBase&& texture) + : TextureBase(std::move(texture)) , tiles_(std::move(tiles)) {} diff --git a/dang-gl/include/dang-gl/Texturing/TextureAtlasTiles.h b/dang-gl/include/dang-gl/Texturing/TextureAtlasTiles.h index e819a6aa..3754f244 100644 --- a/dang-gl/include/dang-gl/Texturing/TextureAtlasTiles.h +++ b/dang-gl/include/dang-gl/Texturing/TextureAtlasTiles.h @@ -1,5 +1,6 @@ #pragma once +#include "dang-gl/Image/MipmapLevels.h" #include "dang-gl/Math/MathTypes.h" #include "dang-gl/global.h" #include "dang-utils/utils.h" @@ -27,8 +28,8 @@ The BorderedImageData concept: /// @brief Holds maximum size restrictions of the texture atlas. struct TextureAtlasLimits { - GLsizei max_texture_size; - GLsizei max_layer_count; + std::size_t max_texture_size; + std::size_t max_layer_count; }; /// @brief Can store a large number of texture tiles in multiple layers of grids. @@ -36,19 +37,22 @@ struct TextureAtlasLimits { template class TextureAtlasTiles { public: + using BorderedImageData = TBorderedImageData; + using MipmapLevels = dang::gl::MipmapLevels; + /// @brief A function that is called with required size (width and height), layers and mipmap levels. - /// @remarks Returns whether actual resizing occurred. - using TextureResizeFunction = std::function; + /// @remark Returns whether actual resizing occurred. + using TextureResizeFunction = std::function; /// @brief A function that uploads the image to a specific position and mipmap level of a texture. - using TextureModifyFunction = std::function; + using TextureModifyFunction = std::function; class TileHandle; private: /// @brief Atlas information which is stored in a unique_ptr, so that it can be shared with all TileData instances. struct AtlasInfo { - GLsizei atlas_size; + std::size_t atlas_size; }; /// @brief Information about the placement of a tile, including whether it has been written to the texture yet. @@ -56,7 +60,7 @@ class TextureAtlasTiles { /// @brief The index of this tile in the layer. std::size_t index = 0; /// @brief The position where the write this tile in the array texture. - svec3 position; + dmath::svec3 position; /// @brief Whether this tile has been written to the array texture yet. bool written = false; @@ -65,20 +69,30 @@ class TextureAtlasTiles { /// @param index The index of this tile in the layer. /// @param layer The index of the layer itself, determining the z position. /// @param position The x and y coordinates of the position. - TilePlacement(std::size_t index, svec2 position, GLsizei layer) + TilePlacement(std::size_t index, dmath::svec2 position, std::size_t layer) : index(index) , position(position.x(), position.y(), layer) {} + + /// @brief The position on the given mipmap layer. + auto pixelPos(std::size_t mipmap_layer = 0) const { return position.xy() >> mipmap_layer; } + + /// @brief The position and atlas layer on the given mipmap layer. + auto pixelPosAndLayer(std::size_t mipmap_layer = 0) const + { + auto [x, y] = pixelPos(mipmap_layer); + return dmath::svec3(x, y, position.z()); + } }; /// @brief Contains data about a single texture tile on a layer. struct TileData { - TBorderedImageData bordered_image_data; + MipmapLevels mipmap_levels; TilePlacement placement; std::shared_ptr atlas_info; - TileData(TBorderedImageData&& bordered_image_data, std::shared_ptr atlas_info) - : bordered_image_data(std::move(bordered_image_data)) + TileData(MipmapLevels&& mipmap_levels, std::shared_ptr atlas_info) + : mipmap_levels(std::move(mipmap_levels)) , atlas_info(std::move(atlas_info)) {} @@ -93,7 +107,7 @@ class TextureAtlasTiles { class Layer { public: /// @brief Creates a new layer with the given tile size, specified as log2. - explicit Layer(const svec2& tile_size_log2, std::size_t max_texture_size) + explicit Layer(const dmath::svec2& tile_size_log2, std::size_t max_texture_size) : tile_size_log2_(tile_size_log2) , max_tiles_(calculateMaxTiles(max_texture_size)) {} @@ -104,22 +118,25 @@ class TextureAtlasTiles { Layer& operator=(Layer&&) = default; /// @brief Returns the log2 of the pixel size of a tile. - svec2 tileSizeLog2() const { return tile_size_log2_; } + dmath::svec2 tileSizeLog2() const { return tile_size_log2_; } /// @brief Returns the pixel size of a single tile. - svec2 tileSize() const { return {GLsizei{1} << tile_size_log2_.x(), GLsizei{1} << tile_size_log2_.y()}; } + dmath::svec2 tileSize() const + { + return {std::size_t{1} << tile_size_log2_.x(), std::size_t{1} << tile_size_log2_.y()}; + } /// @brief Calculates the required grid size (for the longer side) to fit all tiles. - GLsizei requiredGridSizeLog2() const + std::size_t requiredGridSizeLog2() const { if (tiles_.empty()) return 0; - auto diff_log2 = std::abs(tile_size_log2_.x() - tile_size_log2_.y()); + auto diff_log2 = dutils::absoluteDifference(tile_size_log2_.x(), tile_size_log2_.y()); auto square_tiles = ((tiles_.size() - 1) >> diff_log2) + 1; return (dutils::ilog2ceil(square_tiles) + 1) >> 1; } /// @brief Calculates the required texture size to fit all tiles. - GLsizei requiredTextureSize() const { return tileSize().maxValue() << requiredGridSizeLog2(); } + std::size_t requiredTextureSize() const { return tileSize().maxValue() << requiredGridSizeLog2(); } /// @brief Whether the grid is empty. bool empty() const { return tiles_.empty(); } @@ -128,7 +145,7 @@ class TextureAtlasTiles { bool full() const { return first_free_tile_ == max_tiles_; } /// @brief Places a single tile in the grid, filling potential gaps that appeared after removing tiles. - void addTile(TileData& tile, GLsizei layer) + void addTile(TileData& tile, std::size_t layer) { assert(!full()); tile.placement = TilePlacement(first_free_tile_, tileSize() * indexToPosition(first_free_tile_), layer); @@ -165,8 +182,10 @@ class TextureAtlasTiles { drawTile(*tile, modify); assert(tile->placement.written); } - if (freeze) - tile->bordered_image_data.free(); + if (freeze) { + for (auto& mipmap_level : tile->mipmap_levels) + mipmap_level.free(); + } } } @@ -182,46 +201,49 @@ class TextureAtlasTiles { /// @brief Draws a single tile onto the texture. void drawTile(TileData& tile, const TextureModifyFunction& modify) const { - assert(tile.bordered_image_data); - modify(tile.bordered_image_data, tile.placement.position, 0); // TODO: Mipmapping + for (std::size_t mipmap_level = 0; mipmap_level < tile.mipmap_levels.count(); mipmap_level++) { + const auto& bordered_image = tile.mipmap_levels[mipmap_level]; + assert(bordered_image); + modify(bordered_image, tile.placement.pixelPosAndLayer(mipmap_level), mipmap_level); + } tile.placement.written = true; } /// @brief Returns the maximum number of tiles, that can fit in a square texture of the given size. std::size_t calculateMaxTiles(std::size_t max_texture_size) const { - assert(static_cast(tileSize().maxValue()) <= max_texture_size); + assert(tileSize().maxValue() <= max_texture_size); auto x_tiles = std::size_t{1} << dutils::ilog2(max_texture_size >> tile_size_log2_.x()); auto y_tiles = std::size_t{1} << dutils::ilog2(max_texture_size >> tile_size_log2_.y()); return x_tiles * y_tiles; } /// @brief Inverse pairing function, returning only even/odd bits as x/y. - svec2 indexToPosition(std::size_t index) + dmath::svec2 indexToPosition(std::size_t index) { - auto size_diff_log2 = std::abs(tile_size_log2_.x() - tile_size_log2_.y()); + auto size_diff_log2 = dutils::absoluteDifference(tile_size_log2_.x(), tile_size_log2_.y()); auto flip = tile_size_log2_.x() < tile_size_log2_.y(); - auto x = static_cast(dutils::removeOddBits(index >> size_diff_log2)); - auto y = static_cast(dutils::removeOddBits(index >> (size_diff_log2 + 1))); + auto x = dutils::removeOddBits(index >> size_diff_log2); + auto y = dutils::removeOddBits(index >> (size_diff_log2 + 1)); y <<= size_diff_log2; y |= index & ~(~std::size_t{0} << size_diff_log2); - return flip ? svec2(y, x) : svec2(x, y); + return flip ? dmath::svec2(y, x) : dmath::svec2(x, y); } /// @brief Pairing function, interleaving x/y as odd/even bits of the resulting integer. - std::size_t positionToIndex(svec2 position) + std::size_t positionToIndex(dmath::svec2 position) { auto size_diff_log2 = std::abs(tile_size_log2_.x() - tile_size_log2_.y()); auto flip = tile_size_log2_.x() < tile_size_log2_.y(); position = flip ? position.yx() : position; auto result = position.y() & ~(~std::size_t{0} << size_diff_log2); position.y() >>= size_diff_log2; - result |= dutils::interleaveZeros(static_cast(position.x())) << size_diff_log2; - result |= dutils::interleaveZeros(static_cast(position.y())) << (size_diff_log2 + 1); + result |= dutils::interleaveZeros(position.x()) << size_diff_log2; + result |= dutils::interleaveZeros(position.y()) << (size_diff_log2 + 1); return result; } - svec2 tile_size_log2_; + dmath::svec2 tile_size_log2_; std::vector tiles_; std::size_t first_free_tile_ = 0; std::size_t max_tiles_; @@ -244,27 +266,42 @@ class TextureAtlasTiles { { return lhs.data_ == rhs.data_; } + [[nodiscard]] friend bool operator!=(const TileHandle& lhs, const TileHandle& rhs) noexcept { return !(lhs == rhs); } - auto atlasPixelSize() const { return dataOrThrow().atlas_info->atlas_size; } - auto pixelPos() const { return dataOrThrow().placement.position.xy(); } - auto pixelSize() const { return dataOrThrow().bordered_image_data.size(); } + auto atlasPixelSize(std::size_t mipmap_layer = 0) const + { + return dataOrThrow().atlas_info->atlas_size >> mipmap_layer; + } + + auto pixelPos(std::size_t mipmap_layer = 0) const { return dataOrThrow().placement.pixelPos(mipmap_layer); } + + auto pixelSize(std::size_t mipmap_layer = 0) const { return dataOrThrow().mipmap_levels[mipmap_layer].size(); } + + auto pixelPosAndLayer(std::size_t mipmap_layer = 0) const + { + return dataOrThrow().placement.pixelPosAndLayer(mipmap_layer); + } - auto pos() const { return static_cast(pixelPos()) / vec2(static_cast(atlasPixelSize())); } - auto size() const { return static_cast(pixelSize()) / vec2(static_cast(atlasPixelSize())); } + auto pos() const { return static_cast(pixelPos()) / static_cast(atlasPixelSize()); } + auto size() const { return static_cast(pixelSize()) / static_cast(atlasPixelSize()); } bounds2 bounds() const { - auto padding = dataOrThrow().bordered_image_data.padding(); - auto inset = static_cast(padding) / (2.0f * atlasPixelSize()); - return {pos() + inset, pos() + size() - inset}; + auto padding = dataOrThrow().mipmap_levels[0].padding(); + auto inset = static_cast(padding) / (2.0f * atlasPixelSize()); + auto top_left = pos(); + auto bottom_right = top_left + size(); + return bounds2(top_left, bottom_right).inset(inset); } auto layer() const { return dataOrThrow().placement.position.z(); } + auto mipmapLevels() const { return data_->mipmap_levels.size(); } + private: TileHandle(const std::shared_ptr& data) : data_(data) @@ -284,12 +321,7 @@ class TextureAtlasTiles { /// @exception std::invalid_argument if either maximum is less than zero. TextureAtlasTiles(const TextureAtlasLimits& limits) : limits_(limits) - { - if (limits.max_texture_size < 0) - throw std::invalid_argument("Maximum texture size cannot be negative."); - if (limits.max_layer_count < 0) - throw std::invalid_argument("Maximum layer count cannot be negative."); - } + {} TextureAtlasTiles(const TextureAtlasTiles&) = delete; TextureAtlasTiles(TextureAtlasTiles&&) = default; @@ -300,9 +332,9 @@ class TextureAtlasTiles { /// @exception std::invalid_argument if the image does not contain any data. /// @exception std::invalid_argument if the image is too big. /// @exception std::length_error if a new layer would exceed the maximum layer count. - [[nodiscard]] TileHandle add(TBorderedImageData bordered_image_data) + [[nodiscard]] TileHandle add(MipmapLevels mipmap_levels) { - return TileHandle(emplaceTile(std::move(bordered_image_data))); + return TileHandle(emplaceTile(std::move(mipmap_levels))); } /// @brief Checks if a tile with the given name exists. @@ -341,13 +373,13 @@ class TextureAtlasTiles { } /// @brief Similar to updateTexture, but also frees image data and returns a frozen atlas. - [[nodiscard]] FrozenTextureAtlasTiles freeze(const TextureResizeFunction& resize, - const TextureModifyFunction& modify) && + [[nodiscard]] FrozenTextureAtlasTiles freeze(const TextureResizeFunction& resize, + const TextureModifyFunction& modify) && { ensureTextureSize(resize); for (auto& layer : layers_) layer.drawTiles(modify, true); - return FrozenTextureAtlasTiles(std::move(*this)); + return FrozenTextureAtlasTiles(std::move(*this)); } // Not really useful outside of debugging and unit-test: @@ -363,36 +395,37 @@ class TextureAtlasTiles { void ensureTextureSize(const TextureResizeFunction& resize) { auto required_size = maxLayerSize(); - auto layers = static_cast(layers_.size()); - if (!resize(required_size, layers, 1)) + if (required_size == 0) + return; + auto layers = layers_.size(); + auto mipmap_levels = maxMipmapLevels(required_size); + if (!resize(required_size, layers, mipmap_levels)) return; for (const auto& tile : tiles_) tile->placement.written = false; } /// @brief Finds the maximum layer size. - GLsizei maxLayerSize() const + std::size_t maxLayerSize() const { - GLsizei result = 0; + std::size_t result = 0; for (auto& layer : layers_) result = std::max(result, layer.requiredTextureSize()); return result; } - using LayerResult = std::pair::Layer*, std::size_t>; + using LayerResult = std::pair::Layer*, std::size_t>; /// @brief Returns a pointer to a (possibly newly created) layer for the given tile size and its index. /// @remark The pointer can be null, in which case a new layer would have exceeded the maximum layer count. - LayerResult layerForTile(const svec2& size) + LayerResult layerForTile(const dmath::svec2& size) { - auto unsigned_width = static_cast>(size.x()); - auto unsigned_height = static_cast>(size.y()); - auto tile_size_log2 = svec2(static_cast(dutils::ilog2ceil(unsigned_width)), - static_cast(dutils::ilog2ceil(unsigned_height))); + auto tile_size_log2 = dmath::svec2(static_cast(dutils::ilog2ceil(size.x())), + static_cast(dutils::ilog2ceil(size.y()))); auto layer_iter = std::find_if(begin(layers_), end(layers_), [&](const Layer& layer) { return !layer.full() && layer.tileSizeLog2() == tile_size_log2; }); - auto layer_index = std::distance(begin(layers_), layer_iter); + auto layer_index = static_cast(std::distance(begin(layers_), layer_iter)); if (layer_iter != end(layers_)) return {&*layer_iter, layer_index}; if (layer_index >= limits_.max_layer_count) @@ -404,24 +437,24 @@ class TextureAtlasTiles { /// @exception std::invalid_argument if the image does not contain any data. /// @exception std::invalid_argument if the image is too big. /// @exception std::length_error if a new layer would exceed the maximum layer count. - const std::shared_ptr& emplaceTile(TBorderedImageData&& bordered_image_data) + const std::shared_ptr& emplaceTile(MipmapLevels&& mipmap_levels) { - if (!bordered_image_data) + if (!mipmap_levels.fullImage()) throw std::invalid_argument("Image does not contain data."); - auto size = bordered_image_data.size(); + auto size = mipmap_levels.fullImage().size(); if (size.greaterThan(limits_.max_texture_size).any()) throw std::invalid_argument("Image is too big for texture atlas. (" + size.format() + " > " + std::to_string(limits_.max_texture_size) + ")"); - auto [layer, index] = layerForTile(static_cast(size)); + auto [layer, index] = layerForTile(size); if (!layer) throw std::length_error("Too many texture atlas layers. (max " + std::to_string(limits_.max_layer_count) + ")"); - const auto& tile = tiles_.emplace_back(std::make_shared(std::move(bordered_image_data), atlas_info_)); - layer->addTile(*tile, static_cast(index)); + const auto& tile = tiles_.emplace_back(std::make_shared(std::move(mipmap_levels), atlas_info_)); + layer->addTile(*tile, index); atlas_info_->atlas_size = maxLayerSize(); return tile; } @@ -456,23 +489,24 @@ class TextureAtlasTiles { template class FrozenTextureAtlasTiles { public: - using TileHandle = typename TextureAtlasTiles::TileHandle; + using BorderedImageData = TBorderedImageData; + using TileHandle = typename TextureAtlasTiles::TileHandle; FrozenTextureAtlasTiles(const FrozenTextureAtlasTiles&) = delete; FrozenTextureAtlasTiles(FrozenTextureAtlasTiles&&) = default; FrozenTextureAtlasTiles& operator=(const FrozenTextureAtlasTiles&) = delete; FrozenTextureAtlasTiles& operator=(FrozenTextureAtlasTiles&&) = default; - friend class TextureAtlasTiles; + friend class TextureAtlasTiles; [[nodiscard]] bool contains(const TileHandle& tile_handle) const { return tiles_.contains(tile_handle); } private: - FrozenTextureAtlasTiles(TextureAtlasTiles&& tiles) + FrozenTextureAtlasTiles(TextureAtlasTiles&& tiles) : tiles_(std::move(tiles)) {} - TextureAtlasTiles tiles_; + TextureAtlasTiles tiles_; }; } // namespace dang::gl diff --git a/dang-gl/include/dang-gl/Texturing/TextureAtlasUtils.h b/dang-gl/include/dang-gl/Texturing/TextureAtlasUtils.h index 7e011a54..22bd8f98 100644 --- a/dang-gl/include/dang-gl/Texturing/TextureAtlasUtils.h +++ b/dang-gl/include/dang-gl/Texturing/TextureAtlasUtils.h @@ -5,9 +5,9 @@ namespace dang::gl::TextureAtlasUtils { -GLsizei checkMaxTextureSize(std::optional max_texture_size); -GLsizei checkMaxLayerCount(std::optional max_layer_count); +std::size_t checkMaxTextureSize(std::optional max_texture_size); +std::size_t checkMaxLayerCount(std::optional max_layer_count); -TextureAtlasLimits checkLimits(std::optional max_texture_size, std::optional max_layer_count); +TextureAtlasLimits checkLimits(std::optional max_texture_size, std::optional max_layer_count); } // namespace dang::gl::TextureAtlasUtils diff --git a/dang-gl/src/Image/BorderedImageMipmapper.cpp b/dang-gl/src/Image/BorderedImageMipmapper.cpp new file mode 100644 index 00000000..0c8863f9 --- /dev/null +++ b/dang-gl/src/Image/BorderedImageMipmapper.cpp @@ -0,0 +1 @@ +#include "dang-gl/Image/BorderedImageMipmapper.h" diff --git a/dang-gl/src/Image/ImageMipmapper.cpp b/dang-gl/src/Image/ImageMipmapper.cpp new file mode 100644 index 00000000..40e6b157 --- /dev/null +++ b/dang-gl/src/Image/ImageMipmapper.cpp @@ -0,0 +1 @@ +#include "dang-gl/Image/ImageMipmapper.h" diff --git a/dang-gl/src/Image/MipmapLevels.cpp b/dang-gl/src/Image/MipmapLevels.cpp new file mode 100644 index 00000000..e761a885 --- /dev/null +++ b/dang-gl/src/Image/MipmapLevels.cpp @@ -0,0 +1 @@ +#include "dang-gl/Image/MipmapLevels.h" diff --git a/dang-gl/src/Texturing/TextureAtlasUtils.cpp b/dang-gl/src/Texturing/TextureAtlasUtils.cpp index aae46622..8e58fcfa 100644 --- a/dang-gl/src/Texturing/TextureAtlasUtils.cpp +++ b/dang-gl/src/Texturing/TextureAtlasUtils.cpp @@ -4,9 +4,9 @@ namespace dang::gl::TextureAtlasUtils { -GLsizei checkMaxTextureSize(std::optional max_texture_size) +std::size_t checkMaxTextureSize(std::optional max_texture_size) { - GLint context_max_texture_size = context()->max_3d_texture_size; + auto context_max_texture_size = static_cast(context()->max_3d_texture_size); if (!max_texture_size) return context_max_texture_size; if (*max_texture_size < 1) @@ -17,9 +17,9 @@ GLsizei checkMaxTextureSize(std::optional max_texture_size) return *max_texture_size; } -GLsizei checkMaxLayerCount(std::optional max_layer_count) +std::size_t checkMaxLayerCount(std::optional max_layer_count) { - GLint context_max_layer_count = context()->max_array_texture_layers; + auto context_max_layer_count = static_cast(context()->max_array_texture_layers); if (!max_layer_count) return context_max_layer_count; if (*max_layer_count < 1) @@ -30,7 +30,7 @@ GLsizei checkMaxLayerCount(std::optional max_layer_count) return *max_layer_count; } -TextureAtlasLimits checkLimits(std::optional max_texture_size, std::optional max_layer_count) +TextureAtlasLimits checkLimits(std::optional max_texture_size, std::optional max_layer_count) { return {checkMaxTextureSize(max_texture_size), checkMaxLayerCount(max_layer_count)}; } diff --git a/dang-gl/tests/Texturing/test-TextureAtlasTiles.cpp b/dang-gl/tests/Texturing/test-TextureAtlasTiles.cpp index 12827fda..5997df27 100644 --- a/dang-gl/tests/Texturing/test-TextureAtlasTiles.cpp +++ b/dang-gl/tests/Texturing/test-TextureAtlasTiles.cpp @@ -19,10 +19,11 @@ using Catch::Matchers::Message; using dutils::Stub; using dutils::Matchers::Called; using dutils::Matchers::CalledWith; +using dutils::Matchers::ignored; class TileData { public: - using Size = dgl::svec2; + using Size = dmath::svec2; using Padding = dmath::svec2; TileData() = default; @@ -34,7 +35,7 @@ class TileData { explicit operator bool() const { return data_; } - auto size() const { return static_cast(size_); } + auto size() const { return size_; } void free() { data_ = false; } @@ -53,6 +54,7 @@ class TileData { bool data_ = false; }; +using MipmapLevels = dgl::MipmapLevels; using TextureAtlasTiles = dgl::TextureAtlasTiles; using FrozenTextureAtlasTiles = dgl::FrozenTextureAtlasTiles; @@ -77,7 +79,7 @@ struct StringMaker { // Utility -auto tileData(TileData::Padding padding = {}) { return TileData(dgl::svec2(4), padding); } +auto tileData(TileData::Padding padding = {}) { return TileData(TileData::Size(4), padding); } auto atlasTiles() { return TextureAtlasTiles({16, 4}); } @@ -90,8 +92,8 @@ auto atlasTilesWithTileHandle(const TileData::Padding& padding = {}) auto freeze(TextureAtlasTiles atlas_tiles) { - auto resize = [](GLsizei, GLsizei, GLsizei) { return true; }; - auto modify = [](const TileData&, dgl::ivec3, GLint) {}; + auto resize = [](std::size_t, std::size_t, std::size_t) { return true; }; + auto modify = [](const TileData&, dmath::svec3, std::size_t) {}; return std::move(atlas_tiles).freeze(resize, modify); } @@ -103,6 +105,12 @@ auto frozenTilesWithTileHandle() return std::pair{freeze(std::move(atlas_tiles)), tile_handle}; } +auto applyMipmap(const TileData& tile_data) +{ + auto mipmapper = [](const TileData& tile_data) { return TileData(dgl::nextMipmapSize(tile_data.size())); }; + return MipmapLevels(tile_data, mipmapper); +} + TEST_CASE("TextureAtlasTiles can be constructed and moved.", "[texturing][texture-atlas-tiles]") { SECTION("Constructing TextureAtlasTiles.") @@ -112,13 +120,6 @@ TEST_CASE("TextureAtlasTiles can be constructed and moved.", "[texturing][textur CHECK_NOTHROW(TextureAtlasTiles({16, 0})); CHECK_NOTHROW(TextureAtlasTiles({0, 0})); } - SECTION("Constructing TextureAtlasTiles with invalid limits throws an invalid_argument exception.") - { - CHECK_THROWS_MATCHES( - TextureAtlasTiles({-1, 4}), std::invalid_argument, Message("Maximum texture size cannot be negative.")); - CHECK_THROWS_MATCHES( - TextureAtlasTiles({16, -1}), std::invalid_argument, Message("Maximum layer count cannot be negative.")); - } SECTION("TextureAtlasTiles cannot be copied, but can be moved.") { STATIC_REQUIRE_FALSE(std::is_copy_assignable_v); @@ -274,17 +275,18 @@ TEST_CASE("TextureAtlasTiles allows arbitrary removal of tiles that were added p TEST_CASE("TextureAtlasTiles can be filled with tiles of the same size, spanning multiple layers.", "[texturing][texture-atlas-tiles]") { - auto max_texture_size = GENERATE(0, 1, 2, 4); - auto max_layer_count = GENERATE(range(0, 5)); - auto tile_width = GENERATE(range(1, 5)); - auto tile_height = GENERATE(range(1, 5)); + auto max_texture_size = GENERATE(as{}, 0, 1, 2, 4); + auto max_layer_count = GENERATE(range(0, 5)); + auto tile_width = GENERATE(range(1, 5)); + auto tile_height = GENERATE(range(1, 5)); - auto tile_size = dgl::svec2(tile_width, tile_height); + auto tile_size = TileData::Size(tile_width, tile_height); + auto tile_data = GENERATE_COPY(MipmapLevels(TileData(tile_size)), applyMipmap(TileData(tile_size))); // Tiles are aligned on powers of two: - auto tile_width_log2 = dutils::ilog2ceil(static_cast(tile_width)); - auto tile_height_log2 = dutils::ilog2ceil(static_cast(tile_height)); - auto tile_size_log2 = dgl::svec2(tile_width_log2, tile_height_log2); + auto tile_width_log2 = dutils::ilog2ceil(tile_width); + auto tile_height_log2 = dutils::ilog2ceil(tile_height); + auto tile_size_log2 = TileData::Size(tile_width_log2, tile_height_log2); auto tile_size_pow2 = 1 << tile_size_log2; auto tile_area_pow2 = tile_size_pow2.product(); @@ -298,7 +300,7 @@ TEST_CASE("TextureAtlasTiles can be filled with tiles of the same size, spanning DYNAMIC_SECTION("Atlas of size " << max_texture_size << " cannot fit any tile of size " << tile_size << ".\nAdding a single tile should throw an invalid_argument exception.") { - CHECK_THROWS_AS(atlas_tiles.add(TileData(tile_size)), std::invalid_argument); + CHECK_THROWS_AS(atlas_tiles.add(tile_data), std::invalid_argument); } } else { @@ -307,10 +309,10 @@ TEST_CASE("TextureAtlasTiles can be filled with tiles of the same size, spanning << "Atlas has " << max_layer_count << " layers and should therefore fit " << total_tiles << " tiles.") { - for (GLsizei layer = 0; layer < max_layer_count; layer++) { - std::set tile_positions; - for (GLsizei i = 0; i < tiles_per_layer; i++) { - auto tile = atlas_tiles.add(TileData(tile_size)); + for (std::size_t layer = 0; layer < max_layer_count; layer++) { + std::set tile_positions; + for (std::size_t i = 0; i < tiles_per_layer; i++) { + auto tile = atlas_tiles.add(tile_data); auto tile_pos = tile.pixelPos(); auto [_, position_is_unique] = tile_positions.insert(tile_pos); @@ -328,7 +330,7 @@ TEST_CASE("TextureAtlasTiles can be filled with tiles of the same size, spanning SECTION("Adding one more tile should throw a length_error exception.") { - CHECK_THROWS_AS(atlas_tiles.add(TileData(tile_size)), std::length_error); + CHECK_THROWS_AS(atlas_tiles.add(tile_data), std::length_error); } } } @@ -336,21 +338,23 @@ TEST_CASE("TextureAtlasTiles can be filled with tiles of the same size, spanning TEST_CASE("TextureAtlasTiles can be used to update a texture.", "[texturing]") { - auto resize = dutils::Stub(); + auto resize = dutils::Stub(); resize.setInfo({"resize", {"required_size", "layer_count", "mipmap_levels"}}); - dutils::Stub modify; + dutils::Stub modify; modify.setInfo({"modify", {"tile_data", "offset", "mipmap_level"}}); - auto size = GENERATE(4, 8, 16); - auto tile_size = GENERATE(1, 2, 4); - auto layers = GENERATE(1, 2, 4); + auto size = GENERATE(as{}, 4, 8, 16); + auto mipmap_levels = dgl::maxMipmapLevels(size); + auto tile_size = GENERATE(as{}, 1, 2, 4); + auto layers = GENERATE(as{}, 1, 2, 4); + auto tile_data = GENERATE_COPY(MipmapLevels(TileData(tile_size)), applyMipmap(TileData(tile_size))); auto tile_count = dutils::sqr(size) / dutils::sqr(tile_size) * layers; auto atlas_tiles = TextureAtlasTiles({size, layers}); - for (auto i = 0; i < tile_count; i++) - (void)atlas_tiles.add(TileData(TileData::Size(tile_size))); + for (std::size_t i = 0; i < tile_count; i++) + (void)atlas_tiles.add(tile_data); SECTION("Using the updateTexture method, which allows further modifications.") { @@ -361,10 +365,10 @@ TEST_CASE("TextureAtlasTiles can be used to update a texture.", "[texturing]") auto frozen_atlas = std::move(atlas_tiles).freeze(resize, modify); } - CHECK_THAT(resize, CalledWith(size, layers, 1)); - CHECK_THAT(modify, Called(tile_count)); + CHECK_THAT(resize, CalledWith(size, layers, mipmap_levels)); + CHECK_THAT(modify, Called(tile_count * tile_data.count())); - std::set> positions_and_mipmap_levels; + std::set> positions_and_mipmap_levels; for (const auto& [tile_data, offset, mipmap_level] : modify.invocations()) { auto [_, position_and_mipmap_level_is_unique] = positions_and_mipmap_levels.insert({offset, mipmap_level}); CHECK(position_and_mipmap_level_is_unique); @@ -435,15 +439,15 @@ TEST_CASE("TextureAtlasTiles::TileHandle provides information about a created ti SECTION("A valid tile handle provides information about its tile data.") { auto padding = GENERATE(dmath::svec2(0), dmath::svec2(1), dmath::svec2(2)); - auto expected_bounds = dgl::bounds2(1).inset((static_cast(padding) * 0.5f) / 4.0f); + auto expected_bounds = dmath::bounds2(1).inset((static_cast(padding) * 0.5f) / 4.0f); auto [atlas_tiles, tile_handle] = atlasTilesWithTileHandle(padding); CHECK(tile_handle.atlasPixelSize() == 4); - CHECK(tile_handle.pixelPos() == dgl::svec2()); - CHECK(tile_handle.pixelSize() == dmath::svec2(4)); - CHECK(tile_handle.pos() == dgl::vec2()); - CHECK(tile_handle.size() == dgl::vec2(1)); + CHECK(tile_handle.pixelPos() == 0); + CHECK(tile_handle.pixelSize() == 4); + CHECK(tile_handle.pos() == 0.0f); + CHECK(tile_handle.size() == 1.0f); CHECK(tile_handle.bounds() == expected_bounds); CHECK(tile_handle.layer() == 0); } @@ -493,7 +497,7 @@ TEST_CASE("TextureAtlasTiles::TileHandle provides information about a created ti tile_handle.reset(); CHECK_FALSE(tile_handle); - SECTION("Reseting an empty tile handle does nothing.") + SECTION("Resetting an empty tile handle does nothing.") { tile_handle.reset(); CHECK_FALSE(tile_handle); diff --git a/dang-gl/tests/Texturing/test-TextureAtlasUtils.cpp b/dang-gl/tests/Texturing/test-TextureAtlasUtils.cpp index 7161fe96..daf02a60 100644 --- a/dang-gl/tests/Texturing/test-TextureAtlasUtils.cpp +++ b/dang-gl/tests/Texturing/test-TextureAtlasUtils.cpp @@ -18,8 +18,9 @@ TEST_CASE("TextureAtlasUtils can be used to query limits for texture atlases.", SECTION("For maximum texture size, if no value is given, GL_MAX_3D_TEXTURE_SIZE is returned.") { + auto expected_size = static_cast(dgl::context()->max_3d_texture_size); auto max_texture_size = dgl::TextureAtlasUtils::checkMaxTextureSize(std::nullopt); - CHECK(max_texture_size == dgl::context()->max_3d_texture_size); + CHECK(max_texture_size == expected_size); SECTION("If the given value is in range, it is returned.") { @@ -38,8 +39,9 @@ TEST_CASE("TextureAtlasUtils can be used to query limits for texture atlases.", SECTION("For maximum layer count, if no value is given, GL_MAX_ARRAY_TEXTURE_LAYERS is returned.") { + auto expected_size = static_cast(dgl::context()->max_3d_texture_size); auto max_layer_count = dgl::TextureAtlasUtils::checkMaxLayerCount(std::nullopt); - CHECK(max_layer_count == dgl::context()->max_array_texture_layers); + CHECK(max_layer_count == expected_size); SECTION("If the given value is in range, it is returned.") { diff --git a/dang-utils/include/dang-utils/utils.h b/dang-utils/include/dang-utils/utils.h index b0048df2..8a08e678 100644 --- a/dang-utils/include/dang-utils/utils.h +++ b/dang-utils/include/dang-utils/utils.h @@ -398,4 +398,10 @@ template return value * value; } +template +[[nodiscard]] constexpr auto absoluteDifference(const T& a, const T& b) +{ + return a < b ? b - a : a - b; +} + } // namespace dang::utils