Implement lossless WebP encoding

This commit is contained in:
Morris Tabor 2021-04-12 18:56:50 +02:00
parent 9990f28d84
commit 1bc1e94208
9 changed files with 114 additions and 37 deletions

View file

@ -2718,10 +2718,11 @@ void (*Image::_image_decompress_bptc)(Image *) = nullptr;
void (*Image::_image_decompress_etc1)(Image *) = nullptr; void (*Image::_image_decompress_etc1)(Image *) = nullptr;
void (*Image::_image_decompress_etc2)(Image *) = nullptr; void (*Image::_image_decompress_etc2)(Image *) = nullptr;
Vector<uint8_t> (*Image::lossy_packer)(const Ref<Image> &, float) = nullptr; Vector<uint8_t> (*Image::webp_lossy_packer)(const Ref<Image> &, float) = nullptr;
Ref<Image> (*Image::lossy_unpacker)(const Vector<uint8_t> &) = nullptr; Vector<uint8_t> (*Image::webp_lossless_packer)(const Ref<Image> &) = nullptr;
Vector<uint8_t> (*Image::lossless_packer)(const Ref<Image> &) = nullptr; Ref<Image> (*Image::webp_unpacker)(const Vector<uint8_t> &) = nullptr;
Ref<Image> (*Image::lossless_unpacker)(const Vector<uint8_t> &) = nullptr; Vector<uint8_t> (*Image::png_packer)(const Ref<Image> &) = nullptr;
Ref<Image> (*Image::png_unpacker)(const Vector<uint8_t> &) = nullptr;
Vector<uint8_t> (*Image::basis_universal_packer)(const Ref<Image> &, Image::UsedChannels) = nullptr; Vector<uint8_t> (*Image::basis_universal_packer)(const Ref<Image> &, Image::UsedChannels) = nullptr;
Ref<Image> (*Image::basis_universal_unpacker)(const Vector<uint8_t> &) = nullptr; Ref<Image> (*Image::basis_universal_unpacker)(const Vector<uint8_t> &) = nullptr;

View file

@ -148,10 +148,11 @@ public:
static void (*_image_decompress_etc1)(Image *); static void (*_image_decompress_etc1)(Image *);
static void (*_image_decompress_etc2)(Image *); static void (*_image_decompress_etc2)(Image *);
static Vector<uint8_t> (*lossy_packer)(const Ref<Image> &p_image, float p_quality); static Vector<uint8_t> (*webp_lossy_packer)(const Ref<Image> &p_image, float p_quality);
static Ref<Image> (*lossy_unpacker)(const Vector<uint8_t> &p_buffer); static Vector<uint8_t> (*webp_lossless_packer)(const Ref<Image> &p_image);
static Vector<uint8_t> (*lossless_packer)(const Ref<Image> &p_image); static Ref<Image> (*webp_unpacker)(const Vector<uint8_t> &p_buffer);
static Ref<Image> (*lossless_unpacker)(const Vector<uint8_t> &p_buffer); static Vector<uint8_t> (*png_packer)(const Ref<Image> &p_image);
static Ref<Image> (*png_unpacker)(const Vector<uint8_t> &p_buffer);
static Vector<uint8_t> (*basis_universal_packer)(const Ref<Image> &p_image, UsedChannels p_channels); static Vector<uint8_t> (*basis_universal_packer)(const Ref<Image> &p_image, UsedChannels p_channels);
static Ref<Image> (*basis_universal_unpacker)(const Vector<uint8_t> &p_buffer); static Ref<Image> (*basis_universal_unpacker)(const Vector<uint8_t> &p_buffer);

View file

@ -1578,6 +1578,12 @@
<member name="rendering/textures/default_filters/use_nearest_mipmap_filter" type="bool" setter="" getter="" default="false"> <member name="rendering/textures/default_filters/use_nearest_mipmap_filter" type="bool" setter="" getter="" default="false">
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], 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.
</member> </member>
<member name="rendering/textures/lossless_compression/force_png" type="bool" setter="" getter="" default="false">
If [code]true[/code], the texture importer will import lossless textures using the PNG format. Otherwise, it will default to using WebP.
</member>
<member name="rendering/textures/lossless_compression/webp_compression_level" type="int" setter="" getter="" default="2">
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.
</member>
<member name="rendering/textures/vram_compression/import_bptc" type="bool" setter="" getter="" default="false"> <member name="rendering/textures/vram_compression/import_bptc" type="bool" setter="" getter="" default="false">
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. 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.
</member> </member>

View file

@ -101,6 +101,6 @@ Vector<uint8_t> ImageLoaderPNG::lossless_pack_png(const Ref<Image> &p_image) {
ImageLoaderPNG::ImageLoaderPNG() { ImageLoaderPNG::ImageLoaderPNG() {
Image::_png_mem_loader_func = load_mem_png; Image::_png_mem_loader_func = load_mem_png;
Image::lossless_unpacker = lossless_unpack_png; Image::png_unpacker = lossless_unpack_png;
Image::lossless_packer = lossless_pack_png; Image::png_packer = lossless_pack_png;
} }

View file

@ -218,14 +218,21 @@ void ResourceImporterTexture::get_import_options(List<ImportOption> *r_options,
void ResourceImporterTexture::save_to_stex_format(FileAccess *f, const Ref<Image> &p_image, CompressMode p_compress_mode, Image::UsedChannels p_channels, Image::CompressMode p_compress_format, float p_lossy_quality) { void ResourceImporterTexture::save_to_stex_format(FileAccess *f, const Ref<Image> &p_image, CompressMode p_compress_mode, Image::UsedChannels p_channels, Image::CompressMode p_compress_format, float p_lossy_quality) {
switch (p_compress_mode) { switch (p_compress_mode) {
case COMPRESS_LOSSLESS: { 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_width());
f->store_16(p_image->get_height()); f->store_16(p_image->get_height());
f->store_32(p_image->get_mipmap_count()); f->store_32(p_image->get_mipmap_count());
f->store_32(p_image->get_format()); f->store_32(p_image->get_format());
for (int i = 0; i < p_image->get_mipmap_count() + 1; i++) { for (int i = 0; i < p_image->get_mipmap_count() + 1; i++) {
Vector<uint8_t> data = Image::lossless_packer(p_image->get_image_from_mipmap(i)); Vector<uint8_t> 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(); int data_len = data.size();
f->store_32(data_len); f->store_32(data_len);
@ -235,14 +242,14 @@ void ResourceImporterTexture::save_to_stex_format(FileAccess *f, const Ref<Image
} break; } break;
case COMPRESS_LOSSY: { case COMPRESS_LOSSY: {
f->store_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_width());
f->store_16(p_image->get_height()); f->store_16(p_image->get_height());
f->store_32(p_image->get_mipmap_count()); f->store_32(p_image->get_mipmap_count());
f->store_32(p_image->get_format()); f->store_32(p_image->get_format());
for (int i = 0; i < p_image->get_mipmap_count() + 1; i++) { for (int i = 0; i < p_image->get_mipmap_count() + 1; i++) {
Vector<uint8_t> data = Image::lossy_packer(p_image->get_image_from_mipmap(i), p_lossy_quality); Vector<uint8_t> data = Image::webp_lossy_packer(p_image->get_image_from_mipmap(i), p_lossy_quality);
int data_len = data.size(); int data_len = data.size();
f->store_32(data_len); f->store_32(data_len);

View file

@ -30,6 +30,7 @@
#include "image_loader_webp.h" #include "image_loader_webp.h"
#include "core/config/project_settings.h"
#include "core/io/marshalls.h" #include "core/io/marshalls.h"
#include "core/os/os.h" #include "core/os/os.h"
#include "core/string/print_string.h" #include "core/string/print_string.h"
@ -69,12 +70,77 @@ static Vector<uint8_t> _webp_lossy_pack(const Ref<Image> &p_image, float p_quali
w[2] = 'B'; w[2] = 'B';
w[3] = 'P'; w[3] = 'P';
memcpy(&w[4], dst_buff, dst_size); memcpy(&w[4], dst_buff, dst_size);
free(dst_buff); WebPFree(dst_buff);
return dst; return dst;
} }
static Ref<Image> _webp_lossy_unpack(const Vector<uint8_t> &p_buffer) { static Vector<uint8_t> _webp_lossless_pack(const Ref<Image> &p_image) {
ERR_FAIL_COND_V(p_image.is_null() || p_image->is_empty(), Vector<uint8_t>());
int compression_level = ProjectSettings::get_singleton()->get("rendering/textures/lossless_compression/webp_compression_level");
compression_level = CLAMP(compression_level, 0, 9);
Ref<Image> 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<uint8_t> 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<uint8_t>());
}
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<uint8_t>(), "WebP packing failed.");
}
// copy from wrt
Vector<uint8_t> 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<Image> _webp_unpack(const Vector<uint8_t> &p_buffer) {
int size = p_buffer.size() - 4; int size = p_buffer.size() - 4;
ERR_FAIL_COND_V(size <= 0, Ref<Image>()); ERR_FAIL_COND_V(size <= 0, Ref<Image>());
const uint8_t *r = p_buffer.ptr(); const uint8_t *r = p_buffer.ptr();
@ -168,6 +234,7 @@ void ImageLoaderWEBP::get_recognized_extensions(List<String> *p_extensions) cons
ImageLoaderWEBP::ImageLoaderWEBP() { ImageLoaderWEBP::ImageLoaderWEBP() {
Image::_webp_mem_loader_func = _webp_mem_loader_func; Image::_webp_mem_loader_func = _webp_mem_loader_func;
Image::lossy_packer = _webp_lossy_pack; Image::webp_lossy_packer = _webp_lossy_pack;
Image::lossy_unpacker = _webp_lossy_unpack; Image::webp_lossless_packer = _webp_lossless_pack;
Image::webp_unpacker = _webp_unpack;
} }

View file

@ -327,7 +327,7 @@ Ref<Image> StreamTexture2D::load_image_from_file(FileAccess *f, int p_size_limit
uint32_t mipmaps = f->get_32(); uint32_t mipmaps = f->get_32();
Image::Format format = Image::Format(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 //look for a PNG or WEBP file inside
int sw = w; int sw = w;
@ -360,10 +360,10 @@ Ref<Image> StreamTexture2D::load_image_from_file(FileAccess *f, int p_size_limit
Ref<Image> img; Ref<Image> img;
if (data_format == DATA_FORMAT_BASIS_UNIVERSAL) { if (data_format == DATA_FORMAT_BASIS_UNIVERSAL) {
img = Image::basis_universal_unpacker(pv); img = Image::basis_universal_unpacker(pv);
} else if (data_format == DATA_FORMAT_LOSSLESS) { } else if (data_format == DATA_FORMAT_PNG) {
img = Image::lossless_unpacker(pv); img = Image::png_unpacker(pv);
} else { } else {
img = Image::lossy_unpacker(pv); img = Image::webp_unpacker(pv);
} }
if (img.is_null() || img->is_empty()) { if (img.is_null() || img->is_empty()) {

View file

@ -136,8 +136,8 @@ class StreamTexture2D : public Texture2D {
public: public:
enum DataFormat { enum DataFormat {
DATA_FORMAT_IMAGE, DATA_FORMAT_IMAGE,
DATA_FORMAT_LOSSLESS, DATA_FORMAT_PNG,
DATA_FORMAT_LOSSY, DATA_FORMAT_WEBP,
DATA_FORMAT_BASIS_UNIVERSAL, DATA_FORMAT_BASIS_UNIVERSAL,
}; };
@ -146,9 +146,6 @@ public:
}; };
enum FormatBits { 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_STREAM = 1 << 22,
FORMAT_BIT_HAS_MIPMAPS = 1 << 23, FORMAT_BIT_HAS_MIPMAPS = 1 << 23,
FORMAT_BIT_DETECT_3D = 1 << 24, FORMAT_BIT_DETECT_3D = 1 << 24,
@ -389,8 +386,8 @@ class StreamTextureLayered : public TextureLayered {
public: public:
enum DataFormat { enum DataFormat {
DATA_FORMAT_IMAGE, DATA_FORMAT_IMAGE,
DATA_FORMAT_LOSSLESS, DATA_FORMAT_PNG,
DATA_FORMAT_LOSSY, DATA_FORMAT_WEBP,
DATA_FORMAT_BASIS_UNIVERSAL, DATA_FORMAT_BASIS_UNIVERSAL,
}; };
@ -399,9 +396,6 @@ public:
}; };
enum FormatBits { 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_STREAM = 1 << 22,
FORMAT_BIT_HAS_MIPMAPS = 1 << 23, FORMAT_BIT_HAS_MIPMAPS = 1 << 23,
}; };
@ -532,8 +526,8 @@ class StreamTexture3D : public Texture3D {
public: public:
enum DataFormat { enum DataFormat {
DATA_FORMAT_IMAGE, DATA_FORMAT_IMAGE,
DATA_FORMAT_LOSSLESS, DATA_FORMAT_PNG,
DATA_FORMAT_LOSSY, DATA_FORMAT_WEBP,
DATA_FORMAT_BASIS_UNIVERSAL, DATA_FORMAT_BASIS_UNIVERSAL,
}; };
@ -542,9 +536,6 @@ public:
}; };
enum FormatBits { 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_STREAM = 1 << 22,
FORMAT_BIT_HAS_MIPMAPS = 1 << 23, FORMAT_BIT_HAS_MIPMAPS = 1 << 23,
}; };

View file

@ -2282,6 +2282,10 @@ RenderingServer::RenderingServer() {
GLOBAL_DEF_RST("rendering/textures/vram_compression/import_etc2", true); GLOBAL_DEF_RST("rendering/textures/vram_compression/import_etc2", true);
GLOBAL_DEF_RST("rendering/textures/vram_compression/import_pvrtc", false); 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); 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")); 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"));