forked from firka/flutter
[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:
@@ -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;
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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_
|
||||
|
||||
Reference in New Issue
Block a user