Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better numerical robustness for stroking #304

Merged
merged 2 commits into from
Oct 3, 2023
Merged

Better numerical robustness for stroking #304

merged 2 commits into from
Oct 3, 2023

Conversation

raphlinus
Copy link
Contributor

A number of fixes to improve stroke robustness.

Infinite recursion on curve fit is avoided by checking whether subdivision has hit the floating point limit. That said, the quality of curve fit is poor when the input is discontinuous (which does happen for degenerate cubics with cusps, where all points are co-linear).

Thus, in stroking, the degenerate cases are detected and drawn with special-case logic (do_linear). This uses existing drawing capabilities to draw the degenerate cubic with lines and round joins.

Finally, do_join mistakenly detected a half-turn as zero turn, as it was only considering the cross product (which vanishes).

Fixes #300

A number of fixes to improve stroke robustness.

Infinite recursion on curve fit is avoided by checking whether subdivision has hit the floating point limit. That said, the quality of curve fit is poor when the input is discontinuous (which does happen for degenerate cubics with cusps, where all points are co-linear).

Thus, in stroking, the degenerate cases are detected and drawn with special-case logic (do_linear). This uses existing drawing capabilities to draw the degenerate cubic with lines and round joins.

Finally, do_join mistakenly detected a half-turn as zero turn, as it was only considering the cross product (which vanishes).

Fixes #300
@dfrg
Copy link
Contributor

dfrg commented Sep 28, 2023

Nice! Since I already have a vello branch hacked up to integrate this, I’ll test on the sample scenes later today and then give this a proper review.

@dfrg
Copy link
Contributor

dfrg commented Sep 29, 2023

So this gets mmark to function without crashing, but the line detection code may be too aggressive. I'm getting these artifacts on tiger now:
tiger-stroke2
If I comment out the linear detection case in do_cubic, tiger looks nice but mmark crashes again :(

@raphlinus
Copy link
Contributor Author

Hmm, these are obviously false positives, but I've desk-checked the code and tried to write a repro (I originally suspected a case where one control point is coincident with an endpoint) and am not finding anything. I'll dig more deeply. Could you perhaps upload your vello branch? (doesn't have to be cleaned up in any way)

@raphlinus
Copy link
Contributor Author

I am only able to repro when I set unreasonably high tolerance values, so I am wondering if we can rule that out. The break between detection and not is happening exactly where I'd expect when I vary the tolerance. That said, I am seeing some distorted rendering when the chord and the tangent don't match, so will probably refine that (and, ideally, write a test).

@dfrg
Copy link
Contributor

dfrg commented Sep 29, 2023

Yes, I also noticed that setting tolerance too low would result in what look like inverted offsets where the curve becomes too thick. I think I’ve been testing with 0.01

@raphlinus
Copy link
Contributor Author

I'd appreciate isolated failing examples. Clearly there's more work to be done to validate this, and perhaps I should invest time to make a test suite (I can see that coming in handy when moving to GPU). But right now I just want to get something "good enough" landed.

@dfrg
Copy link
Contributor

dfrg commented Sep 29, 2023

I'll try to extract a failing case or two from tiger if I can identify them.

@raphlinus
Copy link
Contributor Author

Got it! For reference, here is the repro case:

Screenshot 2023-09-29 at 10 21 56 AM
    let curve = CubicBez::new((83.083, 120.279), (83.004, 119.572), (83.004, 119.572), (83.711, 119.886));
    let path = curve.into_path(0.1);
    let stroke_style = Stroke::new(0.039);
    let stroked = stroke(path, &stroke_style, &Default::default(), 1.0);

And I can see what's going on. Because the control arm is longer than the chord, that's being chosen as the reference chord, but the p01 cross-product is zero because p0 = p1 here.

So I was right about the coincident endpoint, it's just I didn't have the creativity to try a short chord when making test cases.

I was missing the case where the chord reference doesn't match the chord; when it does, the cross-product is clearly zero, but otherwise there are cases where the other two cross-products are zero but the chord isn't.
@raphlinus
Copy link
Contributor Author

A followup on the "distorted rendering" comment above. What I had seen was when the tolerance was set to a very high value, so was within spec. When I create a test case with tangents not matching the chord (short control handles), it renders as expected. In particular, stroking (10., 10.), (9.99, 9.99), (19.99, 9.99), (20., 10.) with a width of 1 and a tolerance of 0.1 gives the following, which is as expected:

Screenshot 2023-10-02 at 9 43 10 AM

This is a challenging case and definitely gets into the territory (as in the Nehab paper) where different renderers will give different results. The correct rendering (tolerance 0.001) is below:

Screenshot 2023-10-02 at 9 50 09 AM

And, for reference, here's what Skia does for a similar example:

Screenshot 2023-10-02 at 9 50 52 AM

So as far as I know, this code is now correct. Of course there may be edge cases still remaining, but I believe to uncover them requires

Copy link
Contributor

@dfrg dfrg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for getting this done! Haven’t gone over the math in detail but this looks solid at a glance and produces correct results in all scenes now. Let’s get it merged.

🚢

@raphlinus raphlinus merged commit 0c0fb6b into main Oct 3, 2023
14 checks passed
@raphlinus raphlinus deleted the robust_stroke branch October 3, 2023 21:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants