💿🐜 Antkeeper source code https://antkeeper.com
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1064 lines
25 KiB

/*
* Copyright (C) 2023 Christopher J. Howard
*
* This file is part of Antkeeper source code.
*
* Antkeeper source code is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Antkeeper source code is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Antkeeper source code. If not, see <http://www.gnu.org/licenses/>.
*/
#include <engine/gl/image.hpp>
#include <engine/gl/cube-map.hpp>
#include <engine/gl/opengl/gl-format-lut.hpp>
#include <engine/resources/resource-loader.hpp>
#include <engine/resources/deserialize-error.hpp>
#include <engine/resources/deserializer.hpp>
#include <engine/debug/log.hpp>
#include <cmath>
#include <stdexcept>
#include <glad/gl.h>
#include <stb/stb_image.h>
#include <tinyexr.h>
namespace gl {
image::image
(
std::uint8_t dimensionality,
gl::format format,
std::uint32_t width,
std::uint32_t height,
std::uint32_t depth,
std::uint32_t mip_levels,
std::uint32_t array_layers,
std::uint32_t flags
)
{
const auto format_index = std::to_underlying(format);
const auto gl_internal_format = gl_format_lut[format_index][0];
const auto gl_type = gl_format_lut[format_index][2];
if (gl_internal_format == 0 || gl_type == 0)
{
throw std::invalid_argument("Image construction used unsupported format.");
}
if (!width || !height || !depth)
{
throw std::invalid_argument("Image dimensions must be nonzero.");
}
if (!mip_levels)
{
throw std::invalid_argument("Image mip levels must be nonzero.");
}
if (mip_levels > static_cast<std::uint32_t>(std::bit_width(std::max(std::max(width, height), depth))))
{
throw std::out_of_range("Image mip levels exceed `1 + log2(max(width, height, depth))`.");
}
if (!array_layers)
{
throw std::invalid_argument("Image array layers must be nonzero.");
}
if (dimensionality == 1)
{
if (height > 1 || depth > 1)
{
throw std::invalid_argument("1D image must have a height and depth of `1`.");
}
}
else if (dimensionality == 2)
{
if (depth > 1)
{
throw std::invalid_argument("2D image must have a depth of `1`.");
}
}
else if (dimensionality == 3)
{
if (array_layers > 1)
{
throw std::invalid_argument("3D image arrays not supported.");
}
}
if (flags & std::to_underlying(image_flag::cube_compatible))
{
if (dimensionality != 2)
{
throw std::invalid_argument("Cube compatible image must be 2D.");
}
if (width != height)
{
throw std::invalid_argument("Cube compatible image width and height must be equal.");
}
if (array_layers % 6 != 0)
{
throw std::invalid_argument("Cube compatible image array layers must be a multiple of 6.");
}
}
m_dimensionality = dimensionality;
m_format = format;
m_dimensions = {width, height, depth};
m_mip_levels = mip_levels;
m_array_layers = array_layers;
m_flags = flags;
if (m_array_layers == 1)
{
switch (m_dimensionality)
{
case 1:
m_gl_texture_target = GL_TEXTURE_1D;
glCreateTextures(m_gl_texture_target, 1, &m_gl_texture_name);
glTextureStorage1D
(
m_gl_texture_name,
static_cast<GLsizei>(m_mip_levels),
gl_internal_format,
static_cast<GLsizei>(m_dimensions[0])
);
break;
case 2:
m_gl_texture_target = GL_TEXTURE_2D;
glCreateTextures(m_gl_texture_target, 1, &m_gl_texture_name);
glTextureStorage2D
(
m_gl_texture_name,
static_cast<GLsizei>(m_mip_levels),
gl_internal_format,
static_cast<GLsizei>(m_dimensions[0]),
static_cast<GLsizei>(m_dimensions[1])
);
break;
case 3:
m_gl_texture_target = GL_TEXTURE_3D;
glCreateTextures(m_gl_texture_target, 1, &m_gl_texture_name);
glTextureStorage3D
(
m_gl_texture_name,
static_cast<GLsizei>(m_mip_levels),
gl_internal_format,
static_cast<GLsizei>(m_dimensions[0]),
static_cast<GLsizei>(m_dimensions[1]),
static_cast<GLsizei>(m_dimensions[2])
);
break;
default:
break;
}
}
else
{
switch (m_dimensionality)
{
case 1:
m_gl_texture_target = GL_TEXTURE_1D_ARRAY;
glCreateTextures(m_gl_texture_target, 1, &m_gl_texture_name);
glTextureStorage2D
(
m_gl_texture_name,
static_cast<GLsizei>(m_mip_levels),
gl_internal_format,
static_cast<GLsizei>(m_dimensions[0]),
static_cast<GLsizei>(m_array_layers)
);
break;
case 2:
if (is_cube_compatible())
{
if (m_array_layers == 6)
{
m_gl_texture_target = GL_TEXTURE_CUBE_MAP;
glCreateTextures(m_gl_texture_target, 1, &m_gl_texture_name);
glTextureStorage2D
(
m_gl_texture_name,
static_cast<GLsizei>(m_mip_levels),
gl_internal_format,
static_cast<GLsizei>(m_dimensions[0]),
static_cast<GLsizei>(m_dimensions[1])
);
}
else
{
m_gl_texture_target = GL_TEXTURE_CUBE_MAP_ARRAY;
glCreateTextures(m_gl_texture_target, 1, &m_gl_texture_name);
glTextureStorage3D
(
m_gl_texture_name,
static_cast<GLsizei>(m_mip_levels),
gl_internal_format,
static_cast<GLsizei>(m_dimensions[0]),
static_cast<GLsizei>(m_dimensions[1]),
static_cast<GLsizei>(m_array_layers)
);
}
}
else
{
m_gl_texture_target = GL_TEXTURE_2D_ARRAY;
glCreateTextures(m_gl_texture_target, 1, &m_gl_texture_name);
glTextureStorage3D
(
m_gl_texture_name,
static_cast<GLsizei>(m_mip_levels),
gl_internal_format,
static_cast<GLsizei>(m_dimensions[0]),
static_cast<GLsizei>(m_dimensions[1]),
static_cast<GLsizei>(m_array_layers)
);
}
break;
default:
break;
}
}
}
image::~image()
{
glDeleteTextures(1, &m_gl_texture_name);
}
void image::read
(
std::uint32_t mip_level,
std::uint32_t offset_x,
std::uint32_t offset_y,
std::uint32_t offset_z,
std::uint32_t width,
std::uint32_t height,
std::uint32_t depth,
gl::format format,
std::span<std::byte> data
) const
{
if (mip_level >= m_mip_levels)
{
throw std::out_of_range("Image read operation mip level out of range.");
}
const auto format_index = std::to_underlying(format);
const auto gl_base_format = gl_format_lut[format_index][1];
const auto gl_type = gl_format_lut[format_index][2];
if (gl_base_format == 0 || gl_type == 0)
{
throw std::invalid_argument("Image read operation used unsupported format.");
}
glGetTextureSubImage
(
m_gl_texture_name,
static_cast<GLint>(mip_level),
static_cast<GLint>(offset_x),
static_cast<GLint>(offset_y),
static_cast<GLint>(offset_z),
static_cast<GLsizei>(width),
static_cast<GLsizei>(height),
static_cast<GLsizei>(depth),
gl_base_format,
gl_type,
static_cast<GLsizei>(data.size()),
data.data()
);
}
void image::write
(
std::uint32_t mip_level,
std::uint32_t offset_x,
std::uint32_t offset_y,
std::uint32_t offset_z,
std::uint32_t width,
std::uint32_t height,
std::uint32_t depth,
gl::format format,
std::span<const std::byte> data
)
{
if (mip_level >= m_mip_levels)
{
throw std::out_of_range("Image write operation mip level out of range.");
}
const auto format_index = std::to_underlying(format);
const auto gl_base_format = gl_format_lut[format_index][1];
const auto gl_type = gl_format_lut[format_index][2];
if (gl_base_format == 0 || gl_type == 0)
{
throw std::invalid_argument("Image write operation used unsupported format.");
}
if (m_array_layers == 1)
{
if ((offset_x + width > std::max<std::uint32_t>(1, m_dimensions[0] >> mip_level)) ||
(offset_y + height > std::max<std::uint32_t>(1, m_dimensions[1] >> mip_level)) ||
(offset_z + depth > std::max<std::uint32_t>(1, m_dimensions[2] >> mip_level)))
{
throw std::out_of_range("Image write operation exceeded image bounds.");
}
switch (m_dimensionality)
{
case 1:
glTextureSubImage1D
(
m_gl_texture_name,
static_cast<GLint>(mip_level),
static_cast<GLint>(offset_x),
static_cast<GLsizei>(width),
gl_base_format,
gl_type,
data.data()
);
break;
case 2:
glTextureSubImage2D
(
m_gl_texture_name,
static_cast<GLint>(mip_level),
static_cast<GLint>(offset_x),
static_cast<GLint>(offset_y),
static_cast<GLsizei>(width),
static_cast<GLsizei>(height),
gl_base_format,
gl_type,
data.data()
);
break;
case 3:
glTextureSubImage3D
(
m_gl_texture_name,
static_cast<GLint>(mip_level),
static_cast<GLint>(offset_x),
static_cast<GLint>(offset_y),
static_cast<GLint>(offset_z),
static_cast<GLsizei>(width),
static_cast<GLsizei>(height),
static_cast<GLsizei>(depth),
gl_base_format,
gl_type,
data.data()
);
break;
default:
break;
}
}
else
{
switch (m_dimensionality)
{
case 1:
if ((offset_x + width > std::max<std::uint32_t>(1, m_dimensions[0] >> mip_level)) ||
(offset_y + height > m_array_layers) ||
(offset_z + depth > 1))
{
throw std::out_of_range("Image write operation exceeded image dimensions.");
}
glTextureSubImage2D
(
m_gl_texture_name,
static_cast<GLint>(mip_level),
static_cast<GLint>(offset_x),
static_cast<GLint>(offset_y),
static_cast<GLsizei>(width),
static_cast<GLsizei>(height),
gl_base_format,
gl_type,
data.data()
);
break;
case 2:
if ((offset_x + width > std::max<std::uint32_t>(1, m_dimensions[0] >> mip_level)) ||
(offset_y + height > std::max<std::uint32_t>(1, m_dimensions[1] >> mip_level)) ||
(offset_z + depth > m_array_layers))
{
throw std::out_of_range("Image write operation exceeded image bounds.");
}
glTextureSubImage3D
(
m_gl_texture_name,
static_cast<GLint>(mip_level),
static_cast<GLint>(offset_x),
static_cast<GLint>(offset_y),
static_cast<GLint>(offset_z),
static_cast<GLsizei>(width),
static_cast<GLsizei>(height),
static_cast<GLsizei>(depth),
gl_base_format,
gl_type,
data.data()
);
break;
default:
break;
}
}
}
void image::copy
(
std::uint32_t src_mip_level,
std::uint32_t src_x,
std::uint32_t src_y,
std::uint32_t src_z,
image& dst_image,
std::uint32_t dst_mip_level,
std::uint32_t dst_x,
std::uint32_t dst_y,
std::uint32_t dst_z,
std::uint32_t width,
std::uint32_t height,
std::uint32_t depth
) const
{
glCopyImageSubData
(
m_gl_texture_name,
m_gl_texture_target,
static_cast<GLint>(src_mip_level),
static_cast<GLint>(src_x),
static_cast<GLint>(src_y),
static_cast<GLint>(src_z),
dst_image.m_gl_texture_name,
dst_image.m_gl_texture_target,
static_cast<GLint>(dst_mip_level),
static_cast<GLint>(dst_x),
static_cast<GLint>(dst_y),
static_cast<GLint>(dst_z),
static_cast<GLsizei>(width),
static_cast<GLsizei>(height),
static_cast<GLsizei>(depth)
);
}
void image::generate_mipmaps()
{
if (m_mip_levels > 1)
{
glGenerateTextureMipmap(m_gl_texture_name);
}
}
image_1d::image_1d
(
gl::format format,
std::uint32_t width,
std::uint32_t mip_levels,
std::uint32_t array_layers,
std::uint32_t flags
):
image
(
1,
format,
width,
1,
1,
mip_levels,
array_layers,
flags
)
{}
image_2d::image_2d
(
gl::format format,
std::uint32_t width,
std::uint32_t height,
std::uint32_t mip_levels,
std::uint32_t array_layers,
std::uint32_t flags
):
image
(
2,
format,
width,
height,
1,
mip_levels,
array_layers,
flags
)
{}
image_3d::image_3d
(
gl::format format,
std::uint32_t width,
std::uint32_t height,
std::uint32_t depth,
std::uint32_t mip_levels,
std::uint32_t flags
):
image
(
3,
format,
width,
height,
depth,
mip_levels,
1,
flags
)
{}
image_cube::image_cube
(
gl::format format,
std::uint32_t width,
std::uint32_t mip_levels,
std::uint32_t array_layers
):
image_2d
(
format,
width,
width,
mip_levels,
array_layers,
std::to_underlying(image_flag::cube_compatible)
)
{}
} // namespace gl
namespace {
int stb_io_read(void* user, char* data, int size)
{
deserialize_context& ctx = *static_cast<deserialize_context*>(user);
return static_cast<int>(ctx.read8(reinterpret_cast<std::byte*>(data), static_cast<std::size_t>(size)));
}
void stb_io_skip(void* user, int n)
{
deserialize_context& ctx = *static_cast<deserialize_context*>(user);
ctx.seek(ctx.tell() + n);
}
int stb_io_eof(void* user)
{
deserialize_context& ctx = *static_cast<deserialize_context*>(user);
return static_cast<int>(ctx.eof());
}
struct stb_image_deleter
{
void operator()(void* p) const
{
stbi_image_free(p);
}
};
[[nodiscard]] std::unique_ptr<gl::image> load_image_stb_image(deserialize_context& ctx, std::uint8_t dimensionality, std::uint32_t mip_levels)
{
// Setup IO callbacks
const stbi_io_callbacks io_callbacks
{
&stb_io_read,
&stb_io_skip,
&stb_io_eof
};
// Determine image bit depth
std::size_t component_size = stbi_is_16_bit_from_callbacks(&io_callbacks, &ctx) ? sizeof(std::uint16_t) : sizeof(std::uint8_t);
ctx.seek(0);
// Set vertical flip on load in order to correctly upload pixel data to OpenGL
stbi_set_flip_vertically_on_load(true);
// Load image data
std::unique_ptr<void, stb_image_deleter> data;
int width;
int height;
int components;
gl::format format;
if (component_size == sizeof(std::uint16_t))
{
// Load 16-bit image data
data = std::unique_ptr<void, stb_image_deleter>(stbi_load_16_from_callbacks(&io_callbacks, &ctx, &width, &height, &components, 0));
// Determine 16-bit image format
format = [components]()
{
switch (components)
{
case 1:
return gl::format::r16_unorm;
case 2:
return gl::format::r16g16_unorm;
case 3:
return gl::format::r16g16b16_unorm;
case 4:
return gl::format::r16g16b16a16_unorm;
default:
return gl::format::undefined;
}
}();
}
else
{
// Load 8-bit image data
data = std::unique_ptr<void, stb_image_deleter>(stbi_load_from_callbacks(&io_callbacks, &ctx, &width, &height, &components, 0));
// Determine 8-bit image format
format = [components]()
{
switch (components)
{
case 1:
return gl::format::r8_unorm;
case 2:
return gl::format::r8g8_unorm;
case 3:
return gl::format::r8g8b8_unorm;
case 4:
return gl::format::r8g8b8a8_unorm;
default:
return gl::format::undefined;
}
}();
}
// Check if image data was loaded
if (!data)
{
throw deserialize_error(stbi_failure_reason());
}
// Determine number mip levels
if (!mip_levels)
{
mip_levels = static_cast<std::uint32_t>(std::bit_width(static_cast<std::uint32_t>(std::max(width, height))));
}
// Allocate image
std::unique_ptr<gl::image> image;
switch (dimensionality)
{
case 1:
image = std::make_unique<gl::image_1d>
(
format,
static_cast<std::uint32_t>(std::max(width, height)),
mip_levels
);
break;
case 2:
image = std::make_unique<gl::image_2d>
(
format,
static_cast<std::uint32_t>(width),
static_cast<std::uint32_t>(height),
mip_levels
);
break;
case 3:
image = std::make_unique<gl::image_3d>
(
format,
static_cast<std::uint32_t>(width),
static_cast<std::uint32_t>(height),
1,
mip_levels
);
break;
default:
break;
}
// Upload image data to image
image->write
(
0,
0,
0,
0,
image->get_dimensions()[0],
image->get_dimensions()[1],
image->get_dimensions()[2],
format,
{
reinterpret_cast<const std::byte*>(data.get()),
image->get_dimensions()[0] *
image->get_dimensions()[1] *
image->get_dimensions()[2] *
static_cast<std::size_t>(components) *
component_size
}
);
// Generate mipmaps
image->generate_mipmaps();
return image;
}
[[nodiscard]] std::unique_ptr<gl::image> load_image_tinyexr(deserialize_context& ctx, std::uint8_t dimensionality, std::uint32_t mip_levels)
{
const char* error = nullptr;
auto tinyexr_error = [&error]()
{
const std::string error_message(error);
FreeEXRErrorMessage(error);
throw deserialize_error(error_message);
};
// Read data into file buffer
std::vector<unsigned char> file_buffer(ctx.size());
ctx.read8(reinterpret_cast<std::byte*>(file_buffer.data()), file_buffer.size());
// Read EXR version
EXRVersion exr_version;
if (ParseEXRVersionFromMemory(&exr_version, file_buffer.data(), file_buffer.size()) != TINYEXR_SUCCESS)
{
tinyexr_error();
}
// Check if image is multipart
if (exr_version.multipart)
{
throw deserialize_error("OpenEXR multipart images not supported.");
}
// Load image header
EXRHeader exr_header;
InitEXRHeader(&exr_header);
if (ParseEXRHeaderFromMemory(&exr_header, &exr_version, file_buffer.data(), file_buffer.size(), &error) != TINYEXR_SUCCESS)
{
tinyexr_error();
}
// Check if image is tiled
if (exr_header.tiled)
{
FreeEXRHeader(&exr_header);
throw deserialize_error("OpenEXR tiled images not supported.");
}
// Check if image has a supported number of channels
if (exr_header.num_channels < 1 || exr_header.num_channels > 4)
{
FreeEXRHeader(&exr_header);
throw deserialize_error("OpenEXR images must have 1-4 channels.");
}
// Check if all channels have the same format
for (int i = 1; i < exr_header.num_channels; ++i)
{
if (exr_header.pixel_types[i] != exr_header.pixel_types[i - 1])
{
FreeEXRHeader(&exr_header);
throw deserialize_error("OpenEXR images must have the same pixel type per channel.");
}
}
// Load image data
EXRImage exr_image;
InitEXRImage(&exr_image);
if (LoadEXRImageFromMemory(&exr_image, &exr_header, file_buffer.data(), file_buffer.size(), &error) != TINYEXR_SUCCESS)
{
FreeEXRHeader(&exr_header);
tinyexr_error();
}
// Free file buffer
file_buffer.clear();
// Determine image format
constexpr gl::format uint_formats[4] =
{
gl::format::r32_uint,
gl::format::r32g32_uint,
gl::format::r32g32b32_uint,
gl::format::r32g32b32a32_uint
};
constexpr gl::format half_formats[4] =
{
gl::format::r16_sfloat,
gl::format::r16g16_sfloat,
gl::format::r16g16b16_sfloat,
gl::format::r16g16b16a16_sfloat
};
constexpr gl::format float_formats[4] =
{
gl::format::r32_sfloat,
gl::format::r32g32_sfloat,
gl::format::r32g32b32_sfloat,
gl::format::r32g32b32a32_sfloat
};
gl::format format;
int component_size;
switch (exr_header.pixel_types[0])
{
case TINYEXR_PIXELTYPE_UINT:
format = uint_formats[exr_header.num_channels - 1];
component_size = static_cast<int>(sizeof(std::uint32_t));
break;
case TINYEXR_PIXELTYPE_HALF:
format = half_formats[exr_header.num_channels - 1];
component_size = static_cast<int>(sizeof(std::uint16_t));//sizeof(float16_t)
break;
case TINYEXR_PIXELTYPE_FLOAT:
format = float_formats[exr_header.num_channels - 1];
component_size = static_cast<int>(sizeof(float));//sizeof(float32_t)
break;
default:
format = gl::format::undefined;
component_size = 0;
break;
}
// Allocate interleaved image data
std::vector<std::byte> data(static_cast<std::size_t>(exr_image.width * exr_image.height * exr_header.num_channels * component_size));
// Interleave image data from layers
std::byte* component = data.data();
for (auto y = exr_image.height - 1; y >= 0; --y)
{
const auto row_offset = y * exr_image.width;
for (auto x = 0; x < exr_image.width; ++x)
{
const auto byte_offset = (row_offset + x) * component_size;
for (auto c = exr_image.num_channels - 1; c >= 0; --c)
{
std::memcpy(component, exr_image.images[c] + byte_offset, static_cast<std::size_t>(component_size));
component += component_size;
}
}
}
// Store image dimensions
const auto width = static_cast<std::uint32_t>(exr_image.width);
const auto height = static_cast<std::uint32_t>(exr_image.height);
// Free loaded image data and image header
FreeEXRImage(&exr_image);
FreeEXRHeader(&exr_header);
// Determine number mip levels
if (!mip_levels)
{
mip_levels = static_cast<std::uint32_t>(std::bit_width(std::max(width, height)));
}
// Allocate image
std::unique_ptr<gl::image> image;
switch (dimensionality)
{
case 1:
image = std::make_unique<gl::image_1d>
(
format,
std::max(width, height),
mip_levels
);
break;
case 2:
image = std::make_unique<gl::image_2d>
(
format,
width,
height,
mip_levels
);
break;
case 3:
image = std::make_unique<gl::image_3d>
(
format,
width,
height,
1,
mip_levels
);
break;
default:
break;
}
// Upload interleaved image data to image
image->write
(
0,
0,
0,
0,
image->get_dimensions()[0],
image->get_dimensions()[1],
image->get_dimensions()[2],
format,
data
);
// Generate mipmaps
image->generate_mipmaps();
return image;
}
[[nodiscard]] std::unique_ptr<gl::image> load_image(deserialize_context& ctx, std::uint8_t dimensionality, std::uint32_t mip_levels)
{
// Select loader according to file extension
if (ctx.path().extension() == ".exr")
{
// Load EXR images with TinyEXR
return load_image_tinyexr(ctx, dimensionality, mip_levels);
}
else
{
// Load other image formats with stb_image
return load_image_stb_image(ctx, dimensionality, mip_levels);
}
}
}
template <>
std::unique_ptr<gl::image_1d> resource_loader<gl::image_1d>::load(::resource_manager& resource_manager, deserialize_context& ctx)
{
return std::unique_ptr<gl::image_1d>(static_cast<gl::image_1d*>(load_image(ctx, 1, 0).release()));
}
template <>
std::unique_ptr<gl::image_2d> resource_loader<gl::image_2d>::load(::resource_manager& resource_manager, deserialize_context& ctx)
{
return std::unique_ptr<gl::image_2d>(static_cast<gl::image_2d*>(load_image(ctx, 2, 0).release()));
}
template <>
std::unique_ptr<gl::image_3d> resource_loader<gl::image_3d>::load(::resource_manager& resource_manager, deserialize_context& ctx)
{
return std::unique_ptr<gl::image_3d>(static_cast<gl::image_3d*>(load_image(ctx, 3, 0).release()));
}
template <>
std::unique_ptr<gl::image_cube> resource_loader<gl::image_cube>::load(::resource_manager& resource_manager, deserialize_context& ctx)
{
// Load cube map
auto cube_map = std::unique_ptr<gl::image_2d>(static_cast<gl::image_2d*>(load_image(ctx, 2, 1).release()));
// Determine cube map layout
const auto layout = gl::infer_cube_map_layout(cube_map->get_dimensions()[0], cube_map->get_dimensions()[1]);
if (layout == gl::cube_map_layout::unknown)
{
throw deserialize_error("Failed to load cube image from cube map with unknown layout.");
}
else if (layout == gl::cube_map_layout::equirectangular || layout == gl::cube_map_layout::spherical)
{
throw deserialize_error("Failed to load cube image from cube map with unsupported layout.");
}
// Determine cube map face width
const auto face_width = gl::infer_cube_map_face_width(cube_map->get_dimensions()[0], cube_map->get_dimensions()[1], layout);
// Allocate cube image
auto image = std::make_unique<gl::image_cube>
(
cube_map->get_format(),
face_width,
static_cast<std::uint32_t>(std::bit_width(face_width))
);
// Vertical cross layout face offsets
constexpr std::uint32_t vcross_offsets[6][2] =
{
{2, 2}, {0, 2}, // -x, +x
{1, 3}, {1, 1}, // -y, +y
{1, 0}, {1, 2} // -z, +z
};
// Horizontal cross layout face offsets
constexpr std::uint32_t hcross_offsets[6][2] =
{
{2, 1}, {0, 1}, // -x, +x
{1, 2}, {1, 0}, // -y, +y
{3, 1}, {1, 1} // -z, +z
};
// Copy cube map faces to cube image
switch (layout)
{
case gl::cube_map_layout::column:
for (std::uint32_t i = 0; i < 6; ++i)
{
cube_map->copy(0, 0, face_width * i, 0, *image, 0, 0, 0, i, face_width, face_width, 1);
}
break;
case gl::cube_map_layout::row:
for (std::uint32_t i = 0; i < 6; ++i)
{
cube_map->copy(0, face_width * i, 0, 0, *image, 0, 0, 0, i, face_width, face_width, 1);
}
break;
case gl::cube_map_layout::vertical_cross:
for (std::uint32_t i = 0; i < 6; ++i)
{
cube_map->copy(0, face_width * vcross_offsets[i][0], face_width * vcross_offsets[i][1], 0, *image, 0, 0, 0, i, face_width, face_width, 1);
}
break;
case gl::cube_map_layout::horizontal_cross:
for (std::uint32_t i = 0; i < 6; ++i)
{
cube_map->copy(0, face_width * hcross_offsets[i][0], face_width * hcross_offsets[i][1], 0, *image, 0, 0, 0, i, face_width, face_width, 1);
}
break;
default:
break;
}
// Generate mipmaps
image->generate_mipmaps();
return image;
}