Chlumsky / msdfgen

Multi-channel signed distance field generator
MIT License
3.9k stars 404 forks source link

Debugging Help Request - A particular う character has artifacts, but only if scaled #135

Closed Earthmark closed 2 years ago

Earthmark commented 2 years ago

A glyph in a font is having some artifact issues, and if possible I'd like some help understanding what may be causing these artifacts, I'm happy to put in a fix if one can be found, but I would like some guidance as to what may be going wrong.

This was reported in the usage of this library in Neos, see https://github.com/Neos-Metaverse/NeosPublic/issues/621

The glyphs in question

The issue occurs with an engine that applies a scale to the curves in glyph, for this particular glyph it scales the curves to around 2x the original values. Only the scaled form of the glyph has the artifacts.

The renders were made using msdfgen -autoframe -size 256 256

Here is the shape descriptor and generated image for the unscaled glyph:

{
    10.96875, 5.1875;
        m(10.96875, 2.578125; 8.40625, 1.203125);
    4.609375, 0.734375;
        c;
    5.515625, -0.78125;
        m(9.640625, -0.171875; 12.671875, 1.796875);
    12.671875, 5.125;
        m(12.671875, 7.421875; 11, 8.734375);
    8.6875, 8.734375;
        m(6.90625, 8.734375; 5.15625, 8.25);
    4.03125, 8;
        m(3.5625, 7.90625; 2.96875, 7.796875);
    2.5, 7.765625;
        y;
    2.984375, 5.984375;
        m(3.390625, 6.140625; 3.90625, 6.34375);
    4.375, 6.46875;
        m(5.234375, 6.71875; 6.75, 7.25);
    8.5, 7.25;
        m(10.03125, 7.25; 10.96875, 6.375);
    #
}
{
    4.65625, 12.359375;
        c;
    4.421875, 10.875;
        m(6.1875, 10.5625; 9.40625, 10.265625);
    11.15625, 10.140625;
        y;
    11.40625, 11.65625;
        m(9.84375, 11.671875; 6.40625, 11.984375);
    #
}

output

And here is the scaled glyph:

{
    22.5374565125, 10.6587400436;
        m(22.5374565125, 5.29726552963; 17.2722969055, 2.47205734253);
    9.47086811066, 1.50891804695;
        c;
    11.3329372406, -1.60523188114;
        m(19.8085632324, -0.353151023388; 26.0368614197, 3.69203329086);
    26.0368614197, 10.5303211212;
        m(26.0368614197, 15.2497034073; 22.6016654968, 17.9464931488);
    17.8501796722, 17.9464931488;
        m(14.1902503967, 17.9464931488; 10.5945310593, 16.9512481689);
    8.28299713135, 16.4375743866;
        m(7.31985759735, 16.2449474335; 6.09988164902, 16.0202140808);
    5.13674211502, 15.9560050964;
        y;
    6.1319861412, 12.2960767746;
        m(6.96670627594, 12.6171226501; 8.0261592865, 13.0344829559);
    8.9892988205, 13.2913208008;
        m(10.7550535202, 13.8049945831; 13.8692035675, 14.8965520859);
    17.4649238586, 14.8965520859;
        m(20.6111774445, 14.8965520859; 22.5374565125, 13.098692894);
    #
}
{
    9.56718254089, 25.3947677612;
        c;
    9.08561325073, 22.3448295593;
        m(12.7134370804, 21.7027359009; 19.3269939423, 21.0927467346);
    22.922712326, 20.8359107971;
        y;
    23.4363861084, 23.9500617981;
        m(20.2259235382, 23.9821643829; 13.1629018784, 24.6242580414);
    #
}

output

Where the bug occurs

The problem occurs during a distanceSignCorrection pass, but I am not sure what fragment of math is presenting this error.

Before distanceSignCorrection, both forms of the glyph are inverted, and neither have artifacts:

output

In this case swapping distanceSignCorrection for negating the colors does resolve the issue, but I am worried about other glyphs that may both render as a negative and require sign correction.

Things I've looked into so far

The case in multiDistanceSignCorrection for if the median value is exactly 0.5 (the ambiguous case) seems possibly applicable here, however I think that's a red herring. At larger resolutions entire areas are flipped, and I believe that section only applies to single pixels.

I think this might be winding direction related, as it generates an inverse when rendering. As the main body of the glyph is concave I could understand some winding direction checks failing to process correctly. That would also explain why in larger resolutions some of the filled in glyph returns to un-filled-in.

It seems like some part of CubicSegment::scanlineIntersections is returning incorrect intersections, however the math is currently beyond me. I'm not quite sure where to begin in understanding this method, so I'm asking ya'll for help!

Thank you for your time, and for making the library! It's quite easy to test with.

Chlumsky commented 2 years ago

Hello. Thanks for reporting this and thanks for the sponsorship. I've noticed that the issue was reported more than a year ago, too bad you didn't let me know about it sooner. It is clearly a bug in my scanline implementation and I will get right to fixing it. In the meantime, I can suggest some workarounds.

The main one is Skia shape preprocessing. Since version 1.8, there is an option to link Skia to the library and use its Simplify function to fix up self-intersections in the vector geometry. When these are resolved, there is no need to perform the scanline pass at all, so this particular bug would no longer manifest. If you manage to build and link Skia (it is quite a big library unfortunately), defining MSDFGEN_USE_SKIA automatically changes the default configuration of the executable, and enables the resolveShapeGeometry function.

Interestingly, self-intersections are actually invalid within the TTF and OTF font formats and any font validator will reject them, but since almost all text renderers are scanline-based, they don't mind the self-intersections and therefore the user doesn't either, so they keep appearing in font files.

Alternatively, if you can either get rid of self-intersections in some other way (please let me know how), or simply decide to not support such font files, there are other options to bypass the scanline pass. A less severe font geometry error is inconsistent contour winding within a single glyph. This can actually be fixed by Shape::orientContours, which I implemented as part of the Skia-based preprocessing, because Skia's AsWinding, which I believe should do the same thing, didn't work. Therefore, Skia is not required for this, and I should probably add this lighter version of preprocessing as a command line option.

Finally, if you believe all of your fonts are free of self-intersections and have consistent winding, but some fonts just have different winding than others, there is the old -guessorder option, which simply takes one distance sample outside the bounding box and determines if the entire distance field should be inverted that way.

In any case, I don't tolerate any known bugs in my software, so as I said, I will try to fix the scanline function anyway ASAP.

Chlumsky commented 2 years ago

Fortunately the fix was easier than expected. It turned out to be another numerical error in the cubic equation solver. Just reducing the TOO_LARGE_RATIO constant was enough to fix the bug, but since it had been selected rather arbitrarily, I did some further analysis of the equation solver function to determine when exactly numerical errors manifest and selected the new threshold constants more carefully. Check out the numerical-error-fix branch to see if this resolves your issue and does not cause any new ones. After some further testing I will merge it into master.

Earthmark commented 2 years ago

Thank you for the very quick turnaround, this is much more than I was expecting in terms of support!

SKIA is likely too large for the project but if the fix didn't resolve it I could certainly look into doing LTO or such to cut down the size. Thank you for the suggested calls and sections to resolve the issue.

That being said, the provided branch does appear to resolve the issue. I'll do testing on my end and report if any glyphs have related rendering artifacts.

Would you like some additional sign off on my end for the fix? I can vouch that in Neos the provided branch does resolve the character glyph, but is there some threshold of coverage you would like to see?

Chlumsky commented 2 years ago

I will be running my own tests, which I have plenty of, so you don't have to go overboard. Just close the issue when you think the fix resolved it and didn't break anything else.

Earthmark commented 2 years ago

This appears resolved, thank you and I hope you have a good one.