diff --git a/builtin/settingtypes.txt b/builtin/settingtypes.txt index 83d0b48e0..9462f616d 100644 --- a/builtin/settingtypes.txt +++ b/builtin/settingtypes.txt @@ -2106,7 +2106,7 @@ world_aligned_mode (World-aligned textures mode) enum enable disable,enable,forc # World-aligned textures may be scaled to span several nodes. However, # the server may not send the scale you want, especially if you use # a specially-designed texture pack; with this option, the client tries -# to determine the scale automatically basing on the texture size. +# to determine the scale automatically based on the texture size. # See also texture_min_size. # Warning: This option is EXPERIMENTAL! autoscale_mode (Autoscaling mode) enum disable disable,enable,force @@ -2118,7 +2118,7 @@ autoscale_mode (Autoscaling mode) enum disable disable,enable,force # This setting is ONLY applied if any of the mentioned filters are enabled. # This is also used as the base node texture size for world-aligned # texture autoscaling. -texture_min_size (Base texture size) int 64 1 32768 +texture_min_size (Base texture size) int 192 192 16384 # Side length of a cube of map blocks that the client will consider together # when generating meshes. diff --git a/doc/lua_api.md b/doc/lua_api.md index e2a55b7e1..31eb35dd2 100644 --- a/doc/lua_api.md +++ b/doc/lua_api.md @@ -518,6 +518,15 @@ stripping out the file extension: Supported texture formats are PNG (`.png`), JPEG (`.jpg`) and Targa (`.tga`). +Luanti generally uses nearest-neighbor upscaling for textures to preserve the crisp +look of pixel art (low-res textures). +Users can optionally enable bilinear and/or trilinear filtering. However, to avoid +everything becoming blurry, textures smaller than 192px will either not be filtered, +or will be upscaled to that minimum resolution first without filtering. + +This is subject to change to move more control to the Lua API, but you can rely on +low-res textures not suddenly becoming filtered. + Texture modifiers ----------------- diff --git a/src/client/content_cao.cpp b/src/client/content_cao.cpp index 624ed4b5b..02e980cab 100644 --- a/src/client/content_cao.cpp +++ b/src/client/content_cao.cpp @@ -144,6 +144,31 @@ void SmoothTranslatorWrappedv3f::translate(f32 dtime) Other stuff */ +static bool setMaterialTextureAndFilters(video::SMaterial &material, + const std::string &texturestring, ITextureSource *tsrc) +{ + bool use_trilinear_filter = g_settings->getBool("trilinear_filter"); + bool use_bilinear_filter = g_settings->getBool("bilinear_filter"); + bool use_anisotropic_filter = g_settings->getBool("anisotropic_filter"); + + video::ITexture *texture = tsrc->getTextureForMesh(texturestring); + if (!texture) + return false; + + material.setTexture(0, texture); + + // don't filter low-res textures, makes them look blurry + const core::dimension2d &size = texture->getOriginalSize(); + if (std::min(size.Width, size.Height) < TEXTURE_FILTER_MIN_SIZE) + use_trilinear_filter = use_bilinear_filter = false; + + material.forEachTexture([=] (auto &tex) { + setMaterialFilters(tex, use_bilinear_filter, use_trilinear_filter, + use_anisotropic_filter); + }); + return true; +} + static void setBillboardTextureMatrix(scene::IBillboardSceneNode *bill, float txs, float tys, int col, int row) { @@ -1238,10 +1263,6 @@ void GenericCAO::updateTextures(std::string mod) { ITextureSource *tsrc = m_client->tsrc(); - bool use_trilinear_filter = g_settings->getBool("trilinear_filter"); - bool use_bilinear_filter = g_settings->getBool("bilinear_filter"); - bool use_anisotropic_filter = g_settings->getBool("anisotropic_filter"); - m_previous_texture_modifier = m_current_texture_modifier; m_current_texture_modifier = mod; @@ -1254,12 +1275,7 @@ void GenericCAO::updateTextures(std::string mod) video::SMaterial &material = m_spritenode->getMaterial(0); material.MaterialType = m_material_type; - material.setTexture(0, tsrc->getTextureForMesh(texturestring)); - - material.forEachTexture([=] (auto &tex) { - setMaterialFilters(tex, use_bilinear_filter, use_trilinear_filter, - use_anisotropic_filter); - }); + setMaterialTextureAndFilters(material, texturestring, tsrc); } } @@ -1273,29 +1289,12 @@ void GenericCAO::updateTextures(std::string mod) if (texturestring.empty()) continue; // Empty texture string means don't modify that material texturestring += mod; - video::ITexture *texture = tsrc->getTextureForMesh(texturestring); - if (!texture) { - errorstream<<"GenericCAO::updateTextures(): Could not load texture "<getMaterial(i); material.MaterialType = m_material_type; - material.TextureLayers[0].Texture = texture; material.BackfaceCulling = m_prop.backface_culling; - - // don't filter low-res textures, makes them look blurry - // player models have a res of 64 - const core::dimension2d &size = texture->getOriginalSize(); - const u32 res = std::min(size.Height, size.Width); - use_trilinear_filter &= res > 64; - use_bilinear_filter &= res > 64; - - material.forEachTexture([=] (auto &tex) { - setMaterialFilters(tex, use_bilinear_filter, use_trilinear_filter, - use_anisotropic_filter); - }); + setMaterialTextureAndFilters(material, texturestring, tsrc); } } } @@ -1313,13 +1312,7 @@ void GenericCAO::updateTextures(std::string mod) // Set material flags and texture video::SMaterial &material = m_meshnode->getMaterial(i); material.MaterialType = m_material_type; - material.setTexture(0, tsrc->getTextureForMesh(texturestring)); - material.getTextureMatrix(0).makeIdentity(); - - material.forEachTexture([=] (auto &tex) { - setMaterialFilters(tex, use_bilinear_filter, use_trilinear_filter, - use_anisotropic_filter); - }); + setMaterialTextureAndFilters(material, texturestring, tsrc); } } else if (m_prop.visual == OBJECTVISUAL_UPRIGHT_SPRITE) { scene::IMesh *mesh = m_meshnode->getMesh(); @@ -1330,12 +1323,7 @@ void GenericCAO::updateTextures(std::string mod) tname += mod; auto &material = m_meshnode->getMaterial(0); - material.setTexture(0, tsrc->getTextureForMesh(tname)); - - material.forEachTexture([=] (auto &tex) { - setMaterialFilters(tex, use_bilinear_filter, use_trilinear_filter, - use_anisotropic_filter); - }); + setMaterialTextureAndFilters(material, tname, tsrc); } { std::string tname = "no_texture.png"; @@ -1346,12 +1334,7 @@ void GenericCAO::updateTextures(std::string mod) tname += mod; auto &material = m_meshnode->getMaterial(1); - material.setTexture(0, tsrc->getTextureForMesh(tname)); - - material.forEachTexture([=] (auto &tex) { - setMaterialFilters(tex, use_bilinear_filter, use_trilinear_filter, - use_anisotropic_filter); - }); + setMaterialTextureAndFilters(material, tname, tsrc); } // Set mesh color (only if lighting is disabled) if (m_prop.glow < 0) diff --git a/src/client/imagesource.cpp b/src/client/imagesource.cpp index 3e55dd386..e39dc2955 100644 --- a/src/client/imagesource.cpp +++ b/src/client/imagesource.cpp @@ -1479,21 +1479,28 @@ bool ImageSource::generateImagePart(std::string_view part_of_name, /* Upscale textures to user's requested minimum size. This is a trick to make * filters look as good on low-res textures as on high-res ones, by making - * low-res textures BECOME high-res ones. This is helpful for worlds that + * low-res textures BECOME high-res ones. This is helpful for worlds that * mix high- and low-res textures, or for mods with least-common-denominator * textures that don't have the resources to offer high-res alternatives. + * + * Q: why not just enable/disable filtering depending on texture size? + * A: large texture resolutions apparently allow getting rid of the Moire + * effect way better than anisotropic filtering alone could. + * see and related + * linked discussions. */ const bool filter = m_setting_trilinear_filter || m_setting_bilinear_filter; - const s32 scaleto = filter ? g_settings->getU16("texture_min_size") : 1; - if (scaleto > 1) { - const core::dimension2d dim = baseimg->getDimension(); + if (filter) { + const f32 scaleto = rangelim(g_settings->getU16("texture_min_size"), + TEXTURE_FILTER_MIN_SIZE, 16384); - /* Calculate scaling needed to make the shortest texture dimension - * equal to the target minimum. If e.g. this is a vertical frames - * animation, the short dimension will be the real size. + /* Calculate integer-scaling needed to make the shortest texture + * dimension equal to the target minimum. If this is e.g. a + * vertical frames animation, the short dimension will be the real size. */ - u32 xscale = scaleto / dim.Width; - u32 yscale = scaleto / dim.Height; + const auto &dim = baseimg->getDimension(); + u32 xscale = std::ceil(scaleto / dim.Width); + u32 yscale = std::ceil(scaleto / dim.Height); const s32 scale = std::max(xscale, yscale); // Never downscale; only scale up by 2x or more. diff --git a/src/client/wieldmesh.cpp b/src/client/wieldmesh.cpp index 11116a5c3..baa72f1d9 100644 --- a/src/client/wieldmesh.cpp +++ b/src/client/wieldmesh.cpp @@ -281,12 +281,11 @@ void WieldMeshSceneNode::setExtruded(const std::string &imagename, material.MaterialType = m_material_type; material.MaterialTypeParam = 0.5f; material.BackfaceCulling = true; - // Enable bi/trilinear filtering only for high resolution textures - bool bilinear_filter = dim.Width > 32 && m_bilinear_filter; - bool trilinear_filter = dim.Width > 32 && m_trilinear_filter; + // don't filter low-res textures, makes them look blurry + bool f_ok = std::min(dim.Width, dim.Height) >= TEXTURE_FILTER_MIN_SIZE; material.forEachTexture([=] (auto &tex) { - setMaterialFilters(tex, bilinear_filter, trilinear_filter, - m_anisotropic_filter); + setMaterialFilters(tex, m_bilinear_filter && f_ok, + m_trilinear_filter && f_ok, m_anisotropic_filter); }); // mipmaps cause "thin black line" artifacts material.UseMipMaps = false; diff --git a/src/constants.h b/src/constants.h index 4a8ed4471..e97bd0507 100644 --- a/src/constants.h +++ b/src/constants.h @@ -99,3 +99,9 @@ #define SCREENSHOT_MAX_SERIAL_TRIES 1000 #define TTF_DEFAULT_FONT_SIZE (16) + +// Minimum texture size enforced/checked for enabling linear filtering +// This serves as the minimum for `texture_min_size`. +// The intent is to ensure that the rendering doesn't turn terribly blurry +// when filtering is enabled. +#define TEXTURE_FILTER_MIN_SIZE 192U diff --git a/src/defaultsettings.cpp b/src/defaultsettings.cpp index 205fe5934..67b090bc6 100644 --- a/src/defaultsettings.cpp +++ b/src/defaultsettings.cpp @@ -246,7 +246,7 @@ void set_default_settings() settings->setDefault("undersampling", "1"); settings->setDefault("world_aligned_mode", "enable"); settings->setDefault("autoscale_mode", "disable"); - settings->setDefault("texture_min_size", "64"); + settings->setDefault("texture_min_size", std::to_string(TEXTURE_FILTER_MIN_SIZE)); settings->setDefault("enable_fog", "true"); settings->setDefault("fog_start", "0.4"); settings->setDefault("3d_mode", "none"); diff --git a/src/nodedef.cpp b/src/nodedef.cpp index 8ef6f8493..45fc8b55d 100644 --- a/src/nodedef.cpp +++ b/src/nodedef.cpp @@ -272,7 +272,8 @@ void TextureSettings::readSettings() connected_glass = g_settings->getBool("connected_glass"); translucent_liquids = g_settings->getBool("translucent_liquids"); enable_minimap = g_settings->getBool("enable_minimap"); - node_texture_size = std::max(g_settings->getU16("texture_min_size"), 1); + node_texture_size = rangelim(g_settings->getU16("texture_min_size"), + TEXTURE_FILTER_MIN_SIZE, 16384); std::string leaves_style_str = g_settings->get("leaves_style"); std::string world_aligned_mode_str = g_settings->get("world_aligned_mode"); std::string autoscale_mode_str = g_settings->get("autoscale_mode");