// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved. #if COMPILE_WITH_TEXTURE_TOOL #include "TextureTool.h" #include "Engine/Core/Log.h" #include "Engine/Core/Types/DateTime.h" #include "Engine/Core/Types/TimeSpan.h" #include "Engine/Core/Math/Packed.h" #include "Engine/Core/Math/Color32.h" #include "Engine/Core/Math/Vector2.h" #include "Engine/Platform/FileSystem.h" #include "Engine/Serialization/JsonWriter.h" #include "Engine/Serialization/JsonTools.h" #include "Engine/Scripting/Enums.h" #include "Engine/Graphics/Textures/TextureData.h" #include "Engine/Graphics/PixelFormatExtensions.h" #if USE_EDITOR #include "Engine/Core/Collections/Dictionary.h" namespace { Dictionary TexturesHasAlphaCache; } #endif String TextureTool::Options::ToString() const { return String::Format(TEXT("Type: {}, IsAtlas: {}, NeverStream: {}, IndependentChannels: {}, sRGB: {}, GenerateMipMaps: {}, FlipY: {}, InvertGreen: {} Scale: {}, MaxSize: {}, Resize: {}, PreserveAlphaCoverage: {}, PreserveAlphaCoverageReference: {}, SizeX: {}, SizeY: {}"), ScriptingEnum::ToString(Type), IsAtlas, NeverStream, IndependentChannels, sRGB, GenerateMipMaps, FlipY, InvertGreenChannel, Scale, MaxSize, MaxSize, Resize, PreserveAlphaCoverage, PreserveAlphaCoverageReference, SizeX, SizeY ); } void TextureTool::Options::Serialize(SerializeStream& stream, const void* otherObj) { stream.JKEY("Type"); stream.Enum(Type); stream.JKEY("IsAtlas"); stream.Bool(IsAtlas); stream.JKEY("NeverStream"); stream.Bool(NeverStream); stream.JKEY("Compress"); stream.Bool(Compress); stream.JKEY("IndependentChannels"); stream.Bool(IndependentChannels); stream.JKEY("sRGB"); stream.Bool(sRGB); stream.JKEY("GenerateMipMaps"); stream.Bool(GenerateMipMaps); stream.JKEY("FlipY"); stream.Bool(FlipY); stream.JKEY("InvertGreenChannel"); stream.Bool(InvertGreenChannel); stream.JKEY("Resize"); stream.Bool(Resize); stream.JKEY("PreserveAlphaCoverage"); stream.Bool(PreserveAlphaCoverage); stream.JKEY("PreserveAlphaCoverageReference"); stream.Float(PreserveAlphaCoverageReference); stream.JKEY("TextureGroup"); stream.Int(TextureGroup); stream.JKEY("Scale"); stream.Float(Scale); stream.JKEY("MaxSize"); stream.Int(MaxSize); stream.JKEY("SizeX"); stream.Int(SizeX); stream.JKEY("SizeY"); stream.Int(SizeY); stream.JKEY("Sprites"); stream.StartArray(); for (int32 i = 0; i < Sprites.Count(); i++) { auto& s = Sprites[i]; stream.StartObject(); stream.JKEY("Position"); stream.Float2(s.Area.Location); stream.JKEY("Size"); stream.Float2(s.Area.Size); stream.JKEY("Name"); stream.String(s.Name); stream.EndObject(); } stream.EndArray(); } void TextureTool::Options::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) { // Restore general import options Type = JsonTools::GetEnum(stream, "Type", Type); IsAtlas = JsonTools::GetBool(stream, "IsAtlas", IsAtlas); NeverStream = JsonTools::GetBool(stream, "NeverStream", NeverStream); Compress = JsonTools::GetBool(stream, "Compress", Compress); IndependentChannels = JsonTools::GetBool(stream, "IndependentChannels", IndependentChannels); sRGB = JsonTools::GetBool(stream, "sRGB", sRGB); GenerateMipMaps = JsonTools::GetBool(stream, "GenerateMipMaps", GenerateMipMaps); FlipY = JsonTools::GetBool(stream, "FlipY", FlipY); InvertGreenChannel = JsonTools::GetBool(stream, "InvertGreenChannel", InvertGreenChannel); Resize = JsonTools::GetBool(stream, "Resize", Resize); PreserveAlphaCoverage = JsonTools::GetBool(stream, "PreserveAlphaCoverage", PreserveAlphaCoverage); PreserveAlphaCoverageReference = JsonTools::GetFloat(stream, "PreserveAlphaCoverageReference", PreserveAlphaCoverageReference); TextureGroup = JsonTools::GetInt(stream, "TextureGroup", TextureGroup); Scale = JsonTools::GetFloat(stream, "Scale", Scale); SizeX = JsonTools::GetInt(stream, "SizeX", SizeX); SizeY = JsonTools::GetInt(stream, "SizeY", SizeY); MaxSize = JsonTools::GetInt(stream, "MaxSize", MaxSize); // Load sprites // Note: we use it if no sprites in texture header has been loaded earlier auto* spritesMember = stream.FindMember("Sprites"); if (spritesMember != stream.MemberEnd() && Sprites.IsEmpty()) { auto& spritesArray = spritesMember->value; ASSERT(spritesArray.IsArray()); Sprites.EnsureCapacity(spritesArray.Size()); for (uint32 i = 0; i < spritesArray.Size(); i++) { Sprite s; auto& stData = spritesArray[i]; s.Area.Location = JsonTools::GetFloat2(stData, "Position", Float2::Zero); s.Area.Size = JsonTools::GetFloat2(stData, "Size", Float2::One); s.Name = JsonTools::GetString(stData, "Name"); Sprites.Add(s); } } } #if USE_EDITOR bool TextureTool::HasAlpha(const StringView& path) { // Try to hit the cache (eg. if texture was already imported before) if (!TexturesHasAlphaCache.ContainsKey(path)) { TextureData textureData; if (ImportTexture(path, textureData)) return false; } return TexturesHasAlphaCache[path]; } #endif bool TextureTool::ImportTexture(const StringView& path, TextureData& textureData) { LOG(Info, "Importing texture from \'{0}\'", path); const auto startTime = DateTime::NowUTC(); // Detect texture format type ImageType type; if (GetImageType(path, type)) return true; // Import bool hasAlpha = false; #if COMPILE_WITH_DIRECTXTEX const auto failed = ImportTextureDirectXTex(type, path, textureData, hasAlpha); #elif COMPILE_WITH_STB const auto failed = ImportTextureStb(type, path, textureData, hasAlpha); #else const auto failed = true; LOG(Warning, "Importing textures is not supported on this platform."); #endif if (failed) { LOG(Warning, "Importing texture failed."); } else { #if USE_EDITOR TexturesHasAlphaCache[path] = hasAlpha; #endif LOG(Info, "Texture imported in {0} ms", static_cast((DateTime::NowUTC() - startTime).GetTotalMilliseconds())); } return failed; } bool TextureTool::ImportTexture(const StringView& path, TextureData& textureData, Options options, String& errorMsg) { LOG(Info, "Importing texture from \'{0}\'. Options: {1}", path, options.ToString()); const auto startTime = DateTime::NowUTC(); // Detect texture format type ImageType type; if (options.InternalLoad.IsBinded()) { type = ImageType::Internal; } else { if (GetImageType(path, type)) return true; } // Clamp values options.MaxSize = Math::Clamp(options.MaxSize, 1, GPU_MAX_TEXTURE_SIZE); options.SizeX = Math::Clamp(options.SizeX, 1, GPU_MAX_TEXTURE_SIZE); options.SizeY = Math::Clamp(options.SizeY, 1, GPU_MAX_TEXTURE_SIZE); // Import bool hasAlpha = false; #if COMPILE_WITH_DIRECTXTEX const auto failed = ImportTextureDirectXTex(type, path, textureData, options, errorMsg, hasAlpha); #elif COMPILE_WITH_STB const auto failed = ImportTextureStb(type, path, textureData, options, errorMsg, hasAlpha); #else const auto failed = true; LOG(Warning, "Importing textures is not supported on this platform."); #endif if (failed) { LOG(Warning, "Importing texture failed. {0}", errorMsg); } else { #if USE_EDITOR TexturesHasAlphaCache[path] = hasAlpha; #endif LOG(Info, "Texture imported in {0} ms", static_cast((DateTime::NowUTC() - startTime).GetTotalMilliseconds())); } return failed; } bool TextureTool::ExportTexture(const StringView& path, const TextureData& textureData) { LOG(Info, "Exporting texture to \'{0}\'.", path); const auto startTime = DateTime::NowUTC(); ImageType type; if (GetImageType(path, type)) return true; if (textureData.Items.IsEmpty()) { LOG(Warning, "Missing texture data."); return true; } // Export #if COMPILE_WITH_DIRECTXTEX const auto failed = ExportTextureDirectXTex(type, path, textureData); #elif COMPILE_WITH_STB const auto failed = ExportTextureStb(type, path, textureData); #else const auto failed = true; LOG(Warning, "Exporting textures is not supported on this platform."); #endif if (failed) { LOG(Warning, "Exporting failed."); } else { LOG(Info, "Texture exported in {0} ms", static_cast((DateTime::NowUTC() - startTime).GetTotalMilliseconds())); } return failed; } bool TextureTool::Convert(TextureData& dst, const TextureData& src, const PixelFormat dstFormat) { // Validate input if (src.GetMipLevels() == 0) { LOG(Warning, "Missing source data."); return true; } if (src.Format == dstFormat) { LOG(Warning, "Source data and destination format are the same. Cannot perform conversion."); return true; } if (src.Depth != 1) { LOG(Warning, "Converting volume texture data is not supported."); return true; } #if COMPILE_WITH_DIRECTXTEX return ConvertDirectXTex(dst, src, dstFormat); #elif COMPILE_WITH_STB return ConvertStb(dst, src, dstFormat); #else LOG(Warning, "Converting textures is not supported on this platform."); return true; #endif } bool TextureTool::Resize(TextureData& dst, const TextureData& src, int32 dstWidth, int32 dstHeight) { // Validate input if (src.GetMipLevels() == 0) { LOG(Warning, "Missing source data."); return true; } if (src.Width == dstWidth && src.Height == dstHeight) { LOG(Warning, "Source data and destination dimensions are the same. Cannot perform resizing."); return true; } if (src.Depth != 1) { LOG(Warning, "Resizing volume texture data is not supported."); return true; } #if COMPILE_WITH_DIRECTXTEX return ResizeDirectXTex(dst, src, dstWidth, dstHeight); #elif COMPILE_WITH_STB return ResizeStb(dst, src, dstWidth, dstHeight); #else LOG(Warning, "Resizing textures is not supported on this platform."); return true; #endif } TextureTool::PixelFormatSampler PixelFormatSamplers[] = { { PixelFormat::R32G32B32A32_Float, sizeof(Float4), [](const void* ptr) { return Color(*(Float4*)ptr); }, [](const void* ptr, const Color& color) { *(Float4*)ptr = color.ToFloat4(); }, }, { PixelFormat::R32G32B32_Float, sizeof(Float3), [](const void* ptr) { return Color(*(Float3*)ptr, 1.0f); }, [](const void* ptr, const Color& color) { *(Float3*)ptr = color.ToFloat3(); }, }, { PixelFormat::R16G16B16A16_Float, sizeof(Half4), [](const void* ptr) { return Color(((Half4*)ptr)->ToFloat4()); }, [](const void* ptr, const Color& color) { *(Half4*)ptr = Half4(color.R, color.G, color.B, color.A); }, }, { PixelFormat::R16G16B16A16_UNorm, sizeof(RGBA16UNorm), [](const void* ptr) { return Color(((RGBA16UNorm*)ptr)->ToFloat4()); }, [](const void* ptr, const Color& color) { *(RGBA16UNorm*)ptr = RGBA16UNorm(color.R, color.G, color.B, color.A); } }, { PixelFormat::R32G32_Float, sizeof(Float2), [](const void* ptr) { return Color(((Float2*)ptr)->X, ((Float2*)ptr)->Y, 1.0f); }, [](const void* ptr, const Color& color) { *(Float2*)ptr = Float2(color.R, color.G); }, }, { PixelFormat::R8G8B8A8_UNorm, sizeof(Color32), [](const void* ptr) { return Color(*(Color32*)ptr); }, [](const void* ptr, const Color& color) { *(Color32*)ptr = Color32(color); }, }, { PixelFormat::R8G8B8A8_UNorm_sRGB, sizeof(Color32), [](const void* ptr) { return Color::SrgbToLinear(Color(*(Color32*)ptr)); }, [](const void* ptr, const Color& color) { Color srgb = Color::LinearToSrgb(color); *(Color32*)ptr = Color32(srgb); }, }, { PixelFormat::R8G8_UNorm, sizeof(uint16), [](const void* ptr) { const uint8* rg = (const uint8*)ptr; return Color((float)rg[0] / MAX_uint8, (float)rg[1] / MAX_uint8, 0, 1); }, [](const void* ptr, const Color& color) { uint8* rg = (uint8*)ptr; rg[0] = (uint8)(color.R * MAX_uint8); rg[1] = (uint8)(color.G * MAX_uint8); }, }, { PixelFormat::R16G16_Float, sizeof(Half2), [](const void* ptr) { const Float2 rg = ((Half2*)ptr)->ToFloat2(); return Color(rg.X, rg.Y, 0, 1); }, [](const void* ptr, const Color& color) { *(Half2*)ptr = Half2(color.R, color.G); }, }, { PixelFormat::R16G16_UNorm, sizeof(RG16UNorm), [](const void* ptr) { const Float2 rg = ((RG16UNorm*)ptr)->ToFloat2(); return Color(rg.X, rg.Y, 0, 1); }, [](const void* ptr, const Color& color) { *(RG16UNorm*)ptr = RG16UNorm(color.R, color.G); }, }, { PixelFormat::R32_Float, sizeof(float), [](const void* ptr) { return Color(*(float*)ptr, 0, 0, 1); }, [](const void* ptr, const Color& color) { *(float*)ptr = color.R; }, }, { PixelFormat::R16_Float, sizeof(Half), [](const void* ptr) { return Color(Float16Compressor::Decompress(*(Half*)ptr), 0, 0, 1); }, [](const void* ptr, const Color& color) { *(Half*)ptr = Float16Compressor::Compress(color.R); }, }, { PixelFormat::R16_UNorm, sizeof(uint16), [](const void* ptr) { return Color((float)*(uint16*)ptr / MAX_uint16, 0, 0, 1); }, [](const void* ptr, const Color& color) { *(uint16*)ptr = (uint16)(color.R * MAX_uint16); }, }, { PixelFormat::R8_UNorm, sizeof(uint8), [](const void* ptr) { return Color((float)*(byte*)ptr / MAX_uint8, 0, 0, 1); }, [](const void* ptr, const Color& color) { *(byte*)ptr = (byte)(color.R * MAX_uint8); }, }, { PixelFormat::A8_UNorm, sizeof(uint8), [](const void* ptr) { return Color(0, 0, 0, (float)*(byte*)ptr / MAX_uint8); }, [](const void* ptr, const Color& color) { *(byte*)ptr = (byte)(color.A * MAX_uint8); }, }, { PixelFormat::B8G8R8A8_UNorm, sizeof(Color32), [](const void* ptr) { const Color32 bgra = *(Color32*)ptr; return Color(Color32(bgra.B, bgra.G, bgra.R, bgra.A)); }, [](const void* ptr, const Color& color) { *(Color32*)ptr = Color32(byte(color.B * MAX_uint8), byte(color.G * MAX_uint8), byte(color.R * MAX_uint8), byte(color.A * MAX_uint8)); }, }, { PixelFormat::B8G8R8A8_UNorm_sRGB, sizeof(Color32), [](const void* ptr) { const Color32 bgra = *(Color32*)ptr; return Color::SrgbToLinear(Color(Color32(bgra.B, bgra.G, bgra.R, bgra.A))); }, [](const void* ptr, const Color& color) { Color srgb = Color::LinearToSrgb(color); *(Color32*)ptr = Color32(byte(srgb.B * MAX_uint8), byte(srgb.G * MAX_uint8), byte(srgb.R * MAX_uint8), byte(srgb.A * MAX_uint8)); }, }, { PixelFormat::B8G8R8X8_UNorm, sizeof(Color32), [](const void* ptr) { const Color32 bgra = *(Color32*)ptr; return Color(Color32(bgra.B, bgra.G, bgra.R, MAX_uint8)); }, [](const void* ptr, const Color& color) { *(Color32*)ptr = Color32(byte(color.B * MAX_uint8), byte(color.G * MAX_uint8), byte(color.R * MAX_uint8), MAX_uint8); }, }, { PixelFormat::B8G8R8X8_UNorm_sRGB, sizeof(Color32), [](const void* ptr) { const Color32 bgra = *(Color32*)ptr; return Color::SrgbToLinear(Color(Color32(bgra.B, bgra.G, bgra.R, MAX_uint8))); }, [](const void* ptr, const Color& color) { Color srgb = Color::LinearToSrgb(color); *(Color32*)ptr = Color32(byte(srgb.B * MAX_uint8), byte(srgb.G * MAX_uint8), byte(srgb.R * MAX_uint8), MAX_uint8); }, }, { PixelFormat::R11G11B10_Float, sizeof(FloatR11G11B10), [](const void* ptr) { const Float3 rgb = ((FloatR11G11B10*)ptr)->ToFloat3(); return Color(rgb.X, rgb.Y, rgb.Z); }, [](const void* ptr, const Color& color) { *(FloatR11G11B10*)ptr = FloatR11G11B10(color.R, color.G, color.B); }, }, { PixelFormat::R10G10B10A2_UNorm, sizeof(Float1010102), [](const void* ptr) { const Float3 rgb = ((Float1010102*)ptr)->ToFloat3(); return Color(rgb.X, rgb.Y, rgb.Z); }, [](const void* ptr, const Color& color) { *(Float1010102*)ptr = Float1010102(color.R, color.G, color.B, color.A); }, }, }; const TextureTool::PixelFormatSampler* TextureTool::GetSampler(PixelFormat format) { format = PixelFormatExtensions::MakeTypelessFloat(format); for (auto& sampler : PixelFormatSamplers) { if (sampler.Format == format) return &sampler; } return nullptr; } void TextureTool::Store(const PixelFormatSampler* sampler, int32 x, int32 y, const void* data, int32 rowPitch, const Color& color) { ASSERT_LOW_LAYER(sampler); sampler->Store((byte*)data + rowPitch * y + sampler->PixelSize * x, color); } Color TextureTool::SamplePoint(const PixelFormatSampler* sampler, const Float2& uv, const void* data, const Int2& size, int32 rowPitch) { ASSERT_LOW_LAYER(sampler); const Int2 end = size - 1; const Int2 uvFloor(Math::Min(Math::FloorToInt(uv.X * size.X), end.X), Math::Min(Math::FloorToInt(uv.Y * size.Y), end.Y)); return sampler->Sample((byte*)data + rowPitch * uvFloor.Y + sampler->PixelSize * uvFloor.X); } Color TextureTool::SamplePoint(const PixelFormatSampler* sampler, int32 x, int32 y, const void* data, int32 rowPitch) { ASSERT_LOW_LAYER(sampler); return sampler->Sample((byte*)data + rowPitch * y + sampler->PixelSize * x); } Color TextureTool::SampleLinear(const PixelFormatSampler* sampler, const Float2& uv, const void* data, const Int2& size, int32 rowPitch) { ASSERT_LOW_LAYER(sampler); const Int2 end = size - 1; const Int2 uvFloor(Math::Min(Math::FloorToInt(uv.X * size.X), end.X), Math::Min(Math::FloorToInt(uv.Y * size.Y), end.Y)); const Int2 uvNext(Math::Min(uvFloor.X + 1, end.X), Math::Min(uvFloor.Y + 1, end.Y)); const Float2 uvFraction(uv.X * size.Y - uvFloor.X, uv.Y * size.Y - uvFloor.Y); const Color v00 = sampler->Sample((byte*)data + rowPitch * uvFloor.Y + sampler->PixelSize * uvFloor.X); const Color v01 = sampler->Sample((byte*)data + rowPitch * uvFloor.Y + sampler->PixelSize * uvNext.X); const Color v10 = sampler->Sample((byte*)data + rowPitch * uvNext.Y + sampler->PixelSize * uvFloor.X); const Color v11 = sampler->Sample((byte*)data + rowPitch * uvNext.Y + sampler->PixelSize * uvNext.X); return Color::Lerp(Color::Lerp(v00, v01, uvFraction.X), Color::Lerp(v10, v11, uvFraction.X), uvFraction.Y); } PixelFormat TextureTool::ToPixelFormat(TextureFormatType format, int32 width, int32 height, bool canCompress) { const bool canUseBlockCompression = width % 4 == 0 && height % 4 == 0; if (canCompress && canUseBlockCompression) { switch (format) { case TextureFormatType::ColorRGB: return PixelFormat::BC1_UNorm; case TextureFormatType::ColorRGBA: return PixelFormat::BC3_UNorm; case TextureFormatType::NormalMap: return PixelFormat::BC5_UNorm; case TextureFormatType::GrayScale: return PixelFormat::BC4_UNorm; case TextureFormatType::HdrRGBA: return PixelFormat::BC7_UNorm; case TextureFormatType::HdrRGB: #if PLATFORM_LINUX // TODO: support BC6H compression for Linux Editor return PixelFormat::BC7_UNorm; #else return PixelFormat::BC6H_Uf16; #endif default: return PixelFormat::Unknown; } } switch (format) { case TextureFormatType::ColorRGB: return PixelFormat::R8G8B8A8_UNorm; case TextureFormatType::ColorRGBA: return PixelFormat::R8G8B8A8_UNorm; case TextureFormatType::NormalMap: return PixelFormat::R16G16_UNorm; case TextureFormatType::GrayScale: return PixelFormat::R8_UNorm; case TextureFormatType::HdrRGBA: return PixelFormat::R16G16B16A16_Float; case TextureFormatType::HdrRGB: return PixelFormat::R11G11B10_Float; default: return PixelFormat::Unknown; } } bool TextureTool::GetImageType(const StringView& path, ImageType& type) { const auto extension = FileSystem::GetExtension(path).ToLower(); if (extension == TEXT("tga")) { type = ImageType::TGA; } else if (extension == TEXT("dds")) { type = ImageType::DDS; } else if (extension == TEXT("png")) { type = ImageType::PNG; } else if (extension == TEXT("bmp")) { type = ImageType::BMP; } else if (extension == TEXT("gif")) { type = ImageType::GIF; } else if (extension == TEXT("tiff") || extension == TEXT("tif")) { type = ImageType::TIFF; } else if (extension == TEXT("hdr")) { type = ImageType::HDR; } else if (extension == TEXT("jpeg") || extension == TEXT("jpg")) { type = ImageType::JPEG; } else if (extension == TEXT("raw")) { type = ImageType::RAW; } else { LOG(Warning, "Unknown file type."); return true; } return false; } #endif