Skip to content
This repository was archived by the owner on Mar 15, 2025. It is now read-only.

Allow for mipmapping in texture atlas. #50

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions dang-gl/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 26 additions & 0 deletions dang-gl/include/dang-gl/Image/BorderedImageMipmapper.h
Original file line number Diff line number Diff line change
@@ -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 <typename TCalcType = float>
struct BorderedImageMipmapper {
template <std::size_t v_dim, PixelFormat v_pixel_format, PixelType v_pixel_type, std::size_t v_row_alignment>
auto operator()(const BorderedImage<v_dim, v_pixel_format, v_pixel_type, v_row_alignment>& bordered_image) const
{
// Mipmaps entire image including border, but sets border of result to "none".
using BorderedImage = BorderedImage<v_dim, v_pixel_format, v_pixel_type, v_row_alignment>;
return BorderedImage(image_mipmapper<TCalcType>(bordered_image.image()));
}
};

template <typename TCalcType = float>
inline constexpr BorderedImageMipmapper<TCalcType> bordered_image_mipmapper;

} // namespace dang::gl
70 changes: 70 additions & 0 deletions dang-gl/include/dang-gl/Image/ImageMipmapper.h
Original file line number Diff line number Diff line change
@@ -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 <typename TCalcType = float>
struct ImageMipmapper {
template <std::size_t v_dim, PixelFormat v_pixel_format, PixelType v_pixel_type, std::size_t v_row_alignment>
auto operator()(const Image<v_dim, v_pixel_format, v_pixel_type, v_row_alignment>& image) const
{
using Image = Image<v_dim, v_pixel_format, v_pixel_type, v_row_alignment>;
using Pixel = typename Image::Pixel;
using Size = typename Image::Size;
using CalcPixel = dmath::Vector<TCalcType, Pixel::dim>;

// 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<CalcPixel>(image[pos * box_size + offset]);
result[pos] = static_cast<Pixel>(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<CalcPixel>(image[Size(x * box_size, y * box_size + offset)]);
result[Size(x, y)] = static_cast<Pixel>(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<CalcPixel>(image[Size(x * box_size + offset, y * box_size)]);
result[Size(x, y)] = static_cast<Pixel>(color / box_size);
}
}

if (odd_width && odd_height)
result[floor_size] = image[floor_size * box_size];

return result;
}
};

template <typename TCalcType = float>
inline constexpr ImageMipmapper<TCalcType> image_mipmapper;

} // namespace dang::gl
77 changes: 77 additions & 0 deletions dang-gl/include/dang-gl/Image/MipmapLevels.h
Original file line number Diff line number Diff line change
@@ -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 <typename TBorderedImageData>
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 <typename TMipmapper>
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 <typename TMipmapper>
static auto generateMipmapLevels(BorderedImageData&& full_image, TMipmapper mipmapper)
{
auto count = maxMipmapLevels(full_image.size().maxValue());
std::vector<BorderedImageData> 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<BorderedImageData> mipmap_levels_;
};

} // namespace dang::gl
5 changes: 3 additions & 2 deletions dang-gl/include/dang-gl/Objects/ObjectType.h
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ enum class TextureTarget {
Texture2DMultisampleArray,
Texture3D,
TextureCubeMap,
TextureCubeMapArray,
TextureRectangle,

COUNT
Expand Down Expand Up @@ -148,8 +149,8 @@ inline constexpr dutils::EnumArray<TextureTarget, GLenum> gl_constants<TextureTa
GL_TEXTURE_2D_MULTISAMPLE_ARRAY,
GL_TEXTURE_3D,
GL_TEXTURE_CUBE_MAP,
GL_TEXTURE_RECTANGLE,
};
GL_TEXTURE_CUBE_MAP_ARRAY,
GL_TEXTURE_RECTANGLE};

/// @brief Maps from framebuffer targets to their respective constants, which need to be supplied to the
/// glBindFramebuffer function.
Expand Down
53 changes: 20 additions & 33 deletions dang-gl/include/dang-gl/Objects/Texture.h
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ class TextureBaseTyped : public TextureBase {
subImage(std::make_index_sequence<v_dim>(), 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();
Expand Down Expand Up @@ -504,7 +504,7 @@ class TextureBaseRegular : public TextureBaseTyped<v_dim, v_target> {
{}

/// @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<v_dim> size,
std::optional<GLsizei> mipmap_levels = std::nullopt,
PixelInternalFormat internal_format = PixelInternalFormat::RGBA8)
Expand All @@ -514,7 +514,7 @@ class TextureBaseRegular : public TextureBaseTyped<v_dim, v_target> {
}

/// @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 <PixelFormat v_pixel_format, PixelType v_pixel_type, std::size_t v_row_alignment>
explicit TextureBaseRegular(const Image<v_dim, v_pixel_format, v_pixel_type, v_row_alignment>& image,
Expand All @@ -531,7 +531,7 @@ class TextureBaseRegular : public TextureBaseTyped<v_dim, v_target> {
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<v_dim> size,
std::optional<GLsizei> mipmap_levels = std::nullopt,
PixelInternalFormat internal_format = PixelInternalFormat::RGBA8)
Expand All @@ -541,7 +541,7 @@ class TextureBaseRegular : public TextureBaseTyped<v_dim, v_target> {
}

/// @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 <PixelFormat v_pixel_format, PixelType v_pixel_type, std::size_t v_row_alignment>
void generate(const Image<v_dim, v_pixel_format, v_pixel_type, v_row_alignment>& image,
Expand All @@ -553,38 +553,17 @@ class TextureBaseRegular : public TextureBaseTyped<v_dim, v_target> {
storage(
std::make_index_sequence<v_dim>(), static_cast<svec<v_dim>>(image.size()), mipmap_levels, internal_format);
this->subImage(std::make_index_sequence<v_dim>(), image);
glGenerateMipmap(toGLConstant(v_target));
}

protected:
TextureBaseRegular(TextureBaseRegular&&) = default;
TextureBaseRegular& operator=(TextureBaseRegular&&) = default;

private:
/// @brief Returns the biggest component of a given vector.
template <std::size_t... v_indices>
static GLsizei maxSize(svec<v_dim> size, std::index_sequence<v_indices...>)
{
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<v_dim> 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<v_dim>()));
return dutils::ilog2(static_cast<std::make_unsigned_t<GLsizei>>(value)) + 1;
}

/// @brief Calls glTexStorage with the provided parameters and index sequence of the textures dimension.
Expand All @@ -594,10 +573,18 @@ class TextureBaseRegular : public TextureBaseTyped<v_dim, v_target> {
std::optional<GLsizei> mipmap_levels = std::nullopt,
PixelInternalFormat internal_format = PixelInternalFormat::RGBA8)
{
glTexStorage<v_dim>(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());
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

size.x() is just a GLsizei and does not have .maxValue().

Can I just use only .maxValue() everywhere and get rid of these if constexpr checks?

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<v_dim>(toGLConstant(v_target), *mipmap_levels, toGLConstant(internal_format), size[v_indices]...);
this->setSize(size);
}
};
Expand Down
25 changes: 15 additions & 10 deletions dang-gl/include/dang-gl/Texturing/MultiTextureAtlas.h
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down Expand Up @@ -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<v_pixel_format>);
for (auto& texture : textures_) {
texture = Texture2DArray(svec3(static_cast<GLsizei>(required_size),
static_cast<GLsizei>(required_size),
static_cast<GLsizei>(layers)),
static_cast<GLsizei>(mipmap_levels),
pixel_format_internal_v<v_pixel_format>);
}
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<TSubTextureEnum>)
textures_[sub_texture].modify(bordered_image_data[sub_texture], offset, mipmap_level);
textures_[sub_texture].modify(
bordered_image_data[sub_texture], static_cast<GLint>(offset), static_cast<GLint>(mipmap_level));
};

private:
Expand Down Expand Up @@ -115,8 +120,8 @@ class MultiTextureAtlas
using Base = TextureAtlasBase<
detail::TextureAtlasMultiTexture<TSubTextureEnum, v_pixel_format, v_pixel_type, v_row_alignment>>;

explicit MultiTextureAtlas(std::optional<GLsizei> max_texture_size = std::nullopt,
std::optional<GLsizei> max_layer_count = std::nullopt)
explicit MultiTextureAtlas(std::optional<std::size_t> max_texture_size = std::nullopt,
std::optional<std::size_t> max_layer_count = std::nullopt)
: Base(TextureAtlasUtils::checkLimits(max_texture_size, max_layer_count))
{}
};
Expand Down
Loading