[Impeller] Directly tessellate conics to linear path segments (#166165)

Impeller has been approximating conic segments with a pair of quadratic
segments - a simplification that only works well for simple 90 degree
circular conics, but is a poor approximation for tighter conics.

We now approximate a reasonable number of line segments to approximate
the conic with directly and directly flatten the conics into the
tessellation buffers.
This commit is contained in:
Jim Graham
2025-03-31 16:38:38 -07:00
committed by GitHub
parent 9b4164035b
commit 0a22ff9962
5 changed files with 245 additions and 12 deletions

View File

@@ -153,6 +153,160 @@ TEST_P(AiksTest, CanRenderQuadraticStrokeWithInstantTurn) {
ASSERT_TRUE(OpenPlaygroundHere(builder.Build()));
}
TEST_P(AiksTest, CanRenderFilledConicPaths) {
DisplayListBuilder builder;
builder.Scale(GetContentScale().x, GetContentScale().y);
DlPaint paint;
paint.setColor(DlColor::kRed());
paint.setDrawStyle(DlDrawStyle::kFill);
DlPaint reference_paint;
reference_paint.setColor(DlColor::kGreen());
reference_paint.setDrawStyle(DlDrawStyle::kFill);
DlPathBuilder path_builder;
DlPathBuilder reference_builder;
// weight of 1.0 is just a quadratic bezier
path_builder.MoveTo(DlPoint(100, 100));
path_builder.ConicCurveTo(DlPoint(150, 150), DlPoint(200, 100), 1.0f);
reference_builder.MoveTo(DlPoint(300, 100));
reference_builder.QuadraticCurveTo(DlPoint(350, 150), DlPoint(400, 100));
// weight of sqrt(2)/2 is a circular section
path_builder.MoveTo(DlPoint(100, 200));
path_builder.ConicCurveTo(DlPoint(150, 250), DlPoint(200, 200), kSqrt2Over2);
reference_builder.MoveTo(DlPoint(300, 200));
auto magic = DlPathBuilder::kArcApproximationMagic;
reference_builder.CubicCurveTo(DlPoint(300, 200) + DlPoint(50, 50) * magic,
DlPoint(400, 200) + DlPoint(-50, 50) * magic,
DlPoint(400, 200));
// weight of .01 is nearly a straight line
path_builder.MoveTo(DlPoint(100, 300));
path_builder.ConicCurveTo(DlPoint(150, 350), DlPoint(200, 300), 0.01f);
reference_builder.MoveTo(DlPoint(300, 300));
reference_builder.LineTo(DlPoint(350, 300.5));
reference_builder.LineTo(DlPoint(400, 300));
// weight of 100.0 is nearly a triangle
path_builder.MoveTo(DlPoint(100, 400));
path_builder.ConicCurveTo(DlPoint(150, 450), DlPoint(200, 400), 100.0f);
reference_builder.MoveTo(DlPoint(300, 400));
reference_builder.LineTo(DlPoint(350, 450));
reference_builder.LineTo(DlPoint(400, 400));
builder.DrawPath(DlPath(path_builder), paint);
builder.DrawPath(DlPath(reference_builder), reference_paint);
ASSERT_TRUE(OpenPlaygroundHere(builder.Build()));
}
TEST_P(AiksTest, CanRenderStrokedConicPaths) {
DisplayListBuilder builder;
builder.Scale(GetContentScale().x, GetContentScale().y);
DlPaint paint;
paint.setColor(DlColor::kRed());
paint.setStrokeWidth(10);
paint.setDrawStyle(DlDrawStyle::kStroke);
paint.setStrokeCap(DlStrokeCap::kRound);
paint.setStrokeJoin(DlStrokeJoin::kRound);
DlPaint reference_paint;
reference_paint.setColor(DlColor::kGreen());
reference_paint.setStrokeWidth(10);
reference_paint.setDrawStyle(DlDrawStyle::kStroke);
reference_paint.setStrokeCap(DlStrokeCap::kRound);
reference_paint.setStrokeJoin(DlStrokeJoin::kRound);
DlPathBuilder path_builder;
DlPathBuilder reference_builder;
// weight of 1.0 is just a quadratic bezier
path_builder.MoveTo(DlPoint(100, 100));
path_builder.ConicCurveTo(DlPoint(150, 150), DlPoint(200, 100), 1.0f);
reference_builder.MoveTo(DlPoint(300, 100));
reference_builder.QuadraticCurveTo(DlPoint(350, 150), DlPoint(400, 100));
// weight of sqrt(2)/2 is a circular section
path_builder.MoveTo(DlPoint(100, 200));
path_builder.ConicCurveTo(DlPoint(150, 250), DlPoint(200, 200), kSqrt2Over2);
reference_builder.MoveTo(DlPoint(300, 200));
auto magic = DlPathBuilder::kArcApproximationMagic;
reference_builder.CubicCurveTo(DlPoint(300, 200) + DlPoint(50, 50) * magic,
DlPoint(400, 200) + DlPoint(-50, 50) * magic,
DlPoint(400, 200));
// weight of .0 is a straight line
path_builder.MoveTo(DlPoint(100, 300));
path_builder.ConicCurveTo(DlPoint(150, 350), DlPoint(200, 300), 0.0f);
reference_builder.MoveTo(DlPoint(300, 300));
reference_builder.LineTo(DlPoint(400, 300));
// weight of 100.0 is nearly a triangle
path_builder.MoveTo(DlPoint(100, 400));
path_builder.ConicCurveTo(DlPoint(150, 450), DlPoint(200, 400), 100.0f);
reference_builder.MoveTo(DlPoint(300, 400));
reference_builder.LineTo(DlPoint(350, 450));
reference_builder.LineTo(DlPoint(400, 400));
builder.DrawPath(DlPath(path_builder), paint);
builder.DrawPath(DlPath(reference_builder), reference_paint);
ASSERT_TRUE(OpenPlaygroundHere(builder.Build()));
}
TEST_P(AiksTest, CanRenderTightConicPath) {
DisplayListBuilder builder;
builder.Scale(GetContentScale().x, GetContentScale().y);
DlPaint paint;
paint.setColor(DlColor::kRed());
paint.setDrawStyle(DlDrawStyle::kFill);
DlPaint reference_paint;
reference_paint.setColor(DlColor::kGreen());
reference_paint.setDrawStyle(DlDrawStyle::kFill);
DlPathBuilder path_builder;
path_builder.MoveTo(DlPoint(100, 100));
path_builder.ConicCurveTo(DlPoint(150, 450), DlPoint(200, 100), 5.0f);
DlPathBuilder reference_builder;
ConicPathComponent component(DlPoint(300, 100), //
DlPoint(350, 450), //
DlPoint(400, 100), //
5.0f);
reference_builder.MoveTo(component.p1);
constexpr int N = 100;
for (int i = 1; i < N; i++) {
reference_builder.LineTo(component.Solve(static_cast<Scalar>(i) / N));
}
reference_builder.LineTo(component.p2);
DlPaint line_paint;
line_paint.setColor(DlColor::kYellow());
line_paint.setDrawStyle(DlDrawStyle::kStroke);
line_paint.setStrokeWidth(1.0f);
// Draw some lines to provide a spacial reference for the curvature of
// the tips of the direct rendering and the manually tessellated versions.
builder.DrawLine(DlPoint(145, 100), DlPoint(145, 450), line_paint);
builder.DrawLine(DlPoint(155, 100), DlPoint(155, 450), line_paint);
builder.DrawLine(DlPoint(345, 100), DlPoint(345, 450), line_paint);
builder.DrawLine(DlPoint(355, 100), DlPoint(355, 450), line_paint);
builder.DrawLine(DlPoint(100, 392.5f), DlPoint(400, 392.5f), line_paint);
// Draw the two paths (direct and manually tessellated) on top of the lines.
builder.DrawPath(DlPath(path_builder), paint);
builder.DrawPath(DlPath(reference_builder), reference_paint);
ASSERT_TRUE(OpenPlaygroundHere(builder.Build()));
}
TEST_P(AiksTest, CanRenderDifferencePaths) {
DisplayListBuilder builder;

View File

@@ -190,9 +190,9 @@ static inline Scalar ConicSolve(Scalar t,
Scalar p2,
Scalar w) {
auto u = (1 - t);
auto coefficient_p0 = t * t;
auto coefficient_p0 = u * u;
auto coefficient_p1 = 2 * t * u * w;
auto coefficient_p2 = u * u;
auto coefficient_p2 = t * t;
return ((p0 * coefficient_p0 + p1 * coefficient_p1 + p2 * coefficient_p2) /
(coefficient_p0 + coefficient_p1 + coefficient_p2));
@@ -331,27 +331,34 @@ Point ConicPathComponent::Solve(Scalar time) const {
};
}
void ConicPathComponent::ToLinearPathComponents(Scalar scale_factor,
const PointProc& proc) const {
Scalar line_count = std::ceilf(ComputeConicSubdivisions(scale_factor, *this));
for (size_t i = 1; i < line_count; i += 1) {
proc(Solve(i / line_count));
}
proc(p2);
}
void ConicPathComponent::AppendPolylinePoints(
Scalar scale_factor,
std::vector<Point>& points) const {
for (auto quad : ToQuadraticPathComponents()) {
quad.AppendPolylinePoints(scale_factor, points);
}
ToLinearPathComponents(scale_factor, [&points](const Point& point) {
points.emplace_back(point);
});
}
void ConicPathComponent::ToLinearPathComponents(Scalar scale,
VertexWriter& writer) const {
for (auto quad : ToQuadraticPathComponents()) {
quad.ToLinearPathComponents(scale, writer);
Scalar line_count = std::ceilf(ComputeConicSubdivisions(scale, *this));
for (size_t i = 1; i < line_count; i += 1) {
writer.Write(Solve(i / line_count));
}
writer.Write(p2);
}
size_t ConicPathComponent::CountLinearPathComponents(Scalar scale) const {
size_t count = 0;
for (auto quad : ToQuadraticPathComponents()) {
count += quad.CountLinearPathComponents(scale);
}
return count;
return std::ceilf(ComputeConicSubdivisions(scale, *this)) + 2;
}
std::vector<Point> ConicPathComponent::Extrema() const {

View File

@@ -252,6 +252,10 @@ struct ConicPathComponent {
void AppendPolylinePoints(Scalar scale_factor,
std::vector<Point>& points) const;
using PointProc = std::function<void(const Point& point)>;
void ToLinearPathComponents(Scalar scale_factor, const PointProc& proc) const;
void ToLinearPathComponents(Scalar scale, VertexWriter& writer) const;
size_t CountLinearPathComponents(Scalar scale) const;

View File

@@ -39,6 +39,49 @@ Scalar ComputeQuadradicSubdivisions(Scalar scale_factor,
return std::sqrt(k * length(p0 - p1 * 2 + p2));
}
// Returns Wang's formula specialized for a conic curve.
//
// This is not actually due to Wang, but is an analogue from:
// (Theorem 3, corollary 1):
// J. Zheng, T. Sederberg. "Estimating Tessellation Parameter Intervals for
// Rational Curves and Surfaces." ACM Transactions on Graphics 19(1). 2000.
Scalar ComputeConicSubdivisions(Scalar scale_factor,
Point p0,
Point p1,
Point p2,
Scalar w) {
// Compute center of bounding box in projected space
const Point C = 0.5f * (p0.Min(p1).Min(p2) + p0.Max(p1).Max(p2));
// Translate by -C. This improves translation-invariance of the formula,
// see Sec. 3.3 of cited paper
p0 -= C;
p1 -= C;
p2 -= C;
// Compute max length
const Scalar max_len =
std::sqrt(std::max(p0.Dot(p0), std::max(p1.Dot(p1), p2.Dot(p2))));
// Compute forward differences
const Point dp = -2 * w * p1 + p0 + p2;
const Scalar dw = std::abs(-2 * w + 2);
// Compute numerator and denominator for parametric step size of
// linearization. Here, the epsilon referenced from the cited paper
// is 1/precision.
Scalar k = scale_factor * kPrecision;
const Scalar rp_minus_1 = std::max(0.0f, max_len * k - 1);
const Scalar numer = std::sqrt(dp.Dot(dp)) * k + rp_minus_1 * dw;
const Scalar denom = 4 * std::min(w, 1.0f);
// Number of segments = sqrt(numer / denom).
// This assumes parametric interval of curve being linearized is
// [t0,t1] = [0, 1].
// If not, the number of segments is (tmax - tmin) / sqrt(denom / numer).
return std::sqrt(numer / denom);
}
Scalar ComputeQuadradicSubdivisions(Scalar scale_factor,
const QuadraticPathComponent& quad) {
return ComputeQuadradicSubdivisions(scale_factor, quad.p1, quad.cp, quad.p2);
@@ -50,4 +93,10 @@ Scalar ComputeCubicSubdivisions(float scale_factor,
cub.p2);
}
Scalar ComputeConicSubdivisions(float scale_factor,
const ConicPathComponent& conic) {
return ComputeConicSubdivisions(scale_factor, conic.p1, conic.cp, conic.p2,
conic.weight.x);
}
} // namespace impeller

View File

@@ -45,6 +45,17 @@ Scalar ComputeQuadradicSubdivisions(Scalar scale_factor,
Point p1,
Point p2);
/// Returns the minimum number of evenly spaced (in the parametric sense) line
/// segments that the conic must be chopped into in order to guarantee all
/// lines stay within a distance of "1/intolerance" pixels from the true curve.
///
/// The scale_factor should be the max basis XY of the current transform.
Scalar ComputeConicSubdivisions(Scalar scale_factor,
Point p0,
Point p1,
Point p2,
Scalar w);
/// Returns the minimum number of evenly spaced (in the parametric sense) line
/// segments that the quadratic must be chopped into in order to guarantee all
/// lines stay within a distance of "1/intolerance" pixels from the true curve.
@@ -60,6 +71,14 @@ Scalar ComputeQuadradicSubdivisions(Scalar scale_factor,
/// The scale_factor should be the max basis XY of the current transform.
Scalar ComputeCubicSubdivisions(float scale_factor,
const CubicPathComponent& cub);
/// Returns the minimum number of evenly spaced (in the parametric sense) line
/// segments that the conic must be chopped into in order to guarantee all lines
/// stay within a distance of "1/intolerance" pixels from the true curve.
///
/// The scale_factor should be the max basis XY of the current transform.
Scalar ComputeConicSubdivisions(float scale_factor,
const ConicPathComponent& conic);
} // namespace impeller
#endif // FLUTTER_IMPELLER_GEOMETRY_WANGS_FORMULA_H_