From 2420fb0f93d181f4ff983a8051cbb81566964c2e Mon Sep 17 00:00:00 2001 From: Jim Graham Date: Thu, 30 Nov 2023 12:54:06 -0800 Subject: [PATCH] [Impeller] Add direct tesselation of circles for DrawCircle and Round end caps (#48103) Note: The GPU tesselation for drawPoints is disabled in this test PR, but will be re-enabled when I can run some benchmarks to see where a good cutoff exists. Tessellation for circular or quarter circular paths is now handled by a single tessellator class that provides services for DrawCircle, DrawPoints (round caps), and DrawLines (round caps). Creating a general path is avoided along with the associated overhead of recognizing and using a generic tessellation step. --- ci/licenses_golden/excluded_files | 2 + ci/licenses_golden/licenses_flutter | 12 + impeller/aiks/aiks_unittests.cc | 26 +++ impeller/aiks/canvas.cc | 40 ++-- impeller/entity/BUILD.gn | 2 + impeller/entity/geometry/ellipse_geometry.cc | 130 +++++++++++ impeller/entity/geometry/ellipse_geometry.h | 68 ++++++ impeller/entity/geometry/geometry.cc | 5 + impeller/entity/geometry/geometry.h | 2 + impeller/entity/geometry/line_geometry.cc | 157 ++++++++++--- impeller/entity/geometry/line_geometry.h | 5 + .../entity/geometry/point_field_geometry.cc | 83 ++++--- impeller/geometry/BUILD.gn | 3 + impeller/geometry/trig.cc | 11 + impeller/geometry/trig.h | 32 +++ impeller/geometry/trig_unittests.cc | 64 ++++++ impeller/renderer/vertex_buffer_builder.h | 5 + impeller/tessellator/BUILD.gn | 16 +- impeller/tessellator/circle_tessellator.cc | 211 ++++++++++++++++++ impeller/tessellator/circle_tessellator.h | 132 +++++++++++ .../circle_tessellator_unittests.cc | 97 ++++++++ impeller/tessellator/tessellator.h | 6 + 22 files changed, 1018 insertions(+), 91 deletions(-) create mode 100644 impeller/entity/geometry/ellipse_geometry.cc create mode 100644 impeller/entity/geometry/ellipse_geometry.h create mode 100644 impeller/geometry/trig.cc create mode 100644 impeller/geometry/trig.h create mode 100644 impeller/geometry/trig_unittests.cc create mode 100644 impeller/tessellator/circle_tessellator.cc create mode 100644 impeller/tessellator/circle_tessellator.h create mode 100644 impeller/tessellator/circle_tessellator_unittests.cc diff --git a/ci/licenses_golden/excluded_files b/ci/licenses_golden/excluded_files index 4a11ee1be417e..3cbe03f25dbda 100644 --- a/ci/licenses_golden/excluded_files +++ b/ci/licenses_golden/excluded_files @@ -156,6 +156,7 @@ ../../../flutter/impeller/geometry/path_unittests.cc ../../../flutter/impeller/geometry/rect_unittests.cc ../../../flutter/impeller/geometry/size_unittests.cc +../../../flutter/impeller/geometry/trig_unittests.cc ../../../flutter/impeller/golden_tests/README.md ../../../flutter/impeller/golden_tests_harvester/.dart_tool ../../../flutter/impeller/golden_tests_harvester/.gitignore @@ -193,6 +194,7 @@ ../../../flutter/impeller/scene/importer/importer_unittests.cc ../../../flutter/impeller/scene/scene_unittests.cc ../../../flutter/impeller/shader_archive/shader_archive_unittests.cc +../../../flutter/impeller/tessellator/circle_tessellator_unittests.cc ../../../flutter/impeller/tessellator/dart/.dart_tool ../../../flutter/impeller/tessellator/dart/pubspec.lock ../../../flutter/impeller/tessellator/dart/pubspec.yaml diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 22d3e68741eb6..5e94ca5d1e55c 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -5101,6 +5101,8 @@ ORIGIN: ../../../flutter/impeller/entity/entity_playground.cc + ../../../flutter ORIGIN: ../../../flutter/impeller/entity/entity_playground.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/impeller/entity/geometry/cover_geometry.cc + ../../../flutter/LICENSE ORIGIN: ../../../flutter/impeller/entity/geometry/cover_geometry.h + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/impeller/entity/geometry/ellipse_geometry.cc + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/impeller/entity/geometry/ellipse_geometry.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/impeller/entity/geometry/fill_path_geometry.cc + ../../../flutter/LICENSE ORIGIN: ../../../flutter/impeller/entity/geometry/fill_path_geometry.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/impeller/entity/geometry/geometry.cc + ../../../flutter/LICENSE @@ -5206,6 +5208,8 @@ ORIGIN: ../../../flutter/impeller/geometry/sigma.cc + ../../../flutter/LICENSE ORIGIN: ../../../flutter/impeller/geometry/sigma.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/impeller/geometry/size.cc + ../../../flutter/LICENSE ORIGIN: ../../../flutter/impeller/geometry/size.h + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/impeller/geometry/trig.cc + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/impeller/geometry/trig.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/impeller/geometry/type_traits.cc + ../../../flutter/LICENSE ORIGIN: ../../../flutter/impeller/geometry/type_traits.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/impeller/geometry/vector.cc + ../../../flutter/LICENSE @@ -5523,6 +5527,8 @@ ORIGIN: ../../../flutter/impeller/shader_archive/shader_archive_writer.cc + ../. ORIGIN: ../../../flutter/impeller/shader_archive/shader_archive_writer.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/impeller/tessellator/c/tessellator.cc + ../../../flutter/LICENSE ORIGIN: ../../../flutter/impeller/tessellator/c/tessellator.h + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/impeller/tessellator/circle_tessellator.cc + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/impeller/tessellator/circle_tessellator.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/impeller/tessellator/dart/lib/tessellator.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/impeller/tessellator/tessellator.cc + ../../../flutter/LICENSE ORIGIN: ../../../flutter/impeller/tessellator/tessellator.h + ../../../flutter/LICENSE @@ -7893,6 +7899,8 @@ FILE: ../../../flutter/impeller/entity/entity_playground.cc FILE: ../../../flutter/impeller/entity/entity_playground.h FILE: ../../../flutter/impeller/entity/geometry/cover_geometry.cc FILE: ../../../flutter/impeller/entity/geometry/cover_geometry.h +FILE: ../../../flutter/impeller/entity/geometry/ellipse_geometry.cc +FILE: ../../../flutter/impeller/entity/geometry/ellipse_geometry.h FILE: ../../../flutter/impeller/entity/geometry/fill_path_geometry.cc FILE: ../../../flutter/impeller/entity/geometry/fill_path_geometry.h FILE: ../../../flutter/impeller/entity/geometry/geometry.cc @@ -7998,6 +8006,8 @@ FILE: ../../../flutter/impeller/geometry/sigma.cc FILE: ../../../flutter/impeller/geometry/sigma.h FILE: ../../../flutter/impeller/geometry/size.cc FILE: ../../../flutter/impeller/geometry/size.h +FILE: ../../../flutter/impeller/geometry/trig.cc +FILE: ../../../flutter/impeller/geometry/trig.h FILE: ../../../flutter/impeller/geometry/type_traits.cc FILE: ../../../flutter/impeller/geometry/type_traits.h FILE: ../../../flutter/impeller/geometry/vector.cc @@ -8316,6 +8326,8 @@ FILE: ../../../flutter/impeller/shader_archive/shader_archive_writer.cc FILE: ../../../flutter/impeller/shader_archive/shader_archive_writer.h FILE: ../../../flutter/impeller/tessellator/c/tessellator.cc FILE: ../../../flutter/impeller/tessellator/c/tessellator.h +FILE: ../../../flutter/impeller/tessellator/circle_tessellator.cc +FILE: ../../../flutter/impeller/tessellator/circle_tessellator.h FILE: ../../../flutter/impeller/tessellator/dart/lib/tessellator.dart FILE: ../../../flutter/impeller/tessellator/tessellator.cc FILE: ../../../flutter/impeller/tessellator/tessellator.h diff --git a/impeller/aiks/aiks_unittests.cc b/impeller/aiks/aiks_unittests.cc index 81c5ed7aaf48b..da402f0d30cce 100644 --- a/impeller/aiks/aiks_unittests.cc +++ b/impeller/aiks/aiks_unittests.cc @@ -2060,6 +2060,32 @@ TEST_P(AiksTest, DrawLinesRenderCorrectly) { ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } +TEST_P(AiksTest, FillCirclesRenderCorrectly) { + Canvas canvas; + canvas.Scale(GetContentScale()); + Paint paint; + const int color_count = 3; + Color colors[color_count] = { + Color::Blue(), + Color::Green(), + Color::Crimson(), + }; + + int c_index = 0; + int radius = 600; + while (radius > 0) { + paint.color = colors[(c_index++) % color_count]; + canvas.DrawCircle({10, 10}, radius, paint); + if (radius > 30) { + radius -= 10; + } else { + radius -= 2; + } + } + + ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); +} + TEST_P(AiksTest, GradientStrokesRenderCorrectly) { // Compare with https://fiddle.skia.org/c/027392122bec8ac2b5d5de00a4b9bbe2 auto callback = [&](AiksContext& renderer) -> std::optional { diff --git a/impeller/aiks/canvas.cc b/impeller/aiks/canvas.cc index 906287c61c8f5..02b215e41c274 100644 --- a/impeller/aiks/canvas.cc +++ b/impeller/aiks/canvas.cc @@ -232,17 +232,6 @@ bool Canvas::AttemptDrawBlurredRRect(const Rect& rect, } void Canvas::DrawLine(const Point& p0, const Point& p1, const Paint& paint) { - if (paint.stroke_cap == Cap::kRound) { - auto path = PathBuilder{} - .AddLine((p0), (p1)) - .SetConvexity(Convexity::kConvex) - .TakePath(); - Paint stroke_paint = paint; - stroke_paint.style = Paint::Style::kStroke; - DrawPath(path, stroke_paint); - return; - } - Entity entity; entity.SetTransform(GetCurrentTransform()); entity.SetClipDepth(GetClipDepth()); @@ -298,20 +287,33 @@ void Canvas::DrawRRect(Rect rect, Point corner_radii, const Paint& paint) { } void Canvas::DrawCircle(Point center, Scalar radius, const Paint& paint) { + if (paint.style == Paint::Style::kStroke) { + auto circle_path = + PathBuilder{} + .AddCircle(center, radius) + .SetConvexity(Convexity::kConvex) + .SetBounds(Rect::MakeLTRB(center.x - radius, center.y - radius, + center.x + radius, center.y + radius)) + .TakePath(); + DrawPath(circle_path, paint); + return; + } + Size half_size(radius, radius); if (AttemptDrawBlurredRRect( Rect::MakeOriginSize(center - half_size, half_size * 2), radius, paint)) { return; } - auto circle_path = - PathBuilder{} - .AddCircle(center, radius) - .SetConvexity(Convexity::kConvex) - .SetBounds(Rect::MakeLTRB(center.x - radius, center.y - radius, - center.x + radius, center.y + radius)) - .TakePath(); - DrawPath(circle_path, paint); + + Entity entity; + entity.SetTransform(GetCurrentTransform()); + entity.SetClipDepth(GetClipDepth()); + entity.SetBlendMode(paint.blend_mode); + entity.SetContents(paint.WithFilters( + paint.CreateContentsForGeometry(Geometry::MakeCircle(center, radius)))); + + GetCurrentPass().AddEntity(entity); } void Canvas::ClipPath(const Path& path, Entity::ClipOperation clip_op) { diff --git a/impeller/entity/BUILD.gn b/impeller/entity/BUILD.gn index d5841d47fd120..24bc4670e94da 100644 --- a/impeller/entity/BUILD.gn +++ b/impeller/entity/BUILD.gn @@ -193,6 +193,8 @@ impeller_component("entity") { "entity_pass_target.h", "geometry/cover_geometry.cc", "geometry/cover_geometry.h", + "geometry/ellipse_geometry.cc", + "geometry/ellipse_geometry.h", "geometry/fill_path_geometry.cc", "geometry/fill_path_geometry.h", "geometry/geometry.cc", diff --git a/impeller/entity/geometry/ellipse_geometry.cc b/impeller/entity/geometry/ellipse_geometry.cc new file mode 100644 index 0000000000000..c0c42f1e8fdc4 --- /dev/null +++ b/impeller/entity/geometry/ellipse_geometry.cc @@ -0,0 +1,130 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "impeller/entity/geometry/ellipse_geometry.h" + +#include "flutter/impeller/tessellator/circle_tessellator.h" + +namespace impeller { + +EllipseGeometry::EllipseGeometry(Point center, Scalar radius) + : center_(center), radius_(radius) { + FML_DCHECK(radius >= 0); +} + +GeometryResult EllipseGeometry::GetPositionBuffer( + const ContentContext& renderer, + const Entity& entity, + RenderPass& pass) const { + auto& host_buffer = pass.GetTransientsBuffer(); + using VT = SolidFillVertexShader::PerVertexData; + + Scalar radius = radius_; + const Point& center = center_; + std::shared_ptr tessellator = renderer.GetTessellator(); + CircleTessellator circle_tessellator(tessellator, entity.GetTransform(), + radius_); + size_t count = circle_tessellator.GetCircleVertexCount(); + auto vertex_buffer = host_buffer.Emplace( + count * sizeof(VT), alignof(VT), + [&circle_tessellator, ¢er, radius](uint8_t* buffer) { + auto vertices = reinterpret_cast(buffer); + circle_tessellator.GenerateCircleTriangleStrip( + [&vertices](const Point& p) { // + *vertices++ = { + .position = p, + }; + }, + center, radius); + }); + + return GeometryResult{ + .type = PrimitiveType::kTriangleStrip, + .vertex_buffer = + { + .vertex_buffer = vertex_buffer, + .vertex_count = count, + .index_type = IndexType::kNone, + }, + .transform = Matrix::MakeOrthographic(pass.GetRenderTargetSize()) * + entity.GetTransform(), + .prevent_overdraw = false, + }; +} + +// |Geometry| +GeometryResult EllipseGeometry::GetPositionUVBuffer( + Rect texture_coverage, + Matrix effect_transform, + const ContentContext& renderer, + const Entity& entity, + RenderPass& pass) const { + auto& host_buffer = pass.GetTransientsBuffer(); + using VT = TextureFillVertexShader::PerVertexData; + auto uv_transform = + texture_coverage.GetNormalizingTransform() * effect_transform; + + Scalar radius = radius_; + const Point& center = center_; + std::shared_ptr tessellator = renderer.GetTessellator(); + CircleTessellator circle_tessellator(tessellator, entity.GetTransform(), + radius_); + size_t count = circle_tessellator.GetCircleVertexCount(); + auto vertex_buffer = host_buffer.Emplace( + count * sizeof(VT), alignof(VT), + [&circle_tessellator, &uv_transform, ¢er, radius](uint8_t* buffer) { + auto vertices = reinterpret_cast(buffer); + circle_tessellator.GenerateCircleTriangleStrip( + [&vertices, &uv_transform](const Point& p) { // + *vertices++ = { + .position = p, + .texture_coords = uv_transform * p, + }; + }, + center, radius); + }); + + return GeometryResult{ + .type = PrimitiveType::kTriangleStrip, + .vertex_buffer = + { + .vertex_buffer = vertex_buffer, + .vertex_count = count, + .index_type = IndexType::kNone, + }, + .transform = Matrix::MakeOrthographic(pass.GetRenderTargetSize()) * + entity.GetTransform(), + .prevent_overdraw = false, + }; +} + +GeometryVertexType EllipseGeometry::GetVertexType() const { + return GeometryVertexType::kPosition; +} + +std::optional EllipseGeometry::GetCoverage( + const Matrix& transform) const { + Point corners[4]{ + {center_.x, center_.y - radius_}, + {center_.x + radius_, center_.y}, + {center_.x, center_.y + radius_}, + {center_.x - radius_, center_.y}, + }; + + for (int i = 0; i < 4; i++) { + corners[i] = transform * corners[i]; + } + return Rect::MakePointBounds(std::begin(corners), std::end(corners)); +} + +bool EllipseGeometry::CoversArea(const Matrix& transform, + const Rect& rect) const { + return false; +} + +bool EllipseGeometry::IsAxisAlignedRect() const { + return false; +} + +} // namespace impeller diff --git a/impeller/entity/geometry/ellipse_geometry.h b/impeller/entity/geometry/ellipse_geometry.h new file mode 100644 index 0000000000000..ecc56331b70ae --- /dev/null +++ b/impeller/entity/geometry/ellipse_geometry.h @@ -0,0 +1,68 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#pragma once + +#include "impeller/entity/geometry/geometry.h" + +namespace impeller { + +class EllipseGeometry final : public Geometry { + public: + explicit EllipseGeometry(Point center, Scalar radius); + + ~EllipseGeometry() = default; + + // |Geometry| + bool CoversArea(const Matrix& transform, const Rect& rect) const override; + + // |Geometry| + bool IsAxisAlignedRect() const override; + + private: + // Computes the 4 corners of a rectangle that defines the line and + // possibly extended endpoints which will be rendered under the given + // transform, and returns true if such a rectangle is defined. + // + // The coordinates will be generated in the original coordinate system + // of the line end points and the transform will only be used to determine + // the minimum line width. + // + // For kButt and kSquare end caps the ends should always be exteded as + // per that decoration, but for kRound caps the ends might be extended + // if the goal is to get a conservative bounds and might not be extended + // if the calling code is planning to draw the round caps on the ends. + // + // @return true if the transform and width were not degenerate + bool ComputeCorners(Point corners[4], + const Matrix& transform, + bool extend_endpoints) const; + + // |Geometry| + GeometryResult GetPositionBuffer(const ContentContext& renderer, + const Entity& entity, + RenderPass& pass) const override; + + // |Geometry| + GeometryVertexType GetVertexType() const override; + + // |Geometry| + std::optional GetCoverage(const Matrix& transform) const override; + + // |Geometry| + GeometryResult GetPositionUVBuffer(Rect texture_coverage, + Matrix effect_transform, + const ContentContext& renderer, + const Entity& entity, + RenderPass& pass) const override; + + Point center_; + Scalar radius_; + + EllipseGeometry(const EllipseGeometry&) = delete; + + EllipseGeometry& operator=(const EllipseGeometry&) = delete; +}; + +} // namespace impeller diff --git a/impeller/entity/geometry/geometry.cc b/impeller/entity/geometry/geometry.cc index 9f787b651e481..4b1a32a6e3fe1 100644 --- a/impeller/entity/geometry/geometry.cc +++ b/impeller/entity/geometry/geometry.cc @@ -8,6 +8,7 @@ #include #include "impeller/entity/geometry/cover_geometry.h" +#include "impeller/entity/geometry/ellipse_geometry.h" #include "impeller/entity/geometry/fill_path_geometry.h" #include "impeller/entity/geometry/line_geometry.h" #include "impeller/entity/geometry/point_field_geometry.h" @@ -118,6 +119,10 @@ std::shared_ptr Geometry::MakeLine(Point p0, return std::make_shared(p0, p1, width, cap); } +std::shared_ptr Geometry::MakeCircle(Point center, Scalar radius) { + return std::make_shared(center, radius); +} + bool Geometry::CoversArea(const Matrix& transform, const Rect& rect) const { return false; } diff --git a/impeller/entity/geometry/geometry.h b/impeller/entity/geometry/geometry.h index 307ce315d1af7..2d1dac945e28e 100644 --- a/impeller/entity/geometry/geometry.h +++ b/impeller/entity/geometry/geometry.h @@ -68,6 +68,8 @@ class Geometry { Scalar width, Cap cap); + static std::shared_ptr MakeCircle(Point center, Scalar radius); + static std::shared_ptr MakePointField(std::vector points, Scalar radius, bool round); diff --git a/impeller/entity/geometry/line_geometry.cc b/impeller/entity/geometry/line_geometry.cc index 6682e56af17a4..2d2dd8317d783 100644 --- a/impeller/entity/geometry/line_geometry.cc +++ b/impeller/entity/geometry/line_geometry.cc @@ -4,40 +4,54 @@ #include "impeller/entity/geometry/line_geometry.h" +#include "flutter/impeller/tessellator/circle_tessellator.h" + namespace impeller { LineGeometry::LineGeometry(Point p0, Point p1, Scalar width, Cap cap) : p0_(p0), p1_(p1), width_(width), cap_(cap) { - // Some of the code below is prepared to deal with things like coverage - // of a line with round caps, but more work is needed to deal with drawing - // the round end caps FML_DCHECK(width >= 0); - FML_DCHECK(cap != Cap::kRound); } -bool LineGeometry::ComputeCorners(Point corners[4], - const Matrix& transform, - bool extend_endpoints) const { +Scalar LineGeometry::ComputeHalfWidth(const Matrix& transform) const { auto determinant = transform.GetDeterminant(); if (determinant == 0) { - return false; + return 0.0f; } Scalar min_size = 1.0f / sqrt(std::abs(determinant)); - Scalar stroke_half_width = std::max(width_, min_size) * 0.5f; + return std::max(width_, min_size) * 0.5f; +} + +Vector2 LineGeometry::ComputeAlongVector(const Matrix& transform, + bool allow_zero_length) const { + Scalar stroke_half_width = ComputeHalfWidth(transform); + if (stroke_half_width < kEhCloseEnough) { + return {}; + } - Point along = p1_ - p0_; + auto along = p1_ - p0_; Scalar length = along.GetLength(); if (length < kEhCloseEnough) { - if (!extend_endpoints) { + if (!allow_zero_length) { // We won't enclose any pixels unless the endpoints are extended - return false; + return {}; } - along = {stroke_half_width, 0}; + return {stroke_half_width, 0}; } else { - along *= stroke_half_width / length; + return along * stroke_half_width / length; + } +} + +bool LineGeometry::ComputeCorners(Point corners[4], + const Matrix& transform, + bool extend_endpoints) const { + auto along = ComputeAlongVector(transform, extend_endpoints); + if (along.IsZero()) { + return false; } - Point across = {along.y, -along.x}; + + auto across = Vector2(along.y, -along.x); corners[0] = p0_ - across; corners[1] = p1_ - across; corners[2] = p0_ + across; @@ -55,19 +69,57 @@ GeometryResult LineGeometry::GetPositionBuffer(const ContentContext& renderer, const Entity& entity, RenderPass& pass) const { auto& host_buffer = pass.GetTransientsBuffer(); - - Point corners[4]; - if (!ComputeCorners(corners, entity.GetTransform(), cap_ == Cap::kSquare)) { - return {}; + using VT = SolidFillVertexShader::PerVertexData; + + auto& transform = entity.GetTransform(); + auto radius = ComputeHalfWidth(transform); + + size_t count; + BufferView vertex_buffer; + if (cap_ == Cap::kRound) { + const Point& p0 = p0_; + const Point& p1 = p1_; + + std::shared_ptr tessellator = renderer.GetTessellator(); + CircleTessellator circle_tessellator(tessellator, entity.GetTransform(), + radius); + count = circle_tessellator.GetCircleVertexCount(); + vertex_buffer = host_buffer.Emplace( + count * sizeof(VT), alignof(VT), + [&circle_tessellator, &p0, &p1, radius](uint8_t* buffer) { + auto vertices = reinterpret_cast(buffer); + circle_tessellator.GenerateRoundCapLineTriangleStrip( + [&vertices](const Point& p) { // + *vertices++ = { + .position = p, + }; + }, + p0, p1, radius); + }); + } else { + Point corners[4]; + if (ComputeCorners(corners, transform, cap_ == Cap::kSquare)) { + count = 4; + vertex_buffer = host_buffer.Emplace( + count * sizeof(VT), alignof(VT), [&corners](uint8_t* buffer) { + auto vertices = reinterpret_cast(buffer); + for (auto& corner : corners) { + *vertices++ = { + .position = corner, + }; + } + }); + } else { + return {}; + } } return GeometryResult{ .type = PrimitiveType::kTriangleStrip, .vertex_buffer = { - .vertex_buffer = host_buffer.Emplace(corners, 8 * sizeof(float), - alignof(float)), - .vertex_count = 4, + .vertex_buffer = vertex_buffer, + .vertex_count = count, .index_type = IndexType::kNone, }, .transform = Matrix::MakeOrthographic(pass.GetRenderTargetSize()) * @@ -83,27 +135,64 @@ GeometryResult LineGeometry::GetPositionUVBuffer(Rect texture_coverage, const Entity& entity, RenderPass& pass) const { auto& host_buffer = pass.GetTransientsBuffer(); + using VT = TextureFillVertexShader::PerVertexData; + + auto& transform = entity.GetTransform(); + auto radius = ComputeHalfWidth(transform); auto uv_transform = texture_coverage.GetNormalizingTransform() * effect_transform; - Point corners[4]; - if (!ComputeCorners(corners, entity.GetTransform(), cap_ == Cap::kSquare)) { - return {}; - } - std::vector data(8); - for (auto i = 0u, j = 0u; i < 8; i += 2, j++) { - data[i] = corners[j]; - data[i + 1] = uv_transform * corners[j]; + size_t count; + BufferView vertex_buffer; + if (cap_ == Cap::kRound) { + const Point& p0 = p0_; + const Point& p1 = p1_; + + std::shared_ptr tessellator = renderer.GetTessellator(); + CircleTessellator circle_tessellator(tessellator, entity.GetTransform(), + radius); + count = circle_tessellator.GetCircleVertexCount(); + vertex_buffer = host_buffer.Emplace( + count * sizeof(VT), alignof(VT), + [&circle_tessellator, &uv_transform, &p0, &p1, + radius](uint8_t* buffer) { + auto vertices = reinterpret_cast(buffer); + circle_tessellator.GenerateRoundCapLineTriangleStrip( + [&vertices, &uv_transform](const Point& p) { // + *vertices++ = { + .position = p, + .texture_coords = uv_transform * p, + }; + }, + p0, p1, radius); + }); + } else { + Point corners[4]; + if (ComputeCorners(corners, transform, cap_ == Cap::kSquare)) { + count = 4; + vertex_buffer = + host_buffer.Emplace(count * sizeof(VT), alignof(VT), + [&uv_transform, &corners](uint8_t* buffer) { + auto vertices = reinterpret_cast(buffer); + for (auto& corner : corners) { + *vertices++ = { + .position = corner, + .texture_coords = uv_transform * corner, + }; + } + }); + } else { + return {}; + } } return GeometryResult{ .type = PrimitiveType::kTriangleStrip, .vertex_buffer = { - .vertex_buffer = host_buffer.Emplace( - data.data(), 16 * sizeof(float), alignof(float)), - .vertex_count = 4, + .vertex_buffer = vertex_buffer, + .vertex_count = count, .index_type = IndexType::kNone, }, .transform = Matrix::MakeOrthographic(pass.GetRenderTargetSize()) * @@ -137,7 +226,7 @@ bool LineGeometry::CoversArea(const Matrix& transform, const Rect& rect) const { } bool LineGeometry::IsAxisAlignedRect() const { - return p0_.x == p1_.x || p0_.y == p1_.y; + return cap_ != Cap::kRound && (p0_.x == p1_.x || p0_.y == p1_.y); } } // namespace impeller diff --git a/impeller/entity/geometry/line_geometry.h b/impeller/entity/geometry/line_geometry.h index fe37de6a62f56..272702d91a42a 100644 --- a/impeller/entity/geometry/line_geometry.h +++ b/impeller/entity/geometry/line_geometry.h @@ -40,6 +40,11 @@ class LineGeometry final : public Geometry { const Matrix& transform, bool extend_endpoints) const; + Vector2 ComputeAlongVector(const Matrix& transform, + bool allow_zero_length) const; + + Scalar ComputeHalfWidth(const Matrix& transform) const; + // |Geometry| GeometryResult GetPositionBuffer(const ContentContext& renderer, const Entity& entity, diff --git a/impeller/entity/geometry/point_field_geometry.cc b/impeller/entity/geometry/point_field_geometry.cc index c1f08c501e2dc..dfc97059dac20 100644 --- a/impeller/entity/geometry/point_field_geometry.cc +++ b/impeller/entity/geometry/point_field_geometry.cc @@ -4,6 +4,7 @@ #include "impeller/entity/geometry/point_field_geometry.h" +#include "flutter/impeller/tessellator/circle_tessellator.h" #include "impeller/renderer/command_buffer.h" #include "impeller/renderer/compute_command.h" @@ -28,7 +29,7 @@ GeometryResult PointFieldGeometry::GetPositionBuffer( auto& host_buffer = pass.GetTransientsBuffer(); return { - .type = PrimitiveType::kTriangle, + .type = PrimitiveType::kTriangleStrip, .vertex_buffer = vtx_builder->CreateVertexBuffer(host_buffer), .transform = Matrix::MakeOrthographic(pass.GetRenderTargetSize()) * entity.GetTransform(), @@ -56,7 +57,7 @@ GeometryResult PointFieldGeometry::GetPositionUVBuffer( auto& host_buffer = pass.GetTransientsBuffer(); return { - .type = PrimitiveType::kTriangle, + .type = PrimitiveType::kTriangleStrip, .vertex_buffer = uv_vtx_builder.CreateVertexBuffer(host_buffer), .transform = Matrix::MakeOrthographic(pass.GetRenderTargetSize()) * entity.GetTransform(), @@ -79,44 +80,54 @@ PointFieldGeometry::GetPositionBufferCPU(const ContentContext& renderer, Scalar min_size = 1.0f / sqrt(std::abs(determinant)); Scalar radius = std::max(radius_, min_size); - auto vertices_per_geom = ComputeCircleDivisions( - entity.GetTransform().GetMaxBasisLength() * radius, round_); - auto points_per_circle = 3 + (vertices_per_geom - 3) * 3; - auto total = points_per_circle * points_.size(); - auto radian_start = round_ ? 0.0f : 0.785398f; - auto radian_step = k2Pi / vertices_per_geom; - VertexBufferBuilder vtx_builder; - vtx_builder.Reserve(total); - - /// Precompute all relative points and angles for a fixed geometry size. - auto elapsed_angle = radian_start; - std::vector angle_table(vertices_per_geom); - for (auto i = 0u; i < vertices_per_geom; i++) { - angle_table[i] = Point(cos(elapsed_angle), sin(elapsed_angle)) * radius; - elapsed_angle += radian_step; - } - for (auto i = 0u; i < points_.size(); i++) { - auto center = points_[i]; - - auto origin = center + angle_table[0]; - vtx_builder.AppendVertex({origin}); - - auto pt1 = center + angle_table[1]; - vtx_builder.AppendVertex({pt1}); - - auto pt2 = center + angle_table[2]; - vtx_builder.AppendVertex({pt2}); - - for (auto j = 0u; j < vertices_per_geom - 3; j++) { - vtx_builder.AppendVertex({origin}); - vtx_builder.AppendVertex({pt2}); - - pt2 = center + angle_table[j + 3]; - vtx_builder.AppendVertex({pt2}); + if (round_) { + std::shared_ptr tessellator = renderer.GetTessellator(); + CircleTessellator circle_tessellator(tessellator, entity.GetTransform(), + radius_); + + // Get triangulation relative to {0, 0} so we can translate it to each + // point in turn. + std::vector circle_vertices; + circle_vertices.reserve(circle_tessellator.GetCircleVertexCount()); + circle_tessellator.GenerateCircleTriangleStrip( + [&circle_vertices](const Point& p) { // + circle_vertices.push_back(p); + }, + {}, radius); + FML_DCHECK(circle_vertices.size() == + circle_tessellator.GetCircleVertexCount()); + + vtx_builder.Reserve((circle_vertices.size() + 2) * points_.size() - 2); + for (auto& center : points_) { + if (vtx_builder.HasVertices()) { + vtx_builder.AppendVertex(vtx_builder.Last()); + vtx_builder.AppendVertex({center + circle_vertices[0]}); + } + + for (auto& vertex : circle_vertices) { + vtx_builder.AppendVertex({center + vertex}); + } + } + } else { + vtx_builder.Reserve(6 * points_.size() - 2); + for (auto& point : points_) { + auto first = Point(point.x - radius, point.y - radius); + + if (vtx_builder.HasVertices()) { + vtx_builder.AppendVertex(vtx_builder.Last()); + vtx_builder.AppendVertex({first}); + } + + // Z pattern from UL -> UR -> LL -> LR + vtx_builder.AppendVertex({first}); + vtx_builder.AppendVertex({{point.x + radius, point.y - radius}}); + vtx_builder.AppendVertex({{point.x - radius, point.y + radius}}); + vtx_builder.AppendVertex({{point.x + radius, point.y + radius}}); } } + return vtx_builder; } diff --git a/impeller/geometry/BUILD.gn b/impeller/geometry/BUILD.gn index 72b8a01160153..ce6fe023c999d 100644 --- a/impeller/geometry/BUILD.gn +++ b/impeller/geometry/BUILD.gn @@ -36,6 +36,8 @@ impeller_component("geometry") { "sigma.h", "size.cc", "size.h", + "trig.cc", + "trig.h", "type_traits.cc", "type_traits.h", "vector.cc", @@ -65,6 +67,7 @@ impeller_component("geometry_unittests") { "path_unittests.cc", "rect_unittests.cc", "size_unittests.cc", + "trig_unittests.cc", ] deps = [ diff --git a/impeller/geometry/trig.cc b/impeller/geometry/trig.cc new file mode 100644 index 0000000000000..c613a7b5c9620 --- /dev/null +++ b/impeller/geometry/trig.cc @@ -0,0 +1,11 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "trig.h" + +namespace impeller { + +// + +} // namespace impeller diff --git a/impeller/geometry/trig.h b/impeller/geometry/trig.h new file mode 100644 index 0000000000000..eddf6552f5ed3 --- /dev/null +++ b/impeller/geometry/trig.h @@ -0,0 +1,32 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#pragma once + +#include +#include + +#include "flutter/impeller/geometry/point.h" + +namespace impeller { + +/// @brief A structure to store the sine and cosine of an angle. +struct Trig { + /// Construct a Trig object from a given angle in radians. + explicit Trig(Radians r) + : cos(std::cos(r.radians)), sin(std::sin(r.radians)) {} + + /// Construct a Trig object from the given cosine and sine values. + Trig(double cos, double sin) : cos(cos), sin(sin) {} + + double cos; + double sin; + + Vector2 operator*(double radius) const { + return Vector2(static_cast(cos * radius), + static_cast(sin * radius)); + } +}; + +} // namespace impeller diff --git a/impeller/geometry/trig_unittests.cc b/impeller/geometry/trig_unittests.cc new file mode 100644 index 0000000000000..dcdc6b6da2934 --- /dev/null +++ b/impeller/geometry/trig_unittests.cc @@ -0,0 +1,64 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "fml/logging.h" +#include "gtest/gtest.h" + +#include "flutter/impeller/geometry/trig.h" + +namespace impeller { +namespace testing { + +TEST(TrigTest, TrigAngles) { + { + Trig trig(Degrees(0.0)); + EXPECT_EQ(trig.cos, 1.0); + EXPECT_EQ(trig.sin, 0.0); + } + + { + Trig trig(Radians(0.0)); + EXPECT_EQ(trig.cos, 1.0); + EXPECT_EQ(trig.sin, 0.0); + } + + { + Trig trig(Degrees(30.0)); + EXPECT_NEAR(trig.cos, sqrt(0.75), kEhCloseEnough); + EXPECT_NEAR(trig.sin, 0.5, kEhCloseEnough); + } + + { + Trig trig(Radians(kPi / 6.0)); + EXPECT_NEAR(trig.cos, sqrt(0.75), kEhCloseEnough); + EXPECT_NEAR(trig.sin, 0.5, kEhCloseEnough); + } + + { + Trig trig(Degrees(60.0)); + EXPECT_NEAR(trig.cos, 0.5, kEhCloseEnough); + EXPECT_NEAR(trig.sin, sqrt(0.75), kEhCloseEnough); + } + + { + Trig trig(Radians(kPi / 3.0)); + EXPECT_NEAR(trig.cos, 0.5, kEhCloseEnough); + EXPECT_NEAR(trig.sin, sqrt(0.75), kEhCloseEnough); + } + + { + Trig trig(Degrees(90.0)); + EXPECT_NEAR(trig.cos, 0.0, kEhCloseEnough); + EXPECT_NEAR(trig.sin, 1.0, kEhCloseEnough); + } + + { + Trig trig(Radians(kPi / 2.0)); + EXPECT_NEAR(trig.cos, 0.0, kEhCloseEnough); + EXPECT_NEAR(trig.sin, 1.0, kEhCloseEnough); + } +} + +} // namespace testing +} // namespace impeller diff --git a/impeller/renderer/vertex_buffer_builder.h b/impeller/renderer/vertex_buffer_builder.h index b70e8a700389a..2f92e24c96eec 100644 --- a/impeller/renderer/vertex_buffer_builder.h +++ b/impeller/renderer/vertex_buffer_builder.h @@ -56,6 +56,11 @@ class VertexBufferBuilder { return indices_.size() > 0 ? indices_.size() : vertices_.size(); } + const VertexType& Last() const { + FML_DCHECK(!vertices_.empty()); + return vertices_.back(); + } + VertexBufferBuilder& AppendVertex(VertexType_ vertex) { vertices_.emplace_back(std::move(vertex)); return *this; diff --git a/impeller/tessellator/BUILD.gn b/impeller/tessellator/BUILD.gn index d3915f9996679..d49c2459492c5 100644 --- a/impeller/tessellator/BUILD.gn +++ b/impeller/tessellator/BUILD.gn @@ -6,13 +6,18 @@ import("//flutter/impeller/tools/impeller.gni") impeller_component("tessellator") { sources = [ + "circle_tessellator.cc", + "circle_tessellator.h", "tessellator.cc", "tessellator.h", ] public_deps = [ "../geometry" ] - deps = [ "//third_party/libtess2" ] + deps = [ + "//flutter/fml", + "//third_party/libtess2", + ] } impeller_component("tessellator_shared") { @@ -26,12 +31,15 @@ impeller_component("tessellator_shared") { sources = [ "c/tessellator.cc", "c/tessellator.h", + "circle_tessellator.cc", + "circle_tessellator.h", "tessellator.cc", "tessellator.h", ] deps = [ "../geometry", + "//flutter/fml", "//third_party/libtess2", ] @@ -42,9 +50,13 @@ impeller_component("tessellator_shared") { impeller_component("tessellator_unittests") { testonly = true - sources = [ "tessellator_unittests.cc" ] + sources = [ + "circle_tessellator_unittests.cc", + "tessellator_unittests.cc", + ] deps = [ ":tessellator", + "../geometry:geometry_asserts", "//flutter/testing", ] } diff --git a/impeller/tessellator/circle_tessellator.cc b/impeller/tessellator/circle_tessellator.cc new file mode 100644 index 0000000000000..35f0f924db38c --- /dev/null +++ b/impeller/tessellator/circle_tessellator.cc @@ -0,0 +1,211 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "flutter/impeller/tessellator/circle_tessellator.h" + +#include "flutter/fml/logging.h" + +namespace impeller { + +int CircleTessellator::kPrecomputedDivisions[kPrecomputedDivisionCount] = { + // clang-format off + 1, 2, 3, 4, 4, 4, 5, 5, 5, 6, 6, 6, 7, 7, 7, 7, + 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 10, 10, 10, 10, 10, + 10, 11, 11, 11, 11, 11, 11, 11, 12, 12, 12, 12, 12, 12, 12, 13, + 13, 13, 13, 13, 13, 13, 13, 14, 14, 14, 14, 14, 14, 14, 14, 14, + 15, 15, 15, 15, 15, 15, 15, 15, 15, 16, 16, 16, 16, 16, 16, 16, + 16, 16, 16, 16, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 18, 18, + 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 19, 19, 19, 19, 19, 19, + 19, 19, 19, 19, 19, 19, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, + 20, 20, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 23, 23, 23, + 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 24, 24, 24, 24, + 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 25, 25, 25, 25, 25, + 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 26, 26, 26, 26, 26, + 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 27, 27, 27, 27, + 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 28, 28, 28, + 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 29, + 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, + 29, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, + 30, 30, 30, 30, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, + 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, + 33, 33, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, + 34, 34, 34, 34, 34, 34, 34, 35, 35, 35, 35, 35, 35, 35, 35, 35, + 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 36, 36, + 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, + 36, 36, 36, 36, 36, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, + 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 38, 38, 38, 38, + 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, + 38, 38, 38, 38, 38, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, + 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 40, 40, + 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, + 40, 40, 40, 40, 40, 40, 40, 41, 41, 41, 41, 41, 41, 41, 41, 41, + 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, + 41, 41, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, + 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 43, 43, 43, 43, + 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, + 43, 43, 43, 43, 43, 43, 43, 43, 44, 44, 44, 44, 44, 44, 44, 44, + 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, + 44, 44, 44, 44, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, + 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, + 45, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, + 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 47, + 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, + 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 48, 48, 48, + 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, + 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 49, 49, 49, 49, + 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, + 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 50, 50, 50, 50, 50, + 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, + 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 51, 51, 51, 51, 51, + 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, + 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 52, 52, 52, 52, + 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, + 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 53, 53, 53, + 53, 53, 53, 53, 53, 53, 53, 53, 53, 53, 53, 53, 53, 53, 53, 53, + 53, 53, 53, 53, 53, 53, 53, 53, 53, 53, 53, 53, 53, 53, 53, 54, + 54, 54, 54, 54, 54, 54, 54, 54, 54, 54, 54, 54, 54, 54, 54, 54, + 54, 54, 54, 54, 54, 54, 54, 54, 54, 54, 54, 54, 54, 54, 54, 54, + 54, 54, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, + 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, + 55, 55, 55, 55, 55, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, + 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, + 56, 56, 56, 56, 56, 56, 56, 56, 56, 57, 57, 57, 57, 57, 57, 57, + // clang-format on +}; + +size_t CircleTessellator::ComputeQuadrantDivisions(Scalar pixel_radius) { + if (pixel_radius <= 0.0) { + return 1; + } + int radius_index = ceil(pixel_radius); + if (radius_index < kPrecomputedDivisionCount) { + return kPrecomputedDivisions[radius_index]; + } + + // For a circle with N divisions per quadrant, the maximum deviation of + // the polgyon approximation from the true circle will be at the center + // of the base of each triangular pie slice. We can compute that distance + // by finding the midpoint of the line of the first slice and compare + // its distance from the center of the circle to the radius. We will aim + // to have the length of that bisector to be within |kCircleTolerance| + // from the radius in pixels. + // + // Each vertex will appear at an angle of: + // theta(i) = (kPi / 2) * (i / N) // for i in [0..N] + // with each point falling at: + // point(i) = r * (cos(theta), sin(theta)) + // If we consider the unit circle to simplify the calculations below then + // we need to scale the tolerance from its absolute quantity into a unit + // circle fraction: + // k = tolerance / radius + // Using this scaled tolerance below to avoid multiplying by the radius + // throughout all of the math, we have: + // first point = (1, 0) // theta(0) == 0 + // theta = kPi / 2 / N // theta(1) + // second point = (cos(theta), sin(theta)) = (c, s) + // midpoint = (first + second) * 0.5 = ((1 + c)/2, s/2) + // |midpoint| = sqrt((1 + c)*(1 + c)/4 + s*s/4) + // = sqrt((1 + c + c + c*c + s*s) / 4) + // = sqrt((1 + 2c + 1) / 4) + // = sqrt((2 + 2c) / 4) + // = sqrt((1 + c) / 2) + // = cos(theta / 2) // using half-angle cosine formula + // error = 1 - |midpoint| = 1 - cos(theta / 2) + // cos(theta/2) = 1 - error + // theta/2 = acos(1 - error) + // kPi / 2 / N / 2 = acos(1 - error) + // kPi / 4 / acos(1 - error) = N + // Since we need error <= k, we want divisions >= N, so we use: + // N = ceil(kPi / 4 / acos(1 - k)) + // + // Math is confirmed in https://math.stackexchange.com/a/4132095 + // (keeping in mind that we are computing quarter circle divisions here) + // which also points out a performance optimization that is accurate + // to within an over-estimation of 1 division would be: + // N = ceil(kPi / 4 / sqrt(2 * k)) + // Since we have precomputed the divisions for radii up to 1024, we can + // afford to be more accurate using the acos formula here for larger radii. + double k = kCircleTolerance / pixel_radius; + return ceil(kPiOver4 / std::acos(1 - k)); +} + +const std::vector& CircleTessellator::GetTrigsForDivisions( + std::shared_ptr& tessellator, + size_t divisions) { + std::vector& trigs = (divisions < Tessellator::kCachedTrigCount) + ? tessellator->precomputed_trigs_[divisions] + : temp_trigs_; + + if (trigs.empty()) { + // Either not cached yet, or we are usig the temp vector... + trigs.reserve(divisions + 1); + + double angle_scale = kPiOver2 / divisions; + + trigs.emplace_back(1.0, 0.0); + for (size_t i = 1; i < divisions; i++) { + trigs.emplace_back(Radians(i * angle_scale)); + } + trigs.emplace_back(0.0, 1.0); + + FML_DCHECK(trigs.size() == divisions + 1); + } + + return trigs; +} + +void CircleTessellator::GenerateCircleTriangleStrip( + const TessellatedPointProc& proc, + const Point& center, + Scalar radius) const { + for (auto& trig : trigs_) { + auto offset = trig * radius; + proc({center.x - offset.x, center.y + offset.y}); + proc({center.x - offset.x, center.y - offset.y}); + } + // The second half of the circle should be iterated in reverse, but + // we can instead iterate forward and swap the x/y values of the + // offset as the angles should be symmetric and thus should generate + // symmetrically reversed offset vectors. + for (auto& trig : trigs_) { + auto offset = trig * radius; + proc({center.x + offset.y, center.y + offset.x}); + proc({center.x + offset.y, center.y - offset.x}); + } +} + +void CircleTessellator::GenerateRoundCapLineTriangleStrip( + const TessellatedPointProc& proc, + const Point& p0, + const Point& p1, + Scalar radius) const { + auto along = p1 - p0; + auto length = along.GetLength(); + if (length < kEhCloseEnough) { + return GenerateCircleTriangleStrip(proc, p0, radius); + } + along *= radius / length; + auto across = Point(-along.y, along.x); + + for (auto& trig : trigs_) { + auto relative_across = across * trig.cos; + auto relative_along = along * trig.sin; + proc({p0 + relative_across - relative_along}); + proc({p1 + relative_across + relative_along}); + } + // The second half of the round caps should be iterated in reverse, but + // we can instead iterate forward and swap the sin/cos values as they + // should be symmetric. + for (auto& trig : trigs_) { + auto relative_across = across * trig.sin; + auto relative_along = along * trig.cos; + proc({p0 - relative_across - relative_along}); + proc({p1 - relative_across + relative_along}); + } +} + +} // namespace impeller diff --git a/impeller/tessellator/circle_tessellator.h b/impeller/tessellator/circle_tessellator.h new file mode 100644 index 0000000000000..74624eb78a8c1 --- /dev/null +++ b/impeller/tessellator/circle_tessellator.h @@ -0,0 +1,132 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#pragma once + +#include +#include + +#include "flutter/impeller/geometry/matrix.h" +#include "flutter/impeller/geometry/point.h" +#include "flutter/impeller/geometry/scalar.h" +#include "flutter/impeller/geometry/trig.h" +#include "flutter/impeller/tessellator/tessellator.h" + +namespace impeller { + +using TessellatedPointProc = std::function; + +/// @brief A utility class to compute the number of divisions for a circle +/// given a transform-adjusted pixel radius and methods for generating +/// a tessellated set of triangles for a quarter or full circle. +/// +/// The constructor will compute the device pixel radius size for +/// the specified geometry-space |radius| when viewed under +/// a specified geometry-to-device |transform|. +/// +/// The object should be constructed with the expected transform and +/// radius of the circle, but can then be used to generate a triangular +/// tessellation with the computed number of divisions for any +/// radius after that. Since the coordinate space in which the +/// circle being tessellated is not necessarily device pixel space, +/// the radius supplied during tessellation might not match the +/// pixel radius computed during construction, but the two values +/// should be related by the transform in place when the tessellated +/// triangles are rendered for maximum tessellation fidelity. +class CircleTessellator { + public: + /// @brief The pixel tolerance used by the algorighm to determine how + /// many divisions to create for a circle. + /// + /// No point on the polygon of vertices should deviate from the + /// true circle by more than this tolerance. + static constexpr Scalar kCircleTolerance = 0.1; + + /// @brief Constructs a CircleTessellator that produces enough segments + /// to reasonably approximate a circle with a specified |radius| + /// when viewed under the specified |transform|. + CircleTessellator(std::shared_ptr& tessellator, + const Matrix& transform, + Scalar radius) + : CircleTessellator(tessellator, transform.GetMaxBasisLength() * radius) { + } + + ~CircleTessellator() = default; + + /// @brief Return the number of divisions computed by the algorithm for + /// a single quarter circle. + size_t GetQuadrantDivisionCount() const { return trigs_.size() - 1; } + + /// @brief Return the number of vertices that will be generated to + /// tessellate a full circle with a triangle strip. + /// + /// This value can be used to pre-allocate space in a vector + /// to hold the vertices that will be produced by the + /// |GenerateCircleTriangleStrip| and + /// |GenerateRoundCapLineTriangleStrip| methods. + size_t GetCircleVertexCount() const { return trigs_.size() * 4; } + + /// @brief Generate the vertices for a triangle strip that covers the + /// circle at a given |radius| from a given |center|, delivering + /// the computed coordinates to the supplied |proc|. + /// + /// This procedure will generate no more than the number of + /// vertices returned by |GetCircleVertexCount| in an order + /// appropriate for rendering as a triangle strip. + void GenerateCircleTriangleStrip(const TessellatedPointProc& proc, + const Point& center, + Scalar radius) const; + + /// @brief Generate the vertices for a triangle strip that covers the + /// line from |p0| to |p1| with round caps of the specified + /// |radius|, delivering the computed coordinates to the supplied + /// |proc|. + /// + /// This procedure will generate no more than the number of + /// vertices returned by |GetCircleVertexCount| in an order + /// appropriate for rendering as a triangle strip. + void GenerateRoundCapLineTriangleStrip(const TessellatedPointProc& proc, + const Point& p0, + const Point& p1, + Scalar radius) const; + + private: + const std::vector& trigs_; + std::vector temp_trigs_; + + /// @brief Constructs a CircleTessellator that produces enough segments + /// to reasonably approximate a circle with a specified radius + /// in pixels. + explicit CircleTessellator(std::shared_ptr& tessellator, + Scalar pixel_radius) + : trigs_(GetTrigsForDivisions(tessellator, + ComputeQuadrantDivisions(pixel_radius))) {} + + CircleTessellator(const CircleTessellator&) = delete; + + CircleTessellator& operator=(const CircleTessellator&) = delete; + + /// @brief Compute the number of vertices to divide each quadrant of + /// the circle into based on the expected pixel space radius. + /// + /// @return the number of vertices. + static size_t ComputeQuadrantDivisions(Scalar pixel_radius); + + /// @brief Compute the sine and cosine for each angle in the number of + /// divisions [0, divisions] of a quarter circle and return the + /// values in a vector of trig objects. + /// + /// Note that since the 0th division is included, the vector will + /// contain (divisions + 1) values. + /// + /// @return The vector of (divisions + 1) trig values. + const std::vector& GetTrigsForDivisions( + std::shared_ptr& tessellator, + size_t divisions); + + static constexpr int kPrecomputedDivisionCount = 1024; + static int kPrecomputedDivisions[kPrecomputedDivisionCount]; +}; + +} // namespace impeller diff --git a/impeller/tessellator/circle_tessellator_unittests.cc b/impeller/tessellator/circle_tessellator_unittests.cc new file mode 100644 index 0000000000000..a84735f35b21d --- /dev/null +++ b/impeller/tessellator/circle_tessellator_unittests.cc @@ -0,0 +1,97 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "fml/logging.h" +#include "gtest/gtest.h" + +#include "flutter/impeller/geometry/geometry_asserts.h" +#include "flutter/impeller/tessellator/circle_tessellator.h" + +namespace impeller { +namespace testing { + +TEST(CircleTessellator, DivisionVertexCounts) { + auto tessellator = std::make_shared(); + + auto test = [&tessellator](const Matrix& transform, Scalar radius) { + CircleTessellator circle_tessellator(tessellator, transform, radius); + size_t quadrant_divisions = circle_tessellator.GetQuadrantDivisionCount(); + + EXPECT_EQ(circle_tessellator.GetCircleVertexCount(), + (quadrant_divisions + 1) * 4) + << "transform = " << transform << ", radius = " << radius; + + // Confirm the approximation error is within the currently accepted + // |kCircleTolerance| value advertised by |CircleTessellator|. + // (With an additional 1% tolerance for floating point rounding.) + double angle = kPiOver2 / quadrant_divisions; + Point first = {radius, 0}; + Point next = {static_cast(cos(angle) * radius), + static_cast(sin(angle) * radius)}; + Point midpoint = (first + next) * 0.5; + EXPECT_GE(midpoint.GetLength(), + radius - CircleTessellator::kCircleTolerance * 1.01) + << ", transform = " << transform << ", radius = " << radius + << ", divisions = " << quadrant_divisions; + }; + + test({}, 0.0); + test({}, 0.9); + test({}, 1.0); + test({}, 1.9); + test(Matrix::MakeScale(Vector2(2.0, 2.0)), 0.95); + test({}, 2.0); + test(Matrix::MakeScale(Vector2(2.0, 2.0)), 1.0); + test({}, 11.9); + test({}, 12.0); + test({}, 35.9); + for (int i = 36; i < 10000; i += 4) { + test({}, i); + } +} + +TEST(CircleTessellator, CircleTessellationVertices) { + auto tessellator = std::make_shared(); + + auto test = [&tessellator](Scalar pixel_radius, Point center, Scalar radius) { + CircleTessellator circle_tessellator(tessellator, {}, pixel_radius); + + auto vertex_count = circle_tessellator.GetCircleVertexCount(); + auto vertices = std::vector(); + circle_tessellator.GenerateCircleTriangleStrip( + [&vertices](const Point& p) { // + vertices.push_back(p); + }, + center, radius); + ASSERT_EQ(vertices.size(), vertex_count); + ASSERT_EQ(vertex_count % 4, 0u); + + auto quadrant_count = vertex_count / 4; + for (size_t i = 0; i < quadrant_count; i++) { + double angle = kPiOver2 * i / (quadrant_count - 1); + double rsin = sin(angle) * radius; + double rcos = cos(angle) * radius; + EXPECT_POINT_NEAR(vertices[i * 2], + Point(center.x - rcos, center.y + rsin)) + << "vertex " << i << ", angle = " << angle * 180.0 / kPi << std::endl; + EXPECT_POINT_NEAR(vertices[i * 2 + 1], + Point(center.x - rcos, center.y - rsin)) + << "vertex " << i << ", angle = " << angle * 180.0 / kPi << std::endl; + EXPECT_POINT_NEAR(vertices[vertex_count - i * 2 - 1], + Point(center.x + rcos, center.y - rsin)) + << "vertex " << i << ", angle = " << angle * 180.0 / kPi << std::endl; + EXPECT_POINT_NEAR(vertices[vertex_count - i * 2 - 2], + Point(center.x + rcos, center.y + rsin)) + << "vertex " << i << ", angle = " << angle * 180.0 / kPi << std::endl; + } + }; + + test(2.0, {}, 2.0); + test(2.0, {10, 10}, 2.0); + test(1000.0, {}, 2.0); + test(2.0, {}, 1000.0); +} + +} // namespace testing +} // namespace impeller diff --git a/impeller/tessellator/tessellator.h b/impeller/tessellator/tessellator.h index 282b47b4829ed..3f5b5d7ebb522 100644 --- a/impeller/tessellator/tessellator.h +++ b/impeller/tessellator/tessellator.h @@ -11,6 +11,7 @@ #include "flutter/fml/macros.h" #include "impeller/geometry/path.h" #include "impeller/geometry/point.h" +#include "impeller/geometry/trig.h" struct TESStesselator; @@ -89,6 +90,11 @@ class Tessellator { std::unique_ptr> point_buffer_; CTessellator c_tessellator_; + // Cached data for CircleTessellator + static constexpr size_t kCachedTrigCount = 300; + std::vector precomputed_trigs_[kCachedTrigCount]; + friend class CircleTessellator; + Tessellator(const Tessellator&) = delete; Tessellator& operator=(const Tessellator&) = delete;