| // Copyright 2017 The Chromium 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 <cmath> |
| |
| #include "base/cxx17_backports.h" |
| #include "base/numerics/math_constants.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "ui/gfx/geometry/quaternion.h" |
| #include "ui/gfx/geometry/vector3d_f.h" |
| |
| namespace gfx { |
| |
| namespace { |
| |
| const double kEpsilon = 1e-7; |
| |
| #define EXPECT_QUATERNION(expected, actual) \ |
| do { \ |
| EXPECT_NEAR(expected.x(), actual.x(), kEpsilon); \ |
| EXPECT_NEAR(expected.y(), actual.y(), kEpsilon); \ |
| EXPECT_NEAR(expected.z(), actual.z(), kEpsilon); \ |
| EXPECT_NEAR(expected.w(), actual.w(), kEpsilon); \ |
| } while (false) |
| |
| void CompareQuaternions(const Quaternion& a, const Quaternion& b) { |
| EXPECT_FLOAT_EQ(a.x(), b.x()); |
| EXPECT_FLOAT_EQ(a.y(), b.y()); |
| EXPECT_FLOAT_EQ(a.z(), b.z()); |
| EXPECT_FLOAT_EQ(a.w(), b.w()); |
| } |
| |
| } // namespace |
| |
| TEST(QuatTest, DefaultConstruction) { |
| CompareQuaternions(Quaternion(0, 0, 0, 1), Quaternion()); |
| } |
| |
| TEST(QuatTest, AxisAngleCommon) { |
| double radians = 0.5; |
| Quaternion q(Vector3dF(1, 0, 0), radians); |
| CompareQuaternions( |
| Quaternion(std::sin(radians / 2), 0, 0, std::cos(radians / 2)), q); |
| } |
| |
| TEST(QuatTest, VectorToVectorRotation) { |
| Quaternion q(Vector3dF(1.0f, 0.0f, 0.0f), Vector3dF(0.0f, 1.0f, 0.0f)); |
| Quaternion r(Vector3dF(0.0f, 0.0f, 1.0f), base::kPiFloat / 2); |
| |
| EXPECT_FLOAT_EQ(r.x(), q.x()); |
| EXPECT_FLOAT_EQ(r.y(), q.y()); |
| EXPECT_FLOAT_EQ(r.z(), q.z()); |
| EXPECT_FLOAT_EQ(r.w(), q.w()); |
| } |
| |
| TEST(QuatTest, AxisAngleWithZeroLengthAxis) { |
| Quaternion q(Vector3dF(0, 0, 0), 0.5); |
| // If the axis of zero length, we should assume the default values. |
| CompareQuaternions(q, Quaternion()); |
| } |
| |
| TEST(QuatTest, Addition) { |
| double values[] = {0, 1, 100}; |
| for (size_t i = 0; i < base::size(values); ++i) { |
| float t = values[i]; |
| Quaternion a(t, 2 * t, 3 * t, 4 * t); |
| Quaternion b(5 * t, 4 * t, 3 * t, 2 * t); |
| Quaternion sum = a + b; |
| CompareQuaternions(Quaternion(t, t, t, t) * 6, sum); |
| } |
| } |
| |
| TEST(QuatTest, Multiplication) { |
| struct { |
| Quaternion a; |
| Quaternion b; |
| Quaternion expected; |
| } cases[] = { |
| {Quaternion(1, 0, 0, 0), Quaternion(1, 0, 0, 0), Quaternion(0, 0, 0, -1)}, |
| {Quaternion(0, 1, 0, 0), Quaternion(0, 1, 0, 0), Quaternion(0, 0, 0, -1)}, |
| {Quaternion(0, 0, 1, 0), Quaternion(0, 0, 1, 0), Quaternion(0, 0, 0, -1)}, |
| {Quaternion(0, 0, 0, 1), Quaternion(0, 0, 0, 1), Quaternion(0, 0, 0, 1)}, |
| {Quaternion(1, 2, 3, 4), Quaternion(5, 6, 7, 8), |
| Quaternion(24, 48, 48, -6)}, |
| {Quaternion(5, 6, 7, 8), Quaternion(1, 2, 3, 4), |
| Quaternion(32, 32, 56, -6)}, |
| }; |
| |
| for (size_t i = 0; i < base::size(cases); ++i) { |
| Quaternion product = cases[i].a * cases[i].b; |
| CompareQuaternions(cases[i].expected, product); |
| } |
| } |
| |
| TEST(QuatTest, Scaling) { |
| double values[] = {0, 10, 100}; |
| for (size_t i = 0; i < base::size(values); ++i) { |
| double s = values[i]; |
| Quaternion q(1, 2, 3, 4); |
| Quaternion expected(s, 2 * s, 3 * s, 4 * s); |
| CompareQuaternions(expected, q * s); |
| CompareQuaternions(expected, s * q); |
| if (s > 0) |
| CompareQuaternions(expected, q / (1 / s)); |
| } |
| } |
| |
| TEST(QuatTest, Normalization) { |
| Quaternion q(1, -1, 1, -1); |
| EXPECT_NEAR(q.Length(), 4, kEpsilon); |
| |
| q = q.Normalized(); |
| |
| EXPECT_NEAR(q.Length(), 1, kEpsilon); |
| EXPECT_NEAR(q.x(), 0.5, kEpsilon); |
| EXPECT_NEAR(q.y(), -0.5, kEpsilon); |
| EXPECT_NEAR(q.z(), 0.5, kEpsilon); |
| EXPECT_NEAR(q.w(), -0.5, kEpsilon); |
| } |
| |
| TEST(QuatTest, Lerp) { |
| for (size_t i = 1; i < 100; ++i) { |
| Quaternion a(0, 0, 0, 0); |
| Quaternion b(1, 2, 3, 4); |
| float t = static_cast<float>(i) / 100.0f; |
| Quaternion interpolated = a.Lerp(b, t); |
| double s = 1.0 / sqrt(30.0); |
| CompareQuaternions(Quaternion(1, 2, 3, 4) * s, interpolated); |
| } |
| |
| Quaternion a(4, 3, 2, 1); |
| Quaternion b(1, 2, 3, 4); |
| CompareQuaternions(a.Normalized(), a.Lerp(b, 0)); |
| CompareQuaternions(b.Normalized(), a.Lerp(b, 1)); |
| CompareQuaternions(Quaternion(1, 1, 1, 1).Normalized(), a.Lerp(b, 0.5)); |
| } |
| |
| TEST(QuatTest, Slerp) { |
| Vector3dF axis(1, 1, 1); |
| double start_radians = -0.5; |
| double stop_radians = 0.5; |
| Quaternion start(axis, start_radians); |
| Quaternion stop(axis, stop_radians); |
| |
| for (size_t i = 0; i < 100; ++i) { |
| float t = static_cast<float>(i) / 100.0f; |
| double radians = (1.0 - t) * start_radians + t * stop_radians; |
| Quaternion expected(axis, radians); |
| Quaternion interpolated = start.Slerp(stop, t); |
| EXPECT_QUATERNION(expected, interpolated); |
| } |
| } |
| |
| TEST(QuatTest, SlerpOppositeAngles) { |
| Vector3dF axis(1, 1, 1); |
| double start_radians = -base::kPiDouble / 2; |
| double stop_radians = base::kPiDouble / 2; |
| Quaternion start(axis, start_radians); |
| Quaternion stop(axis, stop_radians); |
| |
| // When quaternions are pointed in the fully opposite direction, this is |
| // ambiguous, so we rotate as per https://www.w3.org/TR/css-transforms-1/ |
| Quaternion expected(axis, 0); |
| |
| Quaternion interpolated = start.Slerp(stop, 0.5f); |
| EXPECT_QUATERNION(expected, interpolated); |
| } |
| |
| TEST(QuatTest, SlerpRotateXRotateY) { |
| Quaternion start(Vector3dF(1, 0, 0), base::kPiDouble / 2); |
| Quaternion stop(Vector3dF(0, 1, 0), base::kPiDouble / 2); |
| Quaternion interpolated = start.Slerp(stop, 0.5f); |
| |
| double expected_angle = std::acos(1.0 / 3.0); |
| double xy = std::sin(0.5 * expected_angle) / std::sqrt(2); |
| Quaternion expected(xy, xy, 0, std::cos(0.5 * expected_angle)); |
| EXPECT_QUATERNION(expected, interpolated); |
| } |
| |
| TEST(QuatTest, Slerp360) { |
| Quaternion start(0, 0, 0, -1); // 360 degree rotation. |
| Quaternion stop(Vector3dF(0, 0, 1), base::kPiDouble / 2); |
| Quaternion interpolated = start.Slerp(stop, 0.5f); |
| double expected_half_angle = base::kPiDouble / 8; |
| Quaternion expected(0, 0, std::sin(expected_half_angle), |
| std::cos(expected_half_angle)); |
| EXPECT_QUATERNION(expected, interpolated); |
| } |
| |
| TEST(QuatTest, SlerpEquivalentQuaternions) { |
| Quaternion start(Vector3dF(1, 0, 0), base::kPiDouble / 3); |
| Quaternion stop = start.flip(); |
| Quaternion interpolated = start.Slerp(stop, 0.5f); |
| EXPECT_QUATERNION(start, interpolated); |
| } |
| |
| TEST(QuatTest, SlerpQuaternionWithInverse) { |
| Quaternion start(Vector3dF(1, 0, 0), base::kPiDouble / 3); |
| Quaternion stop = start.inverse(); |
| Quaternion interpolated = start.Slerp(stop, 0.5f); |
| Quaternion expected(0, 0, 0, 1); |
| EXPECT_QUATERNION(expected, interpolated); |
| } |
| |
| TEST(QuatTest, SlerpObtuseAngle) { |
| Quaternion start(Vector3dF(1, 1, 0), base::kPiDouble / 2); |
| Quaternion stop(Vector3dF(0, 1, -1), 3 * base::kPiDouble / 2); |
| Quaternion interpolated = start.Slerp(stop, 0.5f); |
| double expected_half_angle = -std::atan(0.5); |
| double xz = std::sin(expected_half_angle) / std::sqrt(2); |
| Quaternion expected(xz, 0, xz, -std::cos(expected_half_angle)); |
| EXPECT_QUATERNION(expected, interpolated); |
| } |
| |
| TEST(QuatTest, Equals) { |
| EXPECT_TRUE(Quaternion() == Quaternion()); |
| EXPECT_TRUE(Quaternion() == Quaternion(0, 0, 0, 1)); |
| EXPECT_TRUE(Quaternion(1, 5.2, -8.5, 222.2) == |
| Quaternion(1, 5.2, -8.5, 222.2)); |
| EXPECT_FALSE(Quaternion() == Quaternion(1, 0, 0, 0)); |
| EXPECT_FALSE(Quaternion() == Quaternion(0, 1, 0, 0)); |
| EXPECT_FALSE(Quaternion() == Quaternion(0, 0, 1, 0)); |
| EXPECT_FALSE(Quaternion() == Quaternion(1, 0, 0, 1)); |
| } |
| |
| TEST(QuatTest, NotEquals) { |
| EXPECT_FALSE(Quaternion() != Quaternion()); |
| EXPECT_FALSE(Quaternion(1, 5.2, -8.5, 222.2) != |
| Quaternion(1, 5.2, -8.5, 222.2)); |
| EXPECT_TRUE(Quaternion() != Quaternion(1, 0, 0, 0)); |
| EXPECT_TRUE(Quaternion() != Quaternion(0, 1, 0, 0)); |
| EXPECT_TRUE(Quaternion() != Quaternion(0, 0, 1, 0)); |
| EXPECT_TRUE(Quaternion() != Quaternion(1, 0, 0, 1)); |
| } |
| |
| } // namespace gfx |