Antkeeper  0.0.1
image.cpp
Go to the documentation of this file.
1 /*
2  * Copyright (C) 2023 Christopher J. Howard
3  *
4  * This file is part of Antkeeper source code.
5  *
6  * Antkeeper source code is free software: you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation, either version 3 of the License, or
9  * (at your option) any later version.
10  *
11  * Antkeeper source code is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with Antkeeper source code. If not, see <http://www.gnu.org/licenses/>.
18  */
19 
20 #include <engine/gl/image.hpp>
21 #include <engine/gl/cube-map.hpp>
26 #include <engine/debug/log.hpp>
27 #include <cmath>
28 #include <stdexcept>
29 #include <glad/gl.h>
30 #include <stb/stb_image.h>
31 #include <tinyexr.h>
32 
33 namespace gl {
34 
36 (
37  std::uint8_t dimensionality,
39  std::uint32_t width,
40  std::uint32_t height,
41  std::uint32_t depth,
42  std::uint32_t mip_levels,
43  std::uint32_t array_layers,
44  std::uint32_t flags
45 )
46 {
47  const auto format_index = std::to_underlying(format);
48  const auto gl_internal_format = gl_format_lut[format_index][0];
49  const auto gl_type = gl_format_lut[format_index][2];
50 
51  if (gl_internal_format == 0 || gl_type == 0)
52  {
53  throw std::invalid_argument("Image construction used unsupported format.");
54  }
55 
56  if (!width || !height || !depth)
57  {
58  throw std::invalid_argument("Image dimensions must be nonzero.");
59  }
60 
61  if (!mip_levels)
62  {
63  throw std::invalid_argument("Image mip levels must be nonzero.");
64  }
65 
66  if (mip_levels > static_cast<std::uint32_t>(std::bit_width(std::max(std::max(width, height), depth))))
67  {
68  throw std::out_of_range("Image mip levels exceed `1 + log2(max(width, height, depth))`.");
69  }
70 
71  if (!array_layers)
72  {
73  throw std::invalid_argument("Image array layers must be nonzero.");
74  }
75 
76  if (dimensionality == 1)
77  {
78  if (height > 1 || depth > 1)
79  {
80  throw std::invalid_argument("1D image must have a height and depth of `1`.");
81  }
82  }
83  else if (dimensionality == 2)
84  {
85  if (depth > 1)
86  {
87  throw std::invalid_argument("2D image must have a depth of `1`.");
88  }
89  }
90  else if (dimensionality == 3)
91  {
92  if (array_layers > 1)
93  {
94  throw std::invalid_argument("3D image arrays not supported.");
95  }
96  }
97 
98  if (flags & std::to_underlying(image_flag::cube_compatible))
99  {
100  if (dimensionality != 2)
101  {
102  throw std::invalid_argument("Cube compatible image must be 2D.");
103  }
104 
105  if (width != height)
106  {
107  throw std::invalid_argument("Cube compatible image width and height must be equal.");
108  }
109 
110  if (array_layers % 6 != 0)
111  {
112  throw std::invalid_argument("Cube compatible image array layers must be a multiple of 6.");
113  }
114  }
115 
116  m_dimensionality = dimensionality;
117  m_format = format;
118  m_dimensions = {width, height, depth};
119  m_mip_levels = mip_levels;
120  m_array_layers = array_layers;
121  m_flags = flags;
122 
123  if (m_array_layers == 1)
124  {
125  switch (m_dimensionality)
126  {
127  case 1:
128  m_gl_texture_target = GL_TEXTURE_1D;
129  glCreateTextures(m_gl_texture_target, 1, &m_gl_texture_name);
130  glTextureStorage1D
131  (
132  m_gl_texture_name,
133  static_cast<GLsizei>(m_mip_levels),
134  gl_internal_format,
135  static_cast<GLsizei>(m_dimensions[0])
136  );
137  break;
138 
139  case 2:
140  m_gl_texture_target = GL_TEXTURE_2D;
141  glCreateTextures(m_gl_texture_target, 1, &m_gl_texture_name);
142  glTextureStorage2D
143  (
144  m_gl_texture_name,
145  static_cast<GLsizei>(m_mip_levels),
146  gl_internal_format,
147  static_cast<GLsizei>(m_dimensions[0]),
148  static_cast<GLsizei>(m_dimensions[1])
149  );
150  break;
151 
152  case 3:
153  m_gl_texture_target = GL_TEXTURE_3D;
154  glCreateTextures(m_gl_texture_target, 1, &m_gl_texture_name);
155  glTextureStorage3D
156  (
157  m_gl_texture_name,
158  static_cast<GLsizei>(m_mip_levels),
159  gl_internal_format,
160  static_cast<GLsizei>(m_dimensions[0]),
161  static_cast<GLsizei>(m_dimensions[1]),
162  static_cast<GLsizei>(m_dimensions[2])
163  );
164  break;
165 
166  default:
167  break;
168  }
169  }
170  else
171  {
172  switch (m_dimensionality)
173  {
174  case 1:
175  m_gl_texture_target = GL_TEXTURE_1D_ARRAY;
176  glCreateTextures(m_gl_texture_target, 1, &m_gl_texture_name);
177  glTextureStorage2D
178  (
179  m_gl_texture_name,
180  static_cast<GLsizei>(m_mip_levels),
181  gl_internal_format,
182  static_cast<GLsizei>(m_dimensions[0]),
183  static_cast<GLsizei>(m_array_layers)
184  );
185  break;
186 
187  case 2:
188  if (is_cube_compatible())
189  {
190  if (m_array_layers == 6)
191  {
192  m_gl_texture_target = GL_TEXTURE_CUBE_MAP;
193  glCreateTextures(m_gl_texture_target, 1, &m_gl_texture_name);
194  glTextureStorage2D
195  (
196  m_gl_texture_name,
197  static_cast<GLsizei>(m_mip_levels),
198  gl_internal_format,
199  static_cast<GLsizei>(m_dimensions[0]),
200  static_cast<GLsizei>(m_dimensions[1])
201  );
202  }
203  else
204  {
205  m_gl_texture_target = GL_TEXTURE_CUBE_MAP_ARRAY;
206  glCreateTextures(m_gl_texture_target, 1, &m_gl_texture_name);
207  glTextureStorage3D
208  (
209  m_gl_texture_name,
210  static_cast<GLsizei>(m_mip_levels),
211  gl_internal_format,
212  static_cast<GLsizei>(m_dimensions[0]),
213  static_cast<GLsizei>(m_dimensions[1]),
214  static_cast<GLsizei>(m_array_layers)
215  );
216  }
217  }
218  else
219  {
220  m_gl_texture_target = GL_TEXTURE_2D_ARRAY;
221  glCreateTextures(m_gl_texture_target, 1, &m_gl_texture_name);
222  glTextureStorage3D
223  (
224  m_gl_texture_name,
225  static_cast<GLsizei>(m_mip_levels),
226  gl_internal_format,
227  static_cast<GLsizei>(m_dimensions[0]),
228  static_cast<GLsizei>(m_dimensions[1]),
229  static_cast<GLsizei>(m_array_layers)
230  );
231  }
232  break;
233 
234  default:
235  break;
236  }
237  }
238 }
239 
241 {
242  glDeleteTextures(1, &m_gl_texture_name);
243 }
244 
246 (
247  std::uint32_t mip_level,
248  std::uint32_t offset_x,
249  std::uint32_t offset_y,
250  std::uint32_t offset_z,
251  std::uint32_t width,
252  std::uint32_t height,
253  std::uint32_t depth,
255  std::span<std::byte> data
256 ) const
257 {
258  if (mip_level >= m_mip_levels)
259  {
260  throw std::out_of_range("Image read operation mip level out of range.");
261  }
262 
263  const auto format_index = std::to_underlying(format);
264  const auto gl_base_format = gl_format_lut[format_index][1];
265  const auto gl_type = gl_format_lut[format_index][2];
266 
267  if (gl_base_format == 0 || gl_type == 0)
268  {
269  throw std::invalid_argument("Image read operation used unsupported format.");
270  }
271 
272  glGetTextureSubImage
273  (
274  m_gl_texture_name,
275  static_cast<GLint>(mip_level),
276  static_cast<GLint>(offset_x),
277  static_cast<GLint>(offset_y),
278  static_cast<GLint>(offset_z),
279  static_cast<GLsizei>(width),
280  static_cast<GLsizei>(height),
281  static_cast<GLsizei>(depth),
282  gl_base_format,
283  gl_type,
284  static_cast<GLsizei>(data.size()),
285  data.data()
286  );
287 }
288 
290 (
291  std::uint32_t mip_level,
292  std::uint32_t offset_x,
293  std::uint32_t offset_y,
294  std::uint32_t offset_z,
295  std::uint32_t width,
296  std::uint32_t height,
297  std::uint32_t depth,
299  std::span<const std::byte> data
300 )
301 {
302  if (mip_level >= m_mip_levels)
303  {
304  throw std::out_of_range("Image write operation mip level out of range.");
305  }
306 
307  const auto format_index = std::to_underlying(format);
308  const auto gl_base_format = gl_format_lut[format_index][1];
309  const auto gl_type = gl_format_lut[format_index][2];
310 
311  if (gl_base_format == 0 || gl_type == 0)
312  {
313  throw std::invalid_argument("Image write operation used unsupported format.");
314  }
315 
316  if (m_array_layers == 1)
317  {
318  if ((offset_x + width > std::max<std::uint32_t>(1, m_dimensions[0] >> mip_level)) ||
319  (offset_y + height > std::max<std::uint32_t>(1, m_dimensions[1] >> mip_level)) ||
320  (offset_z + depth > std::max<std::uint32_t>(1, m_dimensions[2] >> mip_level)))
321  {
322  throw std::out_of_range("Image write operation exceeded image bounds.");
323  }
324 
325  switch (m_dimensionality)
326  {
327  case 1:
328  glTextureSubImage1D
329  (
330  m_gl_texture_name,
331  static_cast<GLint>(mip_level),
332  static_cast<GLint>(offset_x),
333  static_cast<GLsizei>(width),
334  gl_base_format,
335  gl_type,
336  data.data()
337  );
338  break;
339 
340  case 2:
341  glTextureSubImage2D
342  (
343  m_gl_texture_name,
344  static_cast<GLint>(mip_level),
345  static_cast<GLint>(offset_x),
346  static_cast<GLint>(offset_y),
347  static_cast<GLsizei>(width),
348  static_cast<GLsizei>(height),
349  gl_base_format,
350  gl_type,
351  data.data()
352  );
353  break;
354 
355  case 3:
356  glTextureSubImage3D
357  (
358  m_gl_texture_name,
359  static_cast<GLint>(mip_level),
360  static_cast<GLint>(offset_x),
361  static_cast<GLint>(offset_y),
362  static_cast<GLint>(offset_z),
363  static_cast<GLsizei>(width),
364  static_cast<GLsizei>(height),
365  static_cast<GLsizei>(depth),
366  gl_base_format,
367  gl_type,
368  data.data()
369  );
370  break;
371 
372  default:
373  break;
374  }
375  }
376  else
377  {
378  switch (m_dimensionality)
379  {
380  case 1:
381  if ((offset_x + width > std::max<std::uint32_t>(1, m_dimensions[0] >> mip_level)) ||
382  (offset_y + height > m_array_layers) ||
383  (offset_z + depth > 1))
384  {
385  throw std::out_of_range("Image write operation exceeded image dimensions.");
386  }
387 
388  glTextureSubImage2D
389  (
390  m_gl_texture_name,
391  static_cast<GLint>(mip_level),
392  static_cast<GLint>(offset_x),
393  static_cast<GLint>(offset_y),
394  static_cast<GLsizei>(width),
395  static_cast<GLsizei>(height),
396  gl_base_format,
397  gl_type,
398  data.data()
399  );
400  break;
401 
402  case 2:
403  if ((offset_x + width > std::max<std::uint32_t>(1, m_dimensions[0] >> mip_level)) ||
404  (offset_y + height > std::max<std::uint32_t>(1, m_dimensions[1] >> mip_level)) ||
405  (offset_z + depth > m_array_layers))
406  {
407  throw std::out_of_range("Image write operation exceeded image bounds.");
408  }
409 
410  glTextureSubImage3D
411  (
412  m_gl_texture_name,
413  static_cast<GLint>(mip_level),
414  static_cast<GLint>(offset_x),
415  static_cast<GLint>(offset_y),
416  static_cast<GLint>(offset_z),
417  static_cast<GLsizei>(width),
418  static_cast<GLsizei>(height),
419  static_cast<GLsizei>(depth),
420  gl_base_format,
421  gl_type,
422  data.data()
423  );
424  break;
425 
426  default:
427  break;
428  }
429  }
430 }
431 
433 (
434  std::uint32_t src_mip_level,
435  std::uint32_t src_x,
436  std::uint32_t src_y,
437  std::uint32_t src_z,
438  image& dst_image,
439  std::uint32_t dst_mip_level,
440  std::uint32_t dst_x,
441  std::uint32_t dst_y,
442  std::uint32_t dst_z,
443  std::uint32_t width,
444  std::uint32_t height,
445  std::uint32_t depth
446 ) const
447 {
448  glCopyImageSubData
449  (
450  m_gl_texture_name,
451  m_gl_texture_target,
452  static_cast<GLint>(src_mip_level),
453  static_cast<GLint>(src_x),
454  static_cast<GLint>(src_y),
455  static_cast<GLint>(src_z),
456  dst_image.m_gl_texture_name,
457  dst_image.m_gl_texture_target,
458  static_cast<GLint>(dst_mip_level),
459  static_cast<GLint>(dst_x),
460  static_cast<GLint>(dst_y),
461  static_cast<GLint>(dst_z),
462  static_cast<GLsizei>(width),
463  static_cast<GLsizei>(height),
464  static_cast<GLsizei>(depth)
465  );
466 }
467 
469 {
470  if (m_mip_levels > 1)
471  {
472  glGenerateTextureMipmap(m_gl_texture_name);
473  }
474 }
475 
477 (
479  std::uint32_t width,
480  std::uint32_t mip_levels,
481  std::uint32_t array_layers,
482  std::uint32_t flags
483 ):
484  image
485  (
486  1,
487  format,
488  width,
489  1,
490  1,
491  mip_levels,
492  array_layers,
493  flags
494  )
495 {}
496 
498 (
500  std::uint32_t width,
501  std::uint32_t height,
502  std::uint32_t mip_levels,
503  std::uint32_t array_layers,
504  std::uint32_t flags
505 ):
506  image
507  (
508  2,
509  format,
510  width,
511  height,
512  1,
513  mip_levels,
514  array_layers,
515  flags
516  )
517 {}
518 
520 (
522  std::uint32_t width,
523  std::uint32_t height,
524  std::uint32_t depth,
525  std::uint32_t mip_levels,
526  std::uint32_t flags
527 ):
528  image
529  (
530  3,
531  format,
532  width,
533  height,
534  depth,
535  mip_levels,
536  1,
537  flags
538  )
539 {}
540 
542 (
544  std::uint32_t width,
545  std::uint32_t mip_levels,
546  std::uint32_t array_layers
547 ):
548  image_2d
549  (
550  format,
551  width,
552  width,
553  mip_levels,
554  array_layers,
555  std::to_underlying(image_flag::cube_compatible)
556  )
557 {}
558 
559 } // namespace gl
560 
561 namespace {
562 
563  int stb_io_read(void* user, char* data, int size)
564  {
565  deserialize_context& ctx = *static_cast<deserialize_context*>(user);
566  return static_cast<int>(ctx.read8(reinterpret_cast<std::byte*>(data), static_cast<std::size_t>(size)));
567  }
568 
569  void stb_io_skip(void* user, int n)
570  {
571  deserialize_context& ctx = *static_cast<deserialize_context*>(user);
572  ctx.seek(ctx.tell() + n);
573  }
574 
575  int stb_io_eof(void* user)
576  {
577  deserialize_context& ctx = *static_cast<deserialize_context*>(user);
578  return static_cast<int>(ctx.eof());
579  }
580 
581  struct stb_image_deleter
582  {
583  void operator()(void* p) const
584  {
585  stbi_image_free(p);
586  }
587  };
588 
589  [[nodiscard]] std::unique_ptr<gl::image> load_image_stb_image(deserialize_context& ctx, std::uint8_t dimensionality, std::uint32_t mip_levels)
590  {
591  // Setup IO callbacks
592  const stbi_io_callbacks io_callbacks
593  {
594  &stb_io_read,
595  &stb_io_skip,
596  &stb_io_eof
597  };
598 
599  // Determine image bit depth
600  std::size_t component_size = stbi_is_16_bit_from_callbacks(&io_callbacks, &ctx) ? sizeof(std::uint16_t) : sizeof(std::uint8_t);
601  ctx.seek(0);
602 
603  // Set vertical flip on load in order to correctly upload pixel data to OpenGL
604  stbi_set_flip_vertically_on_load(true);
605 
606  // Load image data
607  std::unique_ptr<void, stb_image_deleter> data;
608  int width;
609  int height;
610  int components;
612  if (component_size == sizeof(std::uint16_t))
613  {
614  // Load 16-bit image data
615  data = std::unique_ptr<void, stb_image_deleter>(stbi_load_16_from_callbacks(&io_callbacks, &ctx, &width, &height, &components, 0));
616 
617  // Determine 16-bit image format
618  format = [components]()
619  {
620  switch (components)
621  {
622  case 1:
623  return gl::format::r16_unorm;
624  case 2:
626  case 3:
628  case 4:
630  default:
631  return gl::format::undefined;
632  }
633  }();
634  }
635  else
636  {
637  // Load 8-bit image data
638  data = std::unique_ptr<void, stb_image_deleter>(stbi_load_from_callbacks(&io_callbacks, &ctx, &width, &height, &components, 0));
639 
640  // Determine 8-bit image format
641  format = [components]()
642  {
643  switch (components)
644  {
645  case 1:
646  return gl::format::r8_unorm;
647  case 2:
648  return gl::format::r8g8_unorm;
649  case 3:
651  case 4:
653  default:
654  return gl::format::undefined;
655  }
656  }();
657  }
658 
659  // Check if image data was loaded
660  if (!data)
661  {
662  throw deserialize_error(stbi_failure_reason());
663  }
664 
665  // Determine number mip levels
666  if (!mip_levels)
667  {
668  mip_levels = static_cast<std::uint32_t>(std::bit_width(static_cast<std::uint32_t>(std::max(width, height))));
669  }
670 
671  // Allocate image
672  std::unique_ptr<gl::image> image;
673  switch (dimensionality)
674  {
675  case 1:
676  image = std::make_unique<gl::image_1d>
677  (
678  format,
679  static_cast<std::uint32_t>(std::max(width, height)),
680  mip_levels
681  );
682  break;
683 
684  case 2:
685  image = std::make_unique<gl::image_2d>
686  (
687  format,
688  static_cast<std::uint32_t>(width),
689  static_cast<std::uint32_t>(height),
690  mip_levels
691  );
692  break;
693 
694  case 3:
695  image = std::make_unique<gl::image_3d>
696  (
697  format,
698  static_cast<std::uint32_t>(width),
699  static_cast<std::uint32_t>(height),
700  1,
701  mip_levels
702  );
703  break;
704 
705  default:
706  break;
707  }
708 
709  // Upload image data to image
710  image->write
711  (
712  0,
713  0,
714  0,
715  0,
716  image->get_dimensions()[0],
717  image->get_dimensions()[1],
718  image->get_dimensions()[2],
719  format,
720  {
721  reinterpret_cast<const std::byte*>(data.get()),
722  image->get_dimensions()[0] *
723  image->get_dimensions()[1] *
724  image->get_dimensions()[2] *
725  static_cast<std::size_t>(components) *
726  component_size
727  }
728  );
729 
730  // Generate mipmaps
731  image->generate_mipmaps();
732 
733  return image;
734  }
735 
736  [[nodiscard]] std::unique_ptr<gl::image> load_image_tinyexr(deserialize_context& ctx, std::uint8_t dimensionality, std::uint32_t mip_levels)
737  {
738  const char* error = nullptr;
739  auto tinyexr_error = [&error]()
740  {
741  const std::string error_message(error);
742  FreeEXRErrorMessage(error);
743  throw deserialize_error(error_message);
744  };
745 
746  // Read data into file buffer
747  std::vector<unsigned char> file_buffer(ctx.size());
748  ctx.read8(reinterpret_cast<std::byte*>(file_buffer.data()), file_buffer.size());
749 
750  // Read EXR version
751  EXRVersion exr_version;
752  if (ParseEXRVersionFromMemory(&exr_version, file_buffer.data(), file_buffer.size()) != TINYEXR_SUCCESS)
753  {
754  tinyexr_error();
755  }
756 
757  // Check if image is multipart
758  if (exr_version.multipart)
759  {
760  throw deserialize_error("OpenEXR multipart images not supported.");
761  }
762 
763  // Load image header
764  EXRHeader exr_header;
765  InitEXRHeader(&exr_header);
766  if (ParseEXRHeaderFromMemory(&exr_header, &exr_version, file_buffer.data(), file_buffer.size(), &error) != TINYEXR_SUCCESS)
767  {
768  tinyexr_error();
769  }
770 
771  // Check if image is tiled
772  if (exr_header.tiled)
773  {
774  FreeEXRHeader(&exr_header);
775  throw deserialize_error("OpenEXR tiled images not supported.");
776  }
777 
778  // Check if image has a supported number of channels
779  if (exr_header.num_channels < 1 || exr_header.num_channels > 4)
780  {
781  FreeEXRHeader(&exr_header);
782  throw deserialize_error("OpenEXR images must have 1-4 channels.");
783  }
784 
785  // Check if all channels have the same format
786  for (int i = 1; i < exr_header.num_channels; ++i)
787  {
788  if (exr_header.pixel_types[i] != exr_header.pixel_types[i - 1])
789  {
790  FreeEXRHeader(&exr_header);
791  throw deserialize_error("OpenEXR images must have the same pixel type per channel.");
792  }
793  }
794 
795  // Load image data
796  EXRImage exr_image;
797  InitEXRImage(&exr_image);
798  if (LoadEXRImageFromMemory(&exr_image, &exr_header, file_buffer.data(), file_buffer.size(), &error) != TINYEXR_SUCCESS)
799  {
800  FreeEXRHeader(&exr_header);
801  tinyexr_error();
802  }
803 
804  // Free file buffer
805  file_buffer.clear();
806 
807  // Determine image format
808  constexpr gl::format uint_formats[4] =
809  {
814  };
815  constexpr gl::format half_formats[4] =
816  {
821  };
822  constexpr gl::format float_formats[4] =
823  {
828  };
830  int component_size;
831  switch (exr_header.pixel_types[0])
832  {
833  case TINYEXR_PIXELTYPE_UINT:
834  format = uint_formats[exr_header.num_channels - 1];
835  component_size = static_cast<int>(sizeof(std::uint32_t));
836  break;
837 
838  case TINYEXR_PIXELTYPE_HALF:
839  format = half_formats[exr_header.num_channels - 1];
840  component_size = static_cast<int>(sizeof(std::uint16_t));//sizeof(float16_t)
841  break;
842 
843  case TINYEXR_PIXELTYPE_FLOAT:
844  format = float_formats[exr_header.num_channels - 1];
845  component_size = static_cast<int>(sizeof(float));//sizeof(float32_t)
846  break;
847 
848  default:
850  component_size = 0;
851  break;
852  }
853 
854  // Allocate interleaved image data
855  std::vector<std::byte> data(static_cast<std::size_t>(exr_image.width * exr_image.height * exr_header.num_channels * component_size));
856 
857  // Interleave image data from layers
858  std::byte* component = data.data();
859  for (auto y = exr_image.height - 1; y >= 0; --y)
860  {
861  const auto row_offset = y * exr_image.width;
862 
863  for (auto x = 0; x < exr_image.width; ++x)
864  {
865  const auto byte_offset = (row_offset + x) * component_size;
866 
867  for (auto c = exr_image.num_channels - 1; c >= 0; --c)
868  {
869  std::memcpy(component, exr_image.images[c] + byte_offset, static_cast<std::size_t>(component_size));
870  component += component_size;
871  }
872  }
873  }
874 
875  // Store image dimensions
876  const auto width = static_cast<std::uint32_t>(exr_image.width);
877  const auto height = static_cast<std::uint32_t>(exr_image.height);
878 
879  // Free loaded image data and image header
880  FreeEXRImage(&exr_image);
881  FreeEXRHeader(&exr_header);
882 
883  // Determine number mip levels
884  if (!mip_levels)
885  {
886  mip_levels = static_cast<std::uint32_t>(std::bit_width(std::max(width, height)));
887  }
888 
889  // Allocate image
890  std::unique_ptr<gl::image> image;
891  switch (dimensionality)
892  {
893  case 1:
894  image = std::make_unique<gl::image_1d>
895  (
896  format,
897  std::max(width, height),
898  mip_levels
899  );
900  break;
901 
902  case 2:
903  image = std::make_unique<gl::image_2d>
904  (
905  format,
906  width,
907  height,
908  mip_levels
909  );
910  break;
911 
912  case 3:
913  image = std::make_unique<gl::image_3d>
914  (
915  format,
916  width,
917  height,
918  1,
919  mip_levels
920  );
921  break;
922 
923  default:
924  break;
925  }
926 
927  // Upload interleaved image data to image
928  image->write
929  (
930  0,
931  0,
932  0,
933  0,
934  image->get_dimensions()[0],
935  image->get_dimensions()[1],
936  image->get_dimensions()[2],
937  format,
938  data
939  );
940 
941  // Generate mipmaps
942  image->generate_mipmaps();
943 
944  return image;
945  }
946 
947  [[nodiscard]] std::unique_ptr<gl::image> load_image(deserialize_context& ctx, std::uint8_t dimensionality, std::uint32_t mip_levels)
948  {
949  // Select loader according to file extension
950  if (ctx.path().extension() == ".exr")
951  {
952  // Load EXR images with TinyEXR
953  return load_image_tinyexr(ctx, dimensionality, mip_levels);
954  }
955  else
956  {
957  // Load other image formats with stb_image
958  return load_image_stb_image(ctx, dimensionality, mip_levels);
959  }
960  }
961 }
962 
963 template <>
965 {
966  return std::unique_ptr<gl::image_1d>(static_cast<gl::image_1d*>(load_image(ctx, 1, 0).release()));
967 }
968 
969 template <>
971 {
972  return std::unique_ptr<gl::image_2d>(static_cast<gl::image_2d*>(load_image(ctx, 2, 0).release()));
973 }
974 
975 template <>
977 {
978  return std::unique_ptr<gl::image_3d>(static_cast<gl::image_3d*>(load_image(ctx, 3, 0).release()));
979 }
980 
981 template <>
983 {
984  // Load cube map
985  auto cube_map = std::unique_ptr<gl::image_2d>(static_cast<gl::image_2d*>(load_image(ctx, 2, 1).release()));
986 
987  // Determine cube map layout
988  const auto layout = gl::infer_cube_map_layout(cube_map->get_dimensions()[0], cube_map->get_dimensions()[1]);
989  if (layout == gl::cube_map_layout::unknown)
990  {
991  throw deserialize_error("Failed to load cube image from cube map with unknown layout.");
992  }
994  {
995  throw deserialize_error("Failed to load cube image from cube map with unsupported layout.");
996  }
997 
998  // Determine cube map face width
999  const auto face_width = gl::infer_cube_map_face_width(cube_map->get_dimensions()[0], cube_map->get_dimensions()[1], layout);
1000 
1001  // Allocate cube image
1002  auto image = std::make_unique<gl::image_cube>
1003  (
1004  cube_map->get_format(),
1005  face_width,
1006  static_cast<std::uint32_t>(std::bit_width(face_width))
1007  );
1008 
1009  // Vertical cross layout face offsets
1010  constexpr std::uint32_t vcross_offsets[6][2] =
1011  {
1012  {2, 2}, {0, 2}, // -x, +x
1013  {1, 3}, {1, 1}, // -y, +y
1014  {1, 0}, {1, 2} // -z, +z
1015  };
1016 
1017  // Horizontal cross layout face offsets
1018  constexpr std::uint32_t hcross_offsets[6][2] =
1019  {
1020  {2, 1}, {0, 1}, // -x, +x
1021  {1, 2}, {1, 0}, // -y, +y
1022  {3, 1}, {1, 1} // -z, +z
1023  };
1024 
1025  // Copy cube map faces to cube image
1026  switch (layout)
1027  {
1029  for (std::uint32_t i = 0; i < 6; ++i)
1030  {
1031  cube_map->copy(0, 0, face_width * i, 0, *image, 0, 0, 0, i, face_width, face_width, 1);
1032  }
1033  break;
1034 
1036  for (std::uint32_t i = 0; i < 6; ++i)
1037  {
1038  cube_map->copy(0, face_width * i, 0, 0, *image, 0, 0, 0, i, face_width, face_width, 1);
1039  }
1040  break;
1041 
1043  for (std::uint32_t i = 0; i < 6; ++i)
1044  {
1045  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);
1046  }
1047  break;
1048 
1050  for (std::uint32_t i = 0; i < 6; ++i)
1051  {
1052  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);
1053  }
1054  break;
1055 
1056  default:
1057  break;
1058  }
1059 
1060  // Generate mipmaps
1061  image->generate_mipmaps();
1062 
1063  return image;
1064 }
An exception of this type is thrown when an error occurs during deserialization.
1D image.
Definition: image.hpp:257
image_1d(gl::format format, std::uint32_t width, std::uint32_t mip_levels=1, std::uint32_t array_layers=1, std::uint32_t flags=0)
Definition: image.cpp:477
2D image.
Definition: image.hpp:274
image_2d(gl::format format, std::uint32_t width, std::uint32_t height, std::uint32_t mip_levels=1, std::uint32_t array_layers=1, std::uint32_t flags=0)
Definition: image.cpp:498
3D image.
Definition: image.hpp:292
image_3d(gl::format format, std::uint32_t width, std::uint32_t height, std::uint32_t depth, std::uint32_t mip_levels=1, std::uint32_t flags=0)
Definition: image.cpp:520
image_cube(gl::format format, std::uint32_t width, std::uint32_t mip_levels=1, std::uint32_t array_layers=6)
Definition: image.cpp:542
constexpr bool is_cube_compatible() const noexcept
Returns true if the image is cube map compatible, false otherwise.
Definition: image.hpp:198
void 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
Reads pixel data from the image.
Definition: image.cpp:246
void generate_mipmaps()
Generates mip subimages.
Definition: image.cpp:468
void 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)
Writes pixel data to the image.
Definition: image.cpp:290
virtual ~image()=0
Destructs an image.
Definition: image.cpp:240
void 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
Copies pixel data from this image into another the image.
Definition: image.cpp:433
image(const image &)=delete
static std::unique_ptr< T > load(::resource_manager &resource_manager, deserialize_context &ctx)
Loads a resource.
Manages the loading, caching, and saving of resources.
@ error
Error message severity.
Graphics library interface.
Definition: window.hpp:28
image_flag
Image flags.
Definition: image-flag.hpp:29
@ cube_compatible
Cube map view compatible image.
format
Image and vertex formats.
Definition: format.hpp:29
@ r32g32b32a32_sfloat
@ r16g16b16a16_sfloat
@ r16g16b16a16_unorm
std::uint32_t infer_cube_map_face_width(std::uint32_t width, std::uint32_t height, cube_map_layout layout) noexcept
Infers the width of a cube map face from its dimensons and layout.
Definition: cube-map.cpp:54
cube_map_layout infer_cube_map_layout(std::uint32_t width, std::uint32_t height) noexcept
Infers the layout of a cube map from its dimensions.
Definition: cube-map.cpp:24
constexpr GLenum gl_format_lut[][3]
Maps gl::format to OpenGL internal format, base format, and pixel type.
@ column
Faces are stored consecutively in a single column.
@ spherical
Faces are stored in a spherical projection.
@ horizontal_cross
Faces are stored in a horizontal cross.
@ unknown
Unknown layout.
@ equirectangular
Faces are stored in an equirectangular projection.
@ vertical_cross
Faces are stored in a vertical cross.
@ row
Faces are stored consecutively in a single row.
constexpr vector< T, N > max(const vector< T, N > &x, const vector< T, N > &y)
Returns a vector containing the maximum elements of two vectors.
Definition: vector.hpp:1328
Provides access to a deserialization state.
virtual bool eof() const noexcept=0
Returns true if the end of a file was reached.
virtual std::size_t tell() const =0
Returns the offsets from the start of the file to the current position, in bytes.
virtual void seek(std::size_t offset)=0
Seeks to a position in the file.
virtual const std::filesystem::path & path() const noexcept=0
Returns the path associated with this deserialize context.
virtual std::size_t size() const noexcept=0
Returns the size of the file, in bytes.
virtual std::size_t read8(std::byte *data, std::size_t count) noexcept(false)=0
Reads 8-bit (byte) data.