From 8c05c2830c62d38b8b159ecf13f2b22293bfe299 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20J=2E=20Est=C3=A9banez?= Date: Fri, 1 Jun 2018 21:53:35 +0200 Subject: [PATCH] Add trilinear filtering to image scaling --- core/image.cpp | 102 +++++++++++++++++++++++++++++++++++++++--- core/image.h | 2 + doc/classes/Image.xml | 6 +++ 3 files changed, 103 insertions(+), 7 deletions(-) diff --git a/core/image.cpp b/core/image.cpp index c08b1ac39b..c862daf282 100644 --- a/core/image.cpp +++ b/core/image.cpp @@ -33,6 +33,7 @@ #include "core/io/image_loader.h" #include "core/os/copymem.h" #include "hash_map.h" +#include "math_funcs.h" #include "print_string.h" #include "thirdparty/misc/hq2x.h" @@ -676,6 +677,16 @@ static void _scale_nearest(const uint8_t *p_src, uint8_t *p_dst, uint32_t p_src_ } } +static void _overlay(const uint8_t *p_src, uint8_t *p_dst, float p_alpha, uint32_t p_width, uint32_t p_height, uint32_t p_pixel_size) { + + uint16_t alpha = CLAMP((uint16_t)(p_alpha * 256.0f), 0, 256); + + for (uint32_t i = 0; i < p_width * p_height * p_pixel_size; i++) { + + p_dst[i] = (p_dst[i] * (256 - alpha) + p_src[i] * alpha) >> 8; + } +} + void Image::resize_to_po2(bool p_square) { if (!_can_modify(format)) { @@ -707,6 +718,8 @@ void Image::resize(int p_width, int p_height, Interpolation p_interpolation) { ERR_FAIL(); } + bool mipmap_aware = p_interpolation == INTERPOLATE_TRILINEAR /* || p_interpolation == INTERPOLATE_TRICUBIC */; + ERR_FAIL_COND(p_width <= 0); ERR_FAIL_COND(p_height <= 0); ERR_FAIL_COND(p_width > MAX_WIDTH); @@ -717,6 +730,32 @@ void Image::resize(int p_width, int p_height, Interpolation p_interpolation) { Image dst(p_width, p_height, 0, format); + // Setup mipmap-aware scaling + Image dst2; + int mip1; + int mip2; + float mip1_weight; + if (mipmap_aware) { + float avg_scale = ((float)p_width / width + (float)p_height / height) * 0.5f; + if (avg_scale >= 1.0f) { + mipmap_aware = false; + } else { + float level = Math::log(1.0f / avg_scale) / Math::log(2.0f); + mip1 = CLAMP((int)Math::floor(level), 0, get_mipmap_count()); + mip2 = CLAMP((int)Math::ceil(level), 0, get_mipmap_count()); + mip1_weight = 1.0f - (level - mip1); + } + } + bool interpolate_mipmaps = mipmap_aware && mip1 != mip2; + if (interpolate_mipmaps) { + dst2.create(p_width, p_height, 0, format); + } + bool had_mipmaps = mipmaps; + if (interpolate_mipmaps && !had_mipmaps) { + generate_mipmaps(); + } + // -- + PoolVector::Read r = data.read(); const unsigned char *r_ptr = r.ptr(); @@ -734,13 +773,57 @@ void Image::resize(int p_width, int p_height, Interpolation p_interpolation) { case 4: _scale_nearest<4>(r_ptr, w_ptr, width, height, p_width, p_height); break; } } break; - case INTERPOLATE_BILINEAR: { + case INTERPOLATE_BILINEAR: + case INTERPOLATE_TRILINEAR: { - switch (get_format_pixel_size(format)) { - case 1: _scale_bilinear<1>(r_ptr, w_ptr, width, height, p_width, p_height); break; - case 2: _scale_bilinear<2>(r_ptr, w_ptr, width, height, p_width, p_height); break; - case 3: _scale_bilinear<3>(r_ptr, w_ptr, width, height, p_width, p_height); break; - case 4: _scale_bilinear<4>(r_ptr, w_ptr, width, height, p_width, p_height); break; + for (int i = 0; i < 2; ++i) { + int src_width; + int src_height; + const unsigned char *src_ptr; + + if (!mipmap_aware) { + if (i == 0) { + // Standard behavior + src_width = width; + src_height = height; + src_ptr = r_ptr; + } else { + // No need for a second iteration + break; + } + } else { + if (i == 0) { + // Read from the first mipmap that will be interpolated + // (if both levels are the same, we will not interpolate, but at least we'll sample from the right level) + int offs; + _get_mipmap_offset_and_size(mip1, offs, src_width, src_height); + src_ptr = r_ptr + offs; + } else if (!interpolate_mipmaps) { + // No need generate a second image + break; + } else { + // Switch to read from the second mipmap that will be interpolated + int offs; + _get_mipmap_offset_and_size(mip2, offs, src_width, src_height); + src_ptr = r_ptr + offs; + // Switch to write to the second destination image + w = dst2.data.write(); + w_ptr = w.ptr(); + } + } + + switch (get_format_pixel_size(format)) { + case 1: _scale_bilinear<1>(src_ptr, w_ptr, src_width, src_height, p_width, p_height); break; + case 2: _scale_bilinear<2>(src_ptr, w_ptr, src_width, src_height, p_width, p_height); break; + case 3: _scale_bilinear<3>(src_ptr, w_ptr, src_width, src_height, p_width, p_height); break; + case 4: _scale_bilinear<4>(src_ptr, w_ptr, src_width, src_height, p_width, p_height); break; + } + } + + if (interpolate_mipmaps) { + // Switch to read again from the first scaled mipmap to overlay it over the second + r = dst.data.read(); + _overlay(r.ptr(), w.ptr(), mip1_weight, p_width, p_height, get_format_pixel_size(format)); } } break; @@ -759,7 +842,11 @@ void Image::resize(int p_width, int p_height, Interpolation p_interpolation) { r = PoolVector::Read(); w = PoolVector::Write(); - if (mipmaps > 0) + if (interpolate_mipmaps) { + dst._copy_internals_from(dst2); + } + + if (had_mipmaps) dst.generate_mipmaps(); _copy_internals_from(dst); @@ -2372,6 +2459,7 @@ void Image::_bind_methods() { BIND_ENUM_CONSTANT(INTERPOLATE_NEAREST); BIND_ENUM_CONSTANT(INTERPOLATE_BILINEAR); BIND_ENUM_CONSTANT(INTERPOLATE_CUBIC); + BIND_ENUM_CONSTANT(INTERPOLATE_TRILINEAR); BIND_ENUM_CONSTANT(ALPHA_NONE); BIND_ENUM_CONSTANT(ALPHA_BIT); diff --git a/core/image.h b/core/image.h index e38fa19ded..fff893ebb1 100644 --- a/core/image.h +++ b/core/image.h @@ -107,6 +107,8 @@ public: INTERPOLATE_NEAREST, INTERPOLATE_BILINEAR, INTERPOLATE_CUBIC, + INTERPOLATE_TRILINEAR, + /* INTERPOLATE_TRICUBIC, */ /* INTERPOLATE GAUSS */ }; diff --git a/doc/classes/Image.xml b/doc/classes/Image.xml index 760b0c6bdc..7d34eccc71 100644 --- a/doc/classes/Image.xml +++ b/doc/classes/Image.xml @@ -578,6 +578,12 @@ + + Performs bilinear separately on the two most suited mipmap levels, then linearly interpolates between them. + It's slower than [code]INTERPOLATE_BILINEAR[/code], but produces higher quality results, with much less aliasing artifacts. + If the image does not have mipmaps, they will be generated and used internally, but no mipmaps will be generated on the resulting image. (Note that if you intend to scale multiple copies of the original image, it's better to call [code]generate_mipmaps[/code] on it in advance, to avoid wasting processing power in generating them again and again.) + On the other hand, if the image already has mipmaps, they will be used, and a new set will be generated for the resulting image. +