mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
[DisplayList] implement shadow bounds without relying on Skia utilities (#172572)
Right now DisplayList defers to Skia to compute the bounds of shadow operations, but we really need to own this code now that we are doing our own rendering so I ported it over and simplified it to just deal with the cases we care about. Eliminates a bullet item in https://github.com/flutter/flutter/issues/161456
This commit is contained in:
parent
0a2ee25955
commit
3deedd23cd
@ -167,6 +167,7 @@ if (enable_unittests) {
|
||||
sources = [
|
||||
"benchmarking/dl_complexity_unittests.cc",
|
||||
"display_list_unittests.cc",
|
||||
"dl_canvas_unittests.cc",
|
||||
"dl_color_unittests.cc",
|
||||
"dl_paint_unittests.cc",
|
||||
"dl_storage_unittests.cc",
|
||||
@ -180,6 +181,7 @@ if (enable_unittests) {
|
||||
"geometry/dl_path_unittests.cc",
|
||||
"geometry/dl_region_unittests.cc",
|
||||
"geometry/dl_rtree_unittests.cc",
|
||||
"skia/dl_sk_canvas_unittests.cc",
|
||||
"skia/dl_sk_conversions_unittests.cc",
|
||||
"skia/dl_sk_paint_dispatcher_unittests.cc",
|
||||
"utils/dl_accumulation_rect_unittests.cc",
|
||||
|
||||
@ -4,9 +4,121 @@
|
||||
|
||||
#include "flutter/display_list/dl_canvas.h"
|
||||
|
||||
#include "flutter/display_list/geometry/dl_geometry_conversions.h"
|
||||
#include "flutter/third_party/skia/include/core/SkPoint3.h"
|
||||
#include "flutter/third_party/skia/include/utils/SkShadowUtils.h"
|
||||
namespace {
|
||||
|
||||
// ShadowBounds code adapted from SkShadowUtils using the Directional flag.
|
||||
|
||||
using DlScalar = flutter::DlScalar;
|
||||
using DlVector3 = flutter::DlVector3;
|
||||
using DlVector2 = flutter::DlVector2;
|
||||
using DlRect = flutter::DlRect;
|
||||
using DlMatrix = flutter::DlMatrix;
|
||||
|
||||
static constexpr DlScalar kAmbientHeightFactor = 1.0f / 128.0f;
|
||||
static constexpr DlScalar kAmbientGeomFactor = 64.0f;
|
||||
// Assuming that we have a light height of 600 for the spot shadow, the spot
|
||||
// values will reach their maximum at a height of approximately 292.3077.
|
||||
// We'll round up to 300 to keep it simple.
|
||||
static constexpr DlScalar kMaxAmbientRadius =
|
||||
300.0f * kAmbientHeightFactor * kAmbientGeomFactor;
|
||||
|
||||
inline DlScalar AmbientBlurRadius(DlScalar height) {
|
||||
return std::min(height * kAmbientHeightFactor * kAmbientGeomFactor,
|
||||
kMaxAmbientRadius);
|
||||
}
|
||||
|
||||
struct DrawShadowRec {
|
||||
DlVector3 light_position;
|
||||
DlScalar light_radius = 0.0f;
|
||||
DlScalar occluder_z = 0.0f;
|
||||
};
|
||||
|
||||
static inline float DivideAndClamp(float numer,
|
||||
float denom,
|
||||
float min,
|
||||
float max) {
|
||||
float result = std::clamp(numer / denom, min, max);
|
||||
// ensure that clamp handled non-finites correctly
|
||||
FML_DCHECK(result >= min && result <= max);
|
||||
return result;
|
||||
}
|
||||
|
||||
inline void GetDirectionalParams(DrawShadowRec params,
|
||||
DlScalar* blur_radius,
|
||||
DlScalar* scale,
|
||||
DlVector2* translate) {
|
||||
*blur_radius = params.light_radius * params.occluder_z;
|
||||
*scale = 1.0f;
|
||||
// Max z-ratio is ("max expected elevation" / "min allowable z").
|
||||
constexpr DlScalar kMaxZRatio = 64.0f / flutter::kEhCloseEnough;
|
||||
DlScalar zRatio = DivideAndClamp(params.occluder_z, params.light_position.z,
|
||||
0.0f, kMaxZRatio);
|
||||
*translate = DlVector2(-zRatio * params.light_position.x,
|
||||
-zRatio * params.light_position.y);
|
||||
}
|
||||
|
||||
DlRect GetLocalBounds(DlRect ambient_bounds,
|
||||
const DlMatrix& matrix,
|
||||
const DrawShadowRec& params) {
|
||||
if (!matrix.IsInvertible() || ambient_bounds.IsEmpty()) {
|
||||
return DlRect();
|
||||
}
|
||||
|
||||
DlScalar ambient_blur;
|
||||
DlScalar spot_blur;
|
||||
DlScalar spot_scale;
|
||||
DlVector2 spot_offset;
|
||||
|
||||
if (matrix.HasPerspective2D()) {
|
||||
// transform ambient and spot bounds into device space
|
||||
ambient_bounds = ambient_bounds.TransformAndClipBounds(matrix);
|
||||
|
||||
// get ambient blur (in device space)
|
||||
ambient_blur = AmbientBlurRadius(params.occluder_z);
|
||||
|
||||
// get spot params (in device space)
|
||||
GetDirectionalParams(params, &spot_blur, &spot_scale, &spot_offset);
|
||||
} else {
|
||||
auto min_scale = matrix.GetMinScale2D();
|
||||
// We've already checked the matrix for perspective elements.
|
||||
FML_DCHECK(min_scale.has_value());
|
||||
DlScalar device_to_local_scale = 1.0f / min_scale.value_or(1.0f);
|
||||
|
||||
// get ambient blur (in local space)
|
||||
DlScalar device_space_ambient_blur = AmbientBlurRadius(params.occluder_z);
|
||||
ambient_blur = device_space_ambient_blur * device_to_local_scale;
|
||||
|
||||
// get spot params (in local space)
|
||||
GetDirectionalParams(params, &spot_blur, &spot_scale, &spot_offset);
|
||||
// light dir is in device space, map spot offset back into local space
|
||||
DlMatrix inverse = matrix.Invert();
|
||||
spot_offset = inverse.TransformDirection(spot_offset);
|
||||
|
||||
// convert spot blur to local space
|
||||
spot_blur *= device_to_local_scale;
|
||||
}
|
||||
|
||||
// in both cases, adjust ambient and spot bounds
|
||||
DlRect spot_bounds = ambient_bounds;
|
||||
ambient_bounds = ambient_bounds.Expand(ambient_blur);
|
||||
spot_bounds = spot_bounds.Scale(spot_scale);
|
||||
spot_bounds = spot_bounds.Shift(spot_offset);
|
||||
spot_bounds = spot_bounds.Expand(spot_blur);
|
||||
|
||||
// merge bounds
|
||||
DlRect result = ambient_bounds.Union(spot_bounds);
|
||||
// outset a bit to account for floating point error
|
||||
result = result.Expand(1.0f, 1.0f);
|
||||
|
||||
// if perspective, transform back to src space
|
||||
if (matrix.HasPerspective2D()) {
|
||||
DlMatrix inverse = matrix.Invert();
|
||||
result = result.TransformAndClipBounds(inverse);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace flutter {
|
||||
|
||||
@ -14,12 +126,13 @@ DlRect DlCanvas::ComputeShadowBounds(const DlPath& path,
|
||||
float elevation,
|
||||
DlScalar dpr,
|
||||
const DlMatrix& ctm) {
|
||||
SkRect shadow_bounds(ToSkRect(path.GetBounds()));
|
||||
SkShadowUtils::GetLocalBounds(
|
||||
ToSkMatrix(ctm), path.GetSkPath(), SkPoint3::Make(0, 0, dpr * elevation),
|
||||
SkPoint3::Make(0, -1, 1), kShadowLightRadius / kShadowLightHeight,
|
||||
SkShadowFlags::kDirectionalLight_ShadowFlag, &shadow_bounds);
|
||||
return ToDlRect(shadow_bounds);
|
||||
return GetLocalBounds(
|
||||
path.GetBounds(), ctm,
|
||||
{
|
||||
.light_position = DlVector3(0.0f, -1.0f, 1.0f),
|
||||
.light_radius = kShadowLightRadius / kShadowLightHeight,
|
||||
.occluder_z = dpr * elevation,
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace flutter
|
||||
|
||||
@ -195,6 +195,23 @@ class DlCanvas {
|
||||
DlScalar x,
|
||||
DlScalar y,
|
||||
const DlPaint& paint) = 0;
|
||||
/// @brief Draws the shadow of the given |path| rendered in the provided
|
||||
/// |color| (which is only consulted for its opacity) as would be
|
||||
/// produced by a directional light source uniformly shining in
|
||||
/// the device space direction {0, -1, 1} against a backdrop
|
||||
/// which is |elevation * dpr| device coordinates below the |path|
|
||||
/// in the Z direction.
|
||||
///
|
||||
/// Normally the renderer might consider omitting the rendering of any
|
||||
/// of the shadow pixels that fall under the |path| itself, as an
|
||||
/// optimization, unless the |transparent_occluder| flag is specified
|
||||
/// which would indicate that the optimization isn't appropriate.
|
||||
///
|
||||
/// Note that the |elevation| and |dpr| are unique in the API for being
|
||||
/// considered in pure device coordinates while the |path| is interpreted
|
||||
/// relative to the current local-to-device transform.
|
||||
///
|
||||
/// @see |ComputeShadowBounds|
|
||||
virtual void DrawShadow(const DlPath& path,
|
||||
const DlColor color,
|
||||
const DlScalar elevation,
|
||||
@ -206,6 +223,16 @@ class DlCanvas {
|
||||
static constexpr DlScalar kShadowLightHeight = 600;
|
||||
static constexpr DlScalar kShadowLightRadius = 800;
|
||||
|
||||
/// @brief Compute the local coverage for a |DrawShadow| operation using
|
||||
/// the given parameters (excluding the color and the transparent
|
||||
/// occluder parameters which do not affect the bounds).
|
||||
///
|
||||
/// Since the elevation is expressed in device coordinates relative to the
|
||||
/// provided |dpr| value, the |ctm| of the final rendering coordinate
|
||||
/// system that will be applied to the path must be provided so the two
|
||||
/// sets of coordinates (path and light source) can be correlated.
|
||||
///
|
||||
/// @see |DrawShadow|
|
||||
static DlRect ComputeShadowBounds(const DlPath& path,
|
||||
float elevation,
|
||||
DlScalar dpr,
|
||||
|
||||
57
engine/src/flutter/display_list/dl_canvas_unittests.cc
Normal file
57
engine/src/flutter/display_list/dl_canvas_unittests.cc
Normal file
@ -0,0 +1,57 @@
|
||||
// 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/display_list/dl_canvas.h"
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
namespace flutter {
|
||||
namespace testing {
|
||||
|
||||
TEST(DisplayListCanvas, GetShadowBoundsScaleTranslate) {
|
||||
DlMatrix matrix =
|
||||
DlMatrix::MakeTranslateScale({5.0f, 7.0f, 1.0f}, {10.0f, 15.0f, 7.0f});
|
||||
DlPath path = DlPath::MakeRectLTRB(100, 100, 200, 200);
|
||||
|
||||
DlRect shadow_bounds =
|
||||
DlCanvas::ComputeShadowBounds(path, 5.0f, 2.0f, matrix);
|
||||
|
||||
EXPECT_FLOAT_EQ(shadow_bounds.GetLeft(), 96.333336f);
|
||||
EXPECT_FLOAT_EQ(shadow_bounds.GetTop(), 97.761909f);
|
||||
EXPECT_FLOAT_EQ(shadow_bounds.GetRight(), 203.66667f);
|
||||
EXPECT_FLOAT_EQ(shadow_bounds.GetBottom(), 205.09525f);
|
||||
}
|
||||
|
||||
TEST(DisplayListCanvas, GetShadowBoundsScaleTranslateRotate) {
|
||||
DlMatrix matrix =
|
||||
DlMatrix::MakeTranslateScale({5.0f, 7.0f, 1.0f}, {10.0f, 15.0f, 7.0f});
|
||||
matrix = matrix * DlMatrix::MakeRotationZ(DlDegrees(45));
|
||||
DlPath path = DlPath::MakeRectLTRB(100, 100, 200, 200);
|
||||
|
||||
DlRect shadow_bounds =
|
||||
DlCanvas::ComputeShadowBounds(path, 5.0f, 2.0f, matrix);
|
||||
|
||||
EXPECT_FLOAT_EQ(shadow_bounds.GetLeft(), 97.343491f);
|
||||
EXPECT_FLOAT_EQ(shadow_bounds.GetTop(), 97.343491f);
|
||||
EXPECT_FLOAT_EQ(shadow_bounds.GetRight(), 204.67682f);
|
||||
EXPECT_FLOAT_EQ(shadow_bounds.GetBottom(), 204.67682f);
|
||||
}
|
||||
|
||||
TEST(DisplayListCanvas, GetShadowBoundsScaleTranslatePerspective) {
|
||||
DlMatrix matrix =
|
||||
DlMatrix::MakeTranslateScale({5.0f, 7.0f, 1.0f}, {10.0f, 15.0f, 7.0f});
|
||||
matrix.m[3] = 0.001f;
|
||||
DlPath path = DlPath::MakeRectLTRB(100, 100, 200, 200);
|
||||
|
||||
DlRect shadow_bounds =
|
||||
DlCanvas::ComputeShadowBounds(path, 5.0f, 2.0f, matrix);
|
||||
|
||||
EXPECT_FLOAT_EQ(shadow_bounds.GetLeft(), 96.535324f);
|
||||
EXPECT_FLOAT_EQ(shadow_bounds.GetTop(), 90.253288f);
|
||||
EXPECT_FLOAT_EQ(shadow_bounds.GetRight(), 204.15054f);
|
||||
EXPECT_FLOAT_EQ(shadow_bounds.GetBottom(), 223.3252f);
|
||||
}
|
||||
|
||||
} // namespace testing
|
||||
} // namespace flutter
|
||||
@ -20,6 +20,7 @@ using DlRadians = impeller::Radians;
|
||||
|
||||
using DlPoint = impeller::Point;
|
||||
using DlVector2 = impeller::Vector2;
|
||||
using DlVector3 = impeller::Vector3;
|
||||
using DlIPoint = impeller::IPoint32;
|
||||
using DlSize = impeller::Size;
|
||||
using DlISize = impeller::ISize32;
|
||||
|
||||
@ -0,0 +1,80 @@
|
||||
// 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/display_list/skia/dl_sk_canvas.h"
|
||||
|
||||
#include "flutter/display_list/skia/dl_sk_conversions.h"
|
||||
#include "flutter/third_party/skia/include/utils/SkShadowUtils.h"
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
namespace flutter {
|
||||
namespace testing {
|
||||
|
||||
namespace {
|
||||
|
||||
void TestShadowBounds(bool with_rotate, bool with_perspective) {
|
||||
const SkVector3 light_position = SkVector3::Make(0.0f, -1.0f, 1.0f);
|
||||
const DlScalar light_radius =
|
||||
DlCanvas::kShadowLightRadius / DlCanvas::kShadowLightHeight;
|
||||
|
||||
DlPath dl_path = DlPath::MakeRectLTRB(100, 100, 200, 200);
|
||||
for (int dpr = 1; dpr <= 2; dpr++) {
|
||||
for (int elevation = 1; elevation <= 5; elevation++) {
|
||||
SkVector3 z_params = SkVector3::Make(0.0f, 0.0f, elevation * dpr);
|
||||
for (int i = 1; i <= 10; i++) {
|
||||
DlScalar xScale = static_cast<DlScalar>(i);
|
||||
for (int j = 1; j <= 10; j++) {
|
||||
DlScalar yScale = static_cast<DlScalar>(j);
|
||||
|
||||
DlMatrix matrix = DlMatrix::MakeTranslateScale({xScale, yScale, 1.0f},
|
||||
{10.0f, 15.0f, 7.0f});
|
||||
if (with_rotate) {
|
||||
matrix = matrix * DlMatrix::MakeRotationZ(DlDegrees(45));
|
||||
}
|
||||
if (with_perspective) {
|
||||
matrix.m[3] = 0.001f;
|
||||
}
|
||||
SkMatrix sk_matrix;
|
||||
ASSERT_TRUE(ToSk(&matrix, sk_matrix) != nullptr);
|
||||
SkMatrix sk_inverse = sk_matrix;
|
||||
ASSERT_TRUE(sk_matrix.invert(&sk_inverse));
|
||||
|
||||
auto label = (std::stringstream()
|
||||
<< "Matrix: " << matrix << ", elevation = " << elevation
|
||||
<< ", dpr = " << dpr)
|
||||
.str();
|
||||
|
||||
DlRect dl_bounds =
|
||||
DlCanvas::ComputeShadowBounds(dl_path, elevation, dpr, matrix);
|
||||
SkRect sk_bounds;
|
||||
ASSERT_TRUE(SkShadowUtils::GetLocalBounds(
|
||||
sk_matrix, dl_path.GetSkPath(), z_params, light_position,
|
||||
light_radius, kDirectionalLight_ShadowFlag, &sk_bounds))
|
||||
<< label;
|
||||
EXPECT_FLOAT_EQ(dl_bounds.GetLeft(), sk_bounds.fLeft) << label;
|
||||
EXPECT_FLOAT_EQ(dl_bounds.GetTop(), sk_bounds.fTop) << label;
|
||||
EXPECT_FLOAT_EQ(dl_bounds.GetRight(), sk_bounds.fRight) << label;
|
||||
EXPECT_FLOAT_EQ(dl_bounds.GetBottom(), sk_bounds.fBottom) << label;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(DlSkCanvas, ShadowBoundsCompatibilityTranslateScale) {
|
||||
TestShadowBounds(false, false);
|
||||
}
|
||||
|
||||
TEST(DlSkCanvas, ShadowBoundsCompatibilityTranslateScaleRotate) {
|
||||
TestShadowBounds(true, false);
|
||||
}
|
||||
|
||||
TEST(DlSkCanvas, ShadowBoundsCompatibilityTranslateScalePerspective) {
|
||||
TestShadowBounds(false, true);
|
||||
}
|
||||
|
||||
} // namespace testing
|
||||
} // namespace flutter
|
||||
@ -7,6 +7,8 @@
|
||||
#include <climits>
|
||||
#include <sstream>
|
||||
|
||||
#include "flutter/fml/logging.h"
|
||||
|
||||
namespace impeller {
|
||||
|
||||
Matrix::Matrix(const MatrixDecomposition& d) : Matrix() {
|
||||
@ -358,6 +360,76 @@ std::optional<MatrixDecomposition> Matrix::Decompose() const {
|
||||
return result;
|
||||
}
|
||||
|
||||
std::optional<std::pair<Scalar, Scalar>> Matrix::GetScales2D() const {
|
||||
if (HasPerspective2D()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// We only operate on the uppermost 2x2 matrix since those are the only
|
||||
// values that can induce a scale on 2D coordinates.
|
||||
// [ a b ]
|
||||
// [ c d ]
|
||||
double a = m[0];
|
||||
double b = m[1];
|
||||
double c = m[4];
|
||||
double d = m[5];
|
||||
|
||||
if (b == 0.0f && c == 0.0f) {
|
||||
return {{std::abs(a), std::abs(d)}};
|
||||
}
|
||||
|
||||
if (a == 0.0f && d == 0.0f) {
|
||||
return {{std::abs(b), std::abs(c)}};
|
||||
}
|
||||
|
||||
// Compute eigenvalues for the matrix (transpose(A) * A):
|
||||
// [ a2 b2 ] == [ a b ] [ a c ] == [ aa + bb ac + bd ]
|
||||
// [ c2 d2 ] [ c d ] [ b d ] [ ac + bd cc + dd ]
|
||||
// (note the reverse diagonal entries in the answer are identical)
|
||||
double a2 = a * a + b * b;
|
||||
double b2 = a * c + b * d;
|
||||
double c2 = b2;
|
||||
double d2 = c * c + d * d;
|
||||
|
||||
//
|
||||
// If L is an eigenvalue, then
|
||||
// det(this - L*Identity) == 0
|
||||
// det([ a - L b ]
|
||||
// [ c d - L ]) == 0
|
||||
// (a - L) * (d - L) - bc == 0
|
||||
// ad - aL - dL + L^2 - bc == 0
|
||||
// L^2 + (-a + -d)L + ad - bc == 0
|
||||
//
|
||||
// Using quadratic equation for (Ax^2 + Bx + C):
|
||||
// A == 1
|
||||
// B == -(a2 + d2)
|
||||
// C == a2d2 - b2c2
|
||||
//
|
||||
// (We use -B for calculations because the square is the same as B and we
|
||||
// need -B for the final quadratic equation computations anyway.)
|
||||
double minus_B = a2 + d2;
|
||||
double C = a2 * d2 - b2 * c2;
|
||||
double B_squared_minus_4AC = minus_B * minus_B - 4 * 1.0f * C;
|
||||
|
||||
double quadratic_sqrt;
|
||||
if (B_squared_minus_4AC <= 0.0f) {
|
||||
// This test should never fail, but we might be slightly negative
|
||||
FML_DCHECK(B_squared_minus_4AC + kEhCloseEnough >= 0.0f);
|
||||
// Uniform scales (possibly rotated) would tend to end up here
|
||||
// in which case both eigenvalues are identical
|
||||
quadratic_sqrt = 0.0f;
|
||||
} else {
|
||||
quadratic_sqrt = std::sqrt(B_squared_minus_4AC);
|
||||
}
|
||||
|
||||
// Since this is returning the sqrt of the values, we can guarantee that
|
||||
// the returned scales are non-negative.
|
||||
FML_DCHECK(minus_B - quadratic_sqrt >= 0.0f);
|
||||
FML_DCHECK(minus_B + quadratic_sqrt >= 0.0f);
|
||||
return {{std::sqrt((minus_B - quadratic_sqrt) / 2.0f),
|
||||
std::sqrt((minus_B + quadratic_sqrt) / 2.0f)}};
|
||||
}
|
||||
|
||||
uint64_t MatrixDecomposition::GetComponentsMask() const {
|
||||
uint64_t mask = 0;
|
||||
|
||||
|
||||
@ -320,6 +320,11 @@ struct Matrix {
|
||||
|
||||
bool IsInvertible() const { return GetDeterminant() != 0; }
|
||||
|
||||
/// @brief Return the maximum scale applied specifically to either the
|
||||
/// X axis or Y axis unit vectors (the bases). The matrix might
|
||||
/// lengthen a non-axis-aligned vector by more than this value.
|
||||
///
|
||||
/// @see |GetMaxScale2D|
|
||||
inline Scalar GetMaxBasisLengthXY() const {
|
||||
// The full basis computation requires computing the squared scaling factor
|
||||
// for translate/scale only matrices. This substantially limits the range of
|
||||
@ -332,6 +337,54 @@ struct Matrix {
|
||||
e[1][0] * e[1][0] + e[1][1] * e[1][1]));
|
||||
}
|
||||
|
||||
/// @brief Return the smaller of the two non-negative scales that will
|
||||
/// be applied to 2D coordinates by this matrix. If the matrix
|
||||
/// has perspective components, the method will return a nullopt.
|
||||
///
|
||||
/// Note that negative scale factors really represent a positive scale
|
||||
/// factor with a flip, so the absolute value (the positive scale factor)
|
||||
/// is returned instead so that the results can be directly applied to
|
||||
/// rendering calculations to compute the potential size of an operation.
|
||||
///
|
||||
/// This method differs from the "basis length" methods in that those
|
||||
/// methods answer the question "how much does this transform stretch
|
||||
/// perfectly horizontal or vertical source vectors, whereas this method
|
||||
/// can answer "what's the smallest scale applied to any vector regardless
|
||||
/// of direction".
|
||||
///
|
||||
/// @see |GetScales2D|
|
||||
std::optional<Scalar> GetMinScale2D() const {
|
||||
auto scales = GetScales2D();
|
||||
if (!scales.has_value()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return std::min(scales->first, scales->second);
|
||||
}
|
||||
|
||||
/// @brief Return the smaller of the two non-negative scales that will
|
||||
/// be applied to 2D coordinates by this matrix. If the matrix
|
||||
/// has perspective components, the method will return a nullopt.
|
||||
///
|
||||
/// Note that negative scale factors really represent a positive scale
|
||||
/// factor with a flip, so the absolute value (the positive scale factor)
|
||||
/// is returned instead so that the results can be directly applied to
|
||||
/// rendering calculations to compute the potential size of an operation.
|
||||
///
|
||||
/// This method differs from the "basis length" methods in that those
|
||||
/// methods answer the question "how much does this transform stretch
|
||||
/// perfectly horizontal or vertical source vectors, whereas this method
|
||||
/// can answer "what's the largest scale applied to any vector regardless
|
||||
/// of direction".
|
||||
///
|
||||
/// @see |GetScales2D|
|
||||
std::optional<Scalar> GetMaxScale2D() const {
|
||||
auto scales = GetScales2D();
|
||||
if (!scales.has_value()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return std::max(scales->first, scales->second);
|
||||
}
|
||||
|
||||
constexpr Vector3 GetBasisX() const { return Vector3(m[0], m[1], m[2]); }
|
||||
|
||||
constexpr Vector3 GetBasisY() const { return Vector3(m[4], m[5], m[6]); }
|
||||
@ -450,6 +503,20 @@ struct Matrix {
|
||||
|
||||
std::optional<MatrixDecomposition> Decompose() const;
|
||||
|
||||
/// @brief Compute the two non-negative scales applied by this matrix to
|
||||
/// 2D coordinates and return them as an optional pair of Scalar
|
||||
/// values in any order. If the matrix has perspective elements,
|
||||
/// this method will return a nullopt.
|
||||
///
|
||||
/// Note that negative scale factors really represent a positive scale
|
||||
/// factor with a flip, so the absolute value (the positive scale factor)
|
||||
/// is returned instead so that the results can be directly applied to
|
||||
/// rendering calculations to compute the potential size of an operation.
|
||||
///
|
||||
/// @see |GetMinScale2D|
|
||||
/// @see |GetMaxScale2D|
|
||||
std::optional<std::pair<Scalar, Scalar>> GetScales2D() const;
|
||||
|
||||
bool Equals(const Matrix& matrix, Scalar epsilon = 1e-5f) const {
|
||||
const Scalar* a = m;
|
||||
const Scalar* b = matrix.m;
|
||||
|
||||
@ -322,5 +322,125 @@ TEST(MatrixTest, To3x3) {
|
||||
EXPECT_TRUE(MatrixNear(x.To3x3(), Matrix()));
|
||||
}
|
||||
|
||||
TEST(MatrixTest, MinMaxScales2D) {
|
||||
// The GetScales2D() method is allowed to return the scales in any
|
||||
// order so we need to take special care in verifying the return
|
||||
// value to test them in either order.
|
||||
auto check_pair = [](const Matrix& matrix, Scalar scale1, Scalar scale2) {
|
||||
auto pair = matrix.GetScales2D();
|
||||
EXPECT_TRUE(pair.has_value())
|
||||
<< "Scales: " << scale1 << ", " << scale2 << ", " << matrix;
|
||||
if (ScalarNearlyEqual(pair->first, scale1)) {
|
||||
EXPECT_FLOAT_EQ(pair->first, scale1) << matrix;
|
||||
EXPECT_FLOAT_EQ(pair->second, scale2) << matrix;
|
||||
} else {
|
||||
EXPECT_FLOAT_EQ(pair->first, scale2) << matrix;
|
||||
EXPECT_FLOAT_EQ(pair->second, scale1) << matrix;
|
||||
}
|
||||
};
|
||||
|
||||
for (int i = 1; i < 10; i++) {
|
||||
Scalar xScale = static_cast<Scalar>(i);
|
||||
for (int j = 1; j < 10; j++) {
|
||||
Scalar yScale = static_cast<Scalar>(j);
|
||||
Scalar minScale = std::min(xScale, yScale);
|
||||
Scalar maxScale = std::max(xScale, yScale);
|
||||
|
||||
{
|
||||
// Simple scale
|
||||
Matrix matrix = Matrix::MakeScale({xScale, yScale, 1.0f});
|
||||
EXPECT_TRUE(matrix.GetMinScale2D().has_value());
|
||||
EXPECT_TRUE(matrix.GetMaxScale2D().has_value());
|
||||
EXPECT_FLOAT_EQ(matrix.GetMinScale2D().value_or(-1.0f), minScale);
|
||||
EXPECT_FLOAT_EQ(matrix.GetMaxScale2D().value_or(-1.0f), maxScale);
|
||||
check_pair(matrix, xScale, yScale);
|
||||
}
|
||||
|
||||
{
|
||||
// Simple scale with Z scale
|
||||
Matrix matrix = Matrix::MakeScale({xScale, yScale, 5.0f});
|
||||
EXPECT_TRUE(matrix.GetMinScale2D().has_value());
|
||||
EXPECT_TRUE(matrix.GetMaxScale2D().has_value());
|
||||
EXPECT_FLOAT_EQ(matrix.GetMinScale2D().value_or(-1.0f), minScale);
|
||||
EXPECT_FLOAT_EQ(matrix.GetMaxScale2D().value_or(-1.0f), maxScale);
|
||||
check_pair(matrix, xScale, yScale);
|
||||
}
|
||||
|
||||
{
|
||||
// Simple scale + translate
|
||||
Matrix matrix = Matrix::MakeTranslateScale({xScale, yScale, 1.0f},
|
||||
{10.0f, 15.0f, 2.0f});
|
||||
EXPECT_TRUE(matrix.GetMinScale2D().has_value());
|
||||
EXPECT_TRUE(matrix.GetMaxScale2D().has_value());
|
||||
EXPECT_FLOAT_EQ(matrix.GetMinScale2D().value_or(-1.0f), minScale);
|
||||
EXPECT_FLOAT_EQ(matrix.GetMaxScale2D().value_or(-1.0f), maxScale);
|
||||
check_pair(matrix, xScale, yScale);
|
||||
}
|
||||
|
||||
for (int d = 45; d < 360; d += 45) {
|
||||
{
|
||||
// Rotation * Scale
|
||||
Matrix matrix = Matrix::MakeScale({xScale, yScale, 1.0f}) *
|
||||
Matrix::MakeRotationZ(Degrees(d));
|
||||
EXPECT_TRUE(matrix.GetMinScale2D().has_value());
|
||||
EXPECT_TRUE(matrix.GetMaxScale2D().has_value());
|
||||
EXPECT_FLOAT_EQ(matrix.GetMinScale2D().value_or(-1.0f), minScale);
|
||||
EXPECT_FLOAT_EQ(matrix.GetMaxScale2D().value_or(-1.0f), maxScale);
|
||||
check_pair(matrix, xScale, yScale);
|
||||
}
|
||||
|
||||
{
|
||||
// Scale * Rotation
|
||||
Matrix matrix = Matrix::MakeRotationZ(Degrees(d)) *
|
||||
Matrix::MakeScale({xScale, yScale, 1.0f});
|
||||
EXPECT_TRUE(matrix.GetMinScale2D().has_value());
|
||||
EXPECT_TRUE(matrix.GetMaxScale2D().has_value());
|
||||
EXPECT_FLOAT_EQ(matrix.GetMinScale2D().value_or(-1.0f), minScale);
|
||||
EXPECT_FLOAT_EQ(matrix.GetMaxScale2D().value_or(-1.0f), maxScale);
|
||||
check_pair(matrix, xScale, yScale);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// Scale + PerspectiveX (returns invalid values)
|
||||
Matrix matrix = Matrix::MakeScale({xScale, yScale, 1.0f});
|
||||
matrix.m[3] = 0.1;
|
||||
EXPECT_FALSE(matrix.GetMinScale2D().has_value());
|
||||
EXPECT_FALSE(matrix.GetMaxScale2D().has_value());
|
||||
EXPECT_FALSE(matrix.GetScales2D().has_value());
|
||||
}
|
||||
|
||||
{
|
||||
// Scale + PerspectiveY (returns invalid values)
|
||||
Matrix matrix = Matrix::MakeScale({xScale, yScale, 1.0f});
|
||||
matrix.m[7] = 0.1;
|
||||
EXPECT_FALSE(matrix.GetMinScale2D().has_value());
|
||||
EXPECT_FALSE(matrix.GetMaxScale2D().has_value());
|
||||
EXPECT_FALSE(matrix.GetScales2D().has_value());
|
||||
}
|
||||
|
||||
{
|
||||
// Scale + PerspectiveZ (Z ignored; returns actual scales)
|
||||
Matrix matrix = Matrix::MakeScale({xScale, yScale, 1.0f});
|
||||
matrix.m[11] = 0.1;
|
||||
EXPECT_TRUE(matrix.GetMinScale2D().has_value());
|
||||
EXPECT_TRUE(matrix.GetMaxScale2D().has_value());
|
||||
EXPECT_FLOAT_EQ(matrix.GetMinScale2D().value_or(-1.0f), minScale);
|
||||
EXPECT_FLOAT_EQ(matrix.GetMaxScale2D().value_or(-1.0f), maxScale);
|
||||
check_pair(matrix, xScale, yScale);
|
||||
}
|
||||
|
||||
{
|
||||
// Scale + PerspectiveW (returns invalid values)
|
||||
Matrix matrix = Matrix::MakeScale({xScale, yScale, 1.0f});
|
||||
matrix.m[15] = 0.1;
|
||||
EXPECT_FALSE(matrix.GetMinScale2D().has_value());
|
||||
EXPECT_FALSE(matrix.GetMaxScale2D().has_value());
|
||||
EXPECT_FALSE(matrix.GetScales2D().has_value());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace testing
|
||||
} // namespace impeller
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user