From 0e68504f44ad156b8e73773d056a98c8dfdc61db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viktor=20Chlumsk=C3=BD?= Date: Thu, 9 Feb 2017 22:44:37 +0100 Subject: [PATCH] V1.4: Overlapping contours, cubic distance fix, guessorder --- .gitignore | 12 ++ Msdfgen.sln | 6 - Msdfgen.vcxproj | 3 +- README.md | 17 +- example.bat => bin/example.bat | 0 {lib => bin}/freetype6.dll | Bin {lib => bin}/zlib1.dll | Bin core/Contour.cpp | 32 +++ core/Contour.h | 2 + core/arithmetics.hpp | 6 + core/edge-segments.cpp | 67 ++---- core/equation-solver.h | 3 - core/msdfgen.cpp | 376 ++++++++++++++++++++++++++++----- ext/import-svg.cpp | 44 ++-- main.cpp | 70 ++++-- msdfgen-ext.h | 4 +- msdfgen.h | 11 +- 17 files changed, 493 insertions(+), 160 deletions(-) create mode 100644 .gitignore rename example.bat => bin/example.bat (100%) rename {lib => bin}/freetype6.dll (100%) rename {lib => bin}/zlib1.dll (100%) diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..a696745c --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +Debug/ +Release/ +*.exe +*.user +*.sdf +*.pdb +*.ipdb +*.iobj +*.suo +*.VC.opendb +output.png +render.png diff --git a/Msdfgen.sln b/Msdfgen.sln index d449a761..92ee98d6 100644 --- a/Msdfgen.sln +++ b/Msdfgen.sln @@ -7,18 +7,12 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Msdfgen", "Msdfgen.vcxproj" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|x64 = Debug|x64 Debug|x86 = Debug|x86 - Release|x64 = Release|x64 Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {84BE2D91-F071-4151-BE12-61460464C494}.Debug|x64.ActiveCfg = Debug|x64 - {84BE2D91-F071-4151-BE12-61460464C494}.Debug|x64.Build.0 = Debug|x64 {84BE2D91-F071-4151-BE12-61460464C494}.Debug|x86.ActiveCfg = Debug|Win32 {84BE2D91-F071-4151-BE12-61460464C494}.Debug|x86.Build.0 = Debug|Win32 - {84BE2D91-F071-4151-BE12-61460464C494}.Release|x64.ActiveCfg = Release|x64 - {84BE2D91-F071-4151-BE12-61460464C494}.Release|x64.Build.0 = Release|x64 {84BE2D91-F071-4151-BE12-61460464C494}.Release|x86.ActiveCfg = Release|Win32 {84BE2D91-F071-4151-BE12-61460464C494}.Release|x86.Build.0 = Release|Win32 EndGlobalSection diff --git a/Msdfgen.vcxproj b/Msdfgen.vcxproj index 2b5d61b9..ce77c2f5 100644 --- a/Msdfgen.vcxproj +++ b/Msdfgen.vcxproj @@ -73,7 +73,7 @@ msdfgen - $(SolutionDir)\ + $(SolutionDir)\bin\ @@ -112,6 +112,7 @@ true Console lib;%(AdditionalLibraryDirectories) + No diff --git a/README.md b/README.md index 7019c905..cf6a860b 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,17 @@ The following comparison demonstrates the improvement in image quality. ![demo-sdf16](https://cloud.githubusercontent.com/assets/18639794/14770360/20c51156-0a70-11e6-8f03-ed7632d07997.png) ![demo-sdf32](https://cloud.githubusercontent.com/assets/18639794/14770361/251a4406-0a70-11e6-95a7-e30e235ac729.png) +## New in version 1.4 + - The procedure of how contours are combined together has been reworked, and now supports overlapping contours, + which are often present in fonts with auto-generated accented glyphs. Since this is a major change to the core algorithm, + the original versions of all functions in [msdfgen.h](msdfgen.h) have been preserved with `_legacy` suffix, + and can be enabled in the command line tool with **-legacy** switch. + - A major bug has been fixed in the evaluation of signed distance of cubic curves, in which at least one of the control points + lies at the endpoint. If you use an older version, you should update now. + - In the standalone program, the orientation of the input is now being automatically detected by sampling the signed distance + at an arbitrary point outside the shape's bounding box, and the output adjusted accordingly. This can be disabled + by new option **-keeporder** or the pre-existing **-reverseorder**. + ## Getting started The project can be used either as a library or as a console program. is divided into two parts, **[core](core)** @@ -86,7 +97,7 @@ in order to generate a distance field. Please note that all classes and function - Acquire a `Shape` object. You can either load it via `loadGlyph` or `loadSvgShape`, or construct it manually. It consists of closed contours, which in turn consist of edges. An edge is represented by a `LinearEdge`, `QuadraticEdge`, - or `CubicEdge`. You can construct them from two endpoints and 0 to 2 Bézier control points. + or `CubicEdge`. You can construct them from two endpoints and 0 to 2 BĂ©zier control points. - Normalize the shape using its `normalize` method and assign colors to edges if you need a multi-channel SDF. This can be performed automatically using the `edgeColoringSimple` heuristic, or manually by setting each edge's `color` member. Keep in mind that at least two color channels must be turned on in each edge, and iff two edges meet @@ -163,7 +174,7 @@ The text shape description has the following syntax. - Points in a contour are separated with semicolons. - The last point of each contour must be equal to the first, or the symbol `#` can be used, which represents the first point. - There can be an edge segment specification between any two points, also separated by semicolons. - This can include the edge's color (`c`, `m`, `y` or `w`) and/or one or two Bézier curve control points inside parentheses. + This can include the edge's color (`c`, `m`, `y` or `w`) and/or one or two BĂ©zier curve control points inside parentheses. For example, ``` @@ -173,4 +184,4 @@ would represent a square with magenta and yellow edges, ``` { 0, 1; (+1.6, -0.8; -1.6, -0.8); # } ``` -is a teardrop shape formed by a single cubic Bézier curve. +is a teardrop shape formed by a single cubic BĂ©zier curve. diff --git a/example.bat b/bin/example.bat similarity index 100% rename from example.bat rename to bin/example.bat diff --git a/lib/freetype6.dll b/bin/freetype6.dll similarity index 100% rename from lib/freetype6.dll rename to bin/freetype6.dll diff --git a/lib/zlib1.dll b/bin/zlib1.dll similarity index 100% rename from lib/zlib1.dll rename to bin/zlib1.dll diff --git a/core/Contour.cpp b/core/Contour.cpp index f34736fb..87b755a2 100644 --- a/core/Contour.cpp +++ b/core/Contour.cpp @@ -1,8 +1,14 @@ #include "Contour.h" +#include "arithmetics.hpp" + namespace msdfgen { +static double shoelace(const Point2 &a, const Point2 &b) { + return (b.x-a.x)*(a.y+b.y); +} + void Contour::addEdge(const EdgeHolder &edge) { edges.push_back(edge); } @@ -23,4 +29,30 @@ void Contour::bounds(double &l, double &b, double &r, double &t) const { (*edge)->bounds(l, b, r, t); } +int Contour::winding() const { + if (edges.empty()) + return 0; + double total = 0; + if (edges.size() == 1) { + Point2 a = edges[0]->point(0), b = edges[0]->point(1/3.), c = edges[0]->point(2/3.); + total += shoelace(a, b); + total += shoelace(b, c); + total += shoelace(c, a); + } else if (edges.size() == 2) { + Point2 a = edges[0]->point(0), b = edges[0]->point(.5), c = edges[1]->point(0), d = edges[1]->point(.5); + total += shoelace(a, b); + total += shoelace(b, c); + total += shoelace(c, d); + total += shoelace(d, a); + } else { + Point2 prev = edges[edges.size()-1]->point(0); + for (std::vector::const_iterator edge = edges.begin(); edge != edges.end(); ++edge) { + Point2 cur = (*edge)->point(0); + total += shoelace(prev, cur); + prev = cur; + } + } + return sign(total); +} + } diff --git a/core/Contour.h b/core/Contour.h index ba7e7e66..09917887 100644 --- a/core/Contour.h +++ b/core/Contour.h @@ -22,6 +22,8 @@ class Contour { EdgeHolder & addEdge(); /// Computes the bounding box of the contour. void bounds(double &l, double &b, double &r, double &t) const; + /// Computes the winding of the contour. Returns 1 if positive, -1 if negative. + int winding() const; }; diff --git a/core/arithmetics.hpp b/core/arithmetics.hpp index 8ec62f1b..78c21d65 100644 --- a/core/arithmetics.hpp +++ b/core/arithmetics.hpp @@ -48,6 +48,12 @@ inline T clamp(T n, T a, T b) { return n >= a && n <= b ? n : n < a ? a : b; } +/// Returns 1 for positive values, -1 for negative values, and 0 for zero. +template +inline int sign(T n) { + return (T(0) < n)-(n < T(0)); +} + /// Returns 1 for non-negative values and -1 for negative values. template inline int nonZeroSign(T n) { diff --git a/core/edge-segments.cpp b/core/edge-segments.cpp index eb21c0e2..b9fd166f 100644 --- a/core/edge-segments.cpp +++ b/core/edge-segments.cpp @@ -38,6 +38,8 @@ LinearSegment::LinearSegment(Point2 p0, Point2 p1, EdgeColor edgeColor) : EdgeSe } QuadraticSegment::QuadraticSegment(Point2 p0, Point2 p1, Point2 p2, EdgeColor edgeColor) : EdgeSegment(edgeColor) { + if (p1 == p0 || p1 == p2) + p1 = 0.5*(p0+p2); p[0] = p0; p[1] = p1; p[2] = p2; @@ -84,7 +86,12 @@ Vector2 QuadraticSegment::direction(double param) const { } Vector2 CubicSegment::direction(double param) const { - return mix(mix(p[1]-p[0], p[2]-p[1], param), mix(p[2]-p[1], p[3]-p[2], param), param); + Vector2 tangent = mix(mix(p[1]-p[0], p[2]-p[1], param), mix(p[2]-p[1], p[3]-p[2], param), param); + if (!tangent) { + if (param == 0) return p[2]-p[0]; + if (param == 1) return p[3]-p[1]; + } + return tangent; } SignedDistance LinearSegment::signedDistance(Point2 origin, double ¶m) const { @@ -146,13 +153,15 @@ SignedDistance CubicSegment::signedDistance(Point2 origin, double ¶m) const Vector2 br = p[2]-p[1]-ab; Vector2 as = (p[3]-p[2])-(p[2]-p[1])-br; - double minDistance = nonZeroSign(crossProduct(ab, qa))*qa.length(); // distance from A - param = -dotProduct(qa, ab)/dotProduct(ab, ab); + Vector2 epDir = direction(0); + double minDistance = nonZeroSign(crossProduct(epDir, qa))*qa.length(); // distance from A + param = -dotProduct(qa, epDir)/dotProduct(epDir, epDir); { - double distance = nonZeroSign(crossProduct(p[3]-p[2], p[3]-origin))*(p[3]-origin).length(); // distance from B + epDir = direction(1); + double distance = nonZeroSign(crossProduct(epDir, p[3]-origin))*(p[3]-origin).length(); // distance from B if (fabs(distance) < fabs(minDistance)) { minDistance = distance; - param = dotProduct(origin-p[2], p[3]-p[2])/dotProduct(p[3]-p[2], p[3]-p[2]); + param = dotProduct(origin+epDir-p[3], epDir)/dotProduct(epDir, epDir); } } // Iterative minimum distance search @@ -179,55 +188,11 @@ SignedDistance CubicSegment::signedDistance(Point2 origin, double ¶m) const if (param >= 0 && param <= 1) return SignedDistance(minDistance, 0); if (param < .5) - return SignedDistance(minDistance, fabs(dotProduct(ab.normalize(), qa.normalize()))); + return SignedDistance(minDistance, fabs(dotProduct(direction(0).normalize(), qa.normalize()))); else - return SignedDistance(minDistance, fabs(dotProduct((p[3]-p[2]).normalize(), (p[3]-origin).normalize()))); + return SignedDistance(minDistance, fabs(dotProduct(direction(1).normalize(), (p[3]-origin).normalize()))); } -// Original method by solving a fifth order polynomial -/*SignedDistance CubicSegment::signedDistance(Point2 origin, double ¶m) const { - Vector2 qa = p[0]-origin; - Vector2 ab = p[1]-p[0]; - Vector2 br = p[2]-p[1]-ab; - Vector2 as = (p[3]-p[2])-(p[2]-p[1])-br; - double a = dotProduct(as, as); - double b = 5*dotProduct(br, as); - double c = 4*dotProduct(ab, as)+6*dotProduct(br, br); - double d = 9*dotProduct(ab, br)+dotProduct(qa, as); - double e = 3*dotProduct(ab, ab)+2*dotProduct(qa, br); - double f = dotProduct(qa, ab); - double t[5]; - int solutions = solveQuintic(t, a, b, c, d, e, f); - - double minDistance = nonZeroSign(crossProduct(ab, qa))*qa.length(); // distance from A - param = -dotProduct(qa, ab)/dotProduct(ab, ab); - { - double distance = nonZeroSign(crossProduct(p[3]-p[2], p[3]-origin))*(p[3]-origin).length(); // distance from B - if (fabs(distance) < fabs(minDistance)) { - minDistance = distance; - param = dotProduct(origin-p[2], p[3]-p[2])/dotProduct(p[3]-p[2], p[3]-p[2]); - } - } - for (int i = 0; i < solutions; ++i) { - if (t[i] > 0 && t[i] < 1) { - Point2 endpoint = p[0]+3*t[i]*ab+3*t[i]*t[i]*br+t[i]*t[i]*t[i]*as; - Vector2 dirVec = t[i]*t[i]*as+2*t[i]*br+ab; - double distance = nonZeroSign(crossProduct(dirVec, endpoint-origin))*(endpoint-origin).length(); - if (fabs(distance) <= fabs(minDistance)) { - minDistance = distance; - param = t[i]; - } - } - } - - if (param >= 0 && param <= 1) - return SignedDistance(minDistance, 0); - if (param < .5) - return SignedDistance(minDistance, fabs(dotProduct(ab.normalize(), qa.normalize()))); - else - return SignedDistance(minDistance, fabs(dotProduct((p[3]-p[2]).normalize(), (p[3]-origin).normalize()))); -}*/ - static void pointBounds(Point2 p, double &l, double &b, double &r, double &t) { if (p.x < l) l = p.x; if (p.y < b) b = p.y; diff --git a/core/equation-solver.h b/core/equation-solver.h index 6ebabe0d..bae097b2 100644 --- a/core/equation-solver.h +++ b/core/equation-solver.h @@ -9,7 +9,4 @@ int solveQuadratic(double x[2], double a, double b, double c); // ax^3 + bx^2 + cx + d = 0 int solveCubic(double x[3], double a, double b, double c, double d); -// ax^5 + bx^4 + cx^3 + dx^2 + ex + f = 0 -//int solveQuintic(double x[5], double a, double b, double c, double d, double e, double f); - } diff --git a/core/msdfgen.cpp b/core/msdfgen.cpp index 0483c30f..2d9d0df8 100644 --- a/core/msdfgen.cpp +++ b/core/msdfgen.cpp @@ -5,56 +5,10 @@ namespace msdfgen { -void generateSDF(Bitmap &output, const Shape &shape, double range, const Vector2 &scale, const Vector2 &translate) { - int w = output.width(), h = output.height(); -#ifdef MSDFGEN_USE_OPENMP - #pragma omp parallel for -#endif - for (int y = 0; y < h; ++y) { - int row = shape.inverseYAxis ? h-y-1 : y; - for (int x = 0; x < w; ++x) { - double dummy; - Point2 p = Vector2(x+.5, y+.5)/scale-translate; - SignedDistance minDistance; - for (std::vector::const_iterator contour = shape.contours.begin(); contour != shape.contours.end(); ++contour) - for (std::vector::const_iterator edge = contour->edges.begin(); edge != contour->edges.end(); ++edge) { - SignedDistance distance = (*edge)->signedDistance(p, dummy); - if (distance < minDistance) - minDistance = distance; - } - output(x, row) = float(minDistance.distance/range+.5); - } - } -} - -void generatePseudoSDF(Bitmap &output, const Shape &shape, double range, const Vector2 &scale, const Vector2 &translate) { - int w = output.width(), h = output.height(); -#ifdef MSDFGEN_USE_OPENMP - #pragma omp parallel for -#endif - for (int y = 0; y < h; ++y) { - int row = shape.inverseYAxis ? h-y-1 : y; - for (int x = 0; x < w; ++x) { - Point2 p = Vector2(x+.5, y+.5)/scale-translate; - SignedDistance minDistance; - const EdgeHolder *nearEdge = NULL; - double nearParam = 0; - for (std::vector::const_iterator contour = shape.contours.begin(); contour != shape.contours.end(); ++contour) - for (std::vector::const_iterator edge = contour->edges.begin(); edge != contour->edges.end(); ++edge) { - double param; - SignedDistance distance = (*edge)->signedDistance(p, param); - if (distance < minDistance) { - minDistance = distance; - nearEdge = &*edge; - nearParam = param; - } - } - if (nearEdge) - (*nearEdge)->distanceToPseudoDistance(minDistance, p, nearParam); - output(x, row) = float(minDistance.distance/range+.5); - } - } -} +struct MultiDistance { + double r, g, b; + double med; +}; static inline bool pixelClash(const FloatRGB &a, const FloatRGB &b, double threshold) { // Only consider pair where both are on the inside or both are on the outside @@ -108,7 +62,329 @@ void msdfErrorCorrection(Bitmap &output, const Vector2 &threshold) { } } +void generateSDF(Bitmap &output, const Shape &shape, double range, const Vector2 &scale, const Vector2 &translate) { + int contourCount = shape.contours.size(); + int w = output.width(), h = output.height(); + std::vector windings; + windings.reserve(contourCount); + for (std::vector::const_iterator contour = shape.contours.begin(); contour != shape.contours.end(); ++contour) + windings.push_back(contour->winding()); + +#ifdef MSDFGEN_USE_OPENMP + #pragma omp parallel +#endif + { + std::vector contourSD; + contourSD.resize(contourCount); +#ifdef MSDFGEN_USE_OPENMP + #pragma omp for +#endif + for (int y = 0; y < h; ++y) { + int row = shape.inverseYAxis ? h-y-1 : y; + for (int x = 0; x < w; ++x) { + double dummy; + Point2 p = Vector2(x+.5, y+.5)/scale-translate; + double negDist = -SignedDistance::INFINITE.distance; + double posDist = SignedDistance::INFINITE.distance; + int winding = 0; + + std::vector::const_iterator contour = shape.contours.begin(); + for (int i = 0; i < contourCount; ++i, ++contour) { + SignedDistance minDistance; + for (std::vector::const_iterator edge = contour->edges.begin(); edge != contour->edges.end(); ++edge) { + SignedDistance distance = (*edge)->signedDistance(p, dummy); + if (distance < minDistance) + minDistance = distance; + } + contourSD[i] = minDistance.distance; + if (windings[i] > 0 && minDistance.distance >= 0 && fabs(minDistance.distance) < fabs(posDist)) + posDist = minDistance.distance; + if (windings[i] < 0 && minDistance.distance <= 0 && fabs(minDistance.distance) < fabs(negDist)) + negDist = minDistance.distance; + } + + double sd = SignedDistance::INFINITE.distance; + if (posDist >= 0 && fabs(posDist) <= fabs(negDist)) { + sd = posDist; + winding = 1; + for (int i = 0; i < contourCount; ++i) + if (windings[i] > 0 && contourSD[i] > sd && fabs(contourSD[i]) < fabs(negDist)) + sd = contourSD[i]; + } else if (negDist <= 0 && fabs(negDist) <= fabs(posDist)) { + sd = negDist; + winding = -1; + for (int i = 0; i < contourCount; ++i) + if (windings[i] < 0 && contourSD[i] < sd && fabs(contourSD[i]) < fabs(posDist)) + sd = contourSD[i]; + } + for (int i = 0; i < contourCount; ++i) + if (windings[i] != winding && fabs(contourSD[i]) < fabs(sd)) + sd = contourSD[i]; + + output(x, row) = float(sd/range+.5); + } + } + } +} + +void generatePseudoSDF(Bitmap &output, const Shape &shape, double range, const Vector2 &scale, const Vector2 &translate) { + int contourCount = shape.contours.size(); + int w = output.width(), h = output.height(); + std::vector windings; + windings.reserve(contourCount); + for (std::vector::const_iterator contour = shape.contours.begin(); contour != shape.contours.end(); ++contour) + windings.push_back(contour->winding()); + +#ifdef MSDFGEN_USE_OPENMP + #pragma omp parallel +#endif + { + std::vector contourSD; + contourSD.resize(contourCount); +#ifdef MSDFGEN_USE_OPENMP + #pragma omp for +#endif + for (int y = 0; y < h; ++y) { + int row = shape.inverseYAxis ? h-y-1 : y; + for (int x = 0; x < w; ++x) { + Point2 p = Vector2(x+.5, y+.5)/scale-translate; + double sd = SignedDistance::INFINITE.distance; + double negDist = -SignedDistance::INFINITE.distance; + double posDist = SignedDistance::INFINITE.distance; + int winding = 0; + + std::vector::const_iterator contour = shape.contours.begin(); + for (int i = 0; i < contourCount; ++i, ++contour) { + SignedDistance minDistance; + const EdgeHolder *nearEdge = NULL; + double nearParam = 0; + for (std::vector::const_iterator edge = contour->edges.begin(); edge != contour->edges.end(); ++edge) { + double param; + SignedDistance distance = (*edge)->signedDistance(p, param); + if (distance < minDistance) { + minDistance = distance; + nearEdge = &*edge; + nearParam = param; + } + } + if (fabs(minDistance.distance) < fabs(sd)) { + sd = minDistance.distance; + winding = -windings[i]; + } + if (nearEdge) + (*nearEdge)->distanceToPseudoDistance(minDistance, p, nearParam); + contourSD[i] = minDistance.distance; + if (windings[i] > 0 && minDistance.distance >= 0 && fabs(minDistance.distance) < fabs(posDist)) + posDist = minDistance.distance; + if (windings[i] < 0 && minDistance.distance <= 0 && fabs(minDistance.distance) < fabs(negDist)) + negDist = minDistance.distance; + } + + double psd = SignedDistance::INFINITE.distance; + if (posDist >= 0 && fabs(posDist) <= fabs(negDist)) { + psd = posDist; + winding = 1; + for (int i = 0; i < contourCount; ++i) + if (windings[i] > 0 && contourSD[i] > psd && fabs(contourSD[i]) < fabs(negDist)) + psd = contourSD[i]; + } else if (negDist <= 0 && fabs(negDist) <= fabs(posDist)) { + psd = negDist; + winding = -1; + for (int i = 0; i < contourCount; ++i) + if (windings[i] < 0 && contourSD[i] < psd && fabs(contourSD[i]) < fabs(posDist)) + psd = contourSD[i]; + } + for (int i = 0; i < contourCount; ++i) + if (windings[i] != winding && fabs(contourSD[i]) < fabs(psd)) + psd = contourSD[i]; + + output(x, row) = float(psd/range+.5); + } + } + } +} + void generateMSDF(Bitmap &output, const Shape &shape, double range, const Vector2 &scale, const Vector2 &translate, double edgeThreshold) { + int contourCount = shape.contours.size(); + int w = output.width(), h = output.height(); + std::vector windings; + windings.reserve(contourCount); + for (std::vector::const_iterator contour = shape.contours.begin(); contour != shape.contours.end(); ++contour) + windings.push_back(contour->winding()); + +#ifdef MSDFGEN_USE_OPENMP + #pragma omp parallel +#endif + { + std::vector contourSD; + contourSD.resize(contourCount); +#ifdef MSDFGEN_USE_OPENMP + #pragma omp for +#endif + for (int y = 0; y < h; ++y) { + int row = shape.inverseYAxis ? h-y-1 : y; + for (int x = 0; x < w; ++x) { + Point2 p = Vector2(x+.5, y+.5)/scale-translate; + + struct EdgePoint { + SignedDistance minDistance; + const EdgeHolder *nearEdge; + double nearParam; + } sr, sg, sb; + sr.nearEdge = sg.nearEdge = sb.nearEdge = NULL; + sr.nearParam = sg.nearParam = sb.nearParam = 0; + double d = fabs(SignedDistance::INFINITE.distance); + double negDist = -SignedDistance::INFINITE.distance; + double posDist = SignedDistance::INFINITE.distance; + int winding = 0; + + std::vector::const_iterator contour = shape.contours.begin(); + for (int i = 0; i < contourCount; ++i, ++contour) { + EdgePoint r, g, b; + r.nearEdge = g.nearEdge = b.nearEdge = NULL; + r.nearParam = g.nearParam = b.nearParam = 0; + + for (std::vector::const_iterator edge = contour->edges.begin(); edge != contour->edges.end(); ++edge) { + double param; + SignedDistance distance = (*edge)->signedDistance(p, param); + if ((*edge)->color&RED && distance < r.minDistance) { + r.minDistance = distance; + r.nearEdge = &*edge; + r.nearParam = param; + } + if ((*edge)->color&GREEN && distance < g.minDistance) { + g.minDistance = distance; + g.nearEdge = &*edge; + g.nearParam = param; + } + if ((*edge)->color&BLUE && distance < b.minDistance) { + b.minDistance = distance; + b.nearEdge = &*edge; + b.nearParam = param; + } + } + if (r.minDistance < sr.minDistance) + sr = r; + if (g.minDistance < sg.minDistance) + sg = g; + if (b.minDistance < sb.minDistance) + sb = b; + + double medMinDistance = fabs(median(r.minDistance.distance, g.minDistance.distance, b.minDistance.distance)); + if (medMinDistance < d) { + d = medMinDistance; + winding = -windings[i]; + } + if (r.nearEdge) + (*r.nearEdge)->distanceToPseudoDistance(r.minDistance, p, r.nearParam); + if (g.nearEdge) + (*g.nearEdge)->distanceToPseudoDistance(g.minDistance, p, g.nearParam); + if (b.nearEdge) + (*b.nearEdge)->distanceToPseudoDistance(b.minDistance, p, b.nearParam); + medMinDistance = median(r.minDistance.distance, g.minDistance.distance, b.minDistance.distance); + contourSD[i].r = r.minDistance.distance; + contourSD[i].g = g.minDistance.distance; + contourSD[i].b = b.minDistance.distance; + contourSD[i].med = medMinDistance; + if (windings[i] > 0 && medMinDistance >= 0 && fabs(medMinDistance) < fabs(posDist)) + posDist = medMinDistance; + if (windings[i] < 0 && medMinDistance <= 0 && fabs(medMinDistance) < fabs(negDist)) + negDist = medMinDistance; + } + if (sr.nearEdge) + (*sr.nearEdge)->distanceToPseudoDistance(sr.minDistance, p, sr.nearParam); + if (sg.nearEdge) + (*sg.nearEdge)->distanceToPseudoDistance(sg.minDistance, p, sg.nearParam); + if (sb.nearEdge) + (*sb.nearEdge)->distanceToPseudoDistance(sb.minDistance, p, sb.nearParam); + + MultiDistance msd; + msd.r = msd.g = msd.b = msd.med = SignedDistance::INFINITE.distance; + if (posDist >= 0 && fabs(posDist) <= fabs(negDist)) { + msd.med = SignedDistance::INFINITE.distance; + winding = 1; + for (int i = 0; i < contourCount; ++i) + if (windings[i] > 0 && contourSD[i].med > msd.med && fabs(contourSD[i].med) < fabs(negDist)) + msd = contourSD[i]; + } else if (negDist <= 0 && fabs(negDist) <= fabs(posDist)) { + msd.med = -SignedDistance::INFINITE.distance; + winding = -1; + for (int i = 0; i < contourCount; ++i) + if (windings[i] < 0 && contourSD[i].med < msd.med && fabs(contourSD[i].med) < fabs(posDist)) + msd = contourSD[i]; + } + for (int i = 0; i < contourCount; ++i) + if (windings[i] != winding && fabs(contourSD[i].med) < fabs(msd.med)) + msd = contourSD[i]; + if (median(sr.minDistance.distance, sg.minDistance.distance, sb.minDistance.distance) == msd.med) { + msd.r = sr.minDistance.distance; + msd.g = sg.minDistance.distance; + msd.b = sb.minDistance.distance; + } + + output(x, row).r = float(msd.r/range+.5); + output(x, row).g = float(msd.g/range+.5); + output(x, row).b = float(msd.b/range+.5); + } + } + } + + if (edgeThreshold > 0) + msdfErrorCorrection(output, edgeThreshold/(scale*range)); +} + +void generateSDF_legacy(Bitmap &output, const Shape &shape, double range, const Vector2 &scale, const Vector2 &translate) { + int w = output.width(), h = output.height(); +#ifdef MSDFGEN_USE_OPENMP + #pragma omp parallel for +#endif + for (int y = 0; y < h; ++y) { + int row = shape.inverseYAxis ? h-y-1 : y; + for (int x = 0; x < w; ++x) { + double dummy; + Point2 p = Vector2(x+.5, y+.5)/scale-translate; + SignedDistance minDistance; + for (std::vector::const_iterator contour = shape.contours.begin(); contour != shape.contours.end(); ++contour) + for (std::vector::const_iterator edge = contour->edges.begin(); edge != contour->edges.end(); ++edge) { + SignedDistance distance = (*edge)->signedDistance(p, dummy); + if (distance < minDistance) + minDistance = distance; + } + output(x, row) = float(minDistance.distance/range+.5); + } + } +} + +void generatePseudoSDF_legacy(Bitmap &output, const Shape &shape, double range, const Vector2 &scale, const Vector2 &translate) { + int w = output.width(), h = output.height(); +#ifdef MSDFGEN_USE_OPENMP + #pragma omp parallel for +#endif + for (int y = 0; y < h; ++y) { + int row = shape.inverseYAxis ? h-y-1 : y; + for (int x = 0; x < w; ++x) { + Point2 p = Vector2(x+.5, y+.5)/scale-translate; + SignedDistance minDistance; + const EdgeHolder *nearEdge = NULL; + double nearParam = 0; + for (std::vector::const_iterator contour = shape.contours.begin(); contour != shape.contours.end(); ++contour) + for (std::vector::const_iterator edge = contour->edges.begin(); edge != contour->edges.end(); ++edge) { + double param; + SignedDistance distance = (*edge)->signedDistance(p, param); + if (distance < minDistance) { + minDistance = distance; + nearEdge = &*edge; + nearParam = param; + } + } + if (nearEdge) + (*nearEdge)->distanceToPseudoDistance(minDistance, p, nearParam); + output(x, row) = float(minDistance.distance/range+.5); + } + } +} + +void generateMSDF_legacy(Bitmap &output, const Shape &shape, double range, const Vector2 &scale, const Vector2 &translate, double edgeThreshold) { int w = output.width(), h = output.height(); #ifdef MSDFGEN_USE_OPENMP #pragma omp parallel for diff --git a/ext/import-svg.cpp b/ext/import-svg.cpp index 37d23368..6ae31341 100644 --- a/ext/import-svg.cpp +++ b/ext/import-svg.cpp @@ -53,12 +53,10 @@ static bool readDouble(double &output, const char *&pathDef) { } static void consumeOptionalComma(const char *&pathDef) { - while (*pathDef == ' ') { + while (*pathDef == ' ') ++pathDef; - } - if (*pathDef == ',') { + if (*pathDef == ',') ++pathDef; - } } static bool buildFromPath(Shape &shape, const char *pathDef) { @@ -74,48 +72,38 @@ static bool buildFromPath(Shape &shape, const char *pathDef) { while (true) { switch (nodeType) { - case 'M': - case 'm': + case 'M': case 'm': REQUIRE(contourStart); REQUIRE(readCoord(node, pathDef)); - if (nodeType == 'm') { + if (nodeType == 'm') node += prevNode; - } startPoint = node; --nodeType; // to 'L' or 'l' break; - case 'Z': - case 'z': + case 'Z': case 'z': if (prevNode != startPoint) contour.addEdge(new LinearSegment(prevNode, startPoint)); prevNode = startPoint; goto NEXT_CONTOUR; - case 'L': - case 'l': + case 'L': case 'l': REQUIRE(readCoord(node, pathDef)); - if (nodeType == 'l') { + if (nodeType == 'l') node += prevNode; - } contour.addEdge(new LinearSegment(prevNode, node)); break; - case 'H': - case 'h': + case 'H': case 'h': REQUIRE(readDouble(node.x, pathDef)); - if (nodeType == 'h') { + if (nodeType == 'h') node.x += prevNode.x; - } contour.addEdge(new LinearSegment(prevNode, node)); break; - case 'V': - case 'v': + case 'V': case 'v': REQUIRE(readDouble(node.y, pathDef)); - if (nodeType == 'v') { + if (nodeType == 'v') node.y += prevNode.y; - } contour.addEdge(new LinearSegment(prevNode, node)); break; - case 'Q': - case 'q': + case 'Q': case 'q': REQUIRE(readCoord(controlPoint[0], pathDef)); consumeOptionalComma(pathDef); REQUIRE(readCoord(node, pathDef)); @@ -126,8 +114,7 @@ static bool buildFromPath(Shape &shape, const char *pathDef) { contour.addEdge(new QuadraticSegment(prevNode, controlPoint[0], node)); break; // TODO T, t - case 'C': - case 'c': + case 'C': case 'c': REQUIRE(readCoord(controlPoint[0], pathDef)); consumeOptionalComma(pathDef); REQUIRE(readCoord(controlPoint[1], pathDef)); @@ -140,9 +127,8 @@ static bool buildFromPath(Shape &shape, const char *pathDef) { } contour.addEdge(new CubicSegment(prevNode, controlPoint[0], controlPoint[1], node)); break; - case 'S': - case 's': - controlPoint[0] = node * 2 - controlPoint[1]; + case 'S': case 's': + controlPoint[0] = node+node-controlPoint[1]; REQUIRE(readCoord(controlPoint[1], pathDef)); consumeOptionalComma(pathDef); REQUIRE(readCoord(node, pathDef)); diff --git a/main.cpp b/main.cpp index b892418b..bb8e1e4c 100644 --- a/main.cpp +++ b/main.cpp @@ -1,8 +1,8 @@ /* - * MULTI-CHANNEL SIGNED DISTANCE FIELD GENERATOR v1.3 (2016-12-07) - standalone console program + * MULTI-CHANNEL SIGNED DISTANCE FIELD GENERATOR v1.4 (2017-02-09) - standalone console program * -------------------------------------------------------------------------------------------- - * A utility by Viktor Chlumsky, (c) 2014 - 2016 + * A utility by Viktor Chlumsky, (c) 2014 - 2017 * */ @@ -300,6 +300,10 @@ static const char *helpText = "\tSpecifies the output format of the distance field. Otherwise it is chosen based on output file extension.\n" " -help\n" "\tDisplays this help.\n" + " -keeporder\n" + "\tDisables the detection of shape orientation and keeps it as is.\n" + " -legacy\n" + "\tUses the original (legacy) distance field algorithms.\n" " -o \n" "\tSets the output file name. The default value is \"output.png\".\n" " -printmetrics\n" @@ -320,12 +324,12 @@ static const char *helpText = "\tRenders an image preview without flattening the color channels.\n" " -translate \n" "\tSets the translation of the shape in shape units.\n" - " -yflip\n" - "\tInverts the Y axis in the output distance field. The default order is bottom to top.\n" " -reverseorder\n" - "\tReverses the order of the points in each contour.\n" + "\tDisables the detection of shape orientation and reverses the order of its vertices.\n" " -seed \n" "\tSets the random seed for edge coloring heuristic.\n" + " -yflip\n" + "\tInverts the Y axis in the output distance field. The default order is bottom to top.\n" "\n"; int main(int argc, const char * const *argv) { @@ -346,6 +350,7 @@ int main(int argc, const char * const *argv) { MULTI, METRICS } mode = MULTI; + bool legacyMode = false; Format format = AUTO; const char *input = NULL; const char *output = "output.png"; @@ -375,7 +380,11 @@ int main(int argc, const char * const *argv) { bool yFlip = false; bool printMetrics = false; bool skipColoring = false; - bool reverseOrder = false; + enum { + KEEP, + REVERSE, + GUESS + } orientation = GUESS; unsigned long long coloringSeed = 0; int argPos = 1; @@ -433,6 +442,11 @@ int main(int argc, const char * const *argv) { argPos += 1; continue; } + ARG_CASE("-legacy", 0) { + legacyMode = true; + argPos += 1; + continue; + } ARG_CASE("-format", 1) { if (!strcmp(argv[argPos+1], "auto")) format = AUTO; else if (!strcmp(argv[argPos+1], "png")) SETFORMAT(PNG, "png"); @@ -566,8 +580,18 @@ int main(int argc, const char * const *argv) { argPos += 1; continue; } + ARG_CASE("-keeporder", 0) { + orientation = KEEP; + argPos += 1; + continue; + } ARG_CASE("-reverseorder", 0) { - reverseOrder = true; + orientation = REVERSE; + argPos += 1; + continue; + } + ARG_CASE("-guessorder", 0) { + orientation = GUESS; argPos += 1; continue; } @@ -653,7 +677,7 @@ int main(int argc, const char * const *argv) { } bounds = { LARGE_VALUE, LARGE_VALUE, -LARGE_VALUE, -LARGE_VALUE }; - if (autoFrame || mode == METRICS || printMetrics) + if (autoFrame || mode == METRICS || printMetrics || orientation == GUESS) shape.bounds(bounds.l, bounds.b, bounds.r, bounds.t); // Auto-frame @@ -719,12 +743,18 @@ int main(int argc, const char * const *argv) { switch (mode) { case SINGLE: { sdf = Bitmap(width, height); - generateSDF(sdf, shape, range, scale, translate); + if (legacyMode) + generateSDF_legacy(sdf, shape, range, scale, translate); + else + generateSDF(sdf, shape, range, scale, translate); break; } case PSEUDO: { sdf = Bitmap(width, height); - generatePseudoSDF(sdf, shape, range, scale, translate); + if (legacyMode) + generatePseudoSDF_legacy(sdf, shape, range, scale, translate); + else + generatePseudoSDF(sdf, shape, range, scale, translate); break; } case MULTI: { @@ -733,14 +763,30 @@ int main(int argc, const char * const *argv) { if (edgeAssignment) parseColoring(shape, edgeAssignment); msdf = Bitmap(width, height); - generateMSDF(msdf, shape, range, scale, translate, edgeThreshold); + if (legacyMode) + generateMSDF_legacy(msdf, shape, range, scale, translate, edgeThreshold); + else + generateMSDF(msdf, shape, range, scale, translate, edgeThreshold); break; } default: break; } - if (reverseOrder) { + if (orientation == GUESS) { + // Get sign of signed distance outside bounds + Point2 p(bounds.l-(bounds.r-bounds.l)-1, bounds.b-(bounds.t-bounds.b)-1); + double dummy; + SignedDistance minDistance; + for (std::vector::const_iterator contour = shape.contours.begin(); contour != shape.contours.end(); ++contour) + for (std::vector::const_iterator edge = contour->edges.begin(); edge != contour->edges.end(); ++edge) { + SignedDistance distance = (*edge)->signedDistance(p, dummy); + if (distance < minDistance) + minDistance = distance; + } + orientation = minDistance.distance <= 0 ? KEEP : REVERSE; + } + if (orientation == REVERSE) { invertColor(sdf); invertColor(msdf); } diff --git a/msdfgen-ext.h b/msdfgen-ext.h index f999e2ec..3b1f82ce 100644 --- a/msdfgen-ext.h +++ b/msdfgen-ext.h @@ -2,9 +2,9 @@ #pragma once /* - * MULTI-CHANNEL SIGNED DISTANCE FIELD GENERATOR v1.1 (2016-05-08) - extensions + * MULTI-CHANNEL SIGNED DISTANCE FIELD GENERATOR v1.4 (2017-02-09) - extensions * ---------------------------------------------------------------------------- - * A utility by Viktor Chlumsky, (c) 2014 - 2016 + * A utility by Viktor Chlumsky, (c) 2014 - 2017 * * The extension module provides ways to easily load input and save output using popular formats. * diff --git a/msdfgen.h b/msdfgen.h index d7b22e53..a80dde3d 100644 --- a/msdfgen.h +++ b/msdfgen.h @@ -2,9 +2,9 @@ #pragma once /* - * MULTI-CHANNEL SIGNED DISTANCE FIELD GENERATOR v1.3 (2016-12-07) + * MULTI-CHANNEL SIGNED DISTANCE FIELD GENERATOR v1.4 (2017-02-09) * --------------------------------------------------------------- - * A utility by Viktor Chlumsky, (c) 2014 - 2016 + * A utility by Viktor Chlumsky, (c) 2014 - 2017 * * The technique used to generate multi-channel distance fields in this code * has been developed by Viktor Chlumsky in 2014 for his master's thesis, @@ -24,7 +24,7 @@ #include "core/save-bmp.h" #include "core/shape-description.h" -#define MSDFGEN_VERSION "1.3" +#define MSDFGEN_VERSION "1.4" namespace msdfgen { @@ -37,4 +37,9 @@ void generatePseudoSDF(Bitmap &output, const Shape &shape, double range, /// Generates a multi-channel signed distance field. Edge colors must be assigned first! (see edgeColoringSimple) void generateMSDF(Bitmap &output, const Shape &shape, double range, const Vector2 &scale, const Vector2 &translate, double edgeThreshold = 1.00000001); +// Original simpler versions of the previous functions, which work well under normal circumstances, but cannot deal with overlapping contours. +void generateSDF_legacy(Bitmap &output, const Shape &shape, double range, const Vector2 &scale, const Vector2 &translate); +void generatePseudoSDF_legacy(Bitmap &output, const Shape &shape, double range, const Vector2 &scale, const Vector2 &translate); +void generateMSDF_legacy(Bitmap &output, const Shape &shape, double range, const Vector2 &scale, const Vector2 &translate, double edgeThreshold = 1.00000001); + }