diff --git a/impeller/entity/contents/clip_contents.cc b/impeller/entity/contents/clip_contents.cc index 2497e4daa9c52..a7ba8e6ec7af6 100644 --- a/impeller/entity/contents/clip_contents.cc +++ b/impeller/entity/contents/clip_contents.cc @@ -121,7 +121,8 @@ bool ClipContents::Render(const ContentContext& renderer, auto allocator = renderer.GetContext()->GetResourceAllocator(); auto geometry_result = geometry_->GetPositionBuffer( allocator, host_buffer, renderer.GetTessellator(), - pass.GetRenderTargetSize()); + pass.GetRenderTargetSize(), + entity.GetTransformation().GetMaxBasisLength()); cmd.BindVertices(geometry_result.vertex_buffer); cmd.primitive_type = geometry_result.type; info.mvp = Matrix::MakeOrthographic(pass.GetRenderTargetSize()) * diff --git a/impeller/entity/contents/linear_gradient_contents.cc b/impeller/entity/contents/linear_gradient_contents.cc index eef7c5d42fd25..1b4d3a77e4dd9 100644 --- a/impeller/entity/contents/linear_gradient_contents.cc +++ b/impeller/entity/contents/linear_gradient_contents.cc @@ -80,7 +80,8 @@ bool LinearGradientContents::Render(const ContentContext& renderer, auto allocator = renderer.GetContext()->GetResourceAllocator(); auto geometry_result = GetGeometry()->GetPositionBuffer( allocator, host_buffer, renderer.GetTessellator(), - pass.GetRenderTargetSize()); + pass.GetRenderTargetSize(), + entity.GetTransformation().GetMaxBasisLength()); cmd.BindVertices(geometry_result.vertex_buffer); cmd.primitive_type = geometry_result.type; FS::BindGradientInfo( diff --git a/impeller/entity/contents/radial_gradient_contents.cc b/impeller/entity/contents/radial_gradient_contents.cc index e674ebce192b7..65352a56ce340 100644 --- a/impeller/entity/contents/radial_gradient_contents.cc +++ b/impeller/entity/contents/radial_gradient_contents.cc @@ -79,7 +79,8 @@ bool RadialGradientContents::Render(const ContentContext& renderer, auto allocator = renderer.GetContext()->GetResourceAllocator(); auto geometry_result = GetGeometry()->GetPositionBuffer( allocator, host_buffer, renderer.GetTessellator(), - pass.GetRenderTargetSize()); + pass.GetRenderTargetSize(), + entity.GetTransformation().GetMaxBasisLength()); cmd.BindVertices(geometry_result.vertex_buffer); cmd.primitive_type = geometry_result.type; FS::BindGradientInfo( diff --git a/impeller/entity/contents/solid_color_contents.cc b/impeller/entity/contents/solid_color_contents.cc index bae283cb42249..e16dc54b8dc18 100644 --- a/impeller/entity/contents/solid_color_contents.cc +++ b/impeller/entity/contents/solid_color_contents.cc @@ -63,7 +63,8 @@ bool SolidColorContents::Render(const ContentContext& renderer, auto allocator = renderer.GetContext()->GetResourceAllocator(); auto geometry_result = geometry_->GetPositionBuffer( allocator, host_buffer, renderer.GetTessellator(), - pass.GetRenderTargetSize()); + pass.GetRenderTargetSize(), + entity.GetTransformation().GetMaxBasisLength()); cmd.BindVertices(geometry_result.vertex_buffer); cmd.primitive_type = geometry_result.type; diff --git a/impeller/entity/contents/solid_stroke_contents.cc b/impeller/entity/contents/solid_stroke_contents.cc index 8ff982c667d14..bf66c5e2a5dee 100644 --- a/impeller/entity/contents/solid_stroke_contents.cc +++ b/impeller/entity/contents/solid_stroke_contents.cc @@ -72,7 +72,7 @@ static VertexBuffer CreateSolidStrokeVertices( const SolidStrokeContents::CapProc& cap_proc, const SolidStrokeContents::JoinProc& join_proc, Scalar miter_limit, - const SmoothingApproximation& smoothing) { + Scalar tolerance) { using VS = SolidFillVertexShader; VertexBufferBuilder vtx_builder; @@ -101,8 +101,8 @@ static VertexBuffer CreateSolidStrokeVertices( switch (contour_end_point_i - contour_start_point_i) { case 1: { Point p = polyline.points[contour_start_point_i]; - cap_proc(vtx_builder, p, {-stroke_width * 0.5f, 0}, smoothing); - cap_proc(vtx_builder, p, {stroke_width * 0.5f, 0}, smoothing); + cap_proc(vtx_builder, p, {-stroke_width * 0.5f, 0}, tolerance); + cap_proc(vtx_builder, p, {stroke_width * 0.5f, 0}, tolerance); continue; } case 0: @@ -140,7 +140,7 @@ static VertexBuffer CreateSolidStrokeVertices( // Generate start cap. if (!polyline.contours[contour_i].is_closed) { cap_proc(vtx_builder, polyline.points[contour_start_point_i], -offset, - smoothing); + tolerance); } // Generate contour geometry. @@ -162,7 +162,7 @@ static VertexBuffer CreateSolidStrokeVertices( // Generate join from the current line to the next line. join_proc(vtx_builder, polyline.points[point_i], previous_offset, - offset, miter_limit, smoothing); + offset, miter_limit, tolerance); } } } @@ -170,10 +170,10 @@ static VertexBuffer CreateSolidStrokeVertices( // Generate end cap or join. if (!polyline.contours[contour_i].is_closed) { cap_proc(vtx_builder, polyline.points[contour_end_point_i - 1], offset, - smoothing); + tolerance); } else { join_proc(vtx_builder, polyline.points[contour_start_point_i], offset, - contour_first_offset, miter_limit, smoothing); + contour_first_offset, miter_limit, tolerance); } } @@ -209,15 +209,15 @@ bool SolidStrokeContents::Render(const ContentContext& renderer, cmd.pipeline = renderer.GetSolidFillPipeline(options); cmd.stencil_reference = entity.GetStencilDepth(); - auto smoothing = SmoothingApproximation( - 60.0 / (stroke_size_ * entity.GetTransformation().GetMaxBasisLength()), - 0.0, 0.0); + auto tolerance = + kDefaultCurveTolerance / + (stroke_size_ * entity.GetTransformation().GetMaxBasisLength()); Scalar min_size = 1.0f / sqrt(std::abs(determinant)); Scalar stroke_width = std::max(stroke_size_, min_size); cmd.BindVertices(CreateSolidStrokeVertices( path_, pass.GetTransientsBuffer(), stroke_width, cap_proc_, join_proc_, - miter_limit_ * stroke_width * 0.5, smoothing)); + miter_limit_ * stroke_width * 0.5, tolerance)); VS::BindVertInfo(cmd, pass.GetTransientsBuffer().EmplaceUniform(vert_info)); FS::BindFragInfo(cmd, pass.GetTransientsBuffer().EmplaceUniform(frag_info)); @@ -256,12 +256,12 @@ void SolidStrokeContents::SetStrokeCap(Cap cap) { case Cap::kButt: cap_proc_ = [](VertexBufferBuilder& vtx_builder, const Point& position, const Point& offset, - const SmoothingApproximation& smoothing) {}; + Scalar tolerance) {}; break; case Cap::kRound: cap_proc_ = [](VertexBufferBuilder& vtx_builder, const Point& position, const Point& offset, - const SmoothingApproximation& smoothing) { + Scalar tolerance) { VS::PerVertexData vtx; Point forward(offset.y, -offset.x); @@ -271,7 +271,7 @@ void SolidStrokeContents::SetStrokeCap(Cap cap) { CubicPathComponent( offset, offset + forward * PathBuilder::kArcApproximationMagic, forward + offset * PathBuilder::kArcApproximationMagic, forward) - .CreatePolyline(smoothing); + .CreatePolyline(tolerance); vtx.position = position + offset; vtx_builder.AppendVertex(vtx); @@ -288,7 +288,7 @@ void SolidStrokeContents::SetStrokeCap(Cap cap) { case Cap::kSquare: cap_proc_ = [](VertexBufferBuilder& vtx_builder, const Point& position, const Point& offset, - const SmoothingApproximation& smoothing) { + Scalar tolerance) { VS::PerVertexData vtx; vtx.position = position; @@ -337,7 +337,7 @@ void SolidStrokeContents::SetStrokeJoin(Join join) { join_proc_ = [](VertexBufferBuilder& vtx_builder, const Point& position, const Point& start_offset, const Point& end_offset, Scalar miter_limit, - const SmoothingApproximation& smoothing) { + Scalar tolerance) { CreateBevelAndGetDirection(vtx_builder, position, start_offset, end_offset); }; @@ -346,7 +346,7 @@ void SolidStrokeContents::SetStrokeJoin(Join join) { join_proc_ = [](VertexBufferBuilder& vtx_builder, const Point& position, const Point& start_offset, const Point& end_offset, Scalar miter_limit, - const SmoothingApproximation& smoothing) { + Scalar tolerance) { Point start_normal = start_offset.Normalize(); Point end_normal = end_offset.Normalize(); @@ -375,7 +375,7 @@ void SolidStrokeContents::SetStrokeJoin(Join join) { join_proc_ = [](VertexBufferBuilder& vtx_builder, const Point& position, const Point& start_offset, const Point& end_offset, Scalar miter_limit, - const SmoothingApproximation& smoothing) { + Scalar tolerance) { Point start_normal = start_offset.Normalize(); Point end_normal = end_offset.Normalize(); @@ -402,7 +402,7 @@ void SolidStrokeContents::SetStrokeJoin(Join join) { auto arc_points = CubicPathComponent(start_offset, start_handle, middle_handle, middle) - .CreatePolyline(smoothing); + .CreatePolyline(tolerance); VS::PerVertexData vtx; for (const auto& point : arc_points) { diff --git a/impeller/entity/contents/solid_stroke_contents.h b/impeller/entity/contents/solid_stroke_contents.h index 526d0d3c7d198..101daf616e2fb 100644 --- a/impeller/entity/contents/solid_stroke_contents.h +++ b/impeller/entity/contents/solid_stroke_contents.h @@ -40,14 +40,14 @@ class SolidStrokeContents final : public Contents { std::function& vtx_builder, const Point& position, const Point& offset, - const SmoothingApproximation& smoothing)>; + Scalar tolerance)>; using JoinProc = std::function& vtx_builder, const Point& position, const Point& start_offset, const Point& end_offset, Scalar miter_limit, - const SmoothingApproximation& smoothing)>; + Scalar tolerance)>; SolidStrokeContents(); diff --git a/impeller/entity/contents/sweep_gradient_contents.cc b/impeller/entity/contents/sweep_gradient_contents.cc index 2371b6ecfcdda..921c8fc967626 100644 --- a/impeller/entity/contents/sweep_gradient_contents.cc +++ b/impeller/entity/contents/sweep_gradient_contents.cc @@ -85,7 +85,8 @@ bool SweepGradientContents::Render(const ContentContext& renderer, auto allocator = renderer.GetContext()->GetResourceAllocator(); auto geometry_result = GetGeometry()->GetPositionBuffer( allocator, host_buffer, renderer.GetTessellator(), - pass.GetRenderTargetSize()); + pass.GetRenderTargetSize(), + entity.GetTransformation().GetMaxBasisLength()); cmd.BindVertices(geometry_result.vertex_buffer); cmd.primitive_type = geometry_result.type; FS::BindGradientInfo( diff --git a/impeller/entity/contents/tiled_texture_contents.cc b/impeller/entity/contents/tiled_texture_contents.cc index 58a535f7c3b6f..18c4691dac83f 100644 --- a/impeller/entity/contents/tiled_texture_contents.cc +++ b/impeller/entity/contents/tiled_texture_contents.cc @@ -70,7 +70,8 @@ bool TiledTextureContents::Render(const ContentContext& renderer, auto allocator = renderer.GetContext()->GetResourceAllocator(); auto geometry_result = GetGeometry()->GetPositionBuffer( allocator, host_buffer, renderer.GetTessellator(), - pass.GetRenderTargetSize()); + pass.GetRenderTargetSize(), + entity.GetTransformation().GetMaxBasisLength()); cmd.BindVertices(geometry_result.vertex_buffer); cmd.primitive_type = geometry_result.type; VS::BindVertInfo(cmd, host_buffer.EmplaceUniform(vert_info)); diff --git a/impeller/entity/contents/vertices_contents.cc b/impeller/entity/contents/vertices_contents.cc index 31f1f48c5381e..e1a2f50fe934b 100644 --- a/impeller/entity/contents/vertices_contents.cc +++ b/impeller/entity/contents/vertices_contents.cc @@ -72,7 +72,8 @@ bool VerticesContents::Render(const ContentContext& renderer, auto geometry_result = geometry_->GetPositionBuffer( allocator, host_buffer, renderer.GetTessellator(), - pass.GetRenderTargetSize()); + pass.GetRenderTargetSize(), + entity.GetTransformation().GetMaxBasisLength()); cmd.pipeline = renderer.GetGeometryPositionPipeline( OptionsFromPassAndEntity(pass, entity)); cmd.primitive_type = geometry_result.type; diff --git a/impeller/entity/geometry.cc b/impeller/entity/geometry.cc index b24452d8b5173..35c3d9f1bbbaa 100644 --- a/impeller/entity/geometry.cc +++ b/impeller/entity/geometry.cc @@ -48,7 +48,8 @@ GeometryResult VerticesGeometry::GetPositionBuffer( std::shared_ptr device_allocator, HostBuffer& host_buffer, std::shared_ptr tessellator, - ISize render_target_size) { + ISize render_target_size, + Scalar max_basis_length) { if (!vertices_.IsValid()) { return {}; } @@ -182,10 +183,12 @@ GeometryResult PathGeometry::GetPositionBuffer( std::shared_ptr device_allocator, HostBuffer& host_buffer, std::shared_ptr tessellator, - ISize render_target_size) { + ISize render_target_size, + Scalar max_basis_length) { VertexBuffer vertex_buffer; auto tesselation_result = tessellator->Tessellate( - path_.GetFillType(), path_.CreatePolyline(), + path_.GetFillType(), + path_.CreatePolyline(kDefaultCurveTolerance / max_basis_length), [&vertex_buffer, &host_buffer]( const float* vertices, size_t vertices_count, const uint16_t* indices, size_t indices_count) { @@ -244,7 +247,8 @@ GeometryResult CoverGeometry::GetPositionBuffer( std::shared_ptr device_allocator, HostBuffer& host_buffer, std::shared_ptr tessellator, - ISize render_target_size) { + ISize render_target_size, + Scalar max_basis_length) { auto rect = Rect(Size(render_target_size)); constexpr uint16_t kRectIndicies[4] = {0, 1, 2, 3}; return GeometryResult{ diff --git a/impeller/entity/geometry.h b/impeller/entity/geometry.h index 293140d2a20b3..666da614f6317 100644 --- a/impeller/entity/geometry.h +++ b/impeller/entity/geometry.h @@ -43,7 +43,8 @@ class Geometry { std::shared_ptr device_allocator, HostBuffer& host_buffer, std::shared_ptr tessellator, - ISize render_target_size) = 0; + ISize render_target_size, + Scalar max_basis_length) = 0; virtual GeometryResult GetPositionColorBuffer( std::shared_ptr device_allocator, @@ -75,7 +76,8 @@ class VerticesGeometry : public Geometry { GeometryResult GetPositionBuffer(std::shared_ptr device_allocator, HostBuffer& host_buffer, std::shared_ptr tessellator, - ISize render_target_size) override; + ISize render_target_size, + Scalar max_basis_length) override; // |Geometry| GeometryResult GetPositionColorBuffer( @@ -115,7 +117,8 @@ class PathGeometry : public Geometry { GeometryResult GetPositionBuffer(std::shared_ptr device_allocator, HostBuffer& host_buffer, std::shared_ptr tessellator, - ISize render_target_size) override; + ISize render_target_size, + Scalar max_basis_length) override; // |Geometry| GeometryResult GetPositionColorBuffer( @@ -156,7 +159,8 @@ class CoverGeometry : public Geometry { GeometryResult GetPositionBuffer(std::shared_ptr device_allocator, HostBuffer& host_buffer, std::shared_ptr tessellator, - ISize render_target_size) override; + ISize render_target_size, + Scalar max_basis_length) override; // |Geometry| GeometryResult GetPositionColorBuffer( diff --git a/impeller/geometry/geometry_unittests.cc b/impeller/geometry/geometry_unittests.cc index a199c689d77fd..f88df791d6bff 100644 --- a/impeller/geometry/geometry_unittests.cc +++ b/impeller/geometry/geometry_unittests.cc @@ -17,6 +17,7 @@ #include "impeller/geometry/rect.h" #include "impeller/geometry/scalar.h" #include "impeller/geometry/size.h" +#include "path_component.h" namespace impeller { namespace testing { @@ -1426,8 +1427,7 @@ TEST(GeometryTest, RectGetPositive) { TEST(GeometryTest, CubicPathComponentPolylineDoesNotIncludePointOne) { CubicPathComponent component({10, 10}, {20, 35}, {35, 20}, {40, 40}); - SmoothingApproximation approximation; - auto polyline = component.CreatePolyline(approximation); + auto polyline = component.CreatePolyline(); ASSERT_NE(polyline.front().x, 10); ASSERT_NE(polyline.front().y, 10); ASSERT_EQ(polyline.back().x, 40); diff --git a/impeller/geometry/path.cc b/impeller/geometry/path.cc index 7e632d7287989..6b2ecf847b614 100644 --- a/impeller/geometry/path.cc +++ b/impeller/geometry/path.cc @@ -221,8 +221,7 @@ bool Path::UpdateContourComponentAtIndex(size_t index, return true; } -Path::Polyline Path::CreatePolyline( - const SmoothingApproximation& approximation) const { +Path::Polyline Path::CreatePolyline(Scalar tolerance) const { Polyline polyline; std::optional previous_contour_point; @@ -251,10 +250,10 @@ Path::Polyline Path::CreatePolyline( collect_points(linears_[component.index].CreatePolyline()); break; case ComponentType::kQuadratic: - collect_points(quads_[component.index].CreatePolyline(approximation)); + collect_points(quads_[component.index].CreatePolyline(tolerance)); break; case ComponentType::kCubic: - collect_points(cubics_[component.index].CreatePolyline(approximation)); + collect_points(cubics_[component.index].CreatePolyline(tolerance)); break; case ComponentType::kContour: if (component_i == components_.size() - 1) { diff --git a/impeller/geometry/path.h b/impeller/geometry/path.h index 77a20fda0ef6a..22879e5de3751 100644 --- a/impeller/geometry/path.h +++ b/impeller/geometry/path.h @@ -117,8 +117,7 @@ class Path { bool UpdateContourComponentAtIndex(size_t index, const ContourComponent& contour); - Polyline CreatePolyline( - const SmoothingApproximation& approximation = {}) const; + Polyline CreatePolyline(Scalar tolerance = kDefaultCurveTolerance) const; std::optional GetBoundingBox() const; diff --git a/impeller/geometry/path_component.cc b/impeller/geometry/path_component.cc index f9c208386e4a9..04eb592cb6ac5 100644 --- a/impeller/geometry/path_component.cc +++ b/impeller/geometry/path_component.cc @@ -8,10 +8,6 @@ namespace impeller { -static const size_t kRecursionLimit = 32; -static const Scalar kCurveCollinearityEpsilon = 1e-30; -static const Scalar kCurveAngleToleranceEpsilon = 0.01; - /* * Based on: https://en.wikipedia.org/wiki/B%C3%A9zier_curve#Specific_cases */ @@ -85,10 +81,57 @@ Point QuadraticPathComponent::SolveDerivative(Scalar time) const { }; } +static Scalar ApproximateParabolaIntegral(Scalar x) { + constexpr Scalar d = 0.67; + return x / (1.0 - d + sqrt(sqrt(pow(d, 4) + 0.25 * x * x))); +} + std::vector QuadraticPathComponent::CreatePolyline( - const SmoothingApproximation& approximation) const { - CubicPathComponent elevated(*this); - return elevated.CreatePolyline(approximation); + Scalar tolerance) const { + std::vector points; + FillPointsForPolyline(points, tolerance); + return points; +} + +void QuadraticPathComponent::FillPointsForPolyline(std::vector& points, + Scalar tolerance) const { + auto sqrt_tolerance = sqrt(tolerance); + + auto d01 = cp - p1; + auto d12 = p2 - cp; + auto dd = d01 - d12; + auto cross = (p2 - p1).Cross(dd); + auto x0 = d01.Dot(dd) * 1 / cross; + auto x2 = d12.Dot(dd) * 1 / cross; + auto scale = abs(cross / (hypot(dd.x, dd.y) * (x2 - x0))); + + auto a0 = ApproximateParabolaIntegral(x0); + auto a2 = ApproximateParabolaIntegral(x2); + Scalar val = 0.f; + if (std::isfinite(scale)) { + auto da = abs(a2 - a0); + auto sqrt_scale = sqrt(scale); + if ((x0 < 0 && x2 < 0) || (x0 >= 0 && x2 >= 0)) { + val = da * sqrt_scale; + } else { + // cusp case + auto xmin = sqrt_tolerance / sqrt_scale; + val = sqrt_tolerance * da / ApproximateParabolaIntegral(xmin); + } + } + auto u0 = ApproximateParabolaIntegral(a0); + auto u2 = ApproximateParabolaIntegral(a2); + auto uscale = 1 / (u2 - u0); + + auto line_count = std::max(1., ceil(0.5 * val / sqrt_tolerance)); + auto step = 1 / line_count; + for (size_t i = 1; i < line_count; i += 1) { + auto u = i * step; + auto a = a0 + (a2 - a0) * u; + auto t = (ApproximateParabolaIntegral(a) - u0) * uscale; + points.emplace_back(Solve(t)); + } + points.emplace_back(p2); } std::vector QuadraticPathComponent::Extrema() const { @@ -110,238 +153,61 @@ Point CubicPathComponent::SolveDerivative(Scalar time) const { }; } -/* - * Paul de Casteljau's subdivision with modifications as described in - * http://agg.sourceforge.net/antigrain.com/research/adaptive_bezier/index.html. - * Refer to the diagram on that page for a description of the points. - */ -static void CubicPathSmoothenRecursive(const SmoothingApproximation& approx, - std::vector& points, - Point p1, - Point p2, - Point p3, - Point p4, - size_t level) { - if (level >= kRecursionLimit) { - return; +std::vector CubicPathComponent::CreatePolyline(Scalar tolerance) const { + auto quads = ToQuadraticPathComponents(.1); + std::vector points; + for (const auto& quad : quads) { + quad.FillPointsForPolyline(points, tolerance); } + return points; +} - /* - * Find all midpoints. - */ - auto p12 = (p1 + p2) / 2.0; - auto p23 = (p2 + p3) / 2.0; - auto p34 = (p3 + p4) / 2.0; - - auto p123 = (p12 + p23) / 2.0; - auto p234 = (p23 + p34) / 2.0; - - auto p1234 = (p123 + p234) / 2.0; - - /* - * Attempt approximation using single straight line. - */ - auto d = p4 - p1; - Scalar d2 = fabs(((p2.x - p4.x) * d.y - (p2.y - p4.y) * d.x)); - Scalar d3 = fabs(((p3.x - p4.x) * d.y - (p3.y - p4.y) * d.x)); - - Scalar da1 = 0; - Scalar da2 = 0; - Scalar k = 0; - - switch ((static_cast(d2 > kCurveCollinearityEpsilon) << 1) + - static_cast(d3 > kCurveCollinearityEpsilon)) { - case 0: - /* - * All collinear OR p1 == p4. - */ - k = d.x * d.x + d.y * d.y; - if (k == 0) { - d2 = p1.GetDistanceSquared(p2); - d3 = p4.GetDistanceSquared(p3); - } else { - k = 1.0 / k; - da1 = p2.x - p1.x; - da2 = p2.y - p1.y; - d2 = k * (da1 * d.x + da2 * d.y); - da1 = p3.x - p1.x; - da2 = p3.y - p1.y; - d3 = k * (da1 * d.x + da2 * d.y); - - if (d2 > 0 && d2 < 1 && d3 > 0 && d3 < 1) { - /* - * Simple collinear case, 1---2---3---4. Leave just two endpoints. - */ - return; - } - - if (d2 <= 0) { - d2 = p2.GetDistanceSquared(p1); - } else if (d2 >= 1) { - d2 = p2.GetDistanceSquared(p4); - } else { - d2 = p2.GetDistanceSquared({p1.x + d2 * d.x, p1.y + d2 * d.y}); - } - - if (d3 <= 0) { - d3 = p3.GetDistanceSquared(p1); - } else if (d3 >= 1) { - d3 = p3.GetDistanceSquared(p4); - } else { - d3 = p3.GetDistanceSquared({p1.x + d3 * d.x, p1.y + d3 * d.y}); - } - } - - if (d2 > d3) { - if (d2 < approx.distance_tolerance_square) { - points.emplace_back(p2); - return; - } - } else { - if (d3 < approx.distance_tolerance_square) { - points.emplace_back(p3); - return; - } - } - break; - case 1: - /* - * p1, p2, p4 are collinear, p3 is significant. - */ - if (d3 * d3 <= - approx.distance_tolerance_square * (d.x * d.x + d.y * d.y)) { - if (approx.angle_tolerance < kCurveAngleToleranceEpsilon) { - points.emplace_back(p23); - return; - } - - /* - * Angle Condition. - */ - da1 = ::fabs(::atan2(p4.y - p3.y, p4.x - p3.x) - - ::atan2(p3.y - p2.y, p3.x - p2.x)); - - if (da1 >= kPi) { - da1 = 2.0 * kPi - da1; - } - - if (da1 < approx.angle_tolerance) { - points.emplace_back(p2); - points.emplace_back(p3); - return; - } - - if (approx.cusp_limit != 0.0) { - if (da1 > approx.cusp_limit) { - points.emplace_back(p3); - return; - } - } - } - break; - - case 2: - /* - * p1,p3,p4 are collinear, p2 is significant. - */ - if (d2 * d2 <= - approx.distance_tolerance_square * (d.x * d.x + d.y * d.y)) { - if (approx.angle_tolerance < kCurveAngleToleranceEpsilon) { - points.emplace_back(p23); - return; - } - - /* - * Angle Condition. - */ - da1 = ::fabs(::atan2(p3.y - p2.y, p3.x - p2.x) - - ::atan2(p2.y - p1.y, p2.x - p1.x)); - - if (da1 >= kPi) { - da1 = 2.0 * kPi - da1; - } - - if (da1 < approx.angle_tolerance) { - points.emplace_back(p2); - points.emplace_back(p3); - return; - } - - if (approx.cusp_limit != 0.0) { - if (da1 > approx.cusp_limit) { - points.emplace_back(p2); - return; - } - } - } - break; - - case 3: - /* - * Regular case. - */ - if ((d2 + d3) * (d2 + d3) <= - approx.distance_tolerance_square * (d.x * d.x + d.y * d.y)) { - /* - * If the curvature doesn't exceed the distance_tolerance value - * we tend to finish subdivisions. - */ - if (approx.angle_tolerance < kCurveAngleToleranceEpsilon) { - points.emplace_back(p23); - return; - } - - /* - * Angle & Cusp Condition. - */ - k = ::atan2(p3.y - p2.y, p3.x - p2.x); - da1 = ::fabs(k - ::atan2(p2.y - p1.y, p2.x - p1.x)); - da2 = ::fabs(::atan2(p4.y - p3.y, p4.x - p3.x) - k); - - if (da1 >= kPi) { - da1 = 2.0 * kPi - da1; - } - - if (da2 >= kPi) { - da2 = 2.0 * kPi - da2; - } - - if (da1 + da2 < approx.angle_tolerance) { - /* - * Finally we can stop the recursion. - */ - points.emplace_back(p23); - return; - } - - if (approx.cusp_limit != 0.0) { - if (da1 > approx.cusp_limit) { - points.emplace_back(p2); - return; - } - - if (da2 > approx.cusp_limit) { - points.emplace_back(p3); - return; - } - } - } - break; - } +inline QuadraticPathComponent CubicPathComponent::Lower() const { + return QuadraticPathComponent(3.0 * (cp1 - p1), 3.0 * (cp2 - cp1), + 3.0 * (p2 - cp2)); +} - /* - * Continue subdivision. - */ - CubicPathSmoothenRecursive(approx, points, p1, p12, p123, p1234, level + 1); - CubicPathSmoothenRecursive(approx, points, p1234, p234, p34, p4, level + 1); +CubicPathComponent CubicPathComponent::Subsegment(Scalar t0, Scalar t1) const { + auto p0 = Solve(t0); + auto p3 = Solve(t1); + auto d = Lower(); + auto scale = (t1 - t0) * (1.0 / 3.0); + auto p1 = p0 + scale * d.Solve(t0); + auto p2 = p3 - scale * d.Solve(t1); + return CubicPathComponent(p0, p1, p2, p3); } -std::vector CubicPathComponent::CreatePolyline( - const SmoothingApproximation& approximation) const { - std::vector points; - CubicPathSmoothenRecursive(approximation, points, p1, cp1, cp2, p2, 0); - points.emplace_back(p2); - return points; +std::vector +CubicPathComponent::ToQuadraticPathComponents(Scalar accuracy) const { + std::vector quads; + // The maximum error, as a vector from the cubic to the best approximating + // quadratic, is proportional to the third derivative, which is constant + // across the segment. Thus, the error scales down as the third power of + // the number of subdivisions. Our strategy then is to subdivide `t` evenly. + // + // This is an overestimate of the error because only the component + // perpendicular to the first derivative is important. But the simplicity is + // appealing. + + // This magic number is the square of 36 / sqrt(3). + // See: http://caffeineowl.com/graphics/2d/vectorial/cubic2quad01.html + auto max_hypot2 = 432.0 * accuracy * accuracy; + auto p1x2 = 3.0 * cp1 - p1; + auto p2x2 = 3.0 * cp2 - p2; + auto p = p2x2 - p1x2; + auto err = p.Dot(p); + auto quad_count = std::max(1., ceil(pow(err / max_hypot2, 1. / 6.0))); + + for (size_t i = 0; i < quad_count; i++) { + auto t0 = i / quad_count; + auto t1 = (i + 1) / quad_count; + auto seg = Subsegment(t0, t1); + auto p1x2 = 3.0 * seg.cp1 - seg.p1; + auto p2x2 = 3.0 * seg.cp2 - seg.p2; + quads.emplace_back( + QuadraticPathComponent(seg.p1, ((p1x2 + p2x2) / 4.0), seg.p2)); + } + return quads; } static inline bool NearEqual(Scalar a, Scalar b, Scalar epsilon) { diff --git a/impeller/geometry/path_component.h b/impeller/geometry/path_component.h index d1cdf3a0b0907..df658eaf580ce 100644 --- a/impeller/geometry/path_component.h +++ b/impeller/geometry/path_component.h @@ -12,46 +12,15 @@ namespace impeller { -/// Information about how to approximate points on a curved path segment. -/// -/// In particular, the values in this object control how many vertices to -/// generate when approximating curves, and what tolerances to use when -/// calculating the sharpness of curves. -struct SmoothingApproximation { - /// The scaling coefficient to use when translating to screen coordinates. - /// - /// Values approaching 0.0 will generate smoother looking curves with a - /// greater number of vertices, and will be more expensive to calculate. - Scalar scale; - - /// The tolerance value in radians for calculating sharp angles. - /// - /// Values approaching 0.0 will provide more accurate approximation of sharp - /// turns. A 0.0 value means angle conditions are not considered at all. - Scalar angle_tolerance; - - /// An angle in radians at which to introduce bevel cuts. - /// - /// Values greater than zero will restirct the sharpness of bevel cuts on - /// turns. - Scalar cusp_limit; - - /// Used to more quickly detect colinear cases. - Scalar distance_tolerance_square; - - SmoothingApproximation(/* default */) - : SmoothingApproximation(1.0 /* scale */, - 0.0 /* angle tolerance */, - 0.0 /* cusp limit */) {} - - SmoothingApproximation(Scalar p_scale, - Scalar p_angle_tolerance, - Scalar p_cusp_limit) - : scale(p_scale), - angle_tolerance(p_angle_tolerance), - cusp_limit(p_cusp_limit), - distance_tolerance_square(0.5 * p_scale * 0.5 * p_scale) {} -}; +// The default tolerance value for QuadraticCurveComponent::CreatePolyline and +// CubicCurveComponent::CreatePolyline. It also impacts the number of quadratics +// created when flattening a cubic curve to a polyline. +// +// Smaller numbers mean more points. This number seems suitable for particularly +// curvy curves at scales close to 1.0. As the scale increases, this number +// should be divided by Matrix::GetMaxBasisLength to avoid generating too few +// points for the given scale. +static constexpr Scalar kDefaultCurveTolerance = .1f; struct LinearPathComponent { Point p1; @@ -86,8 +55,21 @@ struct QuadraticPathComponent { Point SolveDerivative(Scalar time) const; + // Uses the algorithm described by Raph Levien in + // https://raphlinus.github.io/graphics/curves/2019/12/23/flatten-quadbez.html. + // + // The algorithm has several benefits: + // - It does not require elevation to cubics for processing. + // - It generates fewer and more accurate points than recursive subdivision. + // - Each turn of the core iteration loop has no dependencies on other turns, + // making it trivially parallelizable. + // + // See also the implementation in kurbo: https://github.com/linebender/kurbo. std::vector CreatePolyline( - const SmoothingApproximation& approximation) const; + Scalar tolerance = kDefaultCurveTolerance) const; + + void FillPointsForPolyline(std::vector& points, + Scalar tolerance = kDefaultCurveTolerance) const; std::vector Extrema() const; @@ -117,15 +99,27 @@ struct CubicPathComponent { Point SolveDerivative(Scalar time) const; + // This method approximates the cubic component with quadratics, and then + // generates a polyline from those quadratics. + // + // See the note on QuadraticPathComponent::CreatePolyline for references. std::vector CreatePolyline( - const SmoothingApproximation& approximation) const; + Scalar tolerance = kDefaultCurveTolerance) const; std::vector Extrema() const; + std::vector ToQuadraticPathComponents( + Scalar accuracy) const; + + CubicPathComponent Subsegment(Scalar t0, Scalar t1) const; + bool operator==(const CubicPathComponent& other) const { return p1 == other.p1 && cp1 == other.cp1 && cp2 == other.cp2 && p2 == other.p2; } + + private: + QuadraticPathComponent Lower() const; }; struct ContourComponent { diff --git a/impeller/tessellator/c/tessellator.cc b/impeller/tessellator/c/tessellator.cc index 86a4cdfe85cee..1bdbfde66db84 100644 --- a/impeller/tessellator/c/tessellator.cc +++ b/impeller/tessellator/c/tessellator.cc @@ -39,12 +39,9 @@ void Close(PathBuilder* builder) { struct Vertices* Tessellate(PathBuilder* builder, int fill_type, - Scalar scale, - Scalar angle_tolerance, - Scalar cusp_limit) { + Scalar tolerance) { auto path = builder->CopyPath(static_cast(fill_type)); - auto smoothing = SmoothingApproximation(scale, angle_tolerance, cusp_limit); - auto polyline = path.CreatePolyline(smoothing); + auto polyline = path.CreatePolyline(tolerance); std::vector points; if (Tessellator{}.Tessellate( path.GetFillType(), polyline, diff --git a/impeller/tessellator/c/tessellator.h b/impeller/tessellator/c/tessellator.h index 23a24e1f9603f..7a323b5413233 100644 --- a/impeller/tessellator/c/tessellator.h +++ b/impeller/tessellator/c/tessellator.h @@ -42,9 +42,7 @@ IMPELLER_API void Close(PathBuilder* builder); IMPELLER_API struct Vertices* Tessellate(PathBuilder* builder, int fill_type, - Scalar scale, - Scalar angle_tolerance, - Scalar cusp_limit); + Scalar tolerance); IMPELLER_API void DestroyVertices(Vertices* vertices);