Merge remote-tracking branch 'origin/1.1' into 1.2
# Conflicts: # Source/Editor/Editor.Build.cs
This commit is contained in:
@@ -518,29 +518,33 @@ bool ImportMaterials(AssimpImporterData& data, String& errorMsg)
|
||||
if (aMaterial->Get(AI_MATKEY_NAME, aName) == AI_SUCCESS)
|
||||
materialSlot.Name = String(aName.C_Str()).TrimTrailing();
|
||||
materialSlot.AssetID = Guid::Empty;
|
||||
aiColor3D aColor;
|
||||
if (aMaterial->Get(AI_MATKEY_COLOR_DIFFUSE, aColor) == AI_SUCCESS)
|
||||
materialSlot.Diffuse.Color = ToColor(aColor);
|
||||
bool aBoolean;
|
||||
if (aMaterial->Get(AI_MATKEY_TWOSIDED, aBoolean) == AI_SUCCESS)
|
||||
materialSlot.TwoSided = aBoolean;
|
||||
bool aFloat;
|
||||
if (aMaterial->Get(AI_MATKEY_OPACITY, aFloat) == AI_SUCCESS)
|
||||
materialSlot.Opacity.Value = aFloat;
|
||||
|
||||
if (data.Model.Types & ImportDataTypes::Textures)
|
||||
if (data.Model.Types & ImportDataTypes::Materials)
|
||||
{
|
||||
ImportMaterialTexture(data, aMaterial, aiTextureType_DIFFUSE, materialSlot.Diffuse.TextureIndex, TextureEntry::TypeHint::ColorRGB);
|
||||
ImportMaterialTexture(data, aMaterial, aiTextureType_EMISSIVE, materialSlot.Emissive.TextureIndex, TextureEntry::TypeHint::ColorRGB);
|
||||
ImportMaterialTexture(data, aMaterial, aiTextureType_NORMALS, materialSlot.Normals.TextureIndex, TextureEntry::TypeHint::Normals);
|
||||
ImportMaterialTexture(data, aMaterial, aiTextureType_OPACITY, materialSlot.Opacity.TextureIndex, TextureEntry::TypeHint::ColorRGBA);
|
||||
aiColor3D aColor;
|
||||
if (aMaterial->Get(AI_MATKEY_COLOR_DIFFUSE, aColor) == AI_SUCCESS)
|
||||
materialSlot.Diffuse.Color = ToColor(aColor);
|
||||
bool aBoolean;
|
||||
if (aMaterial->Get(AI_MATKEY_TWOSIDED, aBoolean) == AI_SUCCESS)
|
||||
materialSlot.TwoSided = aBoolean;
|
||||
bool aFloat;
|
||||
if (aMaterial->Get(AI_MATKEY_OPACITY, aFloat) == AI_SUCCESS)
|
||||
materialSlot.Opacity.Value = aFloat;
|
||||
|
||||
if (materialSlot.Diffuse.TextureIndex != -1)
|
||||
if (data.Model.Types & ImportDataTypes::Textures)
|
||||
{
|
||||
// Detect using alpha mask in diffuse texture
|
||||
materialSlot.Diffuse.HasAlphaMask = TextureTool::HasAlpha(data.Model.Textures[materialSlot.Diffuse.TextureIndex].FilePath);
|
||||
if (materialSlot.Diffuse.HasAlphaMask)
|
||||
data.Model.Textures[materialSlot.Diffuse.TextureIndex].Type = TextureEntry::TypeHint::ColorRGBA;
|
||||
ImportMaterialTexture(data, aMaterial, aiTextureType_DIFFUSE, materialSlot.Diffuse.TextureIndex, TextureEntry::TypeHint::ColorRGB);
|
||||
ImportMaterialTexture(data, aMaterial, aiTextureType_EMISSIVE, materialSlot.Emissive.TextureIndex, TextureEntry::TypeHint::ColorRGB);
|
||||
ImportMaterialTexture(data, aMaterial, aiTextureType_NORMALS, materialSlot.Normals.TextureIndex, TextureEntry::TypeHint::Normals);
|
||||
ImportMaterialTexture(data, aMaterial, aiTextureType_OPACITY, materialSlot.Opacity.TextureIndex, TextureEntry::TypeHint::ColorRGBA);
|
||||
|
||||
if (materialSlot.Diffuse.TextureIndex != -1)
|
||||
{
|
||||
// Detect using alpha mask in diffuse texture
|
||||
materialSlot.Diffuse.HasAlphaMask = TextureTool::HasAlpha(data.Model.Textures[materialSlot.Diffuse.TextureIndex].FilePath);
|
||||
if (materialSlot.Diffuse.HasAlphaMask)
|
||||
data.Model.Textures[materialSlot.Diffuse.TextureIndex].Type = TextureEntry::TypeHint::ColorRGBA;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -691,13 +695,10 @@ bool ModelTool::ImportDataAssimp(const char* path, ImportedModelData& data, cons
|
||||
ProcessNodes(assimpData, scene->mRootNode, -1);
|
||||
|
||||
// Import materials
|
||||
if (data.Types & ImportDataTypes::Materials)
|
||||
if (ImportMaterials(assimpData, errorMsg))
|
||||
{
|
||||
if (ImportMaterials(assimpData, errorMsg))
|
||||
{
|
||||
LOG(Warning, "Failed to import materials.");
|
||||
return true;
|
||||
}
|
||||
LOG(Warning, "Failed to import materials.");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Import geometry
|
||||
|
||||
@@ -48,6 +48,11 @@ public class TextureTool : EngineModule
|
||||
{
|
||||
options.PrivateDependencies.Add("stb");
|
||||
options.SourceFiles.Add(Path.Combine(FolderPath, "TextureTool.stb.cpp"));
|
||||
if (options.Target.IsEditor)
|
||||
{
|
||||
// Use helper lib for decompression
|
||||
options.PrivateDependencies.Add("detex");
|
||||
}
|
||||
}
|
||||
|
||||
options.PublicDefinitions.Add("COMPILE_WITH_TEXTURE_TOOL");
|
||||
|
||||
@@ -250,6 +250,8 @@ bool TextureTool::ImportTexture(const StringView& path, TextureData& textureData
|
||||
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.");
|
||||
@@ -327,6 +329,8 @@ bool TextureTool::Convert(TextureData& dst, const TextureData& src, const PixelF
|
||||
|
||||
#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;
|
||||
@@ -441,11 +445,12 @@ TextureTool::PixelFormatSampler PixelFormatSamplers[] =
|
||||
sizeof(Color32),
|
||||
[](const void* ptr)
|
||||
{
|
||||
return Color(*(Color32*)ptr);
|
||||
return Color::SrgbToLinear(Color(*(Color32*)ptr));
|
||||
},
|
||||
[](const void* ptr, const Color& color)
|
||||
{
|
||||
*(Color32*)ptr = Color32(color);
|
||||
Color srgb = Color::LinearToSrgb(color);
|
||||
*(Color32*)ptr = Color32(srgb);
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -553,11 +558,12 @@ TextureTool::PixelFormatSampler PixelFormatSamplers[] =
|
||||
[](const void* ptr)
|
||||
{
|
||||
const Color32 bgra = *(Color32*)ptr;
|
||||
return Color(Color32(bgra.B, bgra.G, bgra.R, bgra.A));
|
||||
return Color::SrgbToLinear(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));
|
||||
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));
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -579,11 +585,12 @@ TextureTool::PixelFormatSampler PixelFormatSamplers[] =
|
||||
[](const void* ptr)
|
||||
{
|
||||
const Color32 bgra = *(Color32*)ptr;
|
||||
return Color(Color32(bgra.B, bgra.G, bgra.R, MAX_uint8));
|
||||
return Color::SrgbToLinear(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);
|
||||
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);
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -218,7 +218,7 @@ public:
|
||||
/// <param name="y">The Y texture coordinates (normalized to range 0-height).</param>
|
||||
/// <param name="data">The data pointer for the texture slice (1D or 2D image).</param>
|
||||
/// <param name="rowPitch">The row pitch (in bytes). The offset between each image rows.</param>
|
||||
/// <param name="color">The color to store.</param>
|
||||
/// <param name="color">The color to store (linear).</param>
|
||||
static void Store(const PixelFormatSampler* sampler, int32 x, int32 y, const void* data, int32 rowPitch, const Color& color);
|
||||
|
||||
/// <summary>
|
||||
@@ -232,7 +232,7 @@ public:
|
||||
/// <param name="data">The data pointer for the texture slice (1D or 2D image).</param>
|
||||
/// <param name="size">The size of the input texture (in pixels).</param>
|
||||
/// <param name="rowPitch">The row pitch (in bytes). The offset between each image rows.</param>
|
||||
/// <returns>The sampled color.</returns>
|
||||
/// <returns>The sampled color (linear).</returns>
|
||||
static Color SamplePoint(const PixelFormatSampler* sampler, const Vector2& uv, const void* data, const Int2& size, int32 rowPitch);
|
||||
|
||||
/// <summary>
|
||||
@@ -246,7 +246,7 @@ public:
|
||||
/// <param name="y">The Y texture coordinates (normalized to range 0-height).</param>
|
||||
/// <param name="data">The data pointer for the texture slice (1D or 2D image).</param>
|
||||
/// <param name="rowPitch">The row pitch (in bytes). The offset between each image rows.</param>
|
||||
/// <returns>The sampled color.</returns>
|
||||
/// <returns>The sampled color (linear).</returns>
|
||||
static Color SamplePoint(const PixelFormatSampler* sampler, int32 x, int32 y, const void* data, int32 rowPitch);
|
||||
|
||||
/// <summary>
|
||||
@@ -260,7 +260,7 @@ public:
|
||||
/// <param name="data">The data pointer for the texture slice (1D or 2D image).</param>
|
||||
/// <param name="size">The size of the input texture (in pixels).</param>
|
||||
/// <param name="rowPitch">The row pitch (in bytes). The offset between each image rows.</param>
|
||||
/// <returns>The sampled color.</returns>
|
||||
/// <returns>The sampled color (linear).</returns>
|
||||
static Color SampleLinear(const PixelFormatSampler* sampler, const Vector2& uv, const void* data, const Int2& size, int32 rowPitch);
|
||||
|
||||
private:
|
||||
@@ -291,6 +291,9 @@ private:
|
||||
#if COMPILE_WITH_STB
|
||||
static bool ExportTextureStb(ImageType type, const StringView& path, const TextureData& textureData);
|
||||
static bool ImportTextureStb(ImageType type, const StringView& path, TextureData& textureData, bool& hasAlpha);
|
||||
static bool ImportTextureStb(ImageType type, const StringView& path, TextureData& textureData, const Options& options, String& errorMsg, bool& hasAlpha);
|
||||
static bool ConvertStb(TextureData& dst, const TextureData& src, const PixelFormat dstFormat);
|
||||
static bool ResizeStb(PixelFormat format, TextureMipData& dstMip, const TextureMipData& srcMip, int32 dstMipWidth, int32 dstMipHeight);
|
||||
static bool ResizeStb(TextureData& dst, const TextureData& src, int32 dstWidth, int32 dstHeight);
|
||||
#endif
|
||||
};
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
#include "Engine/Core/Log.h"
|
||||
#include "Engine/Core/Math/Color32.h"
|
||||
#include "Engine/Serialization/FileWriteStream.h"
|
||||
#include "Engine/Graphics/RenderTools.h"
|
||||
#include "Engine/Graphics/Textures/TextureData.h"
|
||||
#include "Engine/Graphics/Textures/TextureUtils.h"
|
||||
#include "Engine/Graphics/PixelFormatExtensions.h"
|
||||
#include "Engine/Platform/File.h"
|
||||
|
||||
@@ -37,6 +39,17 @@
|
||||
#define STB_IMAGE_RESIZE_IMPLEMENTATION
|
||||
#include <ThirdParty/stb/stb_image_resize.h>
|
||||
|
||||
#define STBD_ABS(i) Math::Abs(i)
|
||||
#define STBD_FABS(x) Math::Abs(x)
|
||||
#define STB_DXT_IMPLEMENTATION
|
||||
#include <ThirdParty/stb/stb_dxt.h>
|
||||
|
||||
#if USE_EDITOR
|
||||
|
||||
#include <ThirdParty/detex/detex.h>
|
||||
|
||||
#endif
|
||||
|
||||
static void stbWrite(void* context, void* data, int size)
|
||||
{
|
||||
auto file = (FileWriteStream*)context;
|
||||
@@ -50,46 +63,142 @@ bool TextureTool::ExportTextureStb(ImageType type, const StringView& path, const
|
||||
LOG(Warning, "Exporting texture arrays and cubemaps is not supported by stb library.");
|
||||
}
|
||||
|
||||
TextureData const* texture = &textureData;
|
||||
|
||||
#if USE_EDITOR
|
||||
// Handle compressed textures
|
||||
TextureData decompressed;
|
||||
if (PixelFormatExtensions::IsCompressed(textureData.Format))
|
||||
{
|
||||
decompressed.Format = PixelFormatExtensions::IsSRGB(textureData.Format) ? PixelFormat::R8G8B8A8_UNorm_sRGB : PixelFormat::R8G8B8A8_UNorm;
|
||||
decompressed.Width = textureData.Width;
|
||||
decompressed.Height = textureData.Height;
|
||||
decompressed.Depth = textureData.Depth;
|
||||
decompressed.Items.Resize(1);
|
||||
decompressed.Items[0].Mips.Resize(1);
|
||||
|
||||
auto decompressedData = decompressed.GetData(0, 0);
|
||||
decompressedData->RowPitch = textureData.Width * sizeof(Color32);
|
||||
decompressedData->Lines = textureData.Height;
|
||||
decompressedData->DepthPitch = decompressedData->RowPitch * decompressedData->Lines;
|
||||
decompressedData->Data.Allocate(decompressedData->DepthPitch);
|
||||
|
||||
Color32 colors[16];
|
||||
int32 blocksWidth = textureData.Width / 4;
|
||||
int32 blocksHeight = textureData.Height / 4;
|
||||
const auto blocksData = texture->GetData(0, 0);
|
||||
byte* decompressedBytes = decompressedData->Data.Get();
|
||||
|
||||
switch (textureData.Format)
|
||||
{
|
||||
case PixelFormat::BC1_UNorm:
|
||||
case PixelFormat::BC1_UNorm_sRGB:
|
||||
{
|
||||
for (int32 yBlock = 0; yBlock < blocksHeight; yBlock++)
|
||||
{
|
||||
for (int32 xBlock = 0; xBlock < blocksWidth; xBlock++)
|
||||
{
|
||||
const byte* block = blocksData->Data.Get() + yBlock * 4 * blocksData->RowPitch + xBlock * 8;
|
||||
detexDecompressBlockBC1(block, 0, 0, (byte*)&colors);
|
||||
for (int32 y = 0; y < 4; y++)
|
||||
{
|
||||
for (int32 x = 0; x < 4; x++)
|
||||
{
|
||||
*((Color32*)decompressedBytes + (yBlock * 4 + y) * textureData.Width + (xBlock * 4 + x)) = colors[y * 4 + x];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case PixelFormat::BC2_UNorm:
|
||||
case PixelFormat::BC2_UNorm_sRGB:
|
||||
{
|
||||
for (int32 yBlock = 0; yBlock < blocksHeight; yBlock++)
|
||||
{
|
||||
for (int32 xBlock = 0; xBlock < blocksWidth; xBlock++)
|
||||
{
|
||||
const byte* block = blocksData->Data.Get() + yBlock * 4 * blocksData->RowPitch + xBlock * 16;
|
||||
detexDecompressBlockBC2(block, 0, 0, (byte*)&colors);
|
||||
for (int32 y = 0; y < 4; y++)
|
||||
{
|
||||
for (int32 x = 0; x < 4; x++)
|
||||
{
|
||||
*((Color32*)decompressedBytes + (yBlock * 4 + y) * textureData.Width + (xBlock * 4 + x)) = colors[y * 4 + x];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case PixelFormat::BC3_UNorm:
|
||||
case PixelFormat::BC3_UNorm_sRGB:
|
||||
{
|
||||
for (int32 yBlock = 0; yBlock < blocksHeight; yBlock++)
|
||||
{
|
||||
for (int32 xBlock = 0; xBlock < blocksWidth; xBlock++)
|
||||
{
|
||||
const byte* block = blocksData->Data.Get() + yBlock * 4 * blocksData->RowPitch + xBlock * 16;
|
||||
detexDecompressBlockBC3(block, 0, 0, (byte*)&colors);
|
||||
for (int32 y = 0; y < 4; y++)
|
||||
{
|
||||
for (int32 x = 0; x < 4; x++)
|
||||
{
|
||||
*((Color32*)decompressedBytes + (yBlock * 4 + y) * textureData.Width + (xBlock * 4 + x)) = colors[y * 4 + x];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
LOG(Warning, "Texture data format {0} is not supported by stb library.", (int32)textureData.Format);
|
||||
return true;
|
||||
}
|
||||
texture = &decompressed;
|
||||
}
|
||||
#endif
|
||||
|
||||
// Convert into RGBA8
|
||||
const auto sampler = GetSampler(textureData.Format);
|
||||
const auto sampler = GetSampler(texture->Format);
|
||||
if (sampler == nullptr)
|
||||
{
|
||||
LOG(Warning, "Texture data format {0} is not supported by stb library.", (int32)textureData.Format);
|
||||
return true;
|
||||
}
|
||||
const auto srcData = textureData.GetData(0, 0);
|
||||
const auto srcData = texture->GetData(0, 0);
|
||||
const int comp = 4;
|
||||
Array<byte> data;
|
||||
bool sRGB = PixelFormatExtensions::IsSRGB(textureData.Format);
|
||||
bool sRGB = PixelFormatExtensions::IsSRGB(texture->Format);
|
||||
if (type == ImageType::HDR)
|
||||
{
|
||||
data.Resize(sizeof(float) * comp * textureData.Width * textureData.Height);
|
||||
data.Resize(sizeof(float) * comp * texture->Width * texture->Height);
|
||||
|
||||
auto ptr = (Vector4*)data.Get();
|
||||
for (int32 y = 0; y < textureData.Height; y++)
|
||||
for (int32 y = 0; y < texture->Height; y++)
|
||||
{
|
||||
for (int32 x = 0; x < textureData.Width; x++)
|
||||
for (int32 x = 0; x < texture->Width; x++)
|
||||
{
|
||||
Color color = SamplePoint(sampler, x, y, srcData->Data.Get(), srcData->RowPitch);
|
||||
if (sRGB)
|
||||
color = Color::SrgbToLinear(color);
|
||||
*(ptr + x + y * textureData.Width) = color.ToVector4();
|
||||
*(ptr + x + y * texture->Width) = color.ToVector4();
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
data.Resize(sizeof(Color32) * comp * textureData.Width * textureData.Height);
|
||||
data.Resize(sizeof(Color32) * comp * texture->Width * texture->Height);
|
||||
|
||||
auto ptr = (Color32*)data.Get();
|
||||
for (int32 y = 0; y < textureData.Height; y++)
|
||||
for (int32 y = 0; y < texture->Height; y++)
|
||||
{
|
||||
for (int32 x = 0; x < textureData.Width; x++)
|
||||
for (int32 x = 0; x < texture->Width; x++)
|
||||
{
|
||||
Color color = SamplePoint(sampler, x, y, srcData->Data.Get(), srcData->RowPitch);
|
||||
if (sRGB)
|
||||
color = Color::SrgbToLinear(color);
|
||||
*(ptr + x + y * textureData.Width) = Color32(color);
|
||||
*(ptr + x + y * texture->Width) = Color32(color);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -109,21 +218,21 @@ bool TextureTool::ExportTextureStb(ImageType type, const StringView& path, const
|
||||
switch (type)
|
||||
{
|
||||
case ImageType::BMP:
|
||||
result = stbi_write_bmp_core(&s, textureData.Width, textureData.Height, comp, data.Get());
|
||||
result = stbi_write_bmp_core(&s, texture->Width, texture->Height, comp, data.Get());
|
||||
break;
|
||||
case ImageType::JPEG:
|
||||
result = stbi_write_jpg_core(&s, textureData.Width, textureData.Height, comp, data.Get(), 90);
|
||||
result = stbi_write_jpg_core(&s, texture->Width, texture->Height, comp, data.Get(), 90);
|
||||
break;
|
||||
case ImageType::TGA:
|
||||
result = stbi_write_tga_core(&s, textureData.Width, textureData.Height, comp, data.Get());
|
||||
result = stbi_write_tga_core(&s, texture->Width, texture->Height, comp, data.Get());
|
||||
break;
|
||||
case ImageType::HDR:
|
||||
result = stbi_write_hdr_core(&s, textureData.Width, textureData.Height, comp, (float*)data.Get());
|
||||
result = stbi_write_hdr_core(&s, texture->Width, texture->Height, comp, (float*)data.Get());
|
||||
break;
|
||||
case ImageType::PNG:
|
||||
{
|
||||
int32 ptrSize = 0;
|
||||
const auto ptr = stbi_write_png_to_mem(data.Get(), 0, textureData.Width, textureData.Height, comp, &ptrSize);
|
||||
const auto ptr = stbi_write_png_to_mem(data.Get(), 0, texture->Width, texture->Height, comp, &ptrSize);
|
||||
if (ptr)
|
||||
{
|
||||
file->WriteBytes(ptr, ptrSize);
|
||||
@@ -185,9 +294,10 @@ bool TextureTool::ImportTextureStb(ImageType type, const StringView& path, Textu
|
||||
stbi_uc* stbData = stbi_load_from_memory(fileData.Get(), fileData.Count(), &width, &height, &components, 4);
|
||||
if (!stbData)
|
||||
{
|
||||
LOG(Warning, "Failed to load image.");
|
||||
LOG(Warning, "Failed to load image. {0}", String(stbi_failure_reason()));
|
||||
return false;
|
||||
}
|
||||
fileData.Resize(0);
|
||||
|
||||
// Setup texture data
|
||||
textureData.Width = width;
|
||||
@@ -260,6 +370,370 @@ bool TextureTool::ImportTextureStb(ImageType type, const StringView& path, Textu
|
||||
return false;
|
||||
}
|
||||
|
||||
bool TextureTool::ImportTextureStb(ImageType type, const StringView& path, TextureData& textureData, const Options& options, String& errorMsg, bool& hasAlpha)
|
||||
{
|
||||
// Load image data
|
||||
if (type == ImageType::Internal)
|
||||
{
|
||||
if (options.FlipY)
|
||||
{
|
||||
errorMsg = TEXT("Flipping images imported from Internal source is not supported by stb.");
|
||||
return true;
|
||||
}
|
||||
|
||||
MISSING_CODE("Importing internal textures with STB.");
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
stbi_set_flip_vertically_on_load_thread(options.FlipY);
|
||||
bool failed = ImportTextureStb(type, path, textureData, hasAlpha);
|
||||
stbi_set_flip_vertically_on_load_thread(false);
|
||||
if (failed)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Use two data containers for texture importing for more optimzied performance
|
||||
TextureData textureDataTmp;
|
||||
TextureData* textureDataSrc = &textureData;
|
||||
TextureData* textureDataDst = &textureDataTmp;
|
||||
|
||||
// Check if resize source image
|
||||
const int32 sourceWidth = textureData.Width;
|
||||
const int32 sourceHeight = textureData.Height;
|
||||
int32 width = Math::Clamp(options.Resize ? options.SizeX : static_cast<int32>(sourceWidth * options.Scale), 1, options.MaxSize);
|
||||
int32 height = Math::Clamp(options.Resize ? options.SizeY : static_cast<int32>(sourceHeight * options.Scale), 1, options.MaxSize);
|
||||
if (sourceWidth != width || sourceHeight != height)
|
||||
{
|
||||
// During resizing we need to keep texture aspect ratio
|
||||
const bool keepAspectRatio = false; // TODO: expose as import option
|
||||
if (keepAspectRatio)
|
||||
{
|
||||
const float aspectRatio = static_cast<float>(sourceWidth) / sourceHeight;
|
||||
if (width >= height)
|
||||
height = Math::CeilToInt(width / aspectRatio);
|
||||
else
|
||||
width = Math::CeilToInt(height / aspectRatio);
|
||||
}
|
||||
|
||||
// Resize source texture
|
||||
LOG(Info, "Resizing texture from {0}x{1} to {2}x{3}.", sourceWidth, sourceHeight, width, height);
|
||||
if (ResizeStb(*textureDataDst, *textureDataSrc, width, height))
|
||||
{
|
||||
errorMsg = String::Format(TEXT("Cannot resize texture."));
|
||||
return true;
|
||||
}
|
||||
::Swap(textureDataSrc, textureDataDst);
|
||||
}
|
||||
|
||||
// Cache data
|
||||
float alphaThreshold = 0.3f;
|
||||
bool isPowerOfTwo = Math::IsPowerOfTwo(width) && Math::IsPowerOfTwo(height);
|
||||
PixelFormat targetFormat = TextureUtils::ToPixelFormat(options.Type, width, height, options.Compress);
|
||||
if (options.sRGB)
|
||||
targetFormat = PixelFormatExtensions::TosRGB(targetFormat);
|
||||
|
||||
// Check mip levels
|
||||
int32 sourceMipLevels = textureDataSrc->GetMipLevels();
|
||||
bool hasSourceMipLevels = isPowerOfTwo && sourceMipLevels > 1;
|
||||
bool useMipLevels = isPowerOfTwo && (options.GenerateMipMaps || hasSourceMipLevels) && (width > 1 || height > 1);
|
||||
int32 arraySize = (int32)textureDataSrc->GetArraySize();
|
||||
int32 mipLevels = MipLevelsCount(width, height, useMipLevels);
|
||||
if (useMipLevels && !options.GenerateMipMaps && mipLevels != sourceMipLevels)
|
||||
{
|
||||
errorMsg = String::Format(TEXT("Imported texture has not full mip chain, loaded mips count: {0}, expected: {1}"), sourceMipLevels, mipLevels);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Decompress if texture is compressed (next steps need decompressed input data, for eg. mip maps generation or format changing)
|
||||
if (PixelFormatExtensions::IsCompressed(textureDataSrc->Format))
|
||||
{
|
||||
// TODO: implement texture decompression
|
||||
errorMsg = String::Format(TEXT("Imported texture used compressed format {0}. Not supported for importing on this platform.."), (int32)textureDataSrc->Format);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Generate mip maps chain
|
||||
if (useMipLevels && options.GenerateMipMaps)
|
||||
{
|
||||
for (int32 arrayIndex = 0; arrayIndex < arraySize; arrayIndex++)
|
||||
{
|
||||
auto& slice = textureDataSrc->Items[arrayIndex];
|
||||
slice.Mips.Resize(mipLevels);
|
||||
for (int32 mipIndex = 1; mipIndex < mipLevels; mipIndex++)
|
||||
{
|
||||
const auto& srcMip = slice.Mips[mipIndex - 1];
|
||||
auto& dstMip = slice.Mips[mipIndex];
|
||||
auto dstMipWidth = Math::Max(textureDataSrc->Width >> mipIndex, 1);
|
||||
auto dstMipHeight = Math::Max(textureDataSrc->Height >> mipIndex, 1);
|
||||
if (ResizeStb(textureDataSrc->Format, dstMip, srcMip, dstMipWidth, dstMipHeight))
|
||||
{
|
||||
errorMsg = TEXT("Failed to generate mip texture.");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Preserve mipmap alpha coverage (if requested)
|
||||
if (PixelFormatExtensions::HasAlpha(textureDataSrc->Format) && options.PreserveAlphaCoverage && useMipLevels)
|
||||
{
|
||||
// TODO: implement alpha coverage preserving
|
||||
errorMsg = TEXT("Importing textures with alpha coverage preserving is not supported on this platform.");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Compress mip maps or convert image
|
||||
if (targetFormat != textureDataSrc->Format)
|
||||
{
|
||||
if (ConvertStb(*textureDataDst, *textureDataSrc, targetFormat))
|
||||
{
|
||||
errorMsg = String::Format(TEXT("Cannot convert/compress texture."));
|
||||
return true;
|
||||
}
|
||||
::Swap(textureDataSrc, textureDataDst);
|
||||
}
|
||||
|
||||
// Copy data to the output if not in the result container
|
||||
if (textureDataSrc != &textureData)
|
||||
{
|
||||
textureData = textureDataTmp;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool TextureTool::ConvertStb(TextureData& dst, const TextureData& src, const PixelFormat dstFormat)
|
||||
{
|
||||
// Setup
|
||||
auto arraySize = src.GetArraySize();
|
||||
dst.Width = src.Width;
|
||||
dst.Height = src.Height;
|
||||
dst.Depth = src.Depth;
|
||||
dst.Format = dstFormat;
|
||||
dst.Items.Resize(arraySize, false);
|
||||
auto formatSize = PixelFormatExtensions::SizeInBytes(src.Format);
|
||||
auto components = PixelFormatExtensions::ComputeComponentsCount(src.Format);
|
||||
auto sampler = TextureTool::GetSampler(src.Format);
|
||||
if (!sampler)
|
||||
{
|
||||
LOG(Warning, "Cannot convert image. Unsupported format {0}", static_cast<int32>(src.Format));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (PixelFormatExtensions::IsCompressed(dstFormat))
|
||||
{
|
||||
int32 bytesPerBlock;
|
||||
switch (dstFormat)
|
||||
{
|
||||
case PixelFormat::BC1_UNorm:
|
||||
case PixelFormat::BC1_UNorm_sRGB:
|
||||
case PixelFormat::BC4_UNorm:
|
||||
bytesPerBlock = 8;
|
||||
break;
|
||||
default:
|
||||
bytesPerBlock = 16;
|
||||
break;
|
||||
}
|
||||
bool isDstSRGB = PixelFormatExtensions::IsSRGB(dstFormat);
|
||||
|
||||
// Compress all array slices
|
||||
for (int32 arrayIndex = 0; arrayIndex < arraySize; arrayIndex++)
|
||||
{
|
||||
const auto& srcSlice = src.Items[arrayIndex];
|
||||
auto& dstSlice = dst.Items[arrayIndex];
|
||||
auto mipLevels = srcSlice.Mips.Count();
|
||||
dstSlice.Mips.Resize(mipLevels, false);
|
||||
|
||||
// Compress all mip levels
|
||||
for (int32 mipIndex = 0; mipIndex < mipLevels; mipIndex++)
|
||||
{
|
||||
const auto& srcMip = srcSlice.Mips[mipIndex];
|
||||
auto& dstMip = dstSlice.Mips[mipIndex];
|
||||
auto mipWidth = Math::Max(src.Width >> mipIndex, 1);
|
||||
auto mipHeight = Math::Max(src.Height >> mipIndex, 1);
|
||||
auto blocksWidth = Math::Max(Math::DivideAndRoundUp(mipWidth, 4), 1);
|
||||
auto blocksHeight = Math::Max(Math::DivideAndRoundUp(mipHeight, 4), 1);
|
||||
|
||||
// Allocate memory
|
||||
dstMip.RowPitch = blocksWidth * bytesPerBlock;
|
||||
dstMip.DepthPitch = dstMip.RowPitch * blocksHeight;
|
||||
dstMip.Lines = blocksHeight;
|
||||
dstMip.Data.Allocate(dstMip.DepthPitch);
|
||||
|
||||
// Compress texture
|
||||
for (int32 yBlock = 0; yBlock < blocksHeight; yBlock++)
|
||||
{
|
||||
for (int32 xBlock = 0; xBlock < blocksWidth; xBlock++)
|
||||
{
|
||||
// Sample source texture 4x4 block
|
||||
Color32 srcBlock[16];
|
||||
for (int32 y = 0; y < 4; y++)
|
||||
{
|
||||
for (int32 x = 0; x < 4; x++)
|
||||
{
|
||||
Color color = TextureTool::SamplePoint(sampler, xBlock * 4 + x, yBlock * 4 + y, srcMip.Data.Get(), srcMip.RowPitch);
|
||||
if (isDstSRGB)
|
||||
color = Color::LinearToSrgb(color);
|
||||
srcBlock[y * 4 + x] = Color32(color);
|
||||
}
|
||||
}
|
||||
|
||||
// Compress block
|
||||
switch (dstFormat)
|
||||
{
|
||||
case PixelFormat::BC1_UNorm:
|
||||
case PixelFormat::BC1_UNorm_sRGB:
|
||||
stb_compress_dxt_block((byte*)dstMip.Data.Get() + (yBlock * blocksWidth + xBlock) * bytesPerBlock, (byte*)&srcBlock, 0, STB_DXT_HIGHQUAL);
|
||||
break;
|
||||
case PixelFormat::BC3_UNorm:
|
||||
case PixelFormat::BC3_UNorm_sRGB:
|
||||
stb_compress_dxt_block((byte*)dstMip.Data.Get() + (yBlock * blocksWidth + xBlock) * bytesPerBlock, (byte*)&srcBlock, 1, STB_DXT_HIGHQUAL);
|
||||
break;
|
||||
case PixelFormat::BC4_UNorm:
|
||||
for (int32 i = 1; i < 16; i++)
|
||||
((byte*)&srcBlock)[i] = srcBlock[i].R;
|
||||
stb_compress_bc4_block((byte*)dstMip.Data.Get() + (yBlock * blocksWidth + xBlock) * bytesPerBlock, (byte*)&srcBlock);
|
||||
break;
|
||||
case PixelFormat::BC5_UNorm:
|
||||
for (int32 i = 0; i < 16; i++)
|
||||
((uint16*)&srcBlock)[i] = srcBlock[i].R << 8 | srcBlock[i].G;
|
||||
stb_compress_bc5_block((byte*)dstMip.Data.Get() + (yBlock * blocksWidth + xBlock) * bytesPerBlock, (byte*)&srcBlock);
|
||||
break;
|
||||
default:
|
||||
LOG(Warning, "Cannot compress image. Unsupported format {0}", static_cast<int32>(dstFormat));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
int32 bytesPerPixel = PixelFormatExtensions::SizeInBytes(dstFormat);
|
||||
auto dstSampler = TextureTool::GetSampler(dstFormat);
|
||||
if (!dstSampler)
|
||||
{
|
||||
LOG(Warning, "Cannot convert image. Unsupported format {0}", static_cast<int32>(dstFormat));
|
||||
return true;
|
||||
}
|
||||
|
||||
// Convert all array slices
|
||||
for (int32 arrayIndex = 0; arrayIndex < arraySize; arrayIndex++)
|
||||
{
|
||||
const auto& srcSlice = src.Items[arrayIndex];
|
||||
auto& dstSlice = dst.Items[arrayIndex];
|
||||
auto mipLevels = srcSlice.Mips.Count();
|
||||
dstSlice.Mips.Resize(mipLevels, false);
|
||||
|
||||
// Convert all mip levels
|
||||
for (int32 mipIndex = 0; mipIndex < mipLevels; mipIndex++)
|
||||
{
|
||||
const auto& srcMip = srcSlice.Mips[mipIndex];
|
||||
auto& dstMip = dstSlice.Mips[mipIndex];
|
||||
auto mipWidth = Math::Max(src.Width >> mipIndex, 1);
|
||||
auto mipHeight = Math::Max(src.Height >> mipIndex, 1);
|
||||
|
||||
// Allocate memory
|
||||
dstMip.RowPitch = mipWidth * bytesPerPixel;
|
||||
dstMip.DepthPitch = dstMip.RowPitch * mipHeight;
|
||||
dstMip.Lines = mipHeight;
|
||||
dstMip.Data.Allocate(dstMip.DepthPitch);
|
||||
|
||||
// Convert texture
|
||||
for (int32 y = 0; y < mipHeight; y++)
|
||||
{
|
||||
for (int32 x = 0; x < mipWidth; x++)
|
||||
{
|
||||
// Sample source texture
|
||||
Color color = TextureTool::SamplePoint(sampler, x, y, srcMip.Data.Get(), srcMip.RowPitch);
|
||||
|
||||
// Store destination texture
|
||||
TextureTool::Store(dstSampler, x, y, dstMip.Data.Get(), dstMip.RowPitch, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool TextureTool::ResizeStb(PixelFormat format, TextureMipData& dstMip, const TextureMipData& srcMip, int32 dstMipWidth, int32 dstMipHeight)
|
||||
{
|
||||
// Setup
|
||||
auto formatSize = PixelFormatExtensions::SizeInBytes(format);
|
||||
auto components = PixelFormatExtensions::ComputeComponentsCount(format);
|
||||
auto srcMipWidth = srcMip.RowPitch / formatSize;
|
||||
auto srcMipHeight = srcMip.DepthPitch / srcMip.RowPitch;
|
||||
|
||||
// Allocate memory
|
||||
dstMip.RowPitch = dstMipWidth * formatSize;
|
||||
dstMip.DepthPitch = dstMip.RowPitch * dstMipHeight;
|
||||
dstMip.Lines = dstMipHeight;
|
||||
dstMip.Data.Allocate(dstMip.DepthPitch);
|
||||
|
||||
// Resize texture
|
||||
switch (format)
|
||||
{
|
||||
case PixelFormat::R8_Typeless:
|
||||
case PixelFormat::R8_SInt:
|
||||
case PixelFormat::R8_SNorm:
|
||||
case PixelFormat::R8G8_Typeless:
|
||||
case PixelFormat::R8G8_SInt:
|
||||
case PixelFormat::R8G8_SNorm:
|
||||
case PixelFormat::R8G8B8A8_Typeless:
|
||||
case PixelFormat::R8G8B8A8_UNorm:
|
||||
case PixelFormat::R8G8B8A8_UInt:
|
||||
case PixelFormat::R8G8B8A8_SNorm:
|
||||
case PixelFormat::R8G8B8A8_SInt:
|
||||
case PixelFormat::B8G8R8A8_UNorm:
|
||||
case PixelFormat::B8G8R8X8_Typeless:
|
||||
case PixelFormat::B8G8R8X8_UNorm:
|
||||
{
|
||||
if (!stbir_resize_uint8((const uint8*)srcMip.Data.Get(), srcMipWidth, srcMipHeight, srcMip.RowPitch, (uint8*)dstMip.Data.Get(), dstMipWidth, dstMipHeight, dstMip.RowPitch, components))
|
||||
{
|
||||
LOG(Warning, "Cannot resize image.");
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case PixelFormat::R8G8B8A8_UNorm_sRGB:
|
||||
case PixelFormat::B8G8R8A8_UNorm_sRGB:
|
||||
case PixelFormat::B8G8R8X8_UNorm_sRGB:
|
||||
{
|
||||
auto alphaChannel = format == PixelFormat::B8G8R8X8_UNorm_sRGB ? STBIR_ALPHA_CHANNEL_NONE : 3;
|
||||
if (!stbir_resize_uint8_srgb((const uint8*)srcMip.Data.Get(), srcMipWidth, srcMipHeight, srcMip.RowPitch, (uint8*)dstMip.Data.Get(), dstMipWidth, dstMipHeight, dstMip.RowPitch, components, alphaChannel, 0))
|
||||
{
|
||||
LOG(Warning, "Cannot resize image.");
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case PixelFormat::R32_Typeless:
|
||||
case PixelFormat::R32_Float:
|
||||
case PixelFormat::R32G32_Float:
|
||||
case PixelFormat::R32G32B32_Float:
|
||||
case PixelFormat::R32G32B32A32_Float:
|
||||
{
|
||||
if (!stbir_resize_float((const float*)srcMip.Data.Get(), srcMipWidth, srcMipHeight, srcMip.RowPitch, (float*)dstMip.Data.Get(), dstMipWidth, dstMipHeight, dstMip.RowPitch, components))
|
||||
{
|
||||
LOG(Warning, "Cannot resize image.");
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
LOG(Warning, "Cannot resize image. Unsupported format {0}", static_cast<int32>(format));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool TextureTool::ResizeStb(TextureData& dst, const TextureData& src, int32 dstWidth, int32 dstHeight)
|
||||
{
|
||||
// Setup
|
||||
@@ -268,7 +742,7 @@ bool TextureTool::ResizeStb(TextureData& dst, const TextureData& src, int32 dstW
|
||||
dst.Height = dstHeight;
|
||||
dst.Depth = src.Depth;
|
||||
dst.Format = src.Format;
|
||||
dst.Items.Resize(arraySize);
|
||||
dst.Items.Resize(arraySize, false);
|
||||
auto formatSize = PixelFormatExtensions::SizeInBytes(src.Format);
|
||||
auto components = PixelFormatExtensions::ComputeComponentsCount(src.Format);
|
||||
|
||||
@@ -278,7 +752,7 @@ bool TextureTool::ResizeStb(TextureData& dst, const TextureData& src, int32 dstW
|
||||
const auto& srcSlice = src.Items[arrayIndex];
|
||||
auto& dstSlice = dst.Items[arrayIndex];
|
||||
auto mipLevels = srcSlice.Mips.Count();
|
||||
dstSlice.Mips.Resize(mipLevels);
|
||||
dstSlice.Mips.Resize(mipLevels, false);
|
||||
|
||||
// Resize all mip levels
|
||||
for (int32 mipIndex = 0; mipIndex < mipLevels; mipIndex++)
|
||||
@@ -287,69 +761,10 @@ bool TextureTool::ResizeStb(TextureData& dst, const TextureData& src, int32 dstW
|
||||
auto& dstMip = dstSlice.Mips[mipIndex];
|
||||
auto srcMipWidth = srcMip.RowPitch / formatSize;
|
||||
auto srcMipHeight = srcMip.DepthPitch / srcMip.RowPitch;
|
||||
auto dstMipWidth = Math::Max(dstWidth << mipIndex, 1);
|
||||
auto dstMipHeight = Math::Max(dstHeight << mipIndex, 1);
|
||||
|
||||
// Allocate memory
|
||||
dstMip.RowPitch = dstMipWidth * formatSize;
|
||||
dstMip.DepthPitch = dstMip.RowPitch * dstMipHeight;
|
||||
dstMip.Lines = dstMipHeight;
|
||||
dstMip.Data.Allocate(dstMip.DepthPitch);
|
||||
|
||||
// Resize texture
|
||||
switch (src.Format)
|
||||
{
|
||||
case PixelFormat::R8_Typeless:
|
||||
case PixelFormat::R8_SInt:
|
||||
case PixelFormat::R8_SNorm:
|
||||
case PixelFormat::R8G8_Typeless:
|
||||
case PixelFormat::R8G8_SInt:
|
||||
case PixelFormat::R8G8_SNorm:
|
||||
case PixelFormat::R8G8B8A8_Typeless:
|
||||
case PixelFormat::R8G8B8A8_UNorm:
|
||||
case PixelFormat::R8G8B8A8_UInt:
|
||||
case PixelFormat::R8G8B8A8_SNorm:
|
||||
case PixelFormat::R8G8B8A8_SInt:
|
||||
case PixelFormat::B8G8R8A8_UNorm:
|
||||
case PixelFormat::B8G8R8X8_Typeless:
|
||||
case PixelFormat::B8G8R8X8_UNorm:
|
||||
{
|
||||
if (!stbir_resize_uint8((const uint8*)srcMip.Data.Get(), srcMipWidth, srcMipHeight, srcMip.RowPitch, (uint8*)dstMip.Data.Get(), dstMipWidth, dstMipHeight, dstMip.RowPitch, components))
|
||||
{
|
||||
LOG(Warning, "Cannot resize image.");
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case PixelFormat::R8G8B8A8_UNorm_sRGB:
|
||||
case PixelFormat::B8G8R8A8_UNorm_sRGB:
|
||||
case PixelFormat::B8G8R8X8_UNorm_sRGB:
|
||||
{
|
||||
auto alphaChannel = src.Format == PixelFormat::B8G8R8X8_UNorm_sRGB ? STBIR_ALPHA_CHANNEL_NONE : 3;
|
||||
if (!stbir_resize_uint8_srgb((const uint8*)srcMip.Data.Get(), srcMipWidth, srcMipHeight, srcMip.RowPitch, (uint8*)dstMip.Data.Get(), dstMipWidth, dstMipHeight, dstMip.RowPitch, components, alphaChannel, 0))
|
||||
{
|
||||
LOG(Warning, "Cannot resize image.");
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case PixelFormat::R32_Typeless:
|
||||
case PixelFormat::R32_Float:
|
||||
case PixelFormat::R32G32_Float:
|
||||
case PixelFormat::R32G32B32_Float:
|
||||
case PixelFormat::R32G32B32A32_Float:
|
||||
{
|
||||
if (!stbir_resize_float((const float*)srcMip.Data.Get(), srcMipWidth, srcMipHeight, srcMip.RowPitch, (float*)dstMip.Data.Get(), dstMipWidth, dstMipHeight, dstMip.RowPitch, components))
|
||||
{
|
||||
LOG(Warning, "Cannot resize image.");
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
LOG(Warning, "Cannot resize image. Unsupported format {0}", static_cast<int32>(src.Format));
|
||||
auto dstMipWidth = Math::Max(dstWidth >> mipIndex, 1);
|
||||
auto dstMipHeight = Math::Max(dstHeight >> mipIndex, 1);
|
||||
if (ResizeStb(src.Format, dstMip, srcMip, dstMipWidth, dstMipHeight))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user