Skip to content

Commit

Permalink
[Impeller] Make conical gradient work as expected (flutter#42567)
Browse files Browse the repository at this point in the history
fix flutter/flutter#128012

In many instances (flutter/flutter#128012 (comment)), the current implementation of conical gradient in impeller produces incorrect results. This pull request proposes to migrate the conical gradient algorithm from skia to impeller.

Please see https://github.com/google/skia/blob/ddf987d2ab3314ee0e80ac1ae7dbffb44a87d394/src/sksl/sksl_graphite_frag.sksl#L541-L666

Test result
![FGcZZRCQZJ](https://github.com/flutter/engine/assets/31977171/4127e73d-81a9-4d3e-8430-cf511367362c)
  • Loading branch information
ColdPaleLight authored Jun 6, 2023
1 parent 722aad8 commit 445db0d
Show file tree
Hide file tree
Showing 8 changed files with 231 additions and 94 deletions.
42 changes: 42 additions & 0 deletions impeller/aiks/aiks_unittests.cc
Original file line number Diff line number Diff line change
Expand Up @@ -749,6 +749,48 @@ TEST_P(AiksTest, CanRenderSweepGradientManyColorsDecal) {
CanRenderSweepGradientManyColors(this, Entity::TileMode::kDecal);
}

TEST_P(AiksTest, CanRenderConicalGradient) {
Scalar size = 256;
Canvas canvas;
Paint paint;
paint.color = Color::White();
canvas.DrawRect({0, 0, size * 3, size * 3}, paint);
std::vector<Color> colors = {Color::MakeRGBA8(0xF4, 0x43, 0x36, 0xFF),
Color::MakeRGBA8(0xFF, 0xEB, 0x3B, 0xFF),
Color::MakeRGBA8(0x4c, 0xAF, 0x50, 0xFF),
Color::MakeRGBA8(0x21, 0x96, 0xF3, 0xFF)};
std::vector<Scalar> stops = {0.0, 1.f / 3.f, 2.f / 3.f, 1.0};
std::array<std::tuple<Point, float, Point, float>, 8> array{
std::make_tuple(Point{size / 2.f, size / 2.f}, 0.f,
Point{size / 2.f, size / 2.f}, size / 2.f),
std::make_tuple(Point{size / 2.f, size / 2.f}, size / 4.f,
Point{size / 2.f, size / 2.f}, size / 2.f),
std::make_tuple(Point{size / 4.f, size / 4.f}, 0.f,
Point{size / 2.f, size / 2.f}, size / 2.f),
std::make_tuple(Point{size / 4.f, size / 4.f}, size / 2.f,
Point{size / 2.f, size / 2.f}, 0),
std::make_tuple(Point{size / 4.f, size / 4.f}, size / 4.f,
Point{size / 2.f, size / 2.f}, size / 2.f),
std::make_tuple(Point{size / 4.f, size / 4.f}, size / 16.f,
Point{size / 2.f, size / 2.f}, size / 8.f),
std::make_tuple(Point{size / 4.f, size / 4.f}, size / 8.f,
Point{size / 2.f, size / 2.f}, size / 16.f),
std::make_tuple(Point{size / 8.f, size / 8.f}, size / 8.f,
Point{size / 2.f, size / 2.f}, size / 8.f),
};
for (int i = 0; i < 8; i++) {
canvas.Save();
canvas.Translate({(i % 3) * size, i / 3 * size, 0});
paint.color_source = ColorSource::MakeConicalGradient(
std::get<0>(array[i]), std::get<1>(array[i]), colors, stops,
std::get<2>(array[i]), std::get<3>(array[i]), Entity::TileMode::kClamp,
{});
canvas.DrawRect({0, 0, size, size}, paint);
canvas.Restore();
}
ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture()));
}

TEST_P(AiksTest, CanRenderDifferentShapesWithSameColorSource) {
Canvas canvas;
Paint paint;
Expand Down
134 changes: 117 additions & 17 deletions impeller/compiler/shader_lib/impeller/gradient.glsl
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,127 @@

#include <impeller/texture.glsl>

mat3 IPMapToUnitX(vec2 p0, vec2 p1) {
// Returns a matrix that maps [p0, p1] to [(0, 0), (1, 0)]. Results are
// undefined if p0 = p1.
return mat3(0.0, -1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0) *
inverse(mat3(p1.y - p0.y, p0.x - p1.x, 0.0, p1.x - p0.x, p1.y - p0.y,
0.0, p0.x, p0.y, 1.0));
}

/// Compute the t value for a conical gradient at point `p` between the 2
/// circles defined by (c0, r0) and (c1, r1).
/// circles defined by (c0, r0) and (c1, r1). The returned vec2 encapsulates 't'
/// as its x component and validity status as its y component, with positive y
/// indicating a valid result.
///
/// This assumes that c0 != c1.
float IPComputeConicalT(vec2 c0, float r0, vec2 c1, float r1, vec2 p) {
float w = 1.0;
float result = 0.0;
vec2 ab = c1 - c0;
float dr = r1 - r0;
// Set sample rate to a minimum for the case where c0 and c1 are close.
float delta = 1.0 / max(length(ab), 100.0);
while (w >= 0.0) {
vec2 cw = w * ab + c0;
float rw = w * dr + r0;
if (length(p - cw) <= rw) {
result = w;
break;
/// The code is migrated from Skia Graphite. See
/// https://github.com/google/skia/blob/ddf987d2ab3314ee0e80ac1ae7dbffb44a87d394/src/sksl/sksl_graphite_frag.sksl#L541-L666.
vec2 IPComputeConicalT(vec2 c0, float r0, vec2 c1, float r1, vec2 pos) {
const float scalar_nearly_zero = 1.0 / float(1 << 12);
float d_center = distance(c0, c1);
float d_radius = r1 - r0;

// Degenerate case: a radial gradient (p0 = p1).
bool radial = d_center < scalar_nearly_zero;

// Degenerate case: a strip with bandwidth 2r (r0 = r1).
bool strip = abs(d_radius) < scalar_nearly_zero;

if (radial) {
if (strip) {
// The start and end inputs are the same in both position and radius.
// We don't expect to see this input, but just in case we avoid dividing
// by zero.
return vec2(0.0, -1.0);
}

float scale = 1.0 / d_radius;
float scale_sign = sign(d_radius);
float bias = r0 / d_radius;

vec2 pt = (pos - c0) * scale;
float t = length(pt) * scale_sign - bias;
return vec2(t, 1.0);

} else if (strip) {
mat3 transform = IPMapToUnitX(c0, c1);
float r = r0 / d_center;
float r_2 = r * r;

vec2 pt = (transform * vec3(pos.xy, 1.0)).xy;
float t = r_2 - pt.y * pt.y;
if (t < 0.0) {
return vec2(0.0, -1.0);
}
t = pt.x + sqrt(t);
return vec2(t, 1.0);

} else {
// See https://skia.org/docs/dev/design/conical/ for details on how this
// algorithm works. Calculate f and swap inputs if necessary (steps 1 and
// 2).
float f = r0 / (r0 - r1);

bool is_swapped = abs(f - 1.0) < scalar_nearly_zero;
if (is_swapped) {
vec2 tmp_pt = c0;
c0 = c1;
c1 = tmp_pt;
f = 0.0;
}

// Apply mapping from [Cf, C1] to unit x, and apply the precalculations from
// steps 3 and 4, all in the same transformation.
vec2 cf = c0 * (1.0 - f) + c1 * f;
mat3 transform = IPMapToUnitX(cf, c1);

float scale_x = abs(1.0 - f);
float scale_y = scale_x;
float r1 = abs(r1 - r0) / d_center;
bool is_focal_on_circle = abs(r1 - 1.0) < scalar_nearly_zero;
if (is_focal_on_circle) {
scale_x *= 0.5;
scale_y *= 0.5;
} else {
scale_x *= r1 / (r1 * r1 - 1.0);
scale_y /= sqrt(abs(r1 * r1 - 1.0));
}
transform =
mat3(scale_x, 0.0, 0.0, 0.0, scale_y, 0.0, 0.0, 0.0, 1.0) * transform;

vec2 pt = (transform * vec3(pos.xy, 1.0)).xy;

// Continue with step 5 onward.
float inv_r1 = 1.0 / r1;
float d_radius_sign = sign(1.0 - f);
bool is_well_behaved = !is_focal_on_circle && r1 > 1.0;

float x_t = -1.0;
if (is_focal_on_circle) {
x_t = dot(pt, pt) / pt.x;
} else if (is_well_behaved) {
x_t = length(pt) - pt.x * inv_r1;
} else {
float temp = pt.x * pt.x - pt.y * pt.y;
if (temp >= 0.0) {
if (is_swapped || d_radius_sign < 0.0) {
x_t = -sqrt(temp) - pt.x * inv_r1;
} else {
x_t = sqrt(temp) - pt.x * inv_r1;
}
}
}

if (!is_well_behaved && x_t < 0.0) {
return vec2(0.0, -1.0);
}

float t = f + d_radius_sign * x_t;
if (is_swapped) {
t = 1.0 - t;
}
w -= delta;
return vec2(t, 1.0);
}
return 1.0 - result;
}

/// Compute the indexes and mix coefficient used to mix colors for an
Expand Down
12 changes: 0 additions & 12 deletions impeller/entity/contents/conical_gradient_contents.cc
Original file line number Diff line number Diff line change
Expand Up @@ -51,18 +51,6 @@ void ConicalGradientContents::SetFocus(std::optional<Point> focus,
focus_radius_ = radius;
}

bool ConicalGradientContents::IsOpaque() const {
if (GetOpacity() < 1) {
return false;
}
for (auto color : colors_) {
if (!color.IsOpaque()) {
return false;
}
}
return true;
}

bool ConicalGradientContents::Render(const ContentContext& renderer,
const Entity& entity,
RenderPass& pass) const {
Expand Down
3 changes: 0 additions & 3 deletions impeller/entity/contents/conical_gradient_contents.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,6 @@ class ConicalGradientContents final : public ColorSourceContents {

~ConicalGradientContents() override;

// |Contents|
bool IsOpaque() const override;

// |Contents|
bool Render(const ContentContext& renderer,
const Entity& entity,
Expand Down
2 changes: 1 addition & 1 deletion impeller/entity/entity_unittests.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2545,7 +2545,7 @@ TEST_P(EntityTest, SolidColorContentsIsOpaque) {
TEST_P(EntityTest, ConicalGradientContentsIsOpaque) {
ConicalGradientContents contents;
contents.SetColors({Color::CornflowerBlue()});
ASSERT_TRUE(contents.IsOpaque());
ASSERT_FALSE(contents.IsOpaque());
contents.SetColors({Color::CornflowerBlue().WithAlpha(0.5)});
ASSERT_FALSE(contents.IsOpaque());
}
Expand Down
11 changes: 8 additions & 3 deletions impeller/entity/shaders/conical_gradient_fill.frag
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,14 @@ highp in vec2 v_position;
out vec4 frag_color;

void main() {
float t =
IPComputeConicalT(frag_info.center, frag_info.radius, frag_info.focus,
frag_info.focus_radius, v_position);
vec2 res = IPComputeConicalT(frag_info.focus, frag_info.focus_radius,
frag_info.center, frag_info.radius, v_position);
if (res.y < 0.0) {
frag_color = vec4(0);
return;
}

float t = res.x;
frag_color = IPSampleLinearWithTileMode(
texture_sampler, vec2(t, 0.5), frag_info.texture_sampler_y_coord_scale,
frag_info.half_texel, frag_info.tile_mode);
Expand Down
10 changes: 7 additions & 3 deletions impeller/entity/shaders/conical_gradient_ssbo_fill.frag
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,14 @@ highp in vec2 v_position;
out vec4 frag_color;

void main() {
float t =
IPComputeConicalT(frag_info.center, frag_info.radius, frag_info.focus,
frag_info.focus_radius, v_position);
vec2 res = IPComputeConicalT(frag_info.focus, frag_info.focus_radius,
frag_info.center, frag_info.radius, v_position);
if (res.y < 0.0) {
frag_color = vec4(0);
return;
}

float t = res.x;
if ((t < 0.0 || t > 1.0) && frag_info.tile_mode == kTileModeDecal) {
frag_color = vec4(0);
return;
Expand Down
Loading

0 comments on commit 445db0d

Please sign in to comment.