-
-
Notifications
You must be signed in to change notification settings - Fork 105
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
Why splitting bezier for length calculation? #18
Comments
I think it gave slightly better results by splitting the Béziers, but not too much. It really depends on the curve, as some curves are very hard to calculate accurately. Your mileage may vary, but it should work fine without splitting. This needs better testing to see if it really improves accuracy. |
Thanks. I have also tried all kind of things to improve accuracy for the worst case curves. Someone recommended to use Romberg's method and I actually implemented it, but results were worse (by a factor 2) than Gauss quadrature. |
We're porting a huge CAD application to Vulkan and they use a lot of striped "pens", and this is what we can tell you:
|
Hi @devshgraphicsprogramming , thanks for the advice. Is this general advice or did you stumble upon a problem of accuracy / performance? So to sum up: you advice to convert cubics to quadratics and use the analytical implementation for the length that I already have implemented (https://github.com/tdewolff/canvas/blob/master/path_util.go#L412)? I don't understand point 4. And how do you mean straight from SDF, is that using OpenGL shaders? Or something built-in the software rasterizer? |
We've measured the per-pixel math for just the SDF computation to be 2x slower to perform than for 2 quad beziers that specify a similar curve + overlap. The arclength functions are also much faster if you want accurate arclength and not LG quadrature with 6 or 8 coefficients. Furthermore when dealing with beziers the SDF is distance to closest point. For a cubic the SDF is numerical root finding of 5 roots, with either mediocre accuracy or lots of ALU waste, also most root finders' convergence is sensitive to input function characteristics like the magnitude of the slope (Newton) or the slope not being 0 (Halley). Quadratic closest point only requires a bit of special care to account for when the control points are close to becoming a straight like. The landscape has changed a lot since 2000s when a lot was written about beziers and their rendering, we now have stupidly fast inverse-trig, trig and special functions, even on GPUs! Accuracy of arclength (for stipple patterns) and SDF (if you want pixel-perfect AA/coverage and feathering) becomes an issue when you zoom in a lot, especially relevant with a 4k or 8k display. Anyway because of FP32 precision and 4k displays, you'll need to clip your beziers' control points dynamically at runtime to the screen. Solving a quadratic equation for the clipping is easy and you always get 0-2 separate curve segments as the result of clipping against an axis-slab, clipping a cubic requires solving a cubic and can produce 0-3 curve segments.
Yes, basically this: https://www.youtube.com/watch?v=8l1SJAx3yoM You implementation has a numerical stability issue, you don't check for float calcArcLength_Analytic(in float t, in vec2 p0, in vec2 p1, in vec2 p2)
{
vec2 A = p0 - 2.0*p1 + p2;
vec2 B = 2.0*(p1 - p0);
vec2 C = p0;
float lenA2 = dot(A,A);
float AdotB = dot(A,B);
float a = 4.0 * lenA2;
float b = 4.0 * AdotB;
float c = dot(B,B);
float b_over_4a = AdotB/a;
float lenTan = sqrt(t*(a*t+b)+c);
float retval = 0.5f*t*lenTan;
// we skip this because when |a| -> we have += 0/0 * 0 here resulting in NaN
if (lenA2>=exp2(-23.f))
retval += b_over_4a*(lenTan-sqrt(c));
// sin2 multiplied by length of A and B
float det_over_16 = AdotB*AdotB-lenA2*c;
// because `b` is linearly dependent on `a` this will also ensure `b_over_4a` is not NaN, ergo `a` has a minimum value
if (det_over_16>=exp2(-23.f))
{
// TODO: finish
//retval += det_over_16*...;
}
return retval;
} I can ping you when the improved version is in our repository.
Well that would be point (5), point (4) is about knowing the arclength and manipulating closest points using inverse arclength per-pixel. Yes pixel-shaders, you use the SDF value at a pixel minus the half-width of the stroke to determine your pixel's alpha. This means you only construct very crude bounding polygons around the beziers. How do you stop "overblending" neighbouring quad beziers on a piecewise line like this, you may ask? We use a very weird algorithm that leverages fragment shader interlock (which AMD does not have). If someone needs to care about platforms that don't have this, they need to very diligently construct mitres on the cages, also shear them off in order to stop "exploding mitres" to infinity, also account of cases when curvature of any bezier is soo great that it exceeds the quarter-width of the stroke. We can just fall back to using an OBB in the last case and not implement mitres because the ends of our beziers can overlap without overblending. Here's a giv that shows (WIP) the bounding polygons and the IEEE754 accuracy issues if you don't handle special cases in the arclength calculation. |
I have been reading your blog and part of you code to learn more about arc length computation for cubic bezier curves and found that in cubicBezierLength() you are splitting the curve before applying the Gauss quadrature to calculate the length of each piece of curve. As I understand it this should not be necessary, because you can choose
t0
andt1
(ora
andb
in your case) to calculate the length of each piece without splitting.Is this a possible optimization for your code?
The text was updated successfully, but these errors were encountered: