From 1bc1e942089043418cc4415af313b86e4155bff6 Mon Sep 17 00:00:00 2001 From: Morris Tabor <80684659+mortarroad@users.noreply.github.com> Date: Mon, 12 Apr 2021 18:56:50 +0200 Subject: [PATCH] Implement lossless WebP encoding --- core/io/image.cpp | 9 +-- core/io/image.h | 9 +-- doc/classes/ProjectSettings.xml | 6 ++ drivers/png/image_loader_png.cpp | 4 +- editor/import/resource_importer_texture.cpp | 15 +++-- modules/webp/image_loader_webp.cpp | 75 +++++++++++++++++++-- scene/resources/texture.cpp | 8 +-- scene/resources/texture.h | 21 ++---- servers/rendering_server.cpp | 4 ++ 9 files changed, 114 insertions(+), 37 deletions(-) diff --git a/core/io/image.cpp b/core/io/image.cpp index c36fa6e45f9..96f7db48829 100644 --- a/core/io/image.cpp +++ b/core/io/image.cpp @@ -2718,10 +2718,11 @@ void (*Image::_image_decompress_bptc)(Image *) = nullptr; void (*Image::_image_decompress_etc1)(Image *) = nullptr; void (*Image::_image_decompress_etc2)(Image *) = nullptr; -Vector (*Image::lossy_packer)(const Ref &, float) = nullptr; -Ref (*Image::lossy_unpacker)(const Vector &) = nullptr; -Vector (*Image::lossless_packer)(const Ref &) = nullptr; -Ref (*Image::lossless_unpacker)(const Vector &) = nullptr; +Vector (*Image::webp_lossy_packer)(const Ref &, float) = nullptr; +Vector (*Image::webp_lossless_packer)(const Ref &) = nullptr; +Ref (*Image::webp_unpacker)(const Vector &) = nullptr; +Vector (*Image::png_packer)(const Ref &) = nullptr; +Ref (*Image::png_unpacker)(const Vector &) = nullptr; Vector (*Image::basis_universal_packer)(const Ref &, Image::UsedChannels) = nullptr; Ref (*Image::basis_universal_unpacker)(const Vector &) = nullptr; diff --git a/core/io/image.h b/core/io/image.h index df8f9b35a1a..060e54a308a 100644 --- a/core/io/image.h +++ b/core/io/image.h @@ -148,10 +148,11 @@ public: static void (*_image_decompress_etc1)(Image *); static void (*_image_decompress_etc2)(Image *); - static Vector (*lossy_packer)(const Ref &p_image, float p_quality); - static Ref (*lossy_unpacker)(const Vector &p_buffer); - static Vector (*lossless_packer)(const Ref &p_image); - static Ref (*lossless_unpacker)(const Vector &p_buffer); + static Vector (*webp_lossy_packer)(const Ref &p_image, float p_quality); + static Vector (*webp_lossless_packer)(const Ref &p_image); + static Ref (*webp_unpacker)(const Vector &p_buffer); + static Vector (*png_packer)(const Ref &p_image); + static Ref (*png_unpacker)(const Vector &p_buffer); static Vector (*basis_universal_packer)(const Ref &p_image, UsedChannels p_channels); static Ref (*basis_universal_unpacker)(const Vector &p_buffer); diff --git a/doc/classes/ProjectSettings.xml b/doc/classes/ProjectSettings.xml index a200858a3ce..8a7a94332b5 100644 --- a/doc/classes/ProjectSettings.xml +++ b/doc/classes/ProjectSettings.xml @@ -1578,6 +1578,12 @@ If [code]true[/code], uses nearest-neighbor mipmap filtering when using mipmaps (also called "bilinear filtering"), which will result in visible seams appearing between mipmap stages. This may increase performance in mobile as less memory bandwidth is used. If [code]false[/code], linear mipmap filtering (also called "trilinear filtering") is used. + + If [code]true[/code], the texture importer will import lossless textures using the PNG format. Otherwise, it will default to using WebP. + + + The default compression level for lossless WebP. Higher levels result in smaller files at the cost of compression speed. Decompression speed is mostly unaffected by the compression level. Supported values are 0 to 9. Note that compression levels above 6 are very slow and offer very little savings. + If [code]true[/code], the texture importer will import VRAM-compressed textures using the BPTC algorithm. This texture compression algorithm is only supported on desktop platforms, and only when using the Vulkan renderer. diff --git a/drivers/png/image_loader_png.cpp b/drivers/png/image_loader_png.cpp index 8b2e786146b..8cc76b01e16 100644 --- a/drivers/png/image_loader_png.cpp +++ b/drivers/png/image_loader_png.cpp @@ -101,6 +101,6 @@ Vector ImageLoaderPNG::lossless_pack_png(const Ref &p_image) { ImageLoaderPNG::ImageLoaderPNG() { Image::_png_mem_loader_func = load_mem_png; - Image::lossless_unpacker = lossless_unpack_png; - Image::lossless_packer = lossless_pack_png; + Image::png_unpacker = lossless_unpack_png; + Image::png_packer = lossless_pack_png; } diff --git a/editor/import/resource_importer_texture.cpp b/editor/import/resource_importer_texture.cpp index de8031af350..0f2c9198200 100644 --- a/editor/import/resource_importer_texture.cpp +++ b/editor/import/resource_importer_texture.cpp @@ -218,14 +218,21 @@ void ResourceImporterTexture::get_import_options(List *r_options, void ResourceImporterTexture::save_to_stex_format(FileAccess *f, const Ref &p_image, CompressMode p_compress_mode, Image::UsedChannels p_channels, Image::CompressMode p_compress_format, float p_lossy_quality) { switch (p_compress_mode) { case COMPRESS_LOSSLESS: { - f->store_32(StreamTexture2D::DATA_FORMAT_LOSSLESS); + bool lossless_force_png = ProjectSettings::get_singleton()->get("rendering/textures/lossless_compression/force_png"); + bool use_webp = !lossless_force_png && p_image->get_width() <= 16383 && p_image->get_height() <= 16383; // WebP has a size limit + f->store_32(use_webp ? StreamTexture2D::DATA_FORMAT_WEBP : StreamTexture2D::DATA_FORMAT_PNG); f->store_16(p_image->get_width()); f->store_16(p_image->get_height()); f->store_32(p_image->get_mipmap_count()); f->store_32(p_image->get_format()); for (int i = 0; i < p_image->get_mipmap_count() + 1; i++) { - Vector data = Image::lossless_packer(p_image->get_image_from_mipmap(i)); + Vector data; + if (use_webp) { + data = Image::webp_lossless_packer(p_image->get_image_from_mipmap(i)); + } else { + data = Image::png_packer(p_image->get_image_from_mipmap(i)); + } int data_len = data.size(); f->store_32(data_len); @@ -235,14 +242,14 @@ void ResourceImporterTexture::save_to_stex_format(FileAccess *f, const Refstore_32(StreamTexture2D::DATA_FORMAT_LOSSY); + f->store_32(StreamTexture2D::DATA_FORMAT_WEBP); f->store_16(p_image->get_width()); f->store_16(p_image->get_height()); f->store_32(p_image->get_mipmap_count()); f->store_32(p_image->get_format()); for (int i = 0; i < p_image->get_mipmap_count() + 1; i++) { - Vector data = Image::lossy_packer(p_image->get_image_from_mipmap(i), p_lossy_quality); + Vector data = Image::webp_lossy_packer(p_image->get_image_from_mipmap(i), p_lossy_quality); int data_len = data.size(); f->store_32(data_len); diff --git a/modules/webp/image_loader_webp.cpp b/modules/webp/image_loader_webp.cpp index 1f2a456619a..772445190c7 100644 --- a/modules/webp/image_loader_webp.cpp +++ b/modules/webp/image_loader_webp.cpp @@ -30,6 +30,7 @@ #include "image_loader_webp.h" +#include "core/config/project_settings.h" #include "core/io/marshalls.h" #include "core/os/os.h" #include "core/string/print_string.h" @@ -69,12 +70,77 @@ static Vector _webp_lossy_pack(const Ref &p_image, float p_quali w[2] = 'B'; w[3] = 'P'; memcpy(&w[4], dst_buff, dst_size); - free(dst_buff); + WebPFree(dst_buff); return dst; } -static Ref _webp_lossy_unpack(const Vector &p_buffer) { +static Vector _webp_lossless_pack(const Ref &p_image) { + ERR_FAIL_COND_V(p_image.is_null() || p_image->is_empty(), Vector()); + + int compression_level = ProjectSettings::get_singleton()->get("rendering/textures/lossless_compression/webp_compression_level"); + compression_level = CLAMP(compression_level, 0, 9); + + Ref img = p_image->duplicate(); + if (img->detect_alpha()) { + img->convert(Image::FORMAT_RGBA8); + } else { + img->convert(Image::FORMAT_RGB8); + } + + Size2 s(img->get_width(), img->get_height()); + Vector data = img->get_data(); + const uint8_t *r = data.ptr(); + + // we need to use the more complex API in order to access the 'exact' flag... + + WebPConfig config; + WebPPicture pic; + if (!WebPConfigInit(&config) || !WebPConfigLosslessPreset(&config, compression_level) || !WebPPictureInit(&pic)) { + ERR_FAIL_V(Vector()); + } + + WebPMemoryWriter wrt; + config.exact = 1; + pic.use_argb = 1; + pic.width = s.width; + pic.height = s.height; + pic.writer = WebPMemoryWrite; + pic.custom_ptr = &wrt; + WebPMemoryWriterInit(&wrt); + + bool success_import = false; + if (img->get_format() == Image::FORMAT_RGB8) { + success_import = WebPPictureImportRGB(&pic, r, 3 * s.width); + } else { + success_import = WebPPictureImportRGBA(&pic, r, 4 * s.width); + } + bool success_encode = false; + if (success_import) { + success_encode = WebPEncode(&config, &pic); + } + WebPPictureFree(&pic); + + if (!success_encode) { + WebPMemoryWriterClear(&wrt); + ERR_FAIL_V_MSG(Vector(), "WebP packing failed."); + } + + // copy from wrt + Vector dst; + dst.resize(4 + wrt.size); + uint8_t *w = dst.ptrw(); + w[0] = 'W'; + w[1] = 'E'; + w[2] = 'B'; + w[3] = 'P'; + memcpy(&w[4], wrt.mem, wrt.size); + WebPMemoryWriterClear(&wrt); + + return dst; +} + +static Ref _webp_unpack(const Vector &p_buffer) { int size = p_buffer.size() - 4; ERR_FAIL_COND_V(size <= 0, Ref()); const uint8_t *r = p_buffer.ptr(); @@ -168,6 +234,7 @@ void ImageLoaderWEBP::get_recognized_extensions(List *p_extensions) cons ImageLoaderWEBP::ImageLoaderWEBP() { Image::_webp_mem_loader_func = _webp_mem_loader_func; - Image::lossy_packer = _webp_lossy_pack; - Image::lossy_unpacker = _webp_lossy_unpack; + Image::webp_lossy_packer = _webp_lossy_pack; + Image::webp_lossless_packer = _webp_lossless_pack; + Image::webp_unpacker = _webp_unpack; } diff --git a/scene/resources/texture.cpp b/scene/resources/texture.cpp index 4475179431e..064563d4b56 100644 --- a/scene/resources/texture.cpp +++ b/scene/resources/texture.cpp @@ -327,7 +327,7 @@ Ref StreamTexture2D::load_image_from_file(FileAccess *f, int p_size_limit uint32_t mipmaps = f->get_32(); Image::Format format = Image::Format(f->get_32()); - if (data_format == DATA_FORMAT_LOSSLESS || data_format == DATA_FORMAT_LOSSY || data_format == DATA_FORMAT_BASIS_UNIVERSAL) { + if (data_format == DATA_FORMAT_PNG || data_format == DATA_FORMAT_WEBP || data_format == DATA_FORMAT_BASIS_UNIVERSAL) { //look for a PNG or WEBP file inside int sw = w; @@ -360,10 +360,10 @@ Ref StreamTexture2D::load_image_from_file(FileAccess *f, int p_size_limit Ref img; if (data_format == DATA_FORMAT_BASIS_UNIVERSAL) { img = Image::basis_universal_unpacker(pv); - } else if (data_format == DATA_FORMAT_LOSSLESS) { - img = Image::lossless_unpacker(pv); + } else if (data_format == DATA_FORMAT_PNG) { + img = Image::png_unpacker(pv); } else { - img = Image::lossy_unpacker(pv); + img = Image::webp_unpacker(pv); } if (img.is_null() || img->is_empty()) { diff --git a/scene/resources/texture.h b/scene/resources/texture.h index df8c00f8ff9..95e960b5a5e 100644 --- a/scene/resources/texture.h +++ b/scene/resources/texture.h @@ -136,8 +136,8 @@ class StreamTexture2D : public Texture2D { public: enum DataFormat { DATA_FORMAT_IMAGE, - DATA_FORMAT_LOSSLESS, - DATA_FORMAT_LOSSY, + DATA_FORMAT_PNG, + DATA_FORMAT_WEBP, DATA_FORMAT_BASIS_UNIVERSAL, }; @@ -146,9 +146,6 @@ public: }; enum FormatBits { - FORMAT_MASK_IMAGE_FORMAT = (1 << 20) - 1, - FORMAT_BIT_LOSSLESS = 1 << 20, - FORMAT_BIT_LOSSY = 1 << 21, FORMAT_BIT_STREAM = 1 << 22, FORMAT_BIT_HAS_MIPMAPS = 1 << 23, FORMAT_BIT_DETECT_3D = 1 << 24, @@ -389,8 +386,8 @@ class StreamTextureLayered : public TextureLayered { public: enum DataFormat { DATA_FORMAT_IMAGE, - DATA_FORMAT_LOSSLESS, - DATA_FORMAT_LOSSY, + DATA_FORMAT_PNG, + DATA_FORMAT_WEBP, DATA_FORMAT_BASIS_UNIVERSAL, }; @@ -399,9 +396,6 @@ public: }; enum FormatBits { - FORMAT_MASK_IMAGE_FORMAT = (1 << 20) - 1, - FORMAT_BIT_LOSSLESS = 1 << 20, - FORMAT_BIT_LOSSY = 1 << 21, FORMAT_BIT_STREAM = 1 << 22, FORMAT_BIT_HAS_MIPMAPS = 1 << 23, }; @@ -532,8 +526,8 @@ class StreamTexture3D : public Texture3D { public: enum DataFormat { DATA_FORMAT_IMAGE, - DATA_FORMAT_LOSSLESS, - DATA_FORMAT_LOSSY, + DATA_FORMAT_PNG, + DATA_FORMAT_WEBP, DATA_FORMAT_BASIS_UNIVERSAL, }; @@ -542,9 +536,6 @@ public: }; enum FormatBits { - FORMAT_MASK_IMAGE_FORMAT = (1 << 20) - 1, - FORMAT_BIT_LOSSLESS = 1 << 20, - FORMAT_BIT_LOSSY = 1 << 21, FORMAT_BIT_STREAM = 1 << 22, FORMAT_BIT_HAS_MIPMAPS = 1 << 23, }; diff --git a/servers/rendering_server.cpp b/servers/rendering_server.cpp index 4741e90a819..349330afd1f 100644 --- a/servers/rendering_server.cpp +++ b/servers/rendering_server.cpp @@ -2282,6 +2282,10 @@ RenderingServer::RenderingServer() { GLOBAL_DEF_RST("rendering/textures/vram_compression/import_etc2", true); GLOBAL_DEF_RST("rendering/textures/vram_compression/import_pvrtc", false); + GLOBAL_DEF("rendering/textures/lossless_compression/force_png", false); + GLOBAL_DEF("rendering/textures/lossless_compression/webp_compression_level", 2); + ProjectSettings::get_singleton()->set_custom_property_info("rendering/textures/lossless_compression/webp_compression_level", PropertyInfo(Variant::INT, "rendering/textures/lossless_compression/webp_compression_level", PROPERTY_HINT_RANGE, "0,9,1")); + GLOBAL_DEF("rendering/limits/time/time_rollover_secs", 3600); ProjectSettings::get_singleton()->set_custom_property_info("rendering/limits/time/time_rollover_secs", PropertyInfo(Variant::FLOAT, "rendering/limits/time/time_rollover_secs", PROPERTY_HINT_RANGE, "0,10000,1,or_greater"));