V1.4: Overlapping contours, cubic distance fix, guessorder

This commit is contained in:
Viktor Chlumský 2017-02-09 22:44:37 +01:00
parent 0215653a8a
commit 0e68504f44
17 changed files with 493 additions and 160 deletions

12
.gitignore vendored Normal file
View File

@ -0,0 +1,12 @@
Debug/
Release/
*.exe
*.user
*.sdf
*.pdb
*.ipdb
*.iobj
*.suo
*.VC.opendb
output.png
render.png

View File

@ -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

View File

@ -73,7 +73,7 @@
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<TargetName>msdfgen</TargetName>
<OutDir>$(SolutionDir)\</OutDir>
<OutDir>$(SolutionDir)\bin\</OutDir>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<ClCompile>
@ -112,6 +112,7 @@
<OptimizeReferences>true</OptimizeReferences>
<SubSystem>Console</SubSystem>
<AdditionalLibraryDirectories>lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
<GenerateDebugInformation>No</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">

View File

@ -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.

View File

@ -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<EdgeHolder>::const_iterator edge = edges.begin(); edge != edges.end(); ++edge) {
Point2 cur = (*edge)->point(0);
total += shoelace(prev, cur);
prev = cur;
}
}
return sign(total);
}
}

View File

@ -22,6 +22,8 @@ public:
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;
};

View File

@ -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 <typename T>
inline int sign(T n) {
return (T(0) < n)-(n < T(0));
}
/// Returns 1 for non-negative values and -1 for negative values.
template <typename T>
inline int nonZeroSign(T n) {

View File

@ -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 &param) const {
@ -146,13 +153,15 @@ SignedDistance CubicSegment::signedDistance(Point2 origin, double &param) 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 &param) 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 &param) 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;

View File

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

View File

@ -5,56 +5,10 @@
namespace msdfgen {
void generateSDF(Bitmap<float> &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<Contour>::const_iterator contour = shape.contours.begin(); contour != shape.contours.end(); ++contour)
for (std::vector<EdgeHolder>::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<float> &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<Contour>::const_iterator contour = shape.contours.begin(); contour != shape.contours.end(); ++contour)
for (std::vector<EdgeHolder>::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<FloatRGB> &output, const Vector2 &threshold) {
}
}
void generateSDF(Bitmap<float> &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<int> windings;
windings.reserve(contourCount);
for (std::vector<Contour>::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<double> 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<Contour>::const_iterator contour = shape.contours.begin();
for (int i = 0; i < contourCount; ++i, ++contour) {
SignedDistance minDistance;
for (std::vector<EdgeHolder>::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<float> &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<int> windings;
windings.reserve(contourCount);
for (std::vector<Contour>::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<double> 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<Contour>::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<EdgeHolder>::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<FloatRGB> &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<int> windings;
windings.reserve(contourCount);
for (std::vector<Contour>::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<MultiDistance> 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<Contour>::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<EdgeHolder>::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<float> &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<Contour>::const_iterator contour = shape.contours.begin(); contour != shape.contours.end(); ++contour)
for (std::vector<EdgeHolder>::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<float> &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<Contour>::const_iterator contour = shape.contours.begin(); contour != shape.contours.end(); ++contour)
for (std::vector<EdgeHolder>::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<FloatRGB> &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

View File

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

View File

@ -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 <filename>\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 <x> <y>\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>\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<float>(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<float>(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<FloatRGB>(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<Contour>::const_iterator contour = shape.contours.begin(); contour != shape.contours.end(); ++contour)
for (std::vector<EdgeHolder>::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);
}

View File

@ -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.
*

View File

@ -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<float> &output, const Shape &shape, double range,
/// Generates a multi-channel signed distance field. Edge colors must be assigned first! (see edgeColoringSimple)
void generateMSDF(Bitmap<FloatRGB> &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<float> &output, const Shape &shape, double range, const Vector2 &scale, const Vector2 &translate);
void generatePseudoSDF_legacy(Bitmap<float> &output, const Shape &shape, double range, const Vector2 &scale, const Vector2 &translate);
void generateMSDF_legacy(Bitmap<FloatRGB> &output, const Shape &shape, double range, const Vector2 &scale, const Vector2 &translate, double edgeThreshold = 1.00000001);
}