Browse Source

TextureTools: rewrite docs for DistanceFieldGL from scratch.

I fixed some minor English crimes in the method docs and then realized
this is all just bad and the utter uselessness and lack of information
led to way too many confused questions over the years. So let's do it
properly, finally, ugh.
pull/674/head
Vladimír Vondruš 1 year ago
parent
commit
683a9fa718
  1. BIN
      doc/distancefield-dst.png
  2. BIN
      doc/distancefield-src.png
  3. 14
      doc/generated/README.md
  4. 3
      doc/snippets/CMakeLists.txt
  5. 92
      doc/snippets/TextureTools-gl.cpp
  6. 175
      src/Magnum/TextureTools/DistanceFieldGL.h

BIN
doc/distancefield-dst.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
doc/distancefield-src.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

14
doc/generated/README.md

@ -41,3 +41,17 @@ output is put into `doc/` directory. The executable requires two textures:
Apply `pngcrush` to the result for smaller file sizes:
for f in $(ls shaders-*.png); do pngcrush -ow $f; done
### Distance field vector images
Similarly as above, just with slightly different parameters. The
`distancefield-src.png` is generated as full-page PNG output at 192 DPI
(512x512) from `vector.svg`, the `distancefield-dst.png` is then converted with
```bash
magnum-distancefieldconverter distancefield-src.png distancefield-dst.png --output-size "128 128" --radius 24
```
This is chosen so it corresponds to the `TextureTools::DistanceFieldGL` doc
snippet where it takes a 256x256 image and converts it to 64x64 with a radius
12, but is scaled twice to look better on HiDPI displays.

3
doc/snippets/CMakeLists.txt

@ -94,7 +94,8 @@ if(MAGNUM_WITH_GL)
GL.cpp
MeshTools-gl.cpp
Shaders-gl.cpp
Text-gl.cpp)
Text-gl.cpp
TextureTools-gl.cpp)
target_link_libraries(snippets-GL PRIVATE MagnumGL)
if(CORRADE_TESTSUITE_TEST_TARGET)
add_dependencies(${CORRADE_TESTSUITE_TEST_TARGET} snippets-GL)

92
doc/snippets/TextureTools-gl.cpp

@ -0,0 +1,92 @@
/*
This file is part of Magnum.
Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019,
2020, 2021, 2022, 2023, 2024, 2025
Vladimír Vondruš <mosra@centrum.cz>
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
*/
#include "Magnum/ImageView.h"
#include "Magnum/GL/Framebuffer.h"
#include "Magnum/GL/Texture.h"
#include "Magnum/GL/TextureFormat.h"
#include "Magnum/Math/Range.h"
#include "Magnum/TextureTools/DistanceFieldGL.h"
#define DOXYGEN_ELLIPSIS(...) __VA_ARGS__
using namespace Magnum;
/* Make sure the name doesn't conflict with any other snippets to avoid linker
warnings, unlike with `int main()` there now has to be a declaration to
avoid -Wmisssing-prototypes */
void mainTextureToolsGL();
void mainTextureToolsGL() {
/* On ES it requires one extra parameter with input image size */
#ifndef MAGNUM_TARGET_GLES
{
/* [DistanceFieldGL] */
ImageView2D image = DOXYGEN_ELLIPSIS(ImageView2D{PixelFormat{}, {}, nullptr});
GL::Texture2D input;
input
.setMinificationFilter(GL::SamplerFilter::Nearest)
.setMagnificationFilter(GL::SamplerFilter::Nearest)
.setStorage(1, GL::textureFormat(image.format()), image.size())
.setSubImage(0, {}, image);
GL::Texture2D output;
output.setStorage(1, GL::TextureFormat::R8, image.size()/4);
TextureTools::DistanceFieldGL distanceField{12};
distanceField(input, output, {{}, image.size()/4});
/* [DistanceFieldGL] */
}
#endif
{
ImageView2D image{PixelFormat{}, {}, nullptr};
TextureTools::DistanceFieldGL distanceField{0};
/* [DistanceFieldGL-parameters-rendering] */
Vector2 renderedSize = DOXYGEN_ELLIPSIS({});
Float ratio = renderedSize.x()/(image.size().x()*distanceField.radius());
/* [DistanceFieldGL-parameters-rendering] */
static_cast<void>(ratio);
}
/* On ES it requires one extra parameter with input image size */
#ifndef MAGNUM_TARGET_GLES
{
ImageView2D image{PixelFormat{}, {}, nullptr};
GL::Texture2D input, output;
/* [DistanceFieldGL-incremental] */
/* Construct and set up just once */
TextureTools::DistanceFieldGL distanceField{DOXYGEN_ELLIPSIS(0)};
GL::Framebuffer outputFramebuffer{{{}, image.size()/4}};
outputFramebuffer.attachTexture(GL::Framebuffer::ColorAttachment{0}, output, 0);
/* Call the distance field processing each time the input texture is updated */
Range2Di updatedRange = DOXYGEN_ELLIPSIS({});
distanceField(input, output, updatedRange);
/* [DistanceFieldGL-incremental] */
}
#endif
}

175
src/Magnum/TextureTools/DistanceFieldGL.h

@ -28,6 +28,7 @@
/** @file
* @brief Class @ref Magnum::TextureTools::DistanceFieldGL
* @m_since_latest
*/
#include "Magnum/configure.h"
@ -43,51 +44,144 @@ namespace Magnum { namespace TextureTools {
/**
@brief Create a signed distance field using OpenGL
@m_since_latest
Converts a high-resolution black and white image (such as vector artwork or
font glyphs) to a low-resolution grayscale image with each pixel being a signed
distance to the nearest edge in the original image. Such a distance field image
then occupies much less memory as the spatial resolution is converted to pixel
values amd can be scaled without it being jaggy at small sizes or blurry when
large. It also makes it possible to implement outlining, glow or drop shadow
essentially for free.
@m_class{m-row}
@parblock
@m_div{m-col-m-6 m-nopadt}
@image html distancefield-src.png width=256px
@m_enddiv
@m_div{m-col-m-6 m-nopadt}
@image html distancefield-dst.png width=256px
@m_enddiv
Converts a binary black/white image (stored in the red channel of @p input) to
a signed distance field (stored in the red channel of @p output @p rectangle).
The purpose of this function is to convert a high-resolution binary image (such
as vector artwork or font glyphs) to a low-resolution grayscale image. The
image will then occupy much less memory and can be scaled without aliasing
issues. Additionally it provides foundation for features like outlining, glow
or drop shadow essentially for free.
@endparblock
You can also use the @ref magnum-distancefieldconverter "magnum-distancefieldconverter"
utility to do distance field conversion on command-line. This functionality is
also used inside the @ref magnum-fontconverter "magnum-fontconverter" utility.
You can use the @ref magnum-distancefieldconverter "magnum-distancefieldconverter"
utility to perform distance field conversion on a command line. Distance field
textures can be rendered with @ref Shaders::DistanceFieldVectorGL, this
functionality is also used to implement @ref Text::DistanceFieldGlyphCacheGL
for text rendering, which is then exposed in the
@ref magnum-fontconverter "magnum-fontconverter" utility.
Algorithm based on: *Chris Green - Improved Alpha-Tested Magnification for
Vector Textures and Special Effects, SIGGRAPH 2007,
http://www.valvesoftware.com/publications/2007/SIGGRAPH2007_AlphaTestedMagnification.pdf*
@note This class is available only if Magnum is compiled with
@ref MAGNUM_TARGET_GL enabled (done by default). See @ref building-features
for more information.
@section TextureTools-DistanceFieldGL-algorithm The algorithm
@section TextureTools-DistanceFieldGL-usage Example usage
For each pixel inside the @p output sub-rectangle the algorithm looks at
corresponding pixel in the input and tries to find nearest pixel of opposite
color in an area defined @p radius. Signed distance between the points is then
saved as value of given pixel in @p output. Value of 1.0 means that the pixel
was originally colored white and nearest black pixel is farther than @p radius,
value of 0.0 means that the pixel was originally black and nearest white pixel
is farther than @p radius. Values around 0.5 are around edges.
The following snippet uploads an image to a @ref GL::Texture2D, creates a
second smaller @ref GL::Texture2D for the output and then performs the distance
field conversion with a radius of @cpp 12 @ce pixels and spanning the whole
output image area:
The resulting texture can be used with bilinear filtering. It can be converted
back to binary form in shader using e.g. GLSL @glsl smoothstep() @ce function
with step around 0.5 to create antialiased edges. Or you can exploit the
distance field features to create many other effects. See also
@ref Shaders::DistanceFieldVectorGL.
@snippet TextureTools-gl.cpp DistanceFieldGL
Based on: *Chris Green - Improved Alpha-Tested Magnification for Vector Textures
and Special Effects, SIGGRAPH 2007,
http://www.valvesoftware.com/publications/2007/SIGGRAPH2007_AlphaTestedMagnification.pdf*
In the output (an example of which is shown above on the right, scaled up to
match the original), value of @cpp 1.0 @ce (when normalized from the actual
pixel format, so @cpp 255 @ce for the @ref GL::TextureFormat::R8 used above)
means that the pixel was originally colored white and nearest black pixel is
further away than the specified radius. Correspondingly, value of @cpp 0.0 @ce
means that the pixel was originally black and nearest white pixel is further
away than the radius. Edges are thus at values around @cpp 0.5 @ce.
The resulting texture is meant to be used with bilinear filtering, i.e. with
@ref GL::SamplerFilter::Linear. To get the original image back, the GLSL
@glsl smoothstep() @ce function can be used as shown in the following snippet,
with a step around @cpp 0.5 @ce and @cpp smoothness @ce being a configurable
factor controlling edge smoothness.
@code{.glsl}
float factor = smoothstep(0.5 - smoothness,
0.5 + smoothness,
texture(distanceFieldTexture, coordinates).r);
@endcode
The @ref Shaders::DistanceFieldVectorGL implements also outlining, edge dilate,
erode, and other effects with the distance field input.
@attention This is a GPU-only implementation, so it expects an active GL
context.
@section TextureTools-DistanceFieldGL-parameters Parameter tuning
Quality of the generated distance field is affected by two variables --- the
ratio between input and output size, and the radius. A bigger size ratio will
result in bigger memory savings but at the cost of losing finer detail, so the
choice depends mainly on the content that's actually being processed. The image
shown above could get away with being reduced down even eight or sixteen times
without noticeable quality loss, on the other hand vector art consisting of
fine lines or for example CJK glyphs might likely have artifacts already with
the ratio of @cpp 4 @ce used above.
The radius should be at least as large as the size ratio in order to contribute
to at least one pixel on every side of an edge in the output, otherwise the
resulting rendering will be extremely blocky. After that, its value is dictated
mainly by the desired use of the output --- if you need to draw the output with
larger antialiasing smoothness, big outlines or shadows, the radius needs to
get bigger. With the size ratio of @cpp 4 @ce and radius of @cpp 12 @ce used
above, the output allows for smoothness, outline or other effect ±3 pixels
around the edge.
Finally, with very large radii you may run into quantization issues with 8-bit
texture formats, causing again blocky artifacts. A solution is then to use
@ref GL::TextureFormat::R16 instead. Using an even larger format probably won't
improve the result any further, and since the distance is normalized to a
@f$ [0, 1] @f$ range, a floating-point format such as
@ref GL::TextureFormat::R16F would also not, but rather resulting in worse
precision than the 16-bit normalized integer format.
@subsection TextureTools-DistanceFieldGL-parameters-rendering Effect of input parameters on final rendered image
In order to ensure consistent look when rendering regardless of the parameters
picked for distance field conversion, the rendering has to take the input size
and radius into account. Assuming @cpp image.size() @ce is size of the input
image and `renderedSize` is pixel size at which the distance field image is
drawn on the screen, the `ratio` calculated below is then distance that
corresponds to one pixel on the screen. Note that the ratio at which the
distance field output is sized down has no effect here, and thus it can be
chosen dynamically to achieve desired quality / memory use tradeoff.
@snippet TextureTools-gl.cpp DistanceFieldGL-parameters-rendering
For a concrete example, if the input was @cpp {256, 256} @ce, it's now rendered
at a size of @cpp {128, 128} @ce and it was converted with a radius of
@cpp 12 @ce, the `ratio` will be @cpp 1.0f/6 @ce. I.e., if you set the shader
`smoothness` to @cpp 1.0f/6 @ce, the edge smoothness radius will be exactly one
pixel.
@section TextureTools-DistanceFieldGL-incremental Incremental distance field calculation
Besides converting whole texture at once, it's possible to process just a part.
This is mainly useful with use cases like dynamically populated texture
atlases, where it'd be wasteful to repeatedly process already filled parts.
The *output* area to process is specified with the third argument to
@ref operator()() (which was above set to the whole output texture size). The
input texture is still taken as a whole, i.e. it's assumed that it contains
exactly the data meant to be processed and placed into the output area.
Additionally, to avoid needless OpenGL state changes, it's recommended to
supply a @ref GL::Framebuffer with the output texture attached so the
implementation doesn't need to create a temporary one each time:
@snippet TextureTools-gl.cpp DistanceFieldGL-incremental
*/
class MAGNUM_TEXTURETOOLS_EXPORT DistanceFieldGL {
public:
/**
* @brief Constructor
* @param radius Max lookup radius in the input texture
* @param radius Distance field calculation radius
*
* Prepares the shader and other internal state for given @p radius.
*/
@ -129,25 +223,25 @@ class MAGNUM_TEXTURETOOLS_EXPORT DistanceFieldGL {
/** @brief Move constructor */
DistanceFieldGL& operator=(DistanceFieldGL&&) noexcept;
/** @brief Max lookup radius */
/** @brief Distance field calculation radius */
UnsignedInt radius() const;
/**
* @brief Calculate the distance field to a framebuffer
* @brief Calculate distance field to a framebuffer
* @param input Input texture
* @param output Output framebuffer
* @param rectangle Rectangle in output texture where to render
* @param rectangle Rectangle in the output where to render
* @param imageSize Input texture size. Needed only for OpenGL ES,
* on desktop GL the information is gathered automatically using
* @ref GL::Texture2D::imageSize().
* on desktop GL the size is queried automatically using
* @ref GL::Texture2D::imageSize() and this parameter is ignored.
* @m_since_latest
*
* The @p output texture is expected to have a framebuffer-drawable
* @ref GL::TextureFormat. On desktop OpenGL and
* OpenGL ES 3.0 it's common to render to @ref GL::TextureFormat::R8.
* On OpenGL ES 2.0 you can use @ref GL::TextureFormat::Red if
* @gl_extension{EXT,texture_rg} is available; if not, the smallest but
* still inefficient supported format is in most cases
* @gl_extension{EXT,texture_rg} is available; if not, the smallest yet
* still quite inefficient supported format is in most cases
* @ref GL::TextureFormat::RGB. The @ref GL::TextureFormat::Luminance
* format usually isn't renderable.
*
@ -176,16 +270,17 @@ class MAGNUM_TEXTURETOOLS_EXPORT DistanceFieldGL {
#endif
/**
* @brief Calculate the distance field to a texture
* @brief Calculate distance field to a texture
* @param input Input texture
* @param output Output texture
* @param rectangle Rectangle in output texture where to render
* @param rectangle Rectangle in the output where to render
* @param imageSize Input texture size. Needed only for OpenGL ES,
* on desktop GL the information is gathered automatically using
* @ref GL::Texture2D::imageSize().
*
* Creates a framebuffer with @p output attached and calls
* @ref operator()(GL::Texture2D&, GL::Framebuffer&, const Range2Di&, const Vector2i&).
* Convenience variant of @ref operator()(GL::Texture2D&, GL::Framebuffer&, const Range2Di&, const Vector2i&)
* that creates a temporary framebuffer with @p output attached and
* destroys it again after the operation.
*/
#ifdef DOXYGEN_GENERATING_OUTPUT
void operator()(GL::Texture2D& input, GL::Texture2D& output, const Range2Di& rectangle, const Vector2i& imageSize

Loading…
Cancel
Save