Implement lossless WebP encoding

This commit is contained in:
Morris Tabor 2021-05-08 19:14:41 +02:00
parent 0bdadd468f
commit 5de08ef1d6
11 changed files with 122 additions and 35 deletions

View File

@ -2487,10 +2487,11 @@ void (*Image::_image_decompress_bptc)(Image *) = nullptr;
void (*Image::_image_decompress_etc1)(Image *) = nullptr;
void (*Image::_image_decompress_etc2)(Image *) = nullptr;
PoolVector<uint8_t> (*Image::lossy_packer)(const Ref<Image> &, float) = nullptr;
Ref<Image> (*Image::lossy_unpacker)(const PoolVector<uint8_t> &) = nullptr;
PoolVector<uint8_t> (*Image::lossless_packer)(const Ref<Image> &) = nullptr;
Ref<Image> (*Image::lossless_unpacker)(const PoolVector<uint8_t> &) = nullptr;
PoolVector<uint8_t> (*Image::webp_lossy_packer)(const Ref<Image> &, float) = nullptr;
PoolVector<uint8_t> (*Image::webp_lossless_packer)(const Ref<Image> &) = nullptr;
Ref<Image> (*Image::webp_unpacker)(const PoolVector<uint8_t> &) = nullptr;
PoolVector<uint8_t> (*Image::png_packer)(const Ref<Image> &) = nullptr;
Ref<Image> (*Image::png_unpacker)(const PoolVector<uint8_t> &) = nullptr;
void Image::_set_data(const Dictionary &p_data) {
ERR_FAIL_COND(!p_data.has("width"));

View File

@ -147,10 +147,11 @@ public:
static void (*_image_decompress_etc1)(Image *);
static void (*_image_decompress_etc2)(Image *);
static PoolVector<uint8_t> (*lossy_packer)(const Ref<Image> &p_image, float p_quality);
static Ref<Image> (*lossy_unpacker)(const PoolVector<uint8_t> &p_buffer);
static PoolVector<uint8_t> (*lossless_packer)(const Ref<Image> &p_image);
static Ref<Image> (*lossless_unpacker)(const PoolVector<uint8_t> &p_buffer);
static PoolVector<uint8_t> (*webp_lossy_packer)(const Ref<Image> &p_image, float p_quality);
static PoolVector<uint8_t> (*webp_lossless_packer)(const Ref<Image> &p_image);
static Ref<Image> (*webp_unpacker)(const PoolVector<uint8_t> &p_buffer);
static PoolVector<uint8_t> (*png_packer)(const Ref<Image> &p_image);
static Ref<Image> (*png_unpacker)(const PoolVector<uint8_t> &p_buffer);
PoolVector<uint8_t>::Write write_lock;

View File

@ -562,10 +562,10 @@ Error ResourceInteractiveLoaderBinary::parse_variant(Variant &r_v) {
Ref<Image> image;
if (encoding == IMAGE_ENCODING_LOSSY && Image::lossy_unpacker) {
image = Image::lossy_unpacker(data);
} else if (encoding == IMAGE_ENCODING_LOSSLESS && Image::lossless_unpacker) {
image = Image::lossless_unpacker(data);
if (encoding == IMAGE_ENCODING_LOSSY && Image::webp_unpacker) {
image = Image::webp_unpacker(data); // IMAGE_ENCODING_LOSSY always meant WebP
} else if (encoding == IMAGE_ENCODING_LOSSLESS && Image::png_unpacker) {
image = Image::png_unpacker(data); // IMAGE_ENCODING_LOSSLESS always meant png
}
_advance_padding(data.size());

View File

@ -1169,6 +1169,12 @@
<member name="rendering/limits/time/time_rollover_secs" type="float" setter="" getter="" default="3600">
Shaders have a time variable that constantly increases. At some point, it needs to be rolled back to zero to avoid precision errors on shader animations. This setting specifies when (in seconds).
</member>
<member name="rendering/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/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/quality/depth/hdr" type="bool" setter="" getter="" default="true">
If [code]true[/code], allocates the main framebuffer with high dynamic range. High dynamic range allows the use of [Color] values greater than 1.
[b]Note:[/b] Only available on the GLES3 backend.

View File

@ -101,6 +101,6 @@ PoolVector<uint8_t> ImageLoaderPNG::lossless_pack_png(const Ref<Image> &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;
}

View File

@ -128,7 +128,9 @@ void ResourceImporterLayeredTexture::_save_tex(const Vector<Ref<Image>> &p_image
image->shrink_x2();
}
PoolVector<uint8_t> data = Image::lossless_packer(image);
// we have to use png here for backward compatibility.
// in 3.x, we don't store type information here
PoolVector<uint8_t> data = Image::png_packer(image);
int data_len = data.size();
f->store_32(data_len);

View File

@ -254,6 +254,8 @@ void ResourceImporterTexture::_save_stex(const Ref<Image> &p_image, const String
switch (p_compress_mode) {
case COMPRESS_LOSSLESS: {
bool lossless_force_png = ProjectSettings::get_singleton()->get("rendering/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
Ref<Image> image = p_image->duplicate();
if (p_mipmaps) {
image->generate_mipmaps();
@ -263,7 +265,11 @@ void ResourceImporterTexture::_save_stex(const Ref<Image> &p_image, const String
int mmc = image->get_mipmap_count() + 1;
format |= StreamTexture::FORMAT_BIT_LOSSLESS;
if (use_webp) {
format |= StreamTexture::FORMAT_BIT_WEBP;
} else {
format |= StreamTexture::FORMAT_BIT_PNG;
}
f->store_32(format);
f->store_32(mmc);
@ -272,7 +278,12 @@ void ResourceImporterTexture::_save_stex(const Ref<Image> &p_image, const String
image->shrink_x2();
}
PoolVector<uint8_t> data = Image::lossless_packer(image);
PoolVector<uint8_t> data;
if (use_webp) {
data = Image::webp_lossless_packer(image);
} else {
data = Image::png_packer(image);
}
int data_len = data.size();
f->store_32(data_len);
@ -291,7 +302,7 @@ void ResourceImporterTexture::_save_stex(const Ref<Image> &p_image, const String
int mmc = image->get_mipmap_count() + 1;
format |= StreamTexture::FORMAT_BIT_LOSSY;
format |= StreamTexture::FORMAT_BIT_WEBP;
f->store_32(format);
f->store_32(mmc);
@ -300,7 +311,7 @@ void ResourceImporterTexture::_save_stex(const Ref<Image> &p_image, const String
image->shrink_x2();
}
PoolVector<uint8_t> data = Image::lossy_packer(image, p_lossy_quality);
PoolVector<uint8_t> data = Image::webp_lossy_packer(image, p_lossy_quality);
int data_len = data.size();
f->store_32(data_len);

View File

@ -33,6 +33,7 @@
#include "core/io/marshalls.h"
#include "core/os/os.h"
#include "core/print_string.h"
#include "core/project_settings.h"
#include <stdlib.h>
#include <webp/decode.h>
@ -69,11 +70,77 @@ static PoolVector<uint8_t> _webp_lossy_pack(const Ref<Image> &p_image, float p_q
w[2] = 'B';
w[3] = 'P';
memcpy(&w[4], dst_buff, dst_size);
free(dst_buff);
WebPFree(dst_buff);
w.release();
return dst;
}
static PoolVector<uint8_t> _webp_lossless_pack(const Ref<Image> &p_image) {
ERR_FAIL_COND_V(p_image.is_null() || p_image->empty(), PoolVector<uint8_t>());
int compression_level = ProjectSettings::get_singleton()->get("rendering/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());
PoolVector<uint8_t> data = img->get_data();
PoolVector<uint8_t>::Read r = data.read();
// 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(PoolVector<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.ptr(), 3 * s.width);
} else {
success_import = WebPPictureImportRGBA(&pic, r.ptr(), 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(PoolVector<uint8_t>(), "WebP packing failed.");
}
// copy from wrt
PoolVector<uint8_t> dst;
dst.resize(4 + wrt.size);
PoolVector<uint8_t>::Write w = dst.write();
w[0] = 'W';
w[1] = 'E';
w[2] = 'B';
w[3] = 'P';
memcpy(&w[4], wrt.mem, wrt.size);
w.release();
WebPMemoryWriterClear(&wrt);
return dst;
}
static Ref<Image> _webp_lossy_unpack(const PoolVector<uint8_t> &p_buffer) {
int size = p_buffer.size() - 4;
ERR_FAIL_COND_V(size <= 0, Ref<Image>());
@ -171,6 +238,7 @@ void ImageLoaderWEBP::get_recognized_extensions(List<String> *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_lossy_unpack;
}

View File

@ -516,7 +516,7 @@ Error StreamTexture::_load_data(const String &p_path, int &tw, int &th, int &tw_
p_size_limit = 0;
}
if (df & FORMAT_BIT_LOSSLESS || df & FORMAT_BIT_LOSSY) {
if (df & FORMAT_BIT_PNG || df & FORMAT_BIT_WEBP) {
//look for a PNG or WEBP file inside
int sw = tw;
@ -554,10 +554,10 @@ Error StreamTexture::_load_data(const String &p_path, int &tw, int &th, int &tw_
}
Ref<Image> img;
if (df & FORMAT_BIT_LOSSLESS) {
img = Image::lossless_unpacker(pv);
if (df & FORMAT_BIT_PNG) {
img = Image::png_unpacker(pv);
} else {
img = Image::lossy_unpacker(pv);
img = Image::webp_unpacker(pv);
}
if (img.is_null() || img->empty()) {
@ -2182,7 +2182,7 @@ Error TextureLayered::load(const String &p_path) {
f->get_buffer(w.ptr(), size);
}
Ref<Image> img = Image::lossless_unpacker(pv);
Ref<Image> img = Image::png_unpacker(pv);
if (img.is_null() || img->empty() || format != img->get_format()) {
f->close();

View File

@ -164,16 +164,10 @@ class StreamTexture : public Texture {
GDCLASS(StreamTexture, Texture);
public:
enum DataFormat {
DATA_FORMAT_IMAGE,
DATA_FORMAT_LOSSLESS,
DATA_FORMAT_LOSSY
};
enum FormatBits {
FORMAT_MASK_IMAGE_FORMAT = (1 << 20) - 1,
FORMAT_BIT_LOSSLESS = 1 << 20,
FORMAT_BIT_LOSSY = 1 << 21,
FORMAT_BIT_PNG = 1 << 20,
FORMAT_BIT_WEBP = 1 << 21,
FORMAT_BIT_STREAM = 1 << 22,
FORMAT_BIT_HAS_MIPMAPS = 1 << 23,
FORMAT_BIT_DETECT_3D = 1 << 24,

View File

@ -2250,6 +2250,10 @@ VisualServer::VisualServer() {
GLOBAL_DEF_RST("rendering/vram_compression/import_etc2", true);
GLOBAL_DEF_RST("rendering/vram_compression/import_pvrtc", false);
GLOBAL_DEF("rendering/lossless_compression/force_png", false);
GLOBAL_DEF("rendering/lossless_compression/webp_compression_level", 2);
ProjectSettings::get_singleton()->set_custom_property_info("rendering/lossless_compression/webp_compression_level", PropertyInfo(Variant::INT, "rendering/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::REAL, "rendering/limits/time/time_rollover_secs", PROPERTY_HINT_RANGE, "0,10000,1,or_greater"));