From 6d61375cc72bad5c569d25c253adca4e3701dd27 Mon Sep 17 00:00:00 2001 From: Aaron Suen Date: Mon, 9 Mar 2015 09:32:11 -0400 Subject: [PATCH] Clean scaling pre-filter for formspec/HUD. --- build/android/jni/Android.mk | 2 + minetest.conf.example | 13 +++ src/CMakeLists.txt | 2 + src/client.cpp | 6 ++ src/client/tile.cpp | 61 ++----------- src/client/tile.h | 16 +--- src/defaultsettings.cpp | 2 + src/drawscene.cpp | 17 ++-- src/guiEngine.cpp | 11 ++- src/guiFormSpecMenu.cpp | 15 +-- src/guiTable.cpp | 1 + src/guiscalingfilter.cpp | 160 ++++++++++++++++++++++++++++++++ src/guiscalingfilter.h | 52 +++++++++++ src/hud.cpp | 13 +-- src/imagefilters.cpp | 172 +++++++++++++++++++++++++++++++++++ src/imagefilters.h | 46 ++++++++++ src/touchscreengui.cpp | 21 +++-- src/touchscreengui.h | 2 +- src/util/numeric.cpp | 1 - src/util/numeric.h | 13 ++- 20 files changed, 524 insertions(+), 102 deletions(-) create mode 100644 src/guiscalingfilter.cpp create mode 100644 src/guiscalingfilter.h create mode 100644 src/imagefilters.cpp create mode 100644 src/imagefilters.h diff --git a/build/android/jni/Android.mk b/build/android/jni/Android.mk index 24367a2a..af7980c5 100644 --- a/build/android/jni/Android.mk +++ b/build/android/jni/Android.mk @@ -143,9 +143,11 @@ LOCAL_SRC_FILES := \ jni/src/guiKeyChangeMenu.cpp \ jni/src/guiPasswordChange.cpp \ jni/src/guiTable.cpp \ + jni/src/guiscalingfilter.cpp \ jni/src/guiVolumeChange.cpp \ jni/src/httpfetch.cpp \ jni/src/hud.cpp \ + jni/src/imagefilters.cpp \ jni/src/inventory.cpp \ jni/src/inventorymanager.cpp \ jni/src/itemdef.cpp \ diff --git a/minetest.conf.example b/minetest.conf.example index fd43d279..d9c3eb61 100644 --- a/minetest.conf.example +++ b/minetest.conf.example @@ -172,6 +172,19 @@ #crosshair_alpha = 255 # Scale gui by a user specified value #gui_scaling = 1.0 +# Use a nearest-neighbor-anti-alias filter to scale the GUI. +# This will smooth over some of the rough edges, and blend +# pixels when scaling down, at the cost of blurring some +# edge pixels when images are scaled by non-integer sizes. +#gui_scaling_filter = false +# When gui_scaling_filter = true, all GUI images need to be +# filtered in software, but some images are generated directly +# to hardare (e.g. render-to-texture for nodes in inventory). +# When gui_scaling_filter_txr2img is true, copy those images +# from hardware to software for scaling. When false, fall back +# to the old scaling method, for video drivers that don't +# propery support downloading textures back from hardware. +#gui_scaling_filter_txr2img = true # Sensitivity multiplier #mouse_sensitivity = 0.2 # Sound settings diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b33bea87..a1c2d013 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -405,9 +405,11 @@ set(client_SRCS guiFormSpecMenu.cpp guiKeyChangeMenu.cpp guiPasswordChange.cpp + guiscalingfilter.cpp guiTable.cpp guiVolumeChange.cpp hud.cpp + imagefilters.cpp keycode.cpp localplayer.cpp main.cpp diff --git a/src/client.cpp b/src/client.cpp index dc2b54e9..ba78cb51 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -49,6 +49,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "drawscene.h" #include "database-sqlite3.h" #include "serialization.h" +#include "guiscalingfilter.h" extern gui::IGUIEnvironment* guienv; @@ -1607,6 +1608,11 @@ void Client::afterContentReceived(IrrlichtDevice *device) const wchar_t* text = wgettext("Loading textures..."); + // Clear cached pre-scaled 2D GUI images, as this cache + // might have images with the same name but different + // content from previous sessions. + guiScalingCacheClear(device->getVideoDriver()); + // Rebuild inherited images and recreate textures infostream<<"- Rebuilding images and textures"< @@ -223,58 +225,9 @@ class SourceImageCache } } - /* Apply the "clean transparent" filter to textures, removing borders on transparent textures. - * PNG optimizers discard RGB values of fully-transparent pixels, but filters may expose the - * replacement colors at borders by blending to them; this filter compensates for that by - * filling in those RGB values from nearby pixels. - */ - if (g_settings->getBool("texture_clean_transparent")) { - const core::dimension2d dim = toadd->getDimension(); - - // Walk each pixel looking for ones that will show as transparent. - for (u32 ctrx = 0; ctrx < dim.Width; ctrx++) - for (u32 ctry = 0; ctry < dim.Height; ctry++) { - irr::video::SColor c = toadd->getPixel(ctrx, ctry); - if (c.getAlpha() > 127) - continue; - - // Sample size and total weighted r, g, b values. - u32 ss = 0, sr = 0, sg = 0, sb = 0; - - // Walk each neighbor pixel (clipped to image bounds). - for (u32 sx = (ctrx < 1) ? 0 : (ctrx - 1); - sx <= (ctrx + 1) && sx < dim.Width; sx++) - for (u32 sy = (ctry < 1) ? 0 : (ctry - 1); - sy <= (ctry + 1) && sy < dim.Height; sy++) { - - // Ignore the center pixel (its RGB is already - // presumed meaningless). - if ((sx == ctrx) && (sy == ctry)) - continue; - - // Ignore other nearby pixels that would be - // transparent upon display. - irr::video::SColor d = toadd->getPixel(sx, sy); - if(d.getAlpha() < 128) - continue; - - // Add one weighted sample. - ss++; - sr += d.getRed(); - sg += d.getGreen(); - sb += d.getBlue(); - } - - // If we found any neighbor RGB data, set pixel to average - // weighted by alpha. - if (ss > 0) { - c.setRed(sr / ss); - c.setGreen(sg / ss); - c.setBlue(sb / ss); - toadd->setPixel(ctrx, ctry, c); - } - } - } + // Apply the "clean transparent" filter, if configured. + if (g_settings->getBool("texture_clean_transparent")) + imageCleanTransparent(toadd, 127); if (need_to_grab) toadd->grab(); @@ -670,6 +623,7 @@ u32 TextureSource::generateTexture(const std::string &name) #endif // Create texture from resulting image tex = driver->addTexture(name.c_str(), img); + guiScalingCache(io::path(name.c_str()), driver, img); img->drop(); } @@ -776,6 +730,7 @@ void TextureSource::rebuildImagesAndTextures() video::ITexture *t = NULL; if (img) { t = driver->addTexture(ti->name.c_str(), img); + guiScalingCache(io::path(ti->name.c_str()), driver, img); img->drop(); } video::ITexture *t_old = ti->texture; @@ -896,6 +851,8 @@ video::ITexture* TextureSource::generateTextureFromMesh( rawImage->copyToScaling(inventory_image); rawImage->drop(); + guiScalingCache(io::path(params.rtt_texture_name.c_str()), driver, inventory_image); + video::ITexture *rtt = driver->addTexture(params.rtt_texture_name.c_str(), inventory_image); inventory_image->drop(); diff --git a/src/client/tile.h b/src/client/tile.h index eadfdc2a..38f8bb62 100644 --- a/src/client/tile.h +++ b/src/client/tile.h @@ -28,6 +28,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "threads.h" #include #include +#include "util/numeric.h" class IGameDef; @@ -135,21 +136,6 @@ class IWritableTextureSource : public ITextureSource IWritableTextureSource* createTextureSource(IrrlichtDevice *device); #ifdef __ANDROID__ -/** - * @param size get next npot2 value - * @return npot2 value - */ -inline unsigned int npot2(unsigned int size) -{ - if (size == 0) return 0; - unsigned int npot = 1; - - while ((size >>= 1) > 0) { - npot <<= 1; - } - return npot; -} - video::IImage * Align2Npot2(video::IImage * image, video::IVideoDriver* driver); #endif diff --git a/src/defaultsettings.cpp b/src/defaultsettings.cpp index 0acb4a73..6fa420b6 100644 --- a/src/defaultsettings.cpp +++ b/src/defaultsettings.cpp @@ -137,6 +137,8 @@ void set_default_settings(Settings *settings) settings->setDefault("crosshair_alpha", "255"); settings->setDefault("hud_scaling", "1.0"); settings->setDefault("gui_scaling", "1.0"); + settings->setDefault("gui_scaling_filter", "false"); + settings->setDefault("gui_scaling_filter_txr2img", "true"); settings->setDefault("mouse_sensitivity", "0.2"); settings->setDefault("enable_sound", "true"); settings->setDefault("sound_volume", "0.8"); diff --git a/src/drawscene.cpp b/src/drawscene.cpp index b089e71e..c3c3b2ef 100644 --- a/src/drawscene.cpp +++ b/src/drawscene.cpp @@ -24,6 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "clientmap.h" #include "util/timetaker.h" #include "fontengine.h" +#include "guiscalingfilter.h" typedef enum { LEFT = -1, @@ -324,19 +325,19 @@ void draw_sidebyside_3d_mode(Camera& camera, bool show_hud, //makeColorKeyTexture mirrors texture so we do it twice to get it right again driver->makeColorKeyTexture(hudtexture, irr::video::SColor(255, 0, 0, 0)); - driver->draw2DImage(left_image, + draw2DImageFilterScaled(driver, left_image, irr::core::rect(0, 0, screensize.X/2, screensize.Y), irr::core::rect(0, 0, screensize.X, screensize.Y), 0, 0, false); - driver->draw2DImage(hudtexture, + draw2DImageFilterScaled(driver, hudtexture, irr::core::rect(0, 0, screensize.X/2, screensize.Y), irr::core::rect(0, 0, screensize.X, screensize.Y), 0, 0, true); - driver->draw2DImage(right_image, + draw2DImageFilterScaled(driver, right_image, irr::core::rect(screensize.X/2, 0, screensize.X, screensize.Y), irr::core::rect(0, 0, screensize.X, screensize.Y), 0, 0, false); - driver->draw2DImage(hudtexture, + draw2DImageFilterScaled(driver, hudtexture, irr::core::rect(screensize.X/2, 0, screensize.X, screensize.Y), irr::core::rect(0, 0, screensize.X, screensize.Y), 0, 0, true); @@ -380,19 +381,19 @@ void draw_top_bottom_3d_mode(Camera& camera, bool show_hud, //makeColorKeyTexture mirrors texture so we do it twice to get it right again driver->makeColorKeyTexture(hudtexture, irr::video::SColor(255, 0, 0, 0)); - driver->draw2DImage(left_image, + draw2DImageFilterScaled(driver, left_image, irr::core::rect(0, 0, screensize.X, screensize.Y/2), irr::core::rect(0, 0, screensize.X, screensize.Y), 0, 0, false); - driver->draw2DImage(hudtexture, + draw2DImageFilterScaled(driver, hudtexture, irr::core::rect(0, 0, screensize.X, screensize.Y/2), irr::core::rect(0, 0, screensize.X, screensize.Y), 0, 0, true); - driver->draw2DImage(right_image, + draw2DImageFilterScaled(driver, right_image, irr::core::rect(0, screensize.Y/2, screensize.X, screensize.Y), irr::core::rect(0, 0, screensize.X, screensize.Y), 0, 0, false); - driver->draw2DImage(hudtexture, + draw2DImageFilterScaled(driver, hudtexture, irr::core::rect(0, screensize.Y/2, screensize.X, screensize.Y), irr::core::rect(0, 0, screensize.X, screensize.Y), 0, 0, true); diff --git a/src/guiEngine.cpp b/src/guiEngine.cpp index c143e511..07fdbb16 100644 --- a/src/guiEngine.cpp +++ b/src/guiEngine.cpp @@ -36,6 +36,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "httpfetch.h" #include "log.h" #include "fontengine.h" +#include "guiscalingfilter.h" #ifdef __ANDROID__ #include "client/tile.h" @@ -409,7 +410,7 @@ void GUIEngine::drawBackground(video::IVideoDriver* driver) { for (unsigned int y = 0; y < screensize.Y; y += tilesize.Y ) { - driver->draw2DImage(texture, + draw2DImageFilterScaled(driver, texture, core::rect(x, y, x+tilesize.X, y+tilesize.Y), core::rect(0, 0, sourcesize.X, sourcesize.Y), NULL, NULL, true); @@ -419,7 +420,7 @@ void GUIEngine::drawBackground(video::IVideoDriver* driver) } /* Draw background texture */ - driver->draw2DImage(texture, + draw2DImageFilterScaled(driver, texture, core::rect(0, 0, screensize.X, screensize.Y), core::rect(0, 0, sourcesize.X, sourcesize.Y), NULL, NULL, true); @@ -438,7 +439,7 @@ void GUIEngine::drawOverlay(video::IVideoDriver* driver) /* Draw background texture */ v2u32 sourcesize = texture->getOriginalSize(); - driver->draw2DImage(texture, + draw2DImageFilterScaled(driver, texture, core::rect(0, 0, screensize.X, screensize.Y), core::rect(0, 0, sourcesize.X, sourcesize.Y), NULL, NULL, true); @@ -471,7 +472,7 @@ void GUIEngine::drawHeader(video::IVideoDriver* driver) video::SColor bgcolor(255,50,50,50); - driver->draw2DImage(texture, splashrect, + draw2DImageFilterScaled(driver, texture, splashrect, core::rect(core::position2d(0,0), core::dimension2di(texture->getOriginalSize())), NULL, NULL, true); @@ -503,7 +504,7 @@ void GUIEngine::drawFooter(video::IVideoDriver* driver) rect += v2s32(screensize.Width/2,screensize.Height-footersize.Y); rect -= v2s32(footersize.X/2, 0); - driver->draw2DImage(texture, rect, + draw2DImageFilterScaled(driver, texture, rect, core::rect(core::position2d(0,0), core::dimension2di(texture->getOriginalSize())), NULL, NULL, true); diff --git a/src/guiFormSpecMenu.cpp b/src/guiFormSpecMenu.cpp index 11360a0f..56729846 100644 --- a/src/guiFormSpecMenu.cpp +++ b/src/guiFormSpecMenu.cpp @@ -51,6 +51,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "util/hex.h" #include "util/numeric.h" #include "util/string.h" // for parseColorString() +#include "guiscalingfilter.h" #define MY_CHECKPOS(a,b) \ if (v_pos.size() != 2) { \ @@ -1307,8 +1308,8 @@ void GUIFormSpecMenu::parseImageButton(parserData* data,std::string element, } e->setUseAlphaChannel(true); - e->setImage(texture); - e->setPressedImage(pressed_texture); + e->setImage(guiScalingImageButton(Environment->getVideoDriver(), texture, geom.X, geom.Y)); + e->setPressedImage(guiScalingImageButton(Environment->getVideoDriver(), pressed_texture, geom.X, geom.Y)); e->setScaleImage(true); e->setNotClipped(noclip); e->setDrawBorder(drawborder); @@ -1452,8 +1453,8 @@ void GUIFormSpecMenu::parseItemImageButton(parserData* data,std::string element) } e->setUseAlphaChannel(true); - e->setImage(texture); - e->setPressedImage(texture); + e->setImage(guiScalingImageButton(Environment->getVideoDriver(), texture, geom.X, geom.Y)); + e->setPressedImage(guiScalingImageButton(Environment->getVideoDriver(), texture, geom.X, geom.Y)); e->setScaleImage(true); spec.ftype = f_Button; rect+=data->basepos-padding; @@ -2283,7 +2284,7 @@ void GUIFormSpecMenu::drawMenu() const video::SColor color(255,255,255,255); const video::SColor colors[] = {color,color,color,color}; - driver->draw2DImage(texture, rect, + draw2DImageFilterScaled(driver, texture, rect, core::rect(core::position2d(0,0), core::dimension2di(texture->getOriginalSize())), NULL/*&AbsoluteClippingRect*/, colors, true); @@ -2333,7 +2334,7 @@ void GUIFormSpecMenu::drawMenu() core::rect rect = imgrect + spec.pos; const video::SColor color(255,255,255,255); const video::SColor colors[] = {color,color,color,color}; - driver->draw2DImage(texture, rect, + draw2DImageFilterScaled(driver, texture, rect, core::rect(core::position2d(0,0),img_origsize), NULL/*&AbsoluteClippingRect*/, colors, true); } @@ -2362,7 +2363,7 @@ void GUIFormSpecMenu::drawMenu() core::rect rect = imgrect + spec.pos; const video::SColor color(255,255,255,255); const video::SColor colors[] = {color,color,color,color}; - driver->draw2DImage(texture, rect, + draw2DImageFilterScaled(driver, texture, rect, core::rect(core::position2d(0,0), core::dimension2di(texture->getOriginalSize())), NULL/*&AbsoluteClippingRect*/, colors, true); diff --git a/src/guiTable.cpp b/src/guiTable.cpp index a7a53f58..6dcd115b 100644 --- a/src/guiTable.cpp +++ b/src/guiTable.cpp @@ -36,6 +36,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "main.h" #include "settings.h" // for settings #include "porting.h" // for dpi +#include "guiscalingfilter.h" /* GUITable diff --git a/src/guiscalingfilter.cpp b/src/guiscalingfilter.cpp new file mode 100644 index 00000000..92dadeae --- /dev/null +++ b/src/guiscalingfilter.cpp @@ -0,0 +1,160 @@ +/* +Copyright (C) 2015 Aaron Suen + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#include "guiscalingfilter.h" +#include "imagefilters.h" +#include "settings.h" +#include "main.h" // for g_settings +#include "util/numeric.h" +#include + +/* Maintain a static cache to store the images that correspond to textures + * in a format that's manipulable by code. Some platforms exhibit issues + * converting textures back into images repeatedly, and some don't even + * allow it at all. + */ +std::map imgCache; + +/* Maintain a static cache of all pre-scaled textures. These need to be + * cleared as well when the cached images. + */ +std::map txrCache; + +/* Manually insert an image into the cache, useful to avoid texture-to-image + * conversion whenever we can intercept it. + */ +void guiScalingCache(io::path key, video::IVideoDriver *driver, video::IImage *value) { + if (!g_settings->getBool("gui_scaling_filter")) + return; + video::IImage *copied = driver->createImage(value->getColorFormat(), + value->getDimension()); + value->copyTo(copied); + imgCache[key] = copied; +} + +// Manually clear the cache, e.g. when switching to different worlds. +void guiScalingCacheClear(video::IVideoDriver *driver) { + for (std::map::iterator it = imgCache.begin(); + it != imgCache.end(); it++) { + if (it->second != NULL) + it->second->drop(); + } + imgCache.clear(); + for (std::map::iterator it = txrCache.begin(); + it != txrCache.end(); it++) { + if (it->second != NULL) + driver->removeTexture(it->second); + } + txrCache.clear(); +} + +/* Get a cached, high-quality pre-scaled texture for display purposes. If the + * texture is not already cached, attempt to create it. Returns a pre-scaled texture, + * or the original texture if unable to pre-scale it. + */ +video::ITexture *guiScalingResizeCached(video::IVideoDriver *driver, video::ITexture *src, + const core::rect &srcrect, const core::rect &destrect) { + + if (!g_settings->getBool("gui_scaling_filter")) + return src; + + // Calculate scaled texture name. + char rectstr[200]; + sprintf(rectstr, "%d:%d:%d:%d:%d:%d", + srcrect.UpperLeftCorner.X, + srcrect.UpperLeftCorner.Y, + srcrect.getWidth(), + srcrect.getHeight(), + destrect.getWidth(), + destrect.getHeight()); + io::path origname = src->getName().getPath(); + io::path scalename = origname + "@guiScalingFilter:" + rectstr; + + // Search for existing scaled texture. + video::ITexture *scaled = txrCache[scalename]; + if (scaled) + return scaled; + + // Try to find the texture converted to an image in the cache. + // If the image was not found, try to extract it from the texture. + video::IImage* srcimg = imgCache[origname]; + if (srcimg == NULL) { + if (!g_settings->getBool("gui_scaling_filter_txr2img")) + return src; + srcimg = driver->createImageFromData(src->getColorFormat(), + src->getSize(), src->lock(), false); + src->unlock(); + imgCache[origname] = srcimg; + } + + // Create a new destination image and scale the source into it. + imageCleanTransparent(srcimg, 0); + video::IImage *destimg = driver->createImage(src->getColorFormat(), + core::dimension2d((u32)destrect.getWidth(), + (u32)destrect.getHeight())); + imageScaleNNAA(srcimg, srcrect, destimg); + +#ifdef __ANDROID__ + // Android is very picky about textures being powers of 2, so expand + // the image dimensions to the next power of 2, if necessary, for + // that platform. + video::IImage *po2img = driver->createImage(src->getColorFormat(), + core::dimension2d(npot2((u32)destrect.getWidth()), + npot2((u32)destrect.getHeight()))); + po2img->fill(video::SColor(0, 0, 0, 0)); + destimg->copyTo(po2img); + destimg->drop(); + destimg = po2img; +#endif + + // Convert the scaled image back into a texture. + scaled = driver->addTexture(scalename, destimg, NULL); + destimg->drop(); + txrCache[scalename] = scaled; + + return scaled; +} + +/* Convenience wrapper for guiScalingResizeCached that accepts parameters that + * are available at GUI imagebutton creation time. + */ +video::ITexture *guiScalingImageButton(video::IVideoDriver *driver, video::ITexture *src, + s32 width, s32 height) { + return guiScalingResizeCached(driver, src, + core::rect(0, 0, src->getSize().Width, src->getSize().Height), + core::rect(0, 0, width, height)); +} + +/* Replacement for driver->draw2DImage() that uses the high-quality pre-scaled + * texture, if configured. + */ +void draw2DImageFilterScaled(video::IVideoDriver *driver, video::ITexture *txr, + const core::rect &destrect, const core::rect &srcrect, + const core::rect *cliprect, const video::SColor *const colors, + bool usealpha) { + + // Attempt to pre-scale image in software in high quality. + video::ITexture *scaled = guiScalingResizeCached(driver, txr, srcrect, destrect); + + // Correct source rect based on scaled image. + const core::rect mysrcrect = (scaled != txr) + ? core::rect(0, 0, destrect.getWidth(), destrect.getHeight()) + : srcrect; + + driver->draw2DImage(scaled, destrect, mysrcrect, cliprect, colors, usealpha); +} diff --git a/src/guiscalingfilter.h b/src/guiscalingfilter.h new file mode 100644 index 00000000..768fe8d5 --- /dev/null +++ b/src/guiscalingfilter.h @@ -0,0 +1,52 @@ +/* +Copyright (C) 2015 Aaron Suen + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ +#ifndef _GUI_SCALING_FILTER_H_ +#define _GUI_SCALING_FILTER_H_ + +#include "irrlichttypes_extrabloated.h" + +/* Manually insert an image into the cache, useful to avoid texture-to-image + * conversion whenever we can intercept it. + */ +void guiScalingCache(io::path key, video::IVideoDriver *driver, video::IImage *value); + +// Manually clear the cache, e.g. when switching to different worlds. +void guiScalingCacheClear(video::IVideoDriver *driver); + +/* Get a cached, high-quality pre-scaled texture for display purposes. If the + * texture is not already cached, attempt to create it. Returns a pre-scaled texture, + * or the original texture if unable to pre-scale it. + */ +video::ITexture *guiScalingResizeCached(video::IVideoDriver *driver, video::ITexture *src, + const core::rect &srcrect, const core::rect &destrect); + +/* Convenience wrapper for guiScalingResizeCached that accepts parameters that + * are available at GUI imagebutton creation time. + */ +video::ITexture *guiScalingImageButton(video::IVideoDriver *driver, video::ITexture *src, + s32 width, s32 height); + +/* Replacement for driver->draw2DImage() that uses the high-quality pre-scaled + * texture, if configured. + */ +void draw2DImageFilterScaled(video::IVideoDriver *driver, video::ITexture *txr, + const core::rect &destrect, const core::rect &srcrect, + const core::rect *cliprect = 0, const video::SColor *const colors = 0, + bool usealpha = false); + +#endif diff --git a/src/hud.cpp b/src/hud.cpp index aabaa066..0b34a7b5 100644 --- a/src/hud.cpp +++ b/src/hud.cpp @@ -32,6 +32,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "camera.h" #include "porting.h" #include "fontengine.h" +#include "guiscalingfilter.h" #include #ifdef HAVE_TOUCHSCREENGUI @@ -94,7 +95,7 @@ void Hud::drawItem(const ItemStack &item, const core::rect& rect, bool sele imgrect2.LowerRightCorner.Y += (m_padding*2); video::ITexture *texture = tsrc->getTexture(hotbar_selected_image); core::dimension2di imgsize(texture->getOriginalSize()); - driver->draw2DImage(texture, imgrect2, + draw2DImageFilterScaled(driver, texture, imgrect2, core::rect(core::position2d(0,0), imgsize), NULL, hbar_colors, true); } else { @@ -200,7 +201,7 @@ void Hud::drawItems(v2s32 upperleftpos, s32 itemcount, s32 offset, core::rect rect2 = imgrect2 + pos; video::ITexture *texture = tsrc->getTexture(hotbar_image); core::dimension2di imgsize(texture->getOriginalSize()); - driver->draw2DImage(texture, rect2, + draw2DImageFilterScaled(driver, texture, rect2, core::rect(core::position2d(0,0), imgsize), NULL, hbar_colors, true); } @@ -266,7 +267,7 @@ void Hud::drawLuaElements(v3s16 camera_offset) { (e->align.Y - 1.0) * dstsize.Y / 2); core::rect rect(0, 0, dstsize.X, dstsize.Y); rect += pos + offset + v2s32(e->offset.X, e->offset.Y); - driver->draw2DImage(texture, rect, + draw2DImageFilterScaled(driver, texture, rect, core::rect(core::position2d(0,0), imgsize), NULL, colors, true); break; } @@ -378,7 +379,7 @@ void Hud::drawStatbar(v2s32 pos, u16 corner, u16 drawdir, std::string texture, core::rect dstrect(0,0, dstd.Width, dstd.Height); dstrect += p; - driver->draw2DImage(stat_texture, dstrect, srcrect, NULL, colors, true); + draw2DImageFilterScaled(driver, stat_texture, dstrect, srcrect, NULL, colors, true); p += steppos; } @@ -388,7 +389,7 @@ void Hud::drawStatbar(v2s32 pos, u16 corner, u16 drawdir, std::string texture, core::rect dstrect(0,0, dstd.Width / 2, dstd.Height); dstrect += p; - driver->draw2DImage(stat_texture, dstrect, srcrect, NULL, colors, true); + draw2DImageFilterScaled(driver, stat_texture, dstrect, srcrect, NULL, colors, true); } } @@ -502,7 +503,7 @@ void drawItemStack(video::IVideoDriver *driver, { const video::SColor color(255,255,255,255); const video::SColor colors[] = {color,color,color,color}; - driver->draw2DImage(texture, rect, + draw2DImageFilterScaled(driver, texture, rect, core::rect(core::position2d(0,0), core::dimension2di(texture->getOriginalSize())), clip, colors, true); diff --git a/src/imagefilters.cpp b/src/imagefilters.cpp new file mode 100644 index 00000000..a995e98d --- /dev/null +++ b/src/imagefilters.cpp @@ -0,0 +1,172 @@ +/* +Copyright (C) 2015 Aaron Suen + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#include "imagefilters.h" +#include "util/numeric.h" +#include + +/* Fill in RGB values for transparent pixels, to correct for odd colors + * appearing at borders when blending. This is because many PNG optimizers + * like to discard RGB values of transparent pixels, but when blending then + * with non-transparent neighbors, their RGB values will shpw up nonetheless. + * + * This function modifies the original image in-place. + * + * Parameter "threshold" is the alpha level below which pixels are considered + * transparent. Should be 127 for 3d where alpha is threshold, but 0 for + * 2d where alpha is blended. + */ +void imageCleanTransparent(video::IImage *src, u32 threshold) { + + core::dimension2d dim = src->getDimension(); + + // Walk each pixel looking for fully transparent ones. + // Note: loop y around x for better cache locality. + for (u32 ctry = 0; ctry < dim.Height; ctry++) + for (u32 ctrx = 0; ctrx < dim.Width; ctrx++) { + + // Ignore opaque pixels. + irr::video::SColor c = src->getPixel(ctrx, ctry); + if (c.getAlpha() > threshold) + continue; + + // Sample size and total weighted r, g, b values. + u32 ss = 0, sr = 0, sg = 0, sb = 0; + + // Walk each neighbor pixel (clipped to image bounds). + for (u32 sy = (ctry < 1) ? 0 : (ctry - 1); + sy <= (ctry + 1) && sy < dim.Height; sy++) + for (u32 sx = (ctrx < 1) ? 0 : (ctrx - 1); + sx <= (ctrx + 1) && sx < dim.Width; sx++) { + + // Ignore transparent pixels. + irr::video::SColor d = src->getPixel(sx, sy); + if (d.getAlpha() <= threshold) + continue; + + // Add RGB values weighted by alpha. + u32 a = d.getAlpha(); + ss += a; + sr += a * d.getRed(); + sg += a * d.getGreen(); + sb += a * d.getBlue(); + } + + // If we found any neighbor RGB data, set pixel to average + // weighted by alpha. + if (ss > 0) { + c.setRed(sr / ss); + c.setGreen(sg / ss); + c.setBlue(sb / ss); + src->setPixel(ctrx, ctry, c); + } + } +} + +/* Scale a region of an image into another image, using nearest-neighbor with + * anti-aliasing; treat pixels as crisp rectangles, but blend them at boundaries + * to prevent non-integer scaling ratio artifacts. Note that this may cause + * some blending at the edges where pixels don't line up perfectly, but this + * filter is designed to produce the most accurate results for both upscaling + * and downscaling. + */ +void imageScaleNNAA(video::IImage *src, const core::rect &srcrect, video::IImage *dest) { + + double sx, sy, minsx, maxsx, minsy, maxsy, area, ra, ga, ba, aa, pw, ph, pa; + u32 dy, dx; + video::SColor pxl; + + // Cache rectsngle boundaries. + double sox = srcrect.UpperLeftCorner.X * 1.0; + double soy = srcrect.UpperLeftCorner.Y * 1.0; + double sw = srcrect.getWidth() * 1.0; + double sh = srcrect.getHeight() * 1.0; + + // Walk each destination image pixel. + // Note: loop y around x for better cache locality. + core::dimension2d dim = dest->getDimension(); + for (dy = 0; dy < dim.Height; dy++) + for (dx = 0; dx < dim.Width; dx++) { + + // Calculate floating-point source rectangle bounds. + // Do some basic clipping, and for mirrored/flipped rects, + // make sure min/max are in the right order. + minsx = sox + (dx * sw / dim.Width); + minsx = rangelim(minsx, 0, sw); + maxsx = minsx + sw / dim.Width; + maxsx = rangelim(maxsx, 0, sw); + if (minsx > maxsx) + SWAP(double, minsx, maxsx); + minsy = soy + (dy * sh / dim.Height); + minsy = rangelim(minsy, 0, sh); + maxsy = minsy + sh / dim.Height; + maxsy = rangelim(maxsy, 0, sh); + if (minsy > maxsy) + SWAP(double, minsy, maxsy); + + // Total area, and integral of r, g, b values over that area, + // initialized to zero, to be summed up in next loops. + area = 0; + ra = 0; + ga = 0; + ba = 0; + aa = 0; + + // Loop over the integral pixel positions described by those bounds. + for (sy = floor(minsy); sy < maxsy; sy++) + for (sx = floor(minsx); sx < maxsx; sx++) { + + // Calculate width, height, then area of dest pixel + // that's covered by this source pixel. + pw = 1; + if (minsx > sx) + pw += sx - minsx; + if (maxsx < (sx + 1)) + pw += maxsx - sx - 1; + ph = 1; + if (minsy > sy) + ph += sy - minsy; + if (maxsy < (sy + 1)) + ph += maxsy - sy - 1; + pa = pw * ph; + + // Get source pixel and add it to totals, weighted + // by covered area and alpha. + pxl = src->getPixel((u32)sx, (u32)sy); + area += pa; + ra += pa * pxl.getRed(); + ga += pa * pxl.getGreen(); + ba += pa * pxl.getBlue(); + aa += pa * pxl.getAlpha(); + } + + // Set the destination image pixel to the average color. + if (area > 0) { + pxl.setRed(ra / area + 0.5); + pxl.setGreen(ga / area + 0.5); + pxl.setBlue(ba / area + 0.5); + pxl.setAlpha(aa / area + 0.5); + } else { + pxl.setRed(0); + pxl.setGreen(0); + pxl.setBlue(0); + pxl.setAlpha(0); + } + dest->setPixel(dx, dy, pxl); + } +} diff --git a/src/imagefilters.h b/src/imagefilters.h new file mode 100644 index 00000000..28787027 --- /dev/null +++ b/src/imagefilters.h @@ -0,0 +1,46 @@ +/* +Copyright (C) 2015 Aaron Suen + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#ifndef _IMAGE_FILTERS_H_ +#define _IMAGE_FILTERS_H_ + +#include "irrlichttypes_extrabloated.h" + +/* Fill in RGB values for transparent pixels, to correct for odd colors + * appearing at borders when blending. This is because many PNG optimizers + * like to discard RGB values of transparent pixels, but when blending then + * with non-transparent neighbors, their RGB values will shpw up nonetheless. + * + * This function modifies the original image in-place. + * + * Parameter "threshold" is the alpha level below which pixels are considered + * transparent. Should be 127 for 3d where alpha is threshold, but 0 for + * 2d where alpha is blended. + */ +void imageCleanTransparent(video::IImage *src, u32 threshold); + +/* Scale a region of an image into another image, using nearest-neighbor with + * anti-aliasing; treat pixels as crisp rectangles, but blend them at boundaries + * to prevent non-integer scaling ratio artifacts. Note that this may cause + * some blending at the edges where pixels don't line up perfectly, but this + * filter is designed to produce the most accurate results for both upscaling + * and downscaling. + */ +void imageScaleNNAA(video::IImage *src, const core::rect &srcrect, video::IImage *dest); + +#endif diff --git a/src/touchscreengui.cpp b/src/touchscreengui.cpp index 2d489c1f..f5868133 100644 --- a/src/touchscreengui.cpp +++ b/src/touchscreengui.cpp @@ -25,6 +25,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "gettime.h" #include "util/numeric.h" #include "porting.h" +#include "guiscalingfilter.h" #include #include @@ -130,15 +131,23 @@ TouchScreenGUI::TouchScreenGUI(IrrlichtDevice *device, IEventReceiver* receiver) m_screensize = m_device->getVideoDriver()->getScreenSize(); } -void TouchScreenGUI::loadButtonTexture(button_info* btn, const char* path) +void TouchScreenGUI::loadButtonTexture(button_info* btn, const char* path, rect button_rect) { unsigned int tid; - video::ITexture *texture = m_texturesource->getTexture(path,&tid); + video::ITexture *texture = guiScalingImageButton(m_device->getVideoDriver(), + m_texturesource->getTexture(path, &tid), button_rect.getWidth(), button_rect.getHeight()); if (texture) { btn->guibutton->setUseAlphaChannel(true); - btn->guibutton->setImage(texture); - btn->guibutton->setPressedImage(texture); - btn->guibutton->setScaleImage(true); + if (g_settings->getBool("gui_scaling_filter")) { + rect txr_rect = rect(0, 0, button_rect.getWidth(), button_rect.getHeight()); + btn->guibutton->setImage(texture, txr_rect); + btn->guibutton->setPressedImage(texture, txr_rect); + btn->guibutton->setScaleImage(false); + } else { + btn->guibutton->setImage(texture); + btn->guibutton->setPressedImage(texture); + btn->guibutton->setScaleImage(true); + } btn->guibutton->setDrawBorder(false); btn->guibutton->setText(L""); } @@ -157,7 +166,7 @@ void TouchScreenGUI::initButton(touch_gui_button_id id, rect button_rect, btn->immediate_release = immediate_release; btn->ids.clear(); - loadButtonTexture(btn,touchgui_button_imagenames[id]); + loadButtonTexture(btn,touchgui_button_imagenames[id], button_rect); } static int getMaxControlPadSize(float density) { diff --git a/src/touchscreengui.h b/src/touchscreengui.h index 2ded26a0..bb323179 100644 --- a/src/touchscreengui.h +++ b/src/touchscreengui.h @@ -130,7 +130,7 @@ class TouchScreenGUI float repeat_delay = BUTTON_REPEAT_DELAY); /* load texture */ - void loadButtonTexture(button_info* btn, const char* path); + void loadButtonTexture(button_info* btn, const char* path, rect button_rect); struct id_status{ int id; diff --git a/src/util/numeric.cpp b/src/util/numeric.cpp index 2306976e..4ddfede9 100644 --- a/src/util/numeric.cpp +++ b/src/util/numeric.cpp @@ -245,4 +245,3 @@ bool isBlockInSight(v3s16 blockpos_b, v3f camera_pos, v3f camera_dir, return true; } - diff --git a/src/util/numeric.h b/src/util/numeric.h index a158a2ea..b4b84191 100644 --- a/src/util/numeric.h +++ b/src/util/numeric.h @@ -411,5 +411,16 @@ inline bool is_power_of_two(u32 n) return n != 0 && (n & (n-1)) == 0; } -#endif +// Compute next-higher power of 2 efficiently, e.g. for power-of-2 texture sizes. +// Public Domain: https://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2 +inline u32 npot2(u32 orig) { + orig--; + orig |= orig >> 1; + orig |= orig >> 2; + orig |= orig >> 4; + orig |= orig >> 8; + orig |= orig >> 16; + return orig + 1; +} +#endif