Magnum::TextureTools::DistanceFieldGL class new in Git master

Create a signed distance field using OpenGL.

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.

Image
Image

You can use the magnum-distancefieldconverter utility to perform distance field conversion on a command line. Distance field textures can be rendered with Shaders::DistanceFieldVectorGL, this functionality is also used to implement Text::DistanceFieldGlyphCacheGL / Text::DistanceFieldGlyphCacheArrayGL for text rendering, which is then exposed in the 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

Example usage

The following snippet uploads an image to a GL::Texture2D, creates a second smaller GL::Texture2D for the output and then performs the distance field conversion with a radius of 12 pixels and spanning the whole output image area:

ImageView2D image = ;

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

In the output (an example of which is shown above on the right, scaled up to match the original), value of 1.0 (when normalized from the actual pixel format, so 255 for the 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 0.0 means that the pixel was originally black and nearest white pixel is further away than the radius. Edges are thus at values around 0.5.

The resulting texture is meant to be used with bilinear filtering, i.e. with GL::SamplerFilter::Linear. To get the original image back, the GLSL smoothstep() function can be used as shown in the following snippet, with a step around 0.5 and smoothness being a configurable factor controlling edge smoothness.

float factor = smoothstep(0.5 - smoothness,
                          0.5 + smoothness,
                          texture(distanceFieldTexture, coordinates).r);

The Shaders::DistanceFieldVectorGL implements also outlining, edge dilate, erode, and other effects with the distance field input.

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 4 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 4 and radius of 12 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 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 $ [0, 1] $ range, a floating-point format such as GL::TextureFormat::R16F would also not, but rather resulting in worse precision than the 16-bit normalized integer format.

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 image.size() 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.

Vector2 renderedSize = ;
Float ratio = renderedSize.x()/(image.size().x()*distanceField.radius());

For a concrete example, if the input was {256, 256}, it's now rendered at a size of {128, 128} and it was converted with a radius of 12, the ratio will be 1.0f/6. I.e., if you set the shader smoothness to 1.0f/6, the edge smoothness radius will be exactly one pixel.

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 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 GL::Framebuffer with the output texture attached so the implementation doesn't need to create a temporary one each time:

/* Construct and set up just once */
TextureTools::DistanceFieldGL distanceField{};
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 = ;
distanceField(input, output, updatedRange);

Constructors, destructors, conversion operators

DistanceFieldGL(UnsignedInt radius) explicit
Constructor.
DistanceFieldGL(NoCreateT) explicit noexcept new in Git master
Construct without creating the internal OpenGL state.
DistanceFieldGL(const DistanceFieldGL&) deleted
Copying is not allowed.
DistanceFieldGL(DistanceFieldGL&&) noexcept new in Git master
Move constructor.

Public functions

auto operator=(const DistanceFieldGL&) -> DistanceFieldGL& deleted
Copying is not allowed.
auto operator=(DistanceFieldGL&&) -> DistanceFieldGL& noexcept
Move constructor.
auto radius() const -> UnsignedInt
Distance field calculation radius.
void operator()(GL::Texture2D& input, GL::Framebuffer& output, const Range2Di& rectangle, const Vector2i& imageSize = {}) new in Git master
Calculate distance field to a framebuffer.
void operator()(GL::Texture2D& input, GL::Texture2D& output, const Range2Di& rectangle, const Vector2i& imageSize = {})
Calculate distance field to a texture.
void operator()(GL::Texture2D& input, GL::Texture2DArray& output, Int layer, const Range2Di& rectangle, const Vector2i& imageSize = {}) new in Git master
Calculate distance field to a texture array layer.

Function documentation

Magnum::TextureTools::DistanceFieldGL::DistanceFieldGL(UnsignedInt radius) explicit

Constructor.

Parameters
radius Distance field calculation radius

Prepares the shader and other internal state for given radius.

Magnum::TextureTools::DistanceFieldGL::DistanceFieldGL(NoCreateT) explicit noexcept new in Git master

Construct without creating the internal OpenGL state.

The constructed instance is equivalent to moved-from state, i.e. no APIs can be safely called on the object. Useful in cases where you will overwrite the instance later anyway. Move another object over it to make it useful.

This function can be safely used for constructing (and later destructing) objects even without any OpenGL context being active. However note that this is a low-level and a potentially dangerous API, see the documentation of NoCreate for alternatives.

Magnum::TextureTools::DistanceFieldGL::DistanceFieldGL(DistanceFieldGL&&) noexcept new in Git master

Move constructor.

Performs a destructive move, i.e. the original object isn't usable afterwards anymore.

void Magnum::TextureTools::DistanceFieldGL::operator()(GL::Texture2D& input, GL::Framebuffer& output, const Range2Di& rectangle, const Vector2i& imageSize = {}) new in Git master

Calculate distance field to a framebuffer.

Parameters
input Input texture
output Output framebuffer
rectangle Rectangle in the output where to render
imageSize Input texture size. Mandatory on OpenGL ES and WebGL, on desktop GL if left at default the size is internally queried using GL::Texture2D::imageSize() instead.

The output texture is expected to have a framebuffer-drawable GL::TextureFormat. On desktop OpenGL and OpenGL ES 3.0 it's common to render to GL::TextureFormat::R8. On OpenGL ES 2.0 you can use GL::TextureFormat::Red if EXT_texture_rg is available; if not, the smallest yet still quite inefficient supported format is in most cases GL::TextureFormat::RGB. The GL::TextureFormat::Luminance format usually isn't renderable.

Additionally, the ratio of the input size (or imageSize on OpenGL ES) and rectangle size is expected to be a multiple of 2, as that's what the generator shader relies on for correct pixel addressing.

void Magnum::TextureTools::DistanceFieldGL::operator()(GL::Texture2D& input, GL::Texture2D& output, const Range2Di& rectangle, const Vector2i& imageSize = {})

Calculate distance field to a texture.

Parameters
input Input texture
output Output texture
rectangle Rectangle in the output where to render
imageSize Input texture size. Mandatory on OpenGL ES and WebGL, on desktop GL if left at default the size is internally queried using GL::Texture2D::imageSize() instead.

Convenience variant of operator()(GL::Texture2D&, GL::Framebuffer&, const Range2Di&, const Vector2i&) that creates a temporary framebuffer with output attached and destroys it again after the operation.

void Magnum::TextureTools::DistanceFieldGL::operator()(GL::Texture2D& input, GL::Texture2DArray& output, Int layer, const Range2Di& rectangle, const Vector2i& imageSize = {}) new in Git master

Calculate distance field to a texture array layer.

Parameters
input Input texture
output Output texture
layer Layer in the output where to render
rectangle Rectangle in the output where to render
imageSize Input texture size. Mandatory on OpenGL ES and WebGL, on desktop GL if left at default the size is internally queried using GL::Texture2D::imageSize() instead.

Convenience variant of operator()(GL::Texture2D&, GL::Framebuffer&, const Range2Di&, const Vector2i&) that creates a temporary framebuffer with output layer attached and destroys it again after the operation.