// Copyright (C) 2023 Jérôme "Lynix" Leclercq (lynix680@gmail.com) // This file is part of the "Nazara Engine - Utility module" // For conditions of distribution and use, see copyright notice in Config.hpp #include #include #include #include #include #include #include #include #include #include ///TODO: Rajouter des warnings (Formats compressés avec les méthodes Copy/Update, tests taille dans Copy) ///TODO: Rendre les méthodes exception-safe (faire usage du RAII) ///FIXME: Gérer correctement les formats utilisant moins d'un octet par pixel namespace Nz { namespace { inline unsigned int GetImageLevelSize(unsigned int size, UInt8 level) { if (size == 0) // Possible dans le cas d'une image invalide return 0; return std::max(size >> level, 1U); } inline UInt8* GetPixelPtr(UInt8* base, UInt8 bpp, unsigned int x, unsigned int y, unsigned int z, unsigned int width, unsigned int height) { return &base[(width*(height*z + y) + x)*bpp]; } } bool ImageParams::IsValid() const { return true; // Rien à vérifier } void ImageParams::Merge(const ImageParams& params) { if (loadFormat == PixelFormat::Undefined) loadFormat = params.loadFormat; } Image::Image() : m_sharedImage(&emptyImage) { } Image::Image(ImageType type, PixelFormat format, unsigned int width, unsigned int height, unsigned int depth, UInt8 levelCount) : m_sharedImage(&emptyImage) { ErrorFlags flags(ErrorMode::ThrowException); Create(type, format, width, height, depth, levelCount); } Image::Image(const Image& image) : AbstractImage(image), Resource(), m_sharedImage(image.m_sharedImage) { if (m_sharedImage != &emptyImage) m_sharedImage->refCount++; } Image::Image(SharedImage* sharedImage) : m_sharedImage(sharedImage) { } Image::~Image() { Destroy(); } bool Image::Convert(PixelFormat newFormat) { #if NAZARA_UTILITY_SAFE if (m_sharedImage == &emptyImage) { NazaraError("image must be valid"); return false; } if (!PixelFormatInfo::IsValid(newFormat)) { NazaraError("invalid pixel format"); return false; } if (!PixelFormatInfo::IsConversionSupported(m_sharedImage->format, newFormat)) { NazaraErrorFmt("conversion from {0} to {1} is not supported", PixelFormatInfo::GetName(m_sharedImage->format), PixelFormatInfo::GetName(newFormat)); return false; } #endif if (m_sharedImage->format == newFormat) return true; SharedImage::PixelContainer levels(m_sharedImage->levels.size()); unsigned int width = m_sharedImage->width; unsigned int height = m_sharedImage->height; // Les images 3D et cubemaps sont stockés de la même façon unsigned int depth = (m_sharedImage->type == ImageType::Cubemap) ? 6 : m_sharedImage->depth; for (unsigned int i = 0; i < levels.size(); ++i) { unsigned int pixelsPerFace = width * height; levels[i] = std::make_unique(pixelsPerFace * depth * PixelFormatInfo::GetBytesPerPixel(newFormat)); UInt8* dst = levels[i].get(); UInt8* src = m_sharedImage->levels[i].get(); unsigned int srcStride = pixelsPerFace * PixelFormatInfo::GetBytesPerPixel(m_sharedImage->format); unsigned int dstStride = pixelsPerFace * PixelFormatInfo::GetBytesPerPixel(newFormat); for (unsigned int d = 0; d < depth; ++d) { if (!PixelFormatInfo::Convert(m_sharedImage->format, newFormat, src, &src[srcStride], dst)) { NazaraError("failed to convert image"); return false; } src += srcStride; dst += dstStride; } if (width > 1) width >>= 1; if (height > 1) height >>= 1; if (depth > 1 && m_sharedImage->type != ImageType::Cubemap) depth >>= 1; } SharedImage* newImage = new SharedImage(1, m_sharedImage->type, newFormat, std::move(levels), m_sharedImage->width, m_sharedImage->height, m_sharedImage->depth); ReleaseImage(); m_sharedImage = newImage; return true; } void Image::Copy(const Image& source, const Boxui& srcBox, const Vector3ui& dstPos) { NazaraAssert(IsValid(), "invalid image"); NazaraAssert(source.IsValid(), "invalid source image"); NazaraAssert(source.GetFormat() == m_sharedImage->format, "image formats don't match"); const UInt8* srcPtr = source.GetConstPixels(srcBox.x, srcBox.y, srcBox.z); #if NAZARA_UTILITY_SAFE if (!srcPtr) { NazaraError("failed to access pixels"); return; } #endif UInt8 bpp = PixelFormatInfo::GetBytesPerPixel(m_sharedImage->format); UInt8* dstPtr = GetPixelPtr(m_sharedImage->levels[0].get(), bpp, dstPos.x, dstPos.y, dstPos.z, m_sharedImage->width, m_sharedImage->height); Copy(dstPtr, srcPtr, m_sharedImage->format, srcBox.width, srcBox.height, srcBox.depth, m_sharedImage->width, m_sharedImage->height, source.GetWidth(), source.GetHeight()); } bool Image::Create(ImageType type, PixelFormat format, unsigned int width, unsigned int height, unsigned int depth, UInt8 levelCount) { Destroy(); #if NAZARA_UTILITY_SAFE if (!PixelFormatInfo::IsValid(format)) { NazaraError("invalid pixel format"); return false; } if (width == 0) { NazaraError("width must be at least 1 (0)"); return false; } if (height == 0) { NazaraError("height must be at least 1 (0)"); return false; } if (depth == 0) { NazaraError("depth must be at least 1 (0)"); return false; } switch (type) { case ImageType::E1D: if (height > 1) { NazaraError("1D textures must be 1 tall"); return false; } if (depth > 1) { NazaraError("1D textures must be 1 deep"); return false; } break; case ImageType::E1D_Array: case ImageType::E2D: if (depth > 1) { NazaraError("2D textures must be 1 deep"); return false; } break; case ImageType::E2D_Array: case ImageType::E3D: break; case ImageType::Cubemap: if (depth > 1) { NazaraError("cubemaps must be 1 deep"); return false; } if (width != height) { NazaraError("cubemaps must have square dimensions"); return false; } break; default: NazaraInternalError("Image type not handled"); return false; } #endif levelCount = std::min(levelCount, GetMaxLevel(type, width, height, depth)); SharedImage::PixelContainer levels(levelCount); unsigned int w = width; unsigned int h = height; unsigned int d = (type == ImageType::Cubemap) ? 6 : depth; for (unsigned int i = 0; i < levelCount; ++i) { // Cette allocation est protégée car sa taille dépend directement de paramètres utilisateurs try { levels[i] = std::make_unique(PixelFormatInfo::ComputeSize(format, w, h, d)); if (w > 1) w >>= 1; if (h > 1) h >>= 1; if (d > 1 && type != ImageType::Cubemap) d >>= 1; } catch (const std::exception& e) { NazaraErrorFmt("failed to allocate image's level {0} ({1})", i, e.what()); return false; } } m_sharedImage = new SharedImage(1, type, format, std::move(levels), width, height, depth); return true; } void Image::Destroy() { if (m_sharedImage != &emptyImage) ReleaseImage(); } bool Image::Fill(const Color& color) { ///FIXME: Pourquoi cette méthode alloue une nouvelle image plutôt que de remplir l'existante ? #if NAZARA_UTILITY_SAFE if (m_sharedImage == &emptyImage) { NazaraError("image must be valid"); return false; } if (PixelFormatInfo::IsCompressed(m_sharedImage->format)) { NazaraError("cannot access pixels from compressed image"); return false; } #endif UInt8 bpp = PixelFormatInfo::GetBytesPerPixel(m_sharedImage->format); StackArray colorBuffer = NazaraStackArrayNoInit(UInt8, bpp); if (!PixelFormatInfo::Convert(PixelFormat::RGBA32F, m_sharedImage->format, &color.r, &colorBuffer[0])) { NazaraErrorFmt("failed to convert RGBA32F to {0}", PixelFormatInfo::GetName(m_sharedImage->format)); return false; } SharedImage::PixelContainer levels(m_sharedImage->levels.size()); unsigned int width = m_sharedImage->width; unsigned int height = m_sharedImage->height; // Les images 3D et cubemaps sont stockés de la même façon unsigned int depth = (m_sharedImage->type == ImageType::Cubemap) ? 6 : m_sharedImage->depth; for (auto & level : levels) { std::size_t size = PixelFormatInfo::ComputeSize(m_sharedImage->format, width, height, depth); level = std::make_unique(size); UInt8* ptr = level.get(); UInt8* end = &ptr[size]; while (ptr < end) { std::memcpy(ptr, &colorBuffer[0], bpp); ptr += bpp; } if (width > 1U) width >>= 1; if (height > 1U) height >>= 1; if (depth > 1U && m_sharedImage->type != ImageType::Cubemap) depth >>= 1; } SharedImage* newImage = new SharedImage(1, m_sharedImage->type, m_sharedImage->format, std::move(levels), m_sharedImage->width, m_sharedImage->height, m_sharedImage->depth); ReleaseImage(); m_sharedImage = newImage; return true; } bool Image::Fill(const Color& color, const Boxui& box) { #if NAZARA_UTILITY_SAFE if (m_sharedImage == &emptyImage) { NazaraError("image must be valid"); return false; } if (PixelFormatInfo::IsCompressed(m_sharedImage->format)) { NazaraError("cannot access pixels from compressed image"); return false; } if (!box.IsValid()) { NazaraError("invalid rectangle"); return false; } if (box.x+box.width > m_sharedImage->width || box.y+box.height > m_sharedImage->height || box.z+box.depth > m_sharedImage->depth) { NazaraError("box dimensions are out of bounds"); return false; } #endif EnsureOwnership(); UInt8 bpp = PixelFormatInfo::GetBytesPerPixel(m_sharedImage->format); StackArray colorBuffer = NazaraStackArrayNoInit(UInt8, bpp); if (!PixelFormatInfo::Convert(PixelFormat::RGBA32F, m_sharedImage->format, &color.r, &colorBuffer[0])) { NazaraErrorFmt("failed to convert RGBA32F to {0}", PixelFormatInfo::GetName(m_sharedImage->format)); return false; } ///FIXME: L'algorithme a du mal avec un bpp non multiple de 2 UInt8* dstPixels = GetPixelPtr(m_sharedImage->levels[0].get(), bpp, box.x, box.y, box.z, m_sharedImage->width, m_sharedImage->height); unsigned int srcStride = box.width * bpp; unsigned int dstStride = m_sharedImage->width * bpp; unsigned int faceSize = dstStride * m_sharedImage->height; for (unsigned int z = 0; z < box.depth; ++z) { UInt8* facePixels = dstPixels; for (unsigned int y = 0; y < box.height; ++y) { UInt8* start = facePixels; UInt8* end = facePixels + srcStride; while (start < end) { std::memcpy(start, &colorBuffer[0], bpp); start += bpp; } facePixels += dstStride; } dstPixels += faceSize; } return true; } bool Image::Fill(const Color& color, const Rectui& rect, unsigned int z) { #if NAZARA_UTILITY_SAFE if (m_sharedImage == &emptyImage) { NazaraError("image must be valid"); return false; } if (PixelFormatInfo::IsCompressed(m_sharedImage->format)) { NazaraError("cannot access pixels from compressed image"); return false; } if (!rect.IsValid()) { NazaraError("invalid rectangle"); return false; } if (rect.x + rect.width > m_sharedImage->width || rect.y + rect.height > m_sharedImage->height) { NazaraError("rectangle dimensions are out of bounds"); return false; } unsigned int depth = (m_sharedImage->type == ImageType::Cubemap) ? 6 : m_sharedImage->depth; if (z >= depth) { NazaraErrorFmt("z value exceeds depth ({0} >= {1})", z, depth); return false; } #endif EnsureOwnership(); UInt8 bpp = PixelFormatInfo::GetBytesPerPixel(m_sharedImage->format); StackArray colorBuffer = NazaraStackArrayNoInit(UInt8, bpp); if (!PixelFormatInfo::Convert(PixelFormat::RGBA32F, m_sharedImage->format, &color.r, &colorBuffer[0])) { NazaraErrorFmt("failed to convert RGBA32F to {0}", PixelFormatInfo::GetName(m_sharedImage->format)); return false; } ///FIXME: L'algorithme a du mal avec un bpp non multiple de 2 UInt8* dstPixels = GetPixelPtr(m_sharedImage->levels[0].get(), bpp, rect.x, rect.y, z, m_sharedImage->width, m_sharedImage->height); unsigned int srcStride = rect.width * bpp; unsigned int dstStride = m_sharedImage->width * bpp; for (unsigned int y = 0; y < rect.height; ++y) { UInt8* start = dstPixels; UInt8* end = dstPixels + srcStride; while (start < end) { std::memcpy(start, &colorBuffer[0], bpp); start += bpp; } dstPixels += dstStride; } return true; } bool Image::FlipHorizontally() { #if NAZARA_UTILITY_SAFE if (m_sharedImage == &emptyImage) { NazaraError("image must be valid"); return false; } #endif EnsureOwnership(); unsigned int width = m_sharedImage->width; unsigned int height = m_sharedImage->height; unsigned int depth = (m_sharedImage->type == ImageType::Cubemap) ? 6 : m_sharedImage->depth; for (auto& level : m_sharedImage->levels) { UInt8* ptr = level.get(); if (!PixelFormatInfo::Flip(PixelFlipping::Horizontally, m_sharedImage->format, width, height, depth, ptr, ptr)) { NazaraError("failed to flip image"); return false; } if (width > 1U) width >>= 1; if (height > 1U) height >>= 1; if (depth > 1U && m_sharedImage->type != ImageType::Cubemap) depth >>= 1; } return true; } bool Image::FlipVertically() { #if NAZARA_UTILITY_SAFE if (m_sharedImage == &emptyImage) { NazaraError("image must be valid"); return false; } if (PixelFormatInfo::IsCompressed(m_sharedImage->format)) { NazaraError("cannot flip compressed image"); return false; } #endif EnsureOwnership(); unsigned int width = m_sharedImage->width; unsigned int height = m_sharedImage->height; unsigned int depth = (m_sharedImage->type == ImageType::Cubemap) ? 6 : m_sharedImage->depth; for (auto& level : m_sharedImage->levels) { UInt8* ptr = level.get(); if (!PixelFormatInfo::Flip(PixelFlipping::Vertically, m_sharedImage->format, width, height, depth, ptr, ptr)) { NazaraError("failed to flip image"); return false; } if (width > 1U) width >>= 1; if (height > 1U) height >>= 1; if (depth > 1U && m_sharedImage->type != ImageType::Cubemap) depth >>= 1; } return true; } const UInt8* Image::GetConstPixels(unsigned int x, unsigned int y, unsigned int z, UInt8 level) const { #if NAZARA_UTILITY_SAFE if (m_sharedImage == &emptyImage) { NazaraError("image must be valid"); return nullptr; } if (level >= m_sharedImage->levels.size()) { NazaraErrorFmt("level out of bounds ({0} >= {1})", level, m_sharedImage->levels.size()); return nullptr; } #endif unsigned int width = GetImageLevelSize(m_sharedImage->width, level); #if NAZARA_UTILITY_SAFE if (x >= width) { NazaraErrorFmt("x value exceeds width ({0} >= {1})", x, width); return nullptr; } #endif unsigned int height = GetImageLevelSize(m_sharedImage->height, level); #if NAZARA_UTILITY_SAFE if (y >= height) { NazaraErrorFmt("y value exceeds height ({0} >= {1})", y, height); return nullptr; } unsigned int depth = (m_sharedImage->type == ImageType::Cubemap) ? 6 : GetImageLevelSize(m_sharedImage->depth, level); if (z >= depth) { NazaraErrorFmt("z value exceeds depth ({0} >= {1})", z, depth); return nullptr; } #endif return GetPixelPtr(m_sharedImage->levels[level].get(), PixelFormatInfo::GetBytesPerPixel(m_sharedImage->format), x, y, z, width, height); } unsigned int Image::GetDepth(UInt8 level) const { #if NAZARA_UTILITY_SAFE if (level >= m_sharedImage->levels.size()) { NazaraErrorFmt("level out of bounds ({0} >= {1})", level, m_sharedImage->levels.size()); return 0; } #endif return GetImageLevelSize(m_sharedImage->depth, level); } PixelFormat Image::GetFormat() const { return m_sharedImage->format; } unsigned int Image::GetHeight(UInt8 level) const { #if NAZARA_UTILITY_SAFE if (level >= m_sharedImage->levels.size()) { NazaraErrorFmt("level out of bounds ({0} >= {1})", level, m_sharedImage->levels.size()); return 0; } #endif return GetImageLevelSize(m_sharedImage->height, level); } UInt8 Image::GetLevelCount() const { return UInt8(m_sharedImage->levels.size()); } UInt8 Image::GetMaxLevel() const { return GetMaxLevel(m_sharedImage->type, m_sharedImage->width, m_sharedImage->height, m_sharedImage->depth); } std::size_t Image::GetMemoryUsage() const { unsigned int width = m_sharedImage->width; unsigned int height = m_sharedImage->height; unsigned int depth = m_sharedImage->depth; unsigned int size = 0; for (unsigned int i = 0; i < m_sharedImage->levels.size(); ++i) { size += width * height * depth; if (width > 1) width >>= 1; if (height > 1) height >>= 1; if (depth > 1) depth >>= 1; } if (m_sharedImage->type == ImageType::Cubemap) size *= 6; return size * PixelFormatInfo::GetBytesPerPixel(m_sharedImage->format); } std::size_t Image::GetMemoryUsage(UInt8 level) const { return PixelFormatInfo::ComputeSize(m_sharedImage->format, GetImageLevelSize(m_sharedImage->width, level), GetImageLevelSize(m_sharedImage->height, level), ((m_sharedImage->type == ImageType::Cubemap) ? 6 : GetImageLevelSize(m_sharedImage->depth, level))); } Color Image::GetPixelColor(unsigned int x, unsigned int y, unsigned int z) const { #if NAZARA_UTILITY_SAFE if (m_sharedImage == &emptyImage) { NazaraError("image must be valid"); return Color(); } if (PixelFormatInfo::IsCompressed(m_sharedImage->format)) { NazaraError("cannot access pixels from compressed image"); return Color(); } if (x >= m_sharedImage->width) { NazaraErrorFmt("x value exceeds width ({0} >= {1})", x, m_sharedImage->width); return Color(); } if (y >= m_sharedImage->height) { NazaraErrorFmt("y value exceeds height ({0} >= {1})", y, m_sharedImage->height); return Color(); } unsigned int depth = (m_sharedImage->type == ImageType::Cubemap) ? 6 : m_sharedImage->depth; if (z >= depth) { NazaraErrorFmt("z value exceeds depth ({0} >= {1})", z, depth); return Color(); } #endif const UInt8* pixel = GetPixelPtr(m_sharedImage->levels[0].get(), PixelFormatInfo::GetBytesPerPixel(m_sharedImage->format), x, y, z, m_sharedImage->width, m_sharedImage->height); Color color; if (!PixelFormatInfo::Convert(m_sharedImage->format, PixelFormat::RGBA32F, pixel, &color.r)) NazaraError("failed to convert image's format to RGBA8"); return color; } UInt8* Image::GetPixels(unsigned int x, unsigned int y, unsigned int z, UInt8 level) { #if NAZARA_UTILITY_SAFE if (m_sharedImage == &emptyImage) { NazaraError("image must be valid"); return nullptr; } if (level >= m_sharedImage->levels.size()) { NazaraErrorFmt("level out of bounds ({0} >= {1})", level, m_sharedImage->levels.size()); return nullptr; } #endif unsigned int width = GetImageLevelSize(m_sharedImage->width, level); #if NAZARA_UTILITY_SAFE if (x >= width) { NazaraErrorFmt("x value exceeds width ({0} >= {1})", x, width); return nullptr; } #endif unsigned int height = GetImageLevelSize(m_sharedImage->height, level); #if NAZARA_UTILITY_SAFE if (y >= height) { NazaraErrorFmt("y value exceeds height ({0} >= {1})", y, height); return nullptr; } unsigned int depth = (m_sharedImage->type == ImageType::Cubemap) ? 6 : GetImageLevelSize(m_sharedImage->depth, level); if (z >= depth) { NazaraErrorFmt("z value exceeds depth ({0} >= {1})", z, depth); return nullptr; } if (level >= m_sharedImage->levels.size()) { NazaraErrorFmt("level out of bounds ({0} >= {1})", level, m_sharedImage->levels.size()); return nullptr; } #endif EnsureOwnership(); return GetPixelPtr(m_sharedImage->levels[level].get(), PixelFormatInfo::GetBytesPerPixel(m_sharedImage->format), x, y, z, width, height); } Vector3ui Image::GetSize(UInt8 level) const { #if NAZARA_UTILITY_SAFE if (level >= m_sharedImage->levels.size()) { NazaraErrorFmt("level out of bounds ({0} >= {1})", level, m_sharedImage->levels.size()); return Vector3ui::Zero(); } #endif return Vector3ui(GetImageLevelSize(m_sharedImage->width, level), GetImageLevelSize(m_sharedImage->height, level), GetImageLevelSize(m_sharedImage->depth, level)); } ImageType Image::GetType() const { return m_sharedImage->type; } unsigned int Image::GetWidth(UInt8 level) const { #if NAZARA_UTILITY_SAFE if (level >= m_sharedImage->levels.size()) { NazaraErrorFmt("level out of bounds ({0} >= {1})", level, m_sharedImage->levels.size()); return 0; } #endif return GetImageLevelSize(m_sharedImage->width, level); } bool Image::HasAlpha() const { NazaraAssert(m_sharedImage != &emptyImage, "Image must be valid"); if (!PixelFormatInfo::HasAlpha(m_sharedImage->format)) return false; if (!PixelFormatInfo::IsCompressed(m_sharedImage->format)) { const PixelFormatDescription& info = PixelFormatInfo::GetInfo(m_sharedImage->format); Bitset<> workingBitset; std::size_t pixelCount = m_sharedImage->width * m_sharedImage->height * ((m_sharedImage->type == ImageType::Cubemap) ? 6 : m_sharedImage->depth); if (pixelCount == 0) return false; auto seq = workingBitset.Write(GetConstPixels(), info.bitsPerPixel); do { workingBitset &= info.alphaMask; if (workingBitset.Count() != info.alphaMask.Count()) //< Means that at least one bit of the alpha mask of this pixel is disabled return true; workingBitset.Clear(); workingBitset.Write(seq, info.bitsPerPixel); } while (--pixelCount > 0); return false; } else { // FIXME: Currently, we assume the pixel format is already the right one return true; } } bool Image::IsValid() const { return m_sharedImage != &emptyImage; } // LoadArray std::shared_ptr Image::LoadFromFile(const std::filesystem::path& filePath, const ImageParams& imageParams, const Vector2ui& atlasSize) { std::shared_ptr image = Image::LoadFromFile(filePath, imageParams); if (!image) { NazaraError("failed to load image"); return nullptr; } return LoadFromImage(*image, atlasSize); } std::shared_ptr Image::LoadFromImage(const Image& image, const Vector2ui& atlasSize) { NazaraAssert(image.IsValid(), "invalid image"); #if NAZARA_UTILITY_SAFE if (atlasSize.x == 0) { NazaraError("atlas width must be over zero"); return nullptr; } if (atlasSize.y == 0) { NazaraError("atlas height must be over zero"); return nullptr; } #endif ImageType type = image.GetType(); #if NAZARA_UTILITY_SAFE if (type != ImageType::E1D && type != ImageType::E2D) { NazaraErrorFmt("image type not handled ({0:#x})", UnderlyingCast(type)); return nullptr; } #endif Vector2ui imageSize(image.GetWidth(), image.GetHeight()); if (imageSize.x % atlasSize.x != 0) NazaraWarningFmt("Image width is not divisible by atlas width ({0} mod {1} != 0)", imageSize.x, atlasSize.x); if (imageSize.y % atlasSize.y != 0) NazaraWarningFmt("Image height is not divisible by atlas height ({0} mod {1} != 0)", imageSize.y, atlasSize.y); Vector2ui faceSize = imageSize/atlasSize; unsigned int layerCount = atlasSize.x*atlasSize.y; std::shared_ptr arrayImage = std::make_shared(); // Selon le type de l'image de base, on va créer un array d'images 2D ou 1D if (type == ImageType::E2D) arrayImage->Create(ImageType::E2D_Array, image.GetFormat(), faceSize.x, faceSize.y, layerCount); else arrayImage->Create(ImageType::E1D_Array, image.GetFormat(), faceSize.x, layerCount); if (!arrayImage->IsValid()) { NazaraError("failed to create image"); return nullptr; } unsigned int layer = 0; for (unsigned int j = 0; j < atlasSize.y; ++j) for (unsigned int i = 0; i < atlasSize.x; ++i) arrayImage->Copy(image, Rectui(i*faceSize.x, j*faceSize.y, faceSize.x, faceSize.y), Vector3ui(0, 0, layer++)); return arrayImage; } std::shared_ptr Image::LoadFromMemory(const void* data, std::size_t size, const ImageParams& imageParams, const Vector2ui& atlasSize) { std::shared_ptr image = Image::LoadFromMemory(data, size, imageParams); if (!image) { NazaraError("failed to load image"); return nullptr; } return LoadFromImage(*image, atlasSize); } std::shared_ptr Image::LoadFromStream(Stream& stream, const ImageParams& imageParams, const Vector2ui& atlasSize) { std::shared_ptr image = Image::LoadFromStream(stream, imageParams); if (!image) { NazaraError("failed to load image"); return nullptr; } return LoadFromImage(*image, atlasSize); } std::shared_ptr Image::LoadFromFile(const std::filesystem::path& filePath, const ImageParams& imageParams, const CubemapParams& cubemapParams) { std::shared_ptr image = Image::LoadFromFile(filePath, imageParams); if (!image) { NazaraError("failed to load image"); return nullptr; } return LoadFromImage(*image, cubemapParams); } std::shared_ptr Image::LoadFromImage(const Image& image, const CubemapParams& params) { NazaraAssert(image.IsValid(), "invalid image"); #if NAZARA_UTILITY_SAFE ImageType type = image.GetType(); if (type != ImageType::E2D) { NazaraErrorFmt("image type not handled ({0:#x})", UnderlyingCast(type)); return nullptr; } #endif unsigned int width = image.GetWidth(); unsigned int height = image.GetHeight(); unsigned int faceSize = (params.faceSize == 0) ? std::max(width, height)/4 : params.faceSize; // Sans cette vérification, celles des rectangles pourrait réussir via un overflow if (width < faceSize || height < faceSize) { NazaraError("image is too small for this face size"); return nullptr; } // Calcul et vérification des surfaces unsigned limitX = width - faceSize; unsigned limitY = height - faceSize; Vector2ui backPos = params.backPosition * faceSize; if (backPos.x > limitX || backPos.y > limitY) { NazaraError("back rectangle is out of image"); return nullptr; } Vector2ui downPos = params.downPosition * faceSize; if (downPos.x > limitX || downPos.y > limitY) { NazaraError("down rectangle is out of image"); return nullptr; } Vector2ui forwardPos = params.forwardPosition * faceSize; if (forwardPos.x > limitX || forwardPos.y > limitY) { NazaraError("forward rectangle is out of image"); return nullptr; } Vector2ui leftPos = params.leftPosition * faceSize; if (leftPos.x > limitX || leftPos.y > limitY) { NazaraError("left rectangle is out of image"); return nullptr; } Vector2ui rightPos = params.rightPosition * faceSize; if (rightPos.x > limitX || rightPos.y > limitY) { NazaraError("right rectangle is out of image"); return nullptr; } Vector2ui upPos = params.upPosition * faceSize; if (upPos.x > limitX || upPos.y > limitY) { NazaraError("up rectangle is out of image"); return nullptr; } std::shared_ptr cubemap = std::make_shared(); if (!cubemap->Create(ImageType::Cubemap, image.GetFormat(), faceSize, faceSize)) { NazaraError("failed to create cubemap"); return nullptr; } cubemap->Copy(image, Boxui(backPos.x, backPos.y, 0, faceSize, faceSize, 1), Vector3ui(0, 0, UnderlyingCast(CubemapFace::NegativeZ))); cubemap->Copy(image, Boxui(downPos.x, downPos.y, 0, faceSize, faceSize, 1), Vector3ui(0, 0, UnderlyingCast(CubemapFace::NegativeY))); cubemap->Copy(image, Boxui(forwardPos.x, forwardPos.y, 0, faceSize, faceSize, 1), Vector3ui(0, 0, UnderlyingCast(CubemapFace::PositiveZ))); cubemap->Copy(image, Boxui(leftPos.x, leftPos.y, 0, faceSize, faceSize, 1), Vector3ui(0, 0, UnderlyingCast(CubemapFace::NegativeX))); cubemap->Copy(image, Boxui(rightPos.x, rightPos.y, 0, faceSize, faceSize, 1), Vector3ui(0, 0, UnderlyingCast(CubemapFace::PositiveX))); cubemap->Copy(image, Boxui(upPos.x, upPos.y, 0, faceSize, faceSize, 1), Vector3ui(0, 0, UnderlyingCast(CubemapFace::PositiveY))); return cubemap; } std::shared_ptr Image::LoadFromMemory(const void* data, std::size_t size, const ImageParams& imageParams, const CubemapParams& cubemapParams) { std::shared_ptr image = Image::LoadFromMemory(data, size, imageParams); if (!image) { NazaraError("failed to load image"); return nullptr; } return LoadFromImage(*image, cubemapParams); } std::shared_ptr Image::LoadFromStream(Stream& stream, const ImageParams& imageParams, const CubemapParams& cubemapParams) { std::shared_ptr image = Image::LoadFromStream(stream, imageParams); if (!image) { NazaraError("failed to load image"); return nullptr; } return LoadFromImage(*image, cubemapParams); } bool Image::LoadFaceFromFile(CubemapFace face, const std::filesystem::path& filePath, const ImageParams& params) { std::shared_ptr image = Image::LoadFromFile(filePath, params); if (!image) { NazaraError("failed to load image"); return false; } return LoadFaceFromImage(face, *image); } bool Image::LoadFaceFromImage(CubemapFace face, const Image& image) { NazaraAssert(IsValid() && IsCubemap(), "Image must be a valid cubemap"); unsigned int faceSize = GetWidth(); if (image.GetWidth() != faceSize || image.GetHeight() != faceSize) { NazaraError("image size must match cubemap face size"); return false; } // Image is based on COW, no useless copy will be made Image convertedImage(image); if (!convertedImage.Convert(GetFormat())) { NazaraError("failed to convert image to cubemap format"); return false; } Copy(convertedImage, Boxui(0, 0, 0, faceSize, faceSize, 1), Vector3ui(0, 0, UnderlyingCast(face))); return false; } bool Image::LoadFaceFromMemory(CubemapFace face, const void* data, std::size_t size, const ImageParams& params) { std::shared_ptr image = Image::LoadFromMemory(data, size, params); if (!image) { NazaraError("failed to load image"); return false; } return LoadFaceFromImage(face, *image); } bool Image::LoadFaceFromStream(CubemapFace face, Stream& stream, const ImageParams& params) { std::shared_ptr image = Image::LoadFromStream(stream, params); if (!image) { NazaraError("failed to load image"); return false; } return LoadFaceFromImage(face, *image); } bool Image::SaveToFile(const std::filesystem::path& filePath, const ImageParams& params) { Utility* utility = Utility::Instance(); NazaraAssert(utility, "Utility module has not been initialized"); return utility->GetImageSaver().SaveToFile(*this, filePath, params); } bool Image::SaveToStream(Stream& stream, const std::string& format, const ImageParams& params) { Utility* utility = Utility::Instance(); NazaraAssert(utility, "Utility module has not been initialized"); return utility->GetImageSaver().SaveToStream(*this, stream, format, params); } void Image::SetLevelCount(UInt8 levelCount) { #if NAZARA_UTILITY_SAFE if (m_sharedImage == &emptyImage) { NazaraError("Image must be valid"); return; } if (levelCount == 0) { NazaraError("Level count must be positive"); return; } #endif levelCount = std::min(levelCount, GetMaxLevel()); if (m_sharedImage->levels.size() == levelCount) return; EnsureOwnership(); UInt8 oldLevelCount = UInt8(m_sharedImage->levels.size()); UInt8 maxLevelCount = std::max(levelCount, oldLevelCount); m_sharedImage->levels.resize(levelCount); for (UInt8 i = oldLevelCount; i < maxLevelCount; ++i) m_sharedImage->levels[i] = std::make_unique(GetMemoryUsage(i)); } bool Image::SetPixelColor(const Color& color, unsigned int x, unsigned int y, unsigned int z) { #if NAZARA_UTILITY_SAFE if (m_sharedImage == &emptyImage) { NazaraError("image must be valid"); return false; } if (PixelFormatInfo::IsCompressed(m_sharedImage->format)) { NazaraError("cannot access pixels from compressed image"); return false; } if (x >= m_sharedImage->width) { NazaraErrorFmt("x value exceeds width ({0} >= {1})", x, m_sharedImage->width); return false; } if (y >= m_sharedImage->height) { NazaraErrorFmt("y value exceeds height ({0} >= {1})", y, m_sharedImage->height); return false; } unsigned int depth = (m_sharedImage->type == ImageType::Cubemap) ? 6 : m_sharedImage->depth; if (z >= depth) { NazaraErrorFmt("z value exceeds depth ({0} >= {1})", z, depth); return false; } #endif UInt8* pixel = GetPixelPtr(m_sharedImage->levels[0].get(), PixelFormatInfo::GetBytesPerPixel(m_sharedImage->format), x, y, z, m_sharedImage->width, m_sharedImage->height); if (!PixelFormatInfo::Convert(PixelFormat::RGBA32F, m_sharedImage->format, &color.r, pixel)) { NazaraError("failed to convert RGBA8 to image's format"); return false; } return true; } bool Image::Update(const void* pixels, const Boxui& box, unsigned int srcWidth, unsigned int srcHeight, UInt8 level) { #if NAZARA_UTILITY_SAFE if (m_sharedImage == &emptyImage) { NazaraError("image must be valid"); return false; } if (!pixels) { NazaraError("invalid pixel source"); return false; } if (level >= m_sharedImage->levels.size()) { NazaraErrorFmt("level out of bounds ({0} >= {1})", level, m_sharedImage->levels.size()); return false; } #endif unsigned int width = GetImageLevelSize(m_sharedImage->width, level); unsigned int height = GetImageLevelSize(m_sharedImage->height, level); #if NAZARA_UTILITY_SAFE if (!box.IsValid()) { NazaraError("invalid box"); return false; } unsigned int depth = (m_sharedImage->type == ImageType::Cubemap) ? 6 : GetImageLevelSize(m_sharedImage->depth, level); if (box.x+box.width > width || box.y+box.height > height || box.z+box.depth > depth || (m_sharedImage->type == ImageType::Cubemap && box.depth > 1)) // We don't allow to update more than one face of the cubemap at once { NazaraError("box dimensions are out of bounds"); return false; } #endif EnsureOwnership(); UInt8 bpp = PixelFormatInfo::GetBytesPerPixel(m_sharedImage->format); UInt8* dstPixels = GetPixelPtr(m_sharedImage->levels[level].get(), bpp, box.x, box.y, box.z, width, height); Copy(dstPixels, static_cast(pixels), m_sharedImage->format, box.width, box.height, box.depth, width, height, srcWidth, srcHeight); return true; } Image& Image::operator=(const Image& image) { ReleaseImage(); m_sharedImage = image.m_sharedImage; if (m_sharedImage != &emptyImage) m_sharedImage->refCount++; return *this; } void Image::Copy(UInt8* destination, const UInt8* source, PixelFormat format, unsigned int width, unsigned int height, unsigned int depth, unsigned int dstWidth, unsigned int dstHeight, unsigned int srcWidth, unsigned int srcHeight) { #if NAZARA_UTILITY_SAFE if (width == 0) NazaraError("width must be greater than zero"); if (height == 0) NazaraError("height must be greater than zero"); if (depth == 0) NazaraError("depth must be greater than zero"); #endif if (dstWidth == 0) dstWidth = width; if (dstHeight == 0) dstHeight = height; if (srcWidth == 0) srcWidth = width; if (srcHeight == 0) srcHeight = height; if ((height == 1 || (dstWidth == width && srcWidth == width)) && (depth == 1 || (dstHeight == height && srcHeight == height))) std::memcpy(destination, source, PixelFormatInfo::ComputeSize(format, width, height, depth)); else { unsigned int bpp = PixelFormatInfo::GetBytesPerPixel(format); unsigned int lineStride = width * bpp; unsigned int dstLineStride = dstWidth * bpp; unsigned int dstFaceStride = dstLineStride * dstHeight; unsigned int srcLineStride = srcWidth * bpp; unsigned int srcFaceStride = srcLineStride * srcHeight; for (unsigned int i = 0; i < depth; ++i) { UInt8* dstFacePtr = destination; const UInt8* srcFacePtr = source; for (unsigned int y = 0; y < height; ++y) { std::memcpy(dstFacePtr, srcFacePtr, lineStride); dstFacePtr += dstLineStride; srcFacePtr += srcLineStride; } destination += dstFaceStride; source += srcFaceStride; } } } UInt8 Image::GetMaxLevel(unsigned int width, unsigned int height, unsigned int depth) { // Le niveau maximal est le niveau requis pour la plus grande taille return SafeCast(std::max(IntegralLog2(std::max({ width, height, depth })), 1U)); } UInt8 Image::GetMaxLevel(ImageType type, unsigned int width, unsigned int height, unsigned int depth) { // Pour éviter que la profondeur ne soit comptée dans le calcul des niveaux switch (type) { case ImageType::E1D: case ImageType::E1D_Array: return GetMaxLevel(width, 1U, 1U); case ImageType::E2D: case ImageType::E2D_Array: case ImageType::Cubemap: return GetMaxLevel(width, height, 1U); case ImageType::E3D: return GetMaxLevel(width, height, depth); } NazaraErrorFmt("Image type not handled ({0:#x})", UnderlyingCast(type)); return 0; } std::shared_ptr Image::LoadFromFile(const std::filesystem::path& filePath, const ImageParams& params) { Utility* utility = Utility::Instance(); NazaraAssert(utility, "Utility module has not been initialized"); return utility->GetImageLoader().LoadFromFile(filePath, params); } std::shared_ptr Image::LoadFromMemory(const void* data, std::size_t size, const ImageParams& params) { Utility* utility = Utility::Instance(); NazaraAssert(utility, "Utility module has not been initialized"); return utility->GetImageLoader().LoadFromMemory(data, size, params); } std::shared_ptr Image::LoadFromStream(Stream& stream, const ImageParams& params) { Utility* utility = Utility::Instance(); NazaraAssert(utility, "Utility module has not been initialized"); return utility->GetImageLoader().LoadFromStream(stream, params); } void Image::EnsureOwnership() { if (m_sharedImage == &emptyImage) return; if (m_sharedImage->refCount > 1) { SharedImage::PixelContainer levels(m_sharedImage->levels.size()); for (std::size_t i = 0; i < levels.size(); ++i) { std::size_t size = GetMemoryUsage(SafeCast(i)); levels[i] = std::make_unique(size); std::memcpy(levels[i].get(), m_sharedImage->levels[i].get(), size); } m_sharedImage->refCount--; m_sharedImage = new SharedImage(1, m_sharedImage->type, m_sharedImage->format, std::move(levels), m_sharedImage->width, m_sharedImage->height, m_sharedImage->depth); } } void Image::ReleaseImage() { if (m_sharedImage == &emptyImage) return; if (--m_sharedImage->refCount == 0) delete m_sharedImage; m_sharedImage = &emptyImage; } Image::SharedImage Image::emptyImage(0, ImageType::E2D, PixelFormat::Undefined, Image::SharedImage::PixelContainer(), 0, 0, 0); }