Browse Source

TextureTools: make distance field output bit-exact for all platforms.

*Finally* having consistent output on desktop, ES1, ES2, WebGL 1 and
WebGL 2, while also cutting 40% off the processing time. For the record,
the benchmark took 2.3 ms before, now it's 1.4.
pull/297/head
Vladimír Vondruš 8 years ago
parent
commit
3cf98026d5
  1. 8
      doc/changelog.dox
  2. 3
      src/Magnum/TextureTools/DistanceField.h
  3. 187
      src/Magnum/TextureTools/DistanceFieldShader.frag

8
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

3
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:

187
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;
}

Loading…
Cancel
Save