diff --git a/doc/changelog.dox b/doc/changelog.dox index 6e0fd29f2..92233b2f1 100644 --- a/doc/changelog.dox +++ b/doc/changelog.dox @@ -66,6 +66,14 @@ See also: code that's not VAO-aware working on core GL profiles (which don't allow default VAOs being used for drawing) +@subsubsection changelog-latest-changes-texturetools TextureTools library + +- Further performance and output quality improvements for + @ref TextureTools::DistanceField, making the ES2/WebGL 1 consistent with + desktop and speeding up the processing to take only 60% of the time + compared to before. It's now also possible to reuse the internal state for + batch processing. + @subsection changelog-latest-bugfixes Bug fixes - Fixed @ref Platform::Sdl2Application and @ref Platform::GlfwApplication to diff --git a/src/Magnum/TextureTools/DistanceField.h b/src/Magnum/TextureTools/DistanceField.h index d6ce620b3..968d35349 100644 --- a/src/Magnum/TextureTools/DistanceField.h +++ b/src/Magnum/TextureTools/DistanceField.h @@ -93,9 +93,6 @@ http://www.valvesoftware.com/publications/2007/SIGGRAPH2007_AlphaTestedMagnifica @note This function is available only if Magnum is compiled with @ref MAGNUM_TARGET_GL enabled (done by default). See @ref building-features for more information. - -@bug ES (and maybe GL < 3.20) implementation behaves slightly different - (jaggies, visible e.g. when rendering outlined fonts) */ class MAGNUM_TEXTURETOOLS_EXPORT DistanceField { public: diff --git a/src/Magnum/TextureTools/DistanceFieldShader.frag b/src/Magnum/TextureTools/DistanceFieldShader.frag index e75ab7da4..7b4cbfb9e 100644 --- a/src/Magnum/TextureTools/DistanceFieldShader.frag +++ b/src/Magnum/TextureTools/DistanceFieldShader.frag @@ -63,22 +63,124 @@ out lowp float value; #endif #ifdef TEXELFETCH_USABLE -mediump ivec2 rotate(const mediump ivec2 vec) { - return ivec2(-vec.y, vec.x); -} - bool hasValue(const mediump ivec2 position, const mediump ivec2 offset) { - return texelFetch(textureData, position+offset, 0).r > 0.5; + return texelFetch(textureData, position + offset, 0).r > 0.5; } #else -mediump vec2 rotate(const mediump vec2 vec) { - return vec2(-vec.y, vec.x); +bool hasValue(const mediump vec2 position, const mediump ivec2 offset) { + return texture(textureData, position + (vec2(offset) + vec2(0.5))*imageSizeInverted).r > 0.5; } +#endif + +mediump int findMinDistanceSquared(const mediump + #ifdef TEXELFETCH_USABLE + ivec2 + #else + vec2 + #endif + position, const bool isInside) +{ + /* Initialize minimal distance to a value just outside the radius */ + mediump int minDistanceSquared = (RADIUS+1)*(RADIUS+1); + + /* Go in cocentric squares around the point */ + for(int i = 1; i <= RADIUS; ++i) { + /* First check the nearest points, since that's only four combinations. + If any of the four values is opposite of what is on `position`, we + found the nearest value. If the distance is not less than what's + found already, we don't even check the texture. + + i = 1 i = 2 i = 3 + + 0 + 0 + 0 + 1o3 1 o 3 1 o 3 + 2 + 2 + 2 + + Since everything else in the cocentric square and all others will be + further away, we can stop if we found something. */ + const mediump int centerDistanceSquared = i*i; + if(centerDistanceSquared >= minDistanceSquared) + return minDistanceSquared; + if(hasValue(position, ivec2(0, i)) != isInside || + hasValue(position, ivec2(-i, 0)) != isInside || + hasValue(position, ivec2(0, -i)) != isInside || + hasValue(position, ivec2(i, 0)) != isInside) { + return centerDistanceSquared; + } + + /* Now check for points further away, except for the corner points. + Every iteration checks all eight rotations/reflections at the same + distance. Again, if the distance is not less than what's found + already, we don't even check the texture -- but can't return, since + next iteration can still have closer values. + + i = 1 i = 2 i = 3 + (none) + 91 08 + 1 0 a f + 2 7 2 7 + o o o + 3 6 3 6 + 4 5 b e + c4 5d + + Once we find something, it's the closest value possible in this + cycle, so we stop the cycle. But next iterations can still have + values that are closer, so can't return. */ + for(int j = 1; j < RADIUS; ++j) { + /* Don't go further than current radius - 1 (i.e., excluding the + corner). The loop needs to be compile-time bound otherwise some + drivers crash on it, so we're breaking inside instead of having + this directly in the loop condition. */ + if(j >= i) break; + + const mediump int sideDistanceSquared = i*i + j*j; + if(sideDistanceSquared >= minDistanceSquared) + break; + if(hasValue(position, ivec2( j, i)) != isInside || + hasValue(position, ivec2(-j, i)) != isInside || + hasValue(position, ivec2(-i, j)) != isInside || + hasValue(position, ivec2(-i, -j)) != isInside || + hasValue(position, ivec2(-j, -i)) != isInside || + hasValue(position, ivec2( j, -i)) != isInside || + hasValue(position, ivec2( i, -j)) != isInside || + hasValue(position, ivec2( i, j)) != isInside) { + minDistanceSquared = sideDistanceSquared; + break; + } + } + + /* Finally, check for the corners, which is again just four cases: + + i = 1 i = 2 i = 3 + + 1 0 + 1 0 + 1 0 + o o o + 2 3 + 2 3 + 2 3 + + If we find something, it's most probably not the nearest distance, + since the following iterations can be much closer. */ + const mediump int cornerDistanceSquared = 2*i*i; + if(cornerDistanceSquared >= minDistanceSquared) + continue; + if(hasValue(position, ivec2( i, i)) != isInside || + hasValue(position, ivec2(-i, i)) != isInside || + hasValue(position, ivec2(-i, -i)) != isInside || + hasValue(position, ivec2( i, -i)) != isInside) { + minDistanceSquared = cornerDistanceSquared; + } + } -bool hasValue(const mediump vec2 position, const mediump vec2 offset) { - return texture(textureData, position+offset).r > 0.5; + return minDistanceSquared; } -#endif void main() { #ifdef TEXELFETCH_USABLE @@ -91,65 +193,14 @@ void main() { const mediump vec2 position = (gl_FragCoord.xy - vec2(0.5))*scaling*imageSizeInverted; #endif - /* If pixel at the position is inside (1), we are looking for nearest pixel - outside and the value will be positive (> 0.5). If it is outside (0), we - are looking for nearest pixel inside and the value will be negative - (< 0.5). */ - #ifdef TEXELFETCH_USABLE + /* If pixel at the position is inside (its value > 0.5), we are looking for + nearest pixel outside and the value will be positive (or > 0.5 after + normalization). If it is outside (its value < 0), we are looking for + nearest pixel inside and the value will be negative (or < 0.5). */ const bool isInside = hasValue(position, ivec2(0, 0)); - #else - const bool isInside = hasValue(position, vec2(0.0, 0.0)); - #endif - const highp float sign = isInside ? 1.0 : -1.0; - - /* Minimal found distance is just out of the radius (i.e. infinity) */ - highp float minDistanceSquared = float((RADIUS+1)*(RADIUS+1)); - - /* Go in circles around the point and find nearest value */ - int radiusLimit = RADIUS; - for(int i = 1; i <= RADIUS; ++i) { - int jmax = i*2; - for(int j = 0; j < RADIUS*2; ++j) { - #ifdef TEXELFETCH_USABLE - const lowp ivec2 offset = ivec2(-i+j, i); - #else - const lowp vec2 pixelOffset = vec2(float(-i+j), float(i)) + vec2(0.5); - const lowp vec2 offset = pixelOffset*imageSizeInverted; - #endif - - /* If any of the four values is opposite of what is on the pixel, - we found nearest value */ - if(hasValue(position, offset) == !isInside || - hasValue(position, rotate(offset)) == !isInside || - hasValue(position, rotate(rotate(offset))) == !isInside || - hasValue(position, rotate(rotate(rotate(offset)))) == !isInside) { - #ifdef TEXELFETCH_USABLE - const mediump float distanceSquared = dot(vec2(offset), vec2(offset)); - #else - const mediump float distanceSquared = dot(pixelOffset, pixelOffset); - #endif - - /* Set smaller distance, if found, or continue with lookup for - smaller */ - if(minDistanceSquared < distanceSquared) continue; - else minDistanceSquared = distanceSquared; - - /* Set radius limit to max radius which can contain smaller - value, e.g. for distance 3.5 we can find smaller value even - in radius 3 */ - #ifdef NEW_GLSL - radiusLimit = min(RADIUS, int(floor(length(vec2(offset))))); - #else - radiusLimit = int(min(float(RADIUS), floor(length(vec2(offset))))); - #endif - } - - if(j + 1 >= jmax) break; - } - - if(i + 1 > radiusLimit) break; - } + const mediump float minDistance = sqrt(float(findMinDistanceSquared(position, isInside))); /* Final signed distance, normalized from [-radius-1, radius+1] to [0, 1] */ - value = sign*sqrt(minDistanceSquared)/float(RADIUS*2+2)+0.5; + const highp float halfSign = isInside ? 0.5 : -0.5; + value = halfSign*minDistance/float(RADIUS + 1) + 0.5; }