From f0bb710cd3c6b500f33a756df64e00ebf62cee13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Mon, 5 Nov 2018 12:13:59 +0100 Subject: [PATCH] TextureTools: make distance field processing into a stateful class. --- doc/changelog.dox | 6 + src/Magnum/Text/DistanceFieldGlyphCache.cpp | 4 +- src/Magnum/Text/DistanceFieldGlyphCache.h | 5 +- src/Magnum/TextureTools/DistanceField.cpp | 90 +++++++++------ src/Magnum/TextureTools/DistanceField.h | 109 ++++++++++++------ .../TextureTools/Test/DistanceFieldGLTest.cpp | 11 +- .../TextureTools/distancefieldconverter.cpp | 2 +- 7 files changed, 147 insertions(+), 80 deletions(-) diff --git a/doc/changelog.dox b/doc/changelog.dox index 10ad28b2c..6e0fd29f2 100644 --- a/doc/changelog.dox +++ b/doc/changelog.dox @@ -82,6 +82,12 @@ See also: - Fixed various broken links (see [mosra/magnum#291](https://github.com/mosra/magnum/issues/291)) +@subsection changelog-latest-deprecated Deprecated APIs + +- `TextureTools::distanceField()` is deprecated due to inefficiency of its + statelessness when doing batch processing. Use the + @ref TextureTools::DistanceField class instead. + @section changelog-2018-10 2018.10 Released 2018-10-23, tagged as diff --git a/src/Magnum/Text/DistanceFieldGlyphCache.cpp b/src/Magnum/Text/DistanceFieldGlyphCache.cpp index 1d8a66850..dc480daf5 100644 --- a/src/Magnum/Text/DistanceFieldGlyphCache.cpp +++ b/src/Magnum/Text/DistanceFieldGlyphCache.cpp @@ -46,7 +46,7 @@ DistanceFieldGlyphCache::DistanceFieldGlyphCache(const Vector2i& originalSize, c #else GlyphCache(GL::TextureFormat::RGB, originalSize, size, Vector2i(radius)), #endif - _scale{Vector2(size)/Vector2(originalSize)}, _radius{radius} + _scale{Vector2(size)/Vector2(originalSize)}, _distanceField{radius} { #ifndef MAGNUM_TARGET_GLES MAGNUM_ASSERT_GL_EXTENSION_SUPPORTED(GL::Extensions::ARB::texture_rg); @@ -89,7 +89,7 @@ void DistanceFieldGlyphCache::setImage(const Vector2i& offset, const ImageView2D #endif /* Create distance field from input texture */ - TextureTools::distanceField(input, texture(), Range2Di::fromSize(offset*_scale, image.size()*_scale), _radius, image.size()); + _distanceField(input, texture(), Range2Di::fromSize(offset*_scale, image.size()*_scale), image.size()); } void DistanceFieldGlyphCache::setDistanceFieldImage(const Vector2i& offset, const ImageView2D& image) { diff --git a/src/Magnum/Text/DistanceFieldGlyphCache.h b/src/Magnum/Text/DistanceFieldGlyphCache.h index a46849642..4defddb77 100644 --- a/src/Magnum/Text/DistanceFieldGlyphCache.h +++ b/src/Magnum/Text/DistanceFieldGlyphCache.h @@ -30,6 +30,7 @@ */ #include "Magnum/Text/GlyphCache.h" +#include "Magnum/TextureTools/DistanceField.h" namespace Magnum { namespace Text { @@ -85,8 +86,8 @@ class MAGNUM_TEXT_EXPORT DistanceFieldGlyphCache: public GlyphCache { void setDistanceFieldImage(const Vector2i& offset, const ImageView2D& image); private: - const Vector2 _scale; - const UnsignedInt _radius; + Vector2 _scale; + TextureTools::DistanceField _distanceField; }; }} diff --git a/src/Magnum/TextureTools/DistanceField.cpp b/src/Magnum/TextureTools/DistanceField.cpp index 9b637b0f0..ab1b68d8b 100644 --- a/src/Magnum/TextureTools/DistanceField.cpp +++ b/src/Magnum/TextureTools/DistanceField.cpp @@ -53,7 +53,7 @@ class DistanceFieldShader: public GL::AbstractShaderProgram { public: typedef GL::Attribute<0, Vector2> Position; - explicit DistanceFieldShader(Int radius); + explicit DistanceFieldShader(UnsignedInt radius); DistanceFieldShader& setScaling(const Vector2& scaling) { setUniform(scalingUniform, scaling); @@ -79,7 +79,7 @@ class DistanceFieldShader: public GL::AbstractShaderProgram { imageSizeInvertedUniform; }; -DistanceFieldShader::DistanceFieldShader(Int radius) { +DistanceFieldShader::DistanceFieldShader(const UnsignedInt radius) { #ifdef MAGNUM_BUILD_STATIC /* Import resources on static build, if not already */ if(!Utility::Resource::hasGroup("MagnumTextureTools")) @@ -142,36 +142,71 @@ DistanceFieldShader::DistanceFieldShader(Int radius) { } } -#ifndef MAGNUM_TARGET_GLES -void distanceField(GL::Texture2D& input, GL::Texture2D& output, const Range2Di& rectangle, const Int radius, const Vector2i&) -#else -void distanceField(GL::Texture2D& input, GL::Texture2D& output, const Range2Di& rectangle, const Int radius, const Vector2i& imageSize) -#endif -{ + +struct DistanceField::State { + explicit State(UnsignedInt radius): shader{radius}, radius{radius} {} + + DistanceFieldShader shader; + UnsignedInt radius; + GL::Mesh mesh; +}; + +DistanceField::DistanceField(const UnsignedInt radius): _state{new State{radius}} { #ifndef MAGNUM_TARGET_GLES MAGNUM_ASSERT_GL_EXTENSION_SUPPORTED(GL::Extensions::ARB::framebuffer_object); #endif + _state->mesh.setPrimitive(GL::MeshPrimitive::Triangles) + .setCount(3); + + /* Older GLSL doesn't have gl_VertexID, vertices must be supplied explicitly */ + #ifndef MAGNUM_TARGET_GLES + if(!GL::Context::current().isVersionSupported(GL::Version::GL300)) + #else + if(!GL::Context::current().isVersionSupported(GL::Version::GLES300)) + #endif + { + constexpr Vector2 triangle[] = { + Vector2(-1.0, 1.0), + Vector2(-1.0, -3.0), + Vector2( 3.0, 1.0) + }; + GL::Buffer buffer; + buffer.setData(triangle, GL::BufferUsage::StaticDraw); + _state->mesh.addVertexBuffer(std::move(buffer), 0, DistanceFieldShader::Position()); + } +} + +DistanceField::~DistanceField() = default; + +UnsignedInt DistanceField::radius() const { return _state->radius; } + +void DistanceField::operator()(GL::Texture2D& input, GL::Texture2D& output, const Range2Di& rectangle, const Vector2i& + #ifdef MAGNUM_TARGET_GLES + imageSize + #endif +) { /** @todo Disable depth test, blending and then enable it back (if was previously) */ #ifndef MAGNUM_TARGET_GLES Vector2i imageSize = input.imageSize(0); #endif - GL::Framebuffer framebuffer(rectangle); - framebuffer.attachTexture(GL::Framebuffer::ColorAttachment(0), output, 0); - framebuffer.bind(); - framebuffer.clear(GL::FramebufferClear::Color); + /* Framebuffer is instantiated here so it gets correctly unbound at the end + (and bound framebuffer reset back to the default) */ + GL::Framebuffer framebuffer{rectangle}; + framebuffer.attachTexture(GL::Framebuffer::ColorAttachment(0), output, 0) + .clear(GL::FramebufferClear::Color) + .bind(); const GL::Framebuffer::Status status = framebuffer.checkStatus(GL::FramebufferTarget::Draw); if(status != GL::Framebuffer::Status::Complete) { - Error() << "TextureTools::distanceField(): cannot render to given output texture, unexpected framebuffer status" + Error() << "TextureTools::DistanceField: cannot render to given output texture, unexpected framebuffer status" << status; return; } - DistanceFieldShader shader{radius}; - shader.setScaling(Vector2(imageSize)/Vector2(rectangle.size())) + _state->shader.setScaling(Vector2(imageSize)/Vector2(rectangle.size())) .bindTexture(input); #ifndef MAGNUM_TARGET_GLES @@ -180,32 +215,11 @@ void distanceField(GL::Texture2D& input, GL::Texture2D& output, const Range2Di& if(!GL::Context::current().isVersionSupported(GL::Version::GLES300)) #endif { - shader.setImageSizeInverted(1.0f/Vector2(imageSize)); - } - - GL::Mesh mesh; - mesh.setPrimitive(GL::MeshPrimitive::Triangles) - .setCount(3); - - /* Older GLSL doesn't have gl_VertexID, vertices must be supplied explicitly */ - GL::Buffer buffer; - #ifndef MAGNUM_TARGET_GLES - if(!GL::Context::current().isVersionSupported(GL::Version::GL300)) - #else - if(!GL::Context::current().isVersionSupported(GL::Version::GLES300)) - #endif - { - constexpr Vector2 triangle[] = { - Vector2(-1.0, 1.0), - Vector2(-1.0, -3.0), - Vector2( 3.0, 1.0) - }; - buffer.setData(triangle, GL::BufferUsage::StaticDraw); - mesh.addVertexBuffer(buffer, 0, DistanceFieldShader::Position()); + _state->shader.setImageSizeInverted(1.0f/Vector2(imageSize)); } /* Draw the mesh */ - mesh.draw(shader); + _state->mesh.draw(_state->shader); } }} diff --git a/src/Magnum/TextureTools/DistanceField.h b/src/Magnum/TextureTools/DistanceField.h index a9adaf986..d6ce620b3 100644 --- a/src/Magnum/TextureTools/DistanceField.h +++ b/src/Magnum/TextureTools/DistanceField.h @@ -29,6 +29,8 @@ * @brief Function @ref Magnum::TextureTools::distanceField() */ +#include + #include "Magnum/configure.h" #ifdef MAGNUM_TARGET_GL @@ -42,37 +44,29 @@ namespace Magnum { namespace TextureTools { /** -@brief Create signed distance field -@param input Input texture -@param output Output texture -@param rectangle Rectangle in output texture where to render -@param radius Max lookup radius in input texture -@param imageSize Input texture size. Needed only in OpenGL ES, in desktop - OpenGL the information is gathered automatically using - @ref GL::Texture2D::imageSize(). - -Converts binary image (stored in red channel of @p input) to signed distance -field (stored in red channel in @p rectangle of @p output). The purpose of this -function is to convert high-resolution binary image (such as vector artwork or -font glyphs) to 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. +@brief Create a signed distance field + +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. You can also use the @ref magnum-distancefieldconverter "magnum-distancefieldconverter" -utility to do distance field conversion on command-line. By extension, this -functionality is also provided through @ref magnum-fontconverter "magnum-fontconverter" -utility. +utility to do distance field conversion on command-line. This functionality is +also used inside the @ref magnum-fontconverter "magnum-fontconverter" utility. -### The algorithm +@section TextureTools-DistanceField-algorithm The algorithm -For each pixel inside @p rectangle the algorithm looks at corresponding pixel in -@p input and tries to find nearest pixel of opposite color in area given by -@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. +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 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 @@ -84,13 +78,14 @@ 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* -@attention This is GPU-only implementation, so it expects active context. +@attention This is a GPU-only implementation, so it expects an active GL + context. @note If internal format of @p output texture is not renderable, this function - prints message to error output and does nothing. In desktop OpenGL and - OpenGL ES 3.0 it's common to render to @ref GL::TextureFormat::R8. In + prints a message to error output and does nothing. 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 + @gl_extension{EXT,texture_rg} is available; if not, the smallest but still inefficient supported format is in most cases @ref GL::TextureFormat::RGB, rendering to @ref GL::TextureFormat::Luminance is not supported in most cases. @@ -102,10 +97,54 @@ http://www.valvesoftware.com/publications/2007/SIGGRAPH2007_AlphaTestedMagnifica @bug ES (and maybe GL < 3.20) implementation behaves slightly different (jaggies, visible e.g. when rendering outlined fonts) */ -#ifndef MAGNUM_TARGET_GLES -void MAGNUM_TEXTURETOOLS_EXPORT distanceField(GL::Texture2D& input, GL::Texture2D& output, const Range2Di& rectangle, Int radius, const Vector2i& imageSize = Vector2i()); -#else -void MAGNUM_TEXTURETOOLS_EXPORT distanceField(GL::Texture2D& input, GL::Texture2D& output, const Range2Di& rectangle, Int radius, const Vector2i& imageSize); +class MAGNUM_TEXTURETOOLS_EXPORT DistanceField { + public: + /** + * @brief Constructor + * @param radius Max lookup radius in the input texture + * + * Prepares the shader and other internal state for given @p radius. + */ + explicit DistanceField(UnsignedInt radius); + + ~DistanceField(); + + /** @brief Max lookup radius */ + UnsignedInt radius() const; + + /** + * @brief Calculate the distance field + * @param input Input texture + * @param output Output texture + * @param rectangle Rectangle in output texture 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(). + */ + void operator()(GL::Texture2D& input, GL::Texture2D& output, const Range2Di& rectangle, const Vector2i& imageSize + #ifndef MAGNUM_TARGET_GLES + = {} + #endif + ); + + private: + struct State; + std::unique_ptr _state; +}; + +#ifdef MAGNUM_BUILD_DEPRECATED +/** +@brief Create a signed distance field +@deprecated Deprecated due to inefficiency of its statelessness when doing + batch processing. Use the @ref DistanceField class instead. +*/ +inline CORRADE_DEPRECATED("use the DistanceField class instead") void distanceField(GL::Texture2D& input, GL::Texture2D& output, const Range2Di& rectangle, Int radius, const Vector2i& imageSize + #ifndef MAGNUM_TARGET_GLES + = Vector2i{} + #endif +) { + DistanceField{UnsignedInt(radius)}(input, output, rectangle, imageSize); +} #endif }} diff --git a/src/Magnum/TextureTools/Test/DistanceFieldGLTest.cpp b/src/Magnum/TextureTools/Test/DistanceFieldGLTest.cpp index 696d22b93..e3eb1dc0b 100644 --- a/src/Magnum/TextureTools/Test/DistanceFieldGLTest.cpp +++ b/src/Magnum/TextureTools/Test/DistanceFieldGLTest.cpp @@ -132,7 +132,12 @@ void DistanceFieldGLTest::test() { .setMagnificationFilter(GL::SamplerFilter::Nearest) .setStorage(1, outputFormat, Vector2i{64}); - TextureTools::distanceField(input, output, {{}, Vector2i{64}}, 32 + TextureTools::DistanceField distanceField{32}; + CORRADE_COMPARE(distanceField.radius(), 32); + + MAGNUM_VERIFY_NO_GL_ERROR(); + + distanceField(input, output, {{}, Vector2i{64}} #ifdef MAGNUM_TARGET_GLES , inputImage->size() #endif @@ -218,13 +223,15 @@ void DistanceFieldGLTest::benchmark() { MAGNUM_VERIFY_NO_GL_ERROR(); + TextureTools::DistanceField distanceField{32}; + /* So it doesn't spam too much */ GL::DebugOutput::setCallback(nullptr); CORRADE_BENCHMARK(5) { /* This is creating the shader from scratch every time, so no wonder it's so freaking slow */ - TextureTools::distanceField(input, output, {{}, Vector2i{64}}, 32 + distanceField(input, output, {{}, Vector2i{64}} #ifdef MAGNUM_TARGET_GLES , inputImage->size() #endif diff --git a/src/Magnum/TextureTools/distancefieldconverter.cpp b/src/Magnum/TextureTools/distancefieldconverter.cpp index 29c8a1a20..9e13bd83a 100644 --- a/src/Magnum/TextureTools/distancefieldconverter.cpp +++ b/src/Magnum/TextureTools/distancefieldconverter.cpp @@ -203,7 +203,7 @@ int DistanceFieldConverter::exec() { /* Do it */ Debug() << "Converting image of size" << image->size() << "to distance field..."; - TextureTools::distanceField(input, output, {{}, args.value("output-size")}, args.value("radius"), image->size()); + TextureTools::DistanceField{args.value("radius")}(input, output, {{}, args.value("output-size")}, image->size()); /* Save image */ Image2D result{PixelFormat::R8Unorm};