diff --git a/impeller/entity/contents.cc b/impeller/entity/contents.cc index 3e99da63f950e..a1f5bcfa789a4 100644 --- a/impeller/entity/contents.cc +++ b/impeller/entity/contents.cc @@ -11,6 +11,7 @@ #include "impeller/entity/content_context.h" #include "impeller/entity/entity.h" #include "impeller/geometry/path_builder.h" +#include "impeller/geometry/path_component.h" #include "impeller/geometry/scalar.h" #include "impeller/geometry/vector.h" #include "impeller/renderer/render_pass.h" @@ -303,7 +304,8 @@ static VertexBuffer CreateSolidStrokeVertices( HostBuffer& buffer, const SolidStrokeContents::CapProc& cap_proc, const SolidStrokeContents::JoinProc& join_proc, - Scalar miter_limit) { + Scalar miter_limit, + const SmoothingApproximation& smoothing) { using VS = SolidStrokeVertexShader; VertexBufferBuilder vtx_builder; @@ -356,7 +358,8 @@ static VertexBuffer CreateSolidStrokeVertices( // Generate start cap. if (!polyline.contours[contour_i].is_closed) { - cap_proc(vtx_builder, polyline.points[contour_start_point_i], -normal); + cap_proc(vtx_builder, polyline.points[contour_start_point_i], -normal, + smoothing); } // Generate contour geometry. @@ -381,17 +384,18 @@ static VertexBuffer CreateSolidStrokeVertices( // Generate join from the current line to the next line. join_proc(vtx_builder, polyline.points[point_i], previous_normal, - normal, miter_limit); + normal, miter_limit, smoothing); } } } // Generate end cap or join. if (!polyline.contours[contour_i].is_closed) { - cap_proc(vtx_builder, polyline.points[contour_end_point_i - 1], normal); + cap_proc(vtx_builder, polyline.points[contour_end_point_i - 1], normal, + smoothing); } else { join_proc(vtx_builder, polyline.points[contour_start_point_i], normal, - contour_first_normal, miter_limit); + contour_first_normal, miter_limit, smoothing); } } @@ -420,9 +424,9 @@ bool SolidStrokeContents::Render(const ContentContext& renderer, cmd.label = "SolidStroke"; cmd.pipeline = renderer.GetSolidStrokePipeline(OptionsFromPass(pass)); cmd.stencil_reference = entity.GetStencilDepth(); - cmd.BindVertices( - CreateSolidStrokeVertices(entity.GetPath(), pass.GetTransientsBuffer(), - cap_proc_, join_proc_, miter_limit_)); + cmd.BindVertices(CreateSolidStrokeVertices( + entity.GetPath(), pass.GetTransientsBuffer(), cap_proc_, join_proc_, + miter_limit_, arc_smoothing_approximation_)); VS::BindFrameInfo(cmd, pass.GetTransientsBuffer().EmplaceUniform(frame_info)); VS::BindStrokeInfo(cmd, pass.GetTransientsBuffer().EmplaceUniform(stroke_info)); @@ -434,6 +438,7 @@ bool SolidStrokeContents::Render(const ContentContext& renderer, void SolidStrokeContents::SetStrokeSize(Scalar size) { stroke_size_ = size; + arc_smoothing_approximation_ = SmoothingApproximation(5.0 / size, 0.0, 0.0); } Scalar SolidStrokeContents::GetStrokeSize() const { @@ -458,14 +463,41 @@ void SolidStrokeContents::SetStrokeCap(Cap cap) { switch (cap) { case Cap::kButt: cap_proc_ = [](VertexBufferBuilder& vtx_builder, - const Point& position, const Point& normal) {}; + const Point& position, const Point& normal, + const SmoothingApproximation& smoothing) {}; break; case Cap::kRound: - FML_DLOG(ERROR) << "Unimplemented."; + cap_proc_ = [](VertexBufferBuilder& vtx_builder, + const Point& position, const Point& normal, + const SmoothingApproximation& smoothing) { + SolidStrokeVertexShader::PerVertexData vtx; + vtx.vertex_position = position; + vtx.pen_down = 1.0; + + Point forward(normal.y, -normal.x); + + auto arc_points = + CubicPathComponent( + normal, normal + forward * PathBuilder::kArcApproximationMagic, + forward + normal * PathBuilder::kArcApproximationMagic, forward) + .CreatePolyline(smoothing); + + vtx.vertex_normal = normal; + vtx_builder.AppendVertex(vtx); + vtx.vertex_normal = -normal; + vtx_builder.AppendVertex(vtx); + for (const auto& point : arc_points) { + vtx.vertex_normal = point; + vtx_builder.AppendVertex(vtx); + vtx.vertex_normal = (-point).Reflect(forward); + vtx_builder.AppendVertex(vtx); + } + }; break; case Cap::kSquare: cap_proc_ = [](VertexBufferBuilder& vtx_builder, - const Point& position, const Point& normal) { + const Point& position, const Point& normal, + const SmoothingApproximation& smoothing) { SolidStrokeVertexShader::PerVertexData vtx; vtx.vertex_position = position; vtx.pen_down = 1.0; @@ -517,22 +549,25 @@ void SolidStrokeContents::SetStrokeJoin(Join join) { case Join::kBevel: join_proc_ = [](VertexBufferBuilder& vtx_builder, const Point& position, const Point& start_normal, - const Point& end_normal, Scalar miter_limit) { - CreateBevelAndGetDirection(vtx_builder, position, start_normal, end_normal); + const Point& end_normal, Scalar miter_limit, + const SmoothingApproximation& smoothing) { + CreateBevelAndGetDirection(vtx_builder, position, start_normal, + end_normal); }; break; case Join::kMiter: join_proc_ = [](VertexBufferBuilder& vtx_builder, const Point& position, const Point& start_normal, - const Point& end_normal, Scalar miter_limit) { + const Point& end_normal, Scalar miter_limit, + const SmoothingApproximation& smoothing) { // 1 for no joint (straight line), 0 for max joint (180 degrees). Scalar alignment = (start_normal.Dot(end_normal) + 1) / 2; if (ScalarNearlyEqual(alignment, 1)) { return; } - Scalar dir = - CreateBevelAndGetDirection(vtx_builder, position, start_normal, end_normal); + Scalar dir = CreateBevelAndGetDirection(vtx_builder, position, + start_normal, end_normal); Point miter_point = (start_normal + end_normal) / 2 / alignment; if (miter_point.GetDistanceSquared({0, 0}) > @@ -549,7 +584,42 @@ void SolidStrokeContents::SetStrokeJoin(Join join) { }; break; case Join::kRound: - FML_DLOG(ERROR) << "Unimplemented."; + join_proc_ = [](VertexBufferBuilder& vtx_builder, + const Point& position, const Point& start_normal, + const Point& end_normal, Scalar miter_limit, + const SmoothingApproximation& smoothing) { + // 0 for no joint (straight line), 1 for max joint (180 degrees). + Scalar alignment = 1 - (start_normal.Dot(end_normal) + 1) / 2; + if (ScalarNearlyEqual(alignment, 0)) { + return; + } + + Scalar dir = + CreateBevel(vtx_builder, position, start_normal, end_normal); + + Point middle = (start_normal + end_normal).Normalize(); + Point middle_handle = middle + Point(-middle.y, middle.x) * + PathBuilder::kArcApproximationMagic * + alignment * dir; + Point start_handle = + start_normal + Point(start_normal.y, -start_normal.x) * + PathBuilder::kArcApproximationMagic * alignment * + dir; + + auto arc_points = CubicPathComponent(start_normal, start_handle, + middle_handle, middle) + .CreatePolyline(smoothing); + + SolidStrokeVertexShader::PerVertexData vtx; + vtx.vertex_position = position; + vtx.pen_down = 1.0; + for (const auto& point : arc_points) { + vtx.vertex_normal = point * dir; + vtx_builder.AppendVertex(vtx); + vtx.vertex_normal = (-point * dir).Reflect(middle); + vtx_builder.AppendVertex(vtx); + } + }; break; } } diff --git a/impeller/entity/contents.h b/impeller/entity/contents.h index 4178dfb6838fc..f3803ff4d0222 100644 --- a/impeller/entity/contents.h +++ b/impeller/entity/contents.h @@ -11,6 +11,7 @@ #include "flutter/fml/macros.h" #include "impeller/entity/solid_stroke.vert.h" #include "impeller/geometry/color.h" +#include "impeller/geometry/path_component.h" #include "impeller/geometry/point.h" #include "impeller/geometry/rect.h" #include "impeller/renderer/texture.h" @@ -132,13 +133,15 @@ class SolidStrokeContents final : public Contents { using CapProc = std::function& vtx_builder, const Point& position, - const Point& normal)>; + const Point& normal, + const SmoothingApproximation& smoothing)>; using JoinProc = std::function& vtx_builder, const Point& position, const Point& start_normal, const Point& end_normal, - Scalar miter_limit)>; + Scalar miter_limit, + const SmoothingApproximation& smoothing)>; SolidStrokeContents(); @@ -170,6 +173,8 @@ class SolidStrokeContents final : public Contents { RenderPass& pass) const override; private: + SmoothingApproximation arc_smoothing_approximation_; + Color color_; Scalar stroke_size_ = 0.0; Scalar miter_limit_ = 4.0; diff --git a/impeller/entity/entity_unittests.cc b/impeller/entity/entity_unittests.cc index 9db0270f832ac..927a32e3b8d31 100644 --- a/impeller/entity/entity_unittests.cc +++ b/impeller/entity/entity_unittests.cc @@ -4,12 +4,12 @@ #include "entity/contents.h" #include "flutter/testing/testing.h" -#include "imgui.h" #include "impeller/entity/entity.h" #include "impeller/entity/entity_playground.h" #include "impeller/geometry/path_builder.h" #include "impeller/playground/playground.h" #include "impeller/playground/widgets.h" +#include "third_party/imgui/imgui.h" namespace impeller { namespace testing { @@ -81,37 +81,50 @@ TEST_F(EntityTest, TriangleInsideASquare) { } TEST_F(EntityTest, StrokeCapAndJoinTest) { - auto callback = [&](ContentContext& context, RenderPass& pass) { - Entity entity; + const Point padding(300, 250); + const Point margin(140, 180); - ImGui::SetNextWindowSize({300, 60}); - ImGui::SetNextWindowPos({100, 300}); + bool first_frame = true; + auto callback = [&](ContentContext& context, RenderPass& pass) { + if (first_frame) { + first_frame = false; + ImGui::SetNextWindowSize({300, 100}); + ImGui::SetNextWindowPos( + {0 * padding.x + margin.x, 1.7f * padding.y + margin.y}); + } ImGui::Begin("Controls"); // Slightly above sqrt(2) by default, so that right angles are just below // the limit and acute angles are over the limit (causing them to get // beveled). static Scalar miter_limit = 1.41421357; + static Scalar width = 30; ImGui::SliderFloat("Miter limit", &miter_limit, 0, 30); + ImGui::SliderFloat("Stroke width", &width, 0, 100); + if (ImGui::Button("Reset")) { + miter_limit = 1.41421357; + width = 30; + } ImGui::End(); - auto create_contents = [](SolidStrokeContents::Cap cap, - SolidStrokeContents::Join join) { + auto create_contents = [width = width](SolidStrokeContents::Cap cap, + SolidStrokeContents::Join join) { auto contents = std::make_unique(); contents->SetColor(Color::Red()); - contents->SetStrokeSize(20.0); + contents->SetStrokeSize(width); contents->SetStrokeCap(cap); contents->SetStrokeJoin(join); contents->SetStrokeMiter(miter_limit); return contents; }; - const Point a_def(100, 100), b_def(100, 150), c_def(200, 100), - d_def(200, 50), e_def(150, 150); - const Scalar r = 10; + Entity entity; + const Point a_def(0, 0), b_def(0, 100), c_def(150, 0), d_def(150, -100), + e_def(75, 75); + const Scalar r = 30; // Cap::kButt demo. { - Point off(0, 0); + Point off = Point(0, 0) * padding + margin; Point a, b, c, d; std::tie(a, b) = IMPELLER_PLAYGROUND_LINE(off + a_def, off + b_def, r, Color::Black(), Color::White()); @@ -125,7 +138,7 @@ TEST_F(EntityTest, StrokeCapAndJoinTest) { // Cap::kSquare demo. { - Point off(0, 100); + Point off = Point(1, 0) * padding + margin; Point a, b, c, d; std::tie(a, b) = IMPELLER_PLAYGROUND_LINE(off + a_def, off + b_def, r, Color::Black(), Color::White()); @@ -137,9 +150,23 @@ TEST_F(EntityTest, StrokeCapAndJoinTest) { entity.Render(context, pass); } + // Cap::kRound demo. + { + Point off = Point(2, 0) * padding + margin; + Point a, b, c, d; + std::tie(a, b) = IMPELLER_PLAYGROUND_LINE(off + a_def, off + b_def, r, + Color::Black(), Color::White()); + std::tie(c, d) = IMPELLER_PLAYGROUND_LINE(off + c_def, off + d_def, r, + Color::Black(), Color::White()); + entity.SetPath(PathBuilder{}.AddCubicCurve(a, b, d, c).TakePath()); + entity.SetContents(create_contents(SolidStrokeContents::Cap::kRound, + SolidStrokeContents::Join::kBevel)); + entity.Render(context, pass); + } + // Join::kBevel demo. { - Point off(200, 0); + Point off = Point(0, 1) * padding + margin; Point a = IMPELLER_PLAYGROUND_POINT(off + a_def, r, Color::White()); Point b = IMPELLER_PLAYGROUND_POINT(off + e_def, r, Color::White()); Point c = IMPELLER_PLAYGROUND_POINT(off + c_def, r, Color::White()); @@ -152,7 +179,7 @@ TEST_F(EntityTest, StrokeCapAndJoinTest) { // Join::kMiter demo. { - Point off(200, 100); + Point off = Point(1, 1) * padding + margin; Point a = IMPELLER_PLAYGROUND_POINT(off + a_def, r, Color::White()); Point b = IMPELLER_PLAYGROUND_POINT(off + e_def, r, Color::White()); Point c = IMPELLER_PLAYGROUND_POINT(off + c_def, r, Color::White()); @@ -163,6 +190,19 @@ TEST_F(EntityTest, StrokeCapAndJoinTest) { entity.Render(context, pass); } + // Join::kRound demo. + { + Point off = Point(2, 1) * padding + margin; + Point a = IMPELLER_PLAYGROUND_POINT(off + a_def, r, Color::White()); + Point b = IMPELLER_PLAYGROUND_POINT(off + e_def, r, Color::White()); + Point c = IMPELLER_PLAYGROUND_POINT(off + c_def, r, Color::White()); + entity.SetPath( + PathBuilder{}.MoveTo(a).LineTo(b).LineTo(c).Close().TakePath()); + entity.SetContents(create_contents(SolidStrokeContents::Cap::kButt, + SolidStrokeContents::Join::kRound)); + entity.Render(context, pass); + } + return true; }; ASSERT_TRUE(OpenPlaygroundHere(callback)); diff --git a/impeller/geometry/path_builder.cc b/impeller/geometry/path_builder.cc index 1e97b52afba6f..81e269806c234 100644 --- a/impeller/geometry/path_builder.cc +++ b/impeller/geometry/path_builder.cc @@ -6,8 +6,6 @@ namespace impeller { -static const Scalar kArcApproximationMagic = 0.551915024494; - PathBuilder::PathBuilder() = default; PathBuilder::~PathBuilder() = default; diff --git a/impeller/geometry/path_builder.h b/impeller/geometry/path_builder.h index 09bd3b825665e..3c495e4f872a6 100644 --- a/impeller/geometry/path_builder.h +++ b/impeller/geometry/path_builder.h @@ -13,6 +13,15 @@ namespace impeller { class PathBuilder { public: + /// Used for approximating quarter circle arcs with cubic curves. This is the + /// control point distance which results in the smallest possible unit circle + /// integration for a right angle arc. It can be used to approximate arcs less + /// than 90 degrees to great effect by simply reducing it proportionally to + /// the angle. However, accuracy rapidly diminishes if magnified for obtuse + /// angle arcs, and so multiple cubic curves should be used when approximating + /// arcs greater than 90 degrees. + constexpr static const Scalar kArcApproximationMagic = 0.551915024494; + PathBuilder(); ~PathBuilder();