Skip to content

Commit 0a22ff9

Browse files
authored
[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.
1 parent 9b41640 commit 0a22ff9

File tree

5 files changed

+245
-12
lines changed

5 files changed

+245
-12
lines changed

engine/src/flutter/impeller/display_list/aiks_dl_path_unittests.cc

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,160 @@ TEST_P(AiksTest, CanRenderQuadraticStrokeWithInstantTurn) {
153153
ASSERT_TRUE(OpenPlaygroundHere(builder.Build()));
154154
}
155155

156+
TEST_P(AiksTest, CanRenderFilledConicPaths) {
157+
DisplayListBuilder builder;
158+
builder.Scale(GetContentScale().x, GetContentScale().y);
159+
160+
DlPaint paint;
161+
paint.setColor(DlColor::kRed());
162+
paint.setDrawStyle(DlDrawStyle::kFill);
163+
164+
DlPaint reference_paint;
165+
reference_paint.setColor(DlColor::kGreen());
166+
reference_paint.setDrawStyle(DlDrawStyle::kFill);
167+
168+
DlPathBuilder path_builder;
169+
DlPathBuilder reference_builder;
170+
171+
// weight of 1.0 is just a quadratic bezier
172+
path_builder.MoveTo(DlPoint(100, 100));
173+
path_builder.ConicCurveTo(DlPoint(150, 150), DlPoint(200, 100), 1.0f);
174+
reference_builder.MoveTo(DlPoint(300, 100));
175+
reference_builder.QuadraticCurveTo(DlPoint(350, 150), DlPoint(400, 100));
176+
177+
// weight of sqrt(2)/2 is a circular section
178+
path_builder.MoveTo(DlPoint(100, 200));
179+
path_builder.ConicCurveTo(DlPoint(150, 250), DlPoint(200, 200), kSqrt2Over2);
180+
reference_builder.MoveTo(DlPoint(300, 200));
181+
auto magic = DlPathBuilder::kArcApproximationMagic;
182+
reference_builder.CubicCurveTo(DlPoint(300, 200) + DlPoint(50, 50) * magic,
183+
DlPoint(400, 200) + DlPoint(-50, 50) * magic,
184+
DlPoint(400, 200));
185+
186+
// weight of .01 is nearly a straight line
187+
path_builder.MoveTo(DlPoint(100, 300));
188+
path_builder.ConicCurveTo(DlPoint(150, 350), DlPoint(200, 300), 0.01f);
189+
reference_builder.MoveTo(DlPoint(300, 300));
190+
reference_builder.LineTo(DlPoint(350, 300.5));
191+
reference_builder.LineTo(DlPoint(400, 300));
192+
193+
// weight of 100.0 is nearly a triangle
194+
path_builder.MoveTo(DlPoint(100, 400));
195+
path_builder.ConicCurveTo(DlPoint(150, 450), DlPoint(200, 400), 100.0f);
196+
reference_builder.MoveTo(DlPoint(300, 400));
197+
reference_builder.LineTo(DlPoint(350, 450));
198+
reference_builder.LineTo(DlPoint(400, 400));
199+
200+
builder.DrawPath(DlPath(path_builder), paint);
201+
builder.DrawPath(DlPath(reference_builder), reference_paint);
202+
203+
ASSERT_TRUE(OpenPlaygroundHere(builder.Build()));
204+
}
205+
206+
TEST_P(AiksTest, CanRenderStrokedConicPaths) {
207+
DisplayListBuilder builder;
208+
builder.Scale(GetContentScale().x, GetContentScale().y);
209+
210+
DlPaint paint;
211+
paint.setColor(DlColor::kRed());
212+
paint.setStrokeWidth(10);
213+
paint.setDrawStyle(DlDrawStyle::kStroke);
214+
paint.setStrokeCap(DlStrokeCap::kRound);
215+
paint.setStrokeJoin(DlStrokeJoin::kRound);
216+
217+
DlPaint reference_paint;
218+
reference_paint.setColor(DlColor::kGreen());
219+
reference_paint.setStrokeWidth(10);
220+
reference_paint.setDrawStyle(DlDrawStyle::kStroke);
221+
reference_paint.setStrokeCap(DlStrokeCap::kRound);
222+
reference_paint.setStrokeJoin(DlStrokeJoin::kRound);
223+
224+
DlPathBuilder path_builder;
225+
DlPathBuilder reference_builder;
226+
227+
// weight of 1.0 is just a quadratic bezier
228+
path_builder.MoveTo(DlPoint(100, 100));
229+
path_builder.ConicCurveTo(DlPoint(150, 150), DlPoint(200, 100), 1.0f);
230+
reference_builder.MoveTo(DlPoint(300, 100));
231+
reference_builder.QuadraticCurveTo(DlPoint(350, 150), DlPoint(400, 100));
232+
233+
// weight of sqrt(2)/2 is a circular section
234+
path_builder.MoveTo(DlPoint(100, 200));
235+
path_builder.ConicCurveTo(DlPoint(150, 250), DlPoint(200, 200), kSqrt2Over2);
236+
reference_builder.MoveTo(DlPoint(300, 200));
237+
auto magic = DlPathBuilder::kArcApproximationMagic;
238+
reference_builder.CubicCurveTo(DlPoint(300, 200) + DlPoint(50, 50) * magic,
239+
DlPoint(400, 200) + DlPoint(-50, 50) * magic,
240+
DlPoint(400, 200));
241+
242+
// weight of .0 is a straight line
243+
path_builder.MoveTo(DlPoint(100, 300));
244+
path_builder.ConicCurveTo(DlPoint(150, 350), DlPoint(200, 300), 0.0f);
245+
reference_builder.MoveTo(DlPoint(300, 300));
246+
reference_builder.LineTo(DlPoint(400, 300));
247+
248+
// weight of 100.0 is nearly a triangle
249+
path_builder.MoveTo(DlPoint(100, 400));
250+
path_builder.ConicCurveTo(DlPoint(150, 450), DlPoint(200, 400), 100.0f);
251+
reference_builder.MoveTo(DlPoint(300, 400));
252+
reference_builder.LineTo(DlPoint(350, 450));
253+
reference_builder.LineTo(DlPoint(400, 400));
254+
255+
builder.DrawPath(DlPath(path_builder), paint);
256+
builder.DrawPath(DlPath(reference_builder), reference_paint);
257+
258+
ASSERT_TRUE(OpenPlaygroundHere(builder.Build()));
259+
}
260+
261+
TEST_P(AiksTest, CanRenderTightConicPath) {
262+
DisplayListBuilder builder;
263+
builder.Scale(GetContentScale().x, GetContentScale().y);
264+
265+
DlPaint paint;
266+
paint.setColor(DlColor::kRed());
267+
paint.setDrawStyle(DlDrawStyle::kFill);
268+
269+
DlPaint reference_paint;
270+
reference_paint.setColor(DlColor::kGreen());
271+
reference_paint.setDrawStyle(DlDrawStyle::kFill);
272+
273+
DlPathBuilder path_builder;
274+
275+
path_builder.MoveTo(DlPoint(100, 100));
276+
path_builder.ConicCurveTo(DlPoint(150, 450), DlPoint(200, 100), 5.0f);
277+
278+
DlPathBuilder reference_builder;
279+
ConicPathComponent component(DlPoint(300, 100), //
280+
DlPoint(350, 450), //
281+
DlPoint(400, 100), //
282+
5.0f);
283+
reference_builder.MoveTo(component.p1);
284+
constexpr int N = 100;
285+
for (int i = 1; i < N; i++) {
286+
reference_builder.LineTo(component.Solve(static_cast<Scalar>(i) / N));
287+
}
288+
reference_builder.LineTo(component.p2);
289+
290+
DlPaint line_paint;
291+
line_paint.setColor(DlColor::kYellow());
292+
line_paint.setDrawStyle(DlDrawStyle::kStroke);
293+
line_paint.setStrokeWidth(1.0f);
294+
295+
// Draw some lines to provide a spacial reference for the curvature of
296+
// the tips of the direct rendering and the manually tessellated versions.
297+
builder.DrawLine(DlPoint(145, 100), DlPoint(145, 450), line_paint);
298+
builder.DrawLine(DlPoint(155, 100), DlPoint(155, 450), line_paint);
299+
builder.DrawLine(DlPoint(345, 100), DlPoint(345, 450), line_paint);
300+
builder.DrawLine(DlPoint(355, 100), DlPoint(355, 450), line_paint);
301+
builder.DrawLine(DlPoint(100, 392.5f), DlPoint(400, 392.5f), line_paint);
302+
303+
// Draw the two paths (direct and manually tessellated) on top of the lines.
304+
builder.DrawPath(DlPath(path_builder), paint);
305+
builder.DrawPath(DlPath(reference_builder), reference_paint);
306+
307+
ASSERT_TRUE(OpenPlaygroundHere(builder.Build()));
308+
}
309+
156310
TEST_P(AiksTest, CanRenderDifferencePaths) {
157311
DisplayListBuilder builder;
158312

engine/src/flutter/impeller/geometry/path_component.cc

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -190,9 +190,9 @@ static inline Scalar ConicSolve(Scalar t,
190190
Scalar p2,
191191
Scalar w) {
192192
auto u = (1 - t);
193-
auto coefficient_p0 = t * t;
193+
auto coefficient_p0 = u * u;
194194
auto coefficient_p1 = 2 * t * u * w;
195-
auto coefficient_p2 = u * u;
195+
auto coefficient_p2 = t * t;
196196

197197
return ((p0 * coefficient_p0 + p1 * coefficient_p1 + p2 * coefficient_p2) /
198198
(coefficient_p0 + coefficient_p1 + coefficient_p2));
@@ -331,27 +331,34 @@ Point ConicPathComponent::Solve(Scalar time) const {
331331
};
332332
}
333333

334+
void ConicPathComponent::ToLinearPathComponents(Scalar scale_factor,
335+
const PointProc& proc) const {
336+
Scalar line_count = std::ceilf(ComputeConicSubdivisions(scale_factor, *this));
337+
for (size_t i = 1; i < line_count; i += 1) {
338+
proc(Solve(i / line_count));
339+
}
340+
proc(p2);
341+
}
342+
334343
void ConicPathComponent::AppendPolylinePoints(
335344
Scalar scale_factor,
336345
std::vector<Point>& points) const {
337-
for (auto quad : ToQuadraticPathComponents()) {
338-
quad.AppendPolylinePoints(scale_factor, points);
339-
}
346+
ToLinearPathComponents(scale_factor, [&points](const Point& point) {
347+
points.emplace_back(point);
348+
});
340349
}
341350

342351
void ConicPathComponent::ToLinearPathComponents(Scalar scale,
343352
VertexWriter& writer) const {
344-
for (auto quad : ToQuadraticPathComponents()) {
345-
quad.ToLinearPathComponents(scale, writer);
353+
Scalar line_count = std::ceilf(ComputeConicSubdivisions(scale, *this));
354+
for (size_t i = 1; i < line_count; i += 1) {
355+
writer.Write(Solve(i / line_count));
346356
}
357+
writer.Write(p2);
347358
}
348359

349360
size_t ConicPathComponent::CountLinearPathComponents(Scalar scale) const {
350-
size_t count = 0;
351-
for (auto quad : ToQuadraticPathComponents()) {
352-
count += quad.CountLinearPathComponents(scale);
353-
}
354-
return count;
361+
return std::ceilf(ComputeConicSubdivisions(scale, *this)) + 2;
355362
}
356363

357364
std::vector<Point> ConicPathComponent::Extrema() const {

engine/src/flutter/impeller/geometry/path_component.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,10 @@ struct ConicPathComponent {
252252
void AppendPolylinePoints(Scalar scale_factor,
253253
std::vector<Point>& points) const;
254254

255+
using PointProc = std::function<void(const Point& point)>;
256+
257+
void ToLinearPathComponents(Scalar scale_factor, const PointProc& proc) const;
258+
255259
void ToLinearPathComponents(Scalar scale, VertexWriter& writer) const;
256260

257261
size_t CountLinearPathComponents(Scalar scale) const;

engine/src/flutter/impeller/geometry/wangs_formula.cc

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,49 @@ Scalar ComputeQuadradicSubdivisions(Scalar scale_factor,
3939
return std::sqrt(k * length(p0 - p1 * 2 + p2));
4040
}
4141

42+
// Returns Wang's formula specialized for a conic curve.
43+
//
44+
// This is not actually due to Wang, but is an analogue from:
45+
// (Theorem 3, corollary 1):
46+
// J. Zheng, T. Sederberg. "Estimating Tessellation Parameter Intervals for
47+
// Rational Curves and Surfaces." ACM Transactions on Graphics 19(1). 2000.
48+
Scalar ComputeConicSubdivisions(Scalar scale_factor,
49+
Point p0,
50+
Point p1,
51+
Point p2,
52+
Scalar w) {
53+
// Compute center of bounding box in projected space
54+
const Point C = 0.5f * (p0.Min(p1).Min(p2) + p0.Max(p1).Max(p2));
55+
56+
// Translate by -C. This improves translation-invariance of the formula,
57+
// see Sec. 3.3 of cited paper
58+
p0 -= C;
59+
p1 -= C;
60+
p2 -= C;
61+
62+
// Compute max length
63+
const Scalar max_len =
64+
std::sqrt(std::max(p0.Dot(p0), std::max(p1.Dot(p1), p2.Dot(p2))));
65+
66+
// Compute forward differences
67+
const Point dp = -2 * w * p1 + p0 + p2;
68+
const Scalar dw = std::abs(-2 * w + 2);
69+
70+
// Compute numerator and denominator for parametric step size of
71+
// linearization. Here, the epsilon referenced from the cited paper
72+
// is 1/precision.
73+
Scalar k = scale_factor * kPrecision;
74+
const Scalar rp_minus_1 = std::max(0.0f, max_len * k - 1);
75+
const Scalar numer = std::sqrt(dp.Dot(dp)) * k + rp_minus_1 * dw;
76+
const Scalar denom = 4 * std::min(w, 1.0f);
77+
78+
// Number of segments = sqrt(numer / denom).
79+
// This assumes parametric interval of curve being linearized is
80+
// [t0,t1] = [0, 1].
81+
// If not, the number of segments is (tmax - tmin) / sqrt(denom / numer).
82+
return std::sqrt(numer / denom);
83+
}
84+
4285
Scalar ComputeQuadradicSubdivisions(Scalar scale_factor,
4386
const QuadraticPathComponent& quad) {
4487
return ComputeQuadradicSubdivisions(scale_factor, quad.p1, quad.cp, quad.p2);
@@ -50,4 +93,10 @@ Scalar ComputeCubicSubdivisions(float scale_factor,
5093
cub.p2);
5194
}
5295

96+
Scalar ComputeConicSubdivisions(float scale_factor,
97+
const ConicPathComponent& conic) {
98+
return ComputeConicSubdivisions(scale_factor, conic.p1, conic.cp, conic.p2,
99+
conic.weight.x);
100+
}
101+
53102
} // namespace impeller

engine/src/flutter/impeller/geometry/wangs_formula.h

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,17 @@ Scalar ComputeQuadradicSubdivisions(Scalar scale_factor,
4545
Point p1,
4646
Point p2);
4747

48+
/// Returns the minimum number of evenly spaced (in the parametric sense) line
49+
/// segments that the conic must be chopped into in order to guarantee all
50+
/// lines stay within a distance of "1/intolerance" pixels from the true curve.
51+
///
52+
/// The scale_factor should be the max basis XY of the current transform.
53+
Scalar ComputeConicSubdivisions(Scalar scale_factor,
54+
Point p0,
55+
Point p1,
56+
Point p2,
57+
Scalar w);
58+
4859
/// Returns the minimum number of evenly spaced (in the parametric sense) line
4960
/// segments that the quadratic must be chopped into in order to guarantee all
5061
/// lines stay within a distance of "1/intolerance" pixels from the true curve.
@@ -60,6 +71,14 @@ Scalar ComputeQuadradicSubdivisions(Scalar scale_factor,
6071
/// The scale_factor should be the max basis XY of the current transform.
6172
Scalar ComputeCubicSubdivisions(float scale_factor,
6273
const CubicPathComponent& cub);
74+
75+
/// Returns the minimum number of evenly spaced (in the parametric sense) line
76+
/// segments that the conic must be chopped into in order to guarantee all lines
77+
/// stay within a distance of "1/intolerance" pixels from the true curve.
78+
///
79+
/// The scale_factor should be the max basis XY of the current transform.
80+
Scalar ComputeConicSubdivisions(float scale_factor,
81+
const ConicPathComponent& conic);
6382
} // namespace impeller
6483

6584
#endif // FLUTTER_IMPELLER_GEOMETRY_WANGS_FORMULA_H_

0 commit comments

Comments
 (0)