diff --git a/builtin/settingtypes.txt b/builtin/settingtypes.txt index 53745f367d6dd..3d7d44fcb488f 100644 --- a/builtin/settingtypes.txt +++ b/builtin/settingtypes.txt @@ -628,6 +628,11 @@ antialiasing (Antialiasing method) enum none none,fsaa,ssaa # Value of 2 means taking 2x2 = 4 samples. fsaa (Anti-aliasing scale) enum 2 2,4,8,16 +# Anti-alias texel edges by using bilinear texture sampling together with a +# sample coordinate transformation in the shader. +# This setting is ONLY applied if bilinear_filter or trilinear_filter is enabled. +texel_antialiasing (Texel anti-aliasing) bool false + # Applies a post-processing filter to detect and smoothen high-contrast edges. # Provides balance between speed and image quality. # fxaa can used in combination with the other anti-aliasing methods @@ -2063,13 +2068,17 @@ autoscale_mode (Autoscaling mode) enum disable disable,enable,force # This is only useful for debugging. array_texture_max (Array texture max layers) int 65535 0 65535 -# When using bilinear/trilinear filtering, low-resolution textures -# can be blurred, so this option automatically upscales them to preserve +# When using bilinear/trilinear filtering and no UV coordinate transformation +# (no texel_antialiasing), low-resolution textures can be blurred, +# so this option automatically upscales them to preserve # crisp pixels. This defines the minimum texture size for the upscaled textures; # higher values look sharper, but require more memory. -# This setting is ONLY applied if any of the mentioned filters are enabled. +# This setting is ONLY applied if any of the mentioned filters are enabled and +# texel_antialiasing is disabled. # This is also used as the base node texture size for world-aligned # texture autoscaling. +# +# Requires: !texel_antialiasing texture_min_size (Base texture size) int 192 192 16384 # Side length of a cube of map blocks that the client will consider together diff --git a/client/shaders/nodes_shader/opengl_fragment.glsl b/client/shaders/nodes_shader/opengl_fragment.glsl index e280e4a2feb95..481469473d8db 100644 --- a/client/shaders/nodes_shader/opengl_fragment.glsl +++ b/client/shaders/nodes_shader/opengl_fragment.glsl @@ -3,6 +3,10 @@ #else uniform sampler2D baseTexture; #endif +#ifdef TEXEL_ANTIALIASING + uniform vec2 texelSize0; + uniform vec2 texelSizeCrack; +#endif #define crackTexture texture1 uniform sampler2D crackTexture; @@ -432,15 +436,48 @@ vec2 uv_repeat(vec2 v) return v; } -void main(void) +#ifdef TEXEL_ANTIALIASING +// Move the uv coordinates within the texel such that when sampling the texture +// with bilinear filtering, the result looks like nearest neighbour sampling +// with anti-aliased texels. +// Based on t3ssel8r's code from https://www.patreon.com/posts/83276362. +vec2 uv_texel_antialias(vec2 uv, vec2 texel_size) { - vec2 uv = varTexCoord.st; + float filter_scale = 0.7; + vec2 box_size = clamp(filter_scale * fwidth(uv) / texel_size, 1e-5, 1.0); + vec2 tx = uv / texel_size - 0.5 * box_size; + vec2 tx_off = clamp((fract(tx) - (1.0 - box_size)) / box_size, 0.0, 1.0); + return (floor(tx) + 0.5 + tx_off) * texel_size; +} +#endif +vec4 sample_base_texture(vec2 uv) +{ +#ifdef TEXEL_ANTIALIASING + vec2 uv_moved = uv_texel_antialias(uv, texelSize0); +#ifdef USE_ARRAY_TEXTURE + return textureGrad(baseTexture, vec3(uv_moved, varTexLayer), dFdx(uv), + dFdy(uv)).rgba; +#elif (!defined GL_ES && __VERSION__ >= 130) || (defined GL_ES && __VERSION__ >= 300) + return textureGrad(baseTexture, uv_moved, dFdx(uv), dFdy(uv)).rgba; +#else + // For the deprecated texture2D there is no texture2DGrad + return texture2D(baseTexture, uv_moved).rgba; +#endif +#else #ifdef USE_ARRAY_TEXTURE - vec4 base = texture(baseTexture, vec3(uv, varTexLayer)).rgba; + return texture(baseTexture, vec3(uv, varTexLayer)).rgba; #else - vec4 base = texture2D(baseTexture, uv).rgba; + return texture2D(baseTexture, uv).rgba; #endif +#endif +} + + +void main(void) +{ + vec2 uv = varTexCoord.st; + vec4 base = sample_base_texture(uv); // Handle transparency by discarding pixel as appropriate. #ifdef USE_DISCARD @@ -460,7 +497,18 @@ void main(void) vec2 cuv_offset = vec2(0.0, crack_progress / crackAnimationLength); vec2 cuv_factor = vec2(1.0, 1.0 / crackAnimationLength); - vec4 crack = texture2D(crackTexture, cuv_offset + orig_uv * cuv_factor); +#ifdef TEXEL_ANTIALIASING + vec2 orig_uv_unmoved = orig_uv; + orig_uv = uv_texel_antialias(orig_uv, texelSizeCrack / cuv_factor); +#endif + vec2 sample_pos = cuv_offset + orig_uv * cuv_factor; +#if defined TEXEL_ANTIALIASING && ((!defined GL_ES && __VERSION__ >= 130) || (defined GL_ES && __VERSION__ >= 300)) + vec4 crack = textureGrad(crackTexture, sample_pos, + dFdx(orig_uv_unmoved) * cuv_factor, + dFdy(orig_uv_unmoved) * cuv_factor); +#else + vec4 crack = texture2D(crackTexture, sample_pos); +#endif base = mix(base, crack, crack.a); } diff --git a/client/shaders/object_shader/opengl_fragment.glsl b/client/shaders/object_shader/opengl_fragment.glsl index 856ac436c92ae..b52a02c3709ea 100644 --- a/client/shaders/object_shader/opengl_fragment.glsl +++ b/client/shaders/object_shader/opengl_fragment.glsl @@ -3,6 +3,9 @@ #else uniform sampler2D baseTexture; #endif +#ifdef TEXEL_ANTIALIASING + uniform vec2 texelSize0; +#endif uniform vec3 dayLight; uniform lowp vec4 fogColor; @@ -362,16 +365,44 @@ float getShadow(sampler2D shadowsampler, vec2 smTexCoord, float realDistance) #endif #endif - -void main(void) +#ifdef TEXEL_ANTIALIASING +// Copied from the nodes fragment shader +vec2 uv_texel_antialias(vec2 uv, vec2 texel_size) { - vec2 uv = varTexCoord.st; + float filter_scale = 0.7; + vec2 box_size = clamp(filter_scale * fwidth(uv) / texel_size, 1e-5, 1.0); + vec2 tx = uv / texel_size - 0.5 * box_size; + vec2 tx_off = clamp((fract(tx) - (1.0 - box_size)) / box_size, 0.0, 1.0); + return (floor(tx) + 0.5 + tx_off) * texel_size; +} +#endif +vec4 sample_base_texture(vec2 uv) +{ +#ifdef TEXEL_ANTIALIASING + vec2 uv_moved = uv_texel_antialias(uv, texelSize0); +#ifdef USE_ARRAY_TEXTURE + return textureGrad(baseTexture, vec3(uv_moved, varTexLayer), dFdx(uv), + dFdy(uv)).rgba; +#elif (!defined GL_ES && __VERSION__ >= 130) || (defined GL_ES && __VERSION__ >= 300) + return textureGrad(baseTexture, uv_moved, dFdx(uv), dFdy(uv)).rgba; +#else + // For the deprecated texture2D there is no texture2DGrad + return texture2D(baseTexture, uv_moved).rgba; +#endif +#else #ifdef USE_ARRAY_TEXTURE - vec4 base = texture(baseTexture, vec3(uv, varTexLayer)).rgba; + return texture(baseTexture, vec3(uv, varTexLayer)).rgba; #else - vec4 base = texture2D(baseTexture, uv).rgba; + return texture2D(baseTexture, uv).rgba; #endif +#endif +} + + +void main(void) +{ + vec4 base = sample_base_texture(varTexCoord.st); // Handle transparency by discarding pixel as appropriate. #ifdef USE_DISCARD diff --git a/src/client/game.cpp b/src/client/game.cpp index d91435f6cda8b..b558f6cdd5c17 100644 --- a/src/client/game.cpp +++ b/src/client/game.cpp @@ -85,7 +85,9 @@ class GameGlobalShaderUniformSetter : public IShaderUniformSetter CachedVertexShaderSetting m_camera_position_vertex{"cameraPosition"}; CachedVertexShaderSetting m_texel_size0_vertex{"texelSize0"}; CachedPixelShaderSetting m_texel_size0_pixel{"texelSize0"}; + CachedPixelShaderSetting m_texel_size_crack_pixel{"texelSizeCrack"}; v2f m_texel_size0; + v2f m_texel_size_crack; CachedStructPixelShaderSetting m_exposure_params_pixel{ "exposureParams", @@ -177,6 +179,7 @@ class GameGlobalShaderUniformSetter : public IShaderUniformSetter m_texel_size0_vertex.set(m_texel_size0, services); m_texel_size0_pixel.set(m_texel_size0, services); + m_texel_size_crack_pixel.set(m_texel_size_crack, services); { float tmp = m_crack_animation_length_i; @@ -272,6 +275,13 @@ class GameGlobalShaderUniformSetter : public IShaderUniformSetter } else { m_texel_size0 = v2f(); } + video::ITexture *texture_crack = material.getTexture(1); + if (texture_crack) { + core::dimension2du size = texture_crack->getSize(); + m_texel_size_crack = v2f(1.f / size.Width, 1.f / size.Height); + } else { + m_texel_size_crack = v2f(); + } } }; @@ -367,6 +377,11 @@ class NodeShaderConstantSetter : public IShaderConstantSetter constants["ENABLE_WAVING_LEAVES"] = g_settings->getBool("enable_waving_leaves") ? 1 : 0; constants["ENABLE_WAVING_PLANTS"] = g_settings->getBool("enable_waving_plants") ? 1 : 0; + + if (g_settings->getBool("texel_antialiasing") + && (g_settings->getBool("trilinear_filter") + || g_settings->getBool("bilinear_filter"))) + constants["TEXEL_ANTIALIASING"] = 1; } }; diff --git a/src/client/imagesource.cpp b/src/client/imagesource.cpp index 5f9e84c611d2b..7bb17c6b3a6ca 100644 --- a/src/client/imagesource.cpp +++ b/src/client/imagesource.cpp @@ -1492,6 +1492,9 @@ bool ImageSource::generateImagePart(std::string_view part_of_name, * 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. + * With texel_antialiasing, the shader moves the UV coordinates such that the + * output looks like nearest neighbour sampling with anti-aliased texel edges; + * in this case the upscaling is redundant and therefore disabled. * * Q: why not just enable/disable filtering depending on texture size? * A: large texture resolutions apparently allow getting rid of the Moire @@ -1499,8 +1502,8 @@ bool ImageSource::generateImagePart(std::string_view part_of_name, * see and related * linked discussions. */ - const bool filter = m_setting_trilinear_filter || m_setting_bilinear_filter; - if (filter) { + if (!m_setting_texel_antialiasing && + (m_setting_trilinear_filter || m_setting_bilinear_filter)) { const f32 scaleto = rangelim(g_settings->getU16("texture_min_size"), TEXTURE_FILTER_MIN_SIZE, 16384); @@ -1802,7 +1805,8 @@ ImageSource::ImageSource() : m_setting_mipmap{g_settings->getBool("mip_map")}, m_setting_trilinear_filter{g_settings->getBool("trilinear_filter")}, m_setting_bilinear_filter{g_settings->getBool("bilinear_filter")}, - m_setting_anisotropic_filter{g_settings->getBool("anisotropic_filter")} + m_setting_anisotropic_filter{g_settings->getBool("anisotropic_filter")}, + m_setting_texel_antialiasing{g_settings->getBool("texel_antialiasing")} {} video::IImage* ImageSource::generateImage(std::string_view name, diff --git a/src/client/imagesource.h b/src/client/imagesource.h index 7e994659ded91..714e7efcd1e7e 100644 --- a/src/client/imagesource.h +++ b/src/client/imagesource.h @@ -63,6 +63,7 @@ struct ImageSource { bool m_setting_trilinear_filter; bool m_setting_bilinear_filter; bool m_setting_anisotropic_filter; + bool m_setting_texel_antialiasing; // Cache of source images SourceImageCache m_sourcecache; diff --git a/src/defaultsettings.cpp b/src/defaultsettings.cpp index 5d2cbb0d1904b..28baa56d6aa50 100644 --- a/src/defaultsettings.cpp +++ b/src/defaultsettings.cpp @@ -233,6 +233,7 @@ void set_default_settings() settings->setDefault("opengl_debug", "true"); #endif settings->setDefault("fsaa", "2"); + settings->setDefault("texel_antialiasing", "false"); settings->setDefault("undersampling", "1"); settings->setDefault("world_aligned_mode", "enable"); settings->setDefault("autoscale_mode", "disable");