From 2e27fb6a9776463fa865197db652ec3016b97b8d Mon Sep 17 00:00:00 2001 From: Joshua Gross Date: Wed, 24 Jun 2020 16:34:24 -0700 Subject: [PATCH] LayoutAnimations: for all Transform matrices, store decomposed matrix and use that for interpolation Summary: Previously, we tried to take a Transform matrix, decompose it into parts, and then interpolate between those parts. This will always be risky at best, and in some cases ambiguous or unsolvable. For example, a scale of -1 is identical to a rotation of 180 degrees. Another issue is that when decomposing a matrix, it is impossible to tell the sign of scaleX, scaleY, scaleZ. This is a problem - flipping a View over an axis via scale then becomes a non-animatable operation. This caused a number of issues. To resolve it, we accumulate the "operations" resulting in a particular transform. This allows us to easily interpolate between two Transform matrices without actually decomposing the matrix, since we have the "path" that resulted in each particular matrix. This will make LayoutAnimations over transforms, including Skew transforms, look and work much better, and more reliably. Changelog: [Internal] Reviewed By: shergin Differential Revision: D22204559 fbshipit-source-id: 0d88ae77e4399a7ea333afbf6062beea977b854a --- .../fabric/components/view/conversions.h | 2 + ReactCommon/fabric/graphics/Quaternion.h | 208 ------------ ReactCommon/fabric/graphics/Transform.cpp | 303 ++++++++++-------- ReactCommon/fabric/graphics/Transform.h | 64 ++-- 4 files changed, 199 insertions(+), 378 deletions(-) delete mode 100644 ReactCommon/fabric/graphics/Quaternion.h diff --git a/ReactCommon/fabric/components/view/conversions.h b/ReactCommon/fabric/components/view/conversions.h index 0ac41614dd0..5a3cb1a8cc2 100644 --- a/ReactCommon/fabric/components/view/conversions.h +++ b/ReactCommon/fabric/components/view/conversions.h @@ -422,6 +422,8 @@ inline void fromRawValue(const RawValue &value, Transform &result) { for (auto number : numbers) { transformMatrix.matrix[i++] = number; } + transformMatrix.operations.push_back( + TransformOperation{TransformOperationType::Arbitrary, 0, 0, 0}); } else if (operation == "perspective") { transformMatrix = transformMatrix * Transform::Perspective((Float)parameters); diff --git a/ReactCommon/fabric/graphics/Quaternion.h b/ReactCommon/fabric/graphics/Quaternion.h deleted file mode 100644 index 4233a2c6cb4..00000000000 --- a/ReactCommon/fabric/graphics/Quaternion.h +++ /dev/null @@ -1,208 +0,0 @@ -/* - * Portions Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -#pragma once - -#include -#include -#include - -// The following is a modified, stripped-down version of the Quaternion class -// by Frank Astier. Copyright notice below. -// The original has many, many more features, and has been stripped down -// to support the exact data-structures and use-cases we need for React Native. - -/** - * The MIT License (MIT) - * - * Copyright (c) 2015 Frank Astier - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -namespace facebook { -namespace react { - -template -class Quaternion { - public: - /** - * Copy constructor. - */ - Quaternion(const Quaternion &y) : a_(y.a_), b_(y.b_), c_(y.c_), d_(y.d_) {} - - Quaternion(T a, T b, T c, T d) : a_(a), b_(b), c_(c), d_(d) {} - - static Quaternion fromRotationMatrix(std::array const &rm) { - T t = rm[0 * 4 + 0] + rm[1 * 4 + 1] + rm[2 * 4 + 2]; - if (t > 0) { - T s = (T)0.5 / std::sqrt(t + 1); - return {(T)0.25 / s, - (rm[2 * 4 + 1] - rm[1 * 4 + 2]) * s, - (rm[0 * 4 + 2] - rm[2 * 4 + 0]) * s, - (rm[1 * 4 + 0] - rm[0 * 4 + 1]) * s}; - } else if (rm[0 * 4 + 0] > rm[1 * 4 + 1] && rm[0 * 4 + 0] > rm[2 * 4 + 2]) { - T s = (T)2.0 * - std::sqrt( - 1.0 + rm[0 * 4 + 0] - rm[1 * 4 + 1] - rm[2 * 4 + 2]); // S=4*qx - return {(rm[2 * 4 + 1] - rm[1 * 4 + 2]) / s, - (T)0.25 * s, - (rm[0 * 4 + 1] + rm[1 * 4 + 0]) / s, - (rm[0 * 4 + 2] + rm[2 * 4 + 0]) / s}; - } else if (rm[1 * 4 + 1] > rm[2 * 4 + 2]) { - T s = (T)2.0 * - std::sqrt( - 1.0 + rm[1 * 4 + 1] - rm[0 * 4 + 0] - rm[2 * 4 + 2]); // S=4*qy - return {(rm[0 * 4 + 2] - rm[2 * 4 + 0]) / s, - (rm[0 * 4 + 1] + rm[1 * 4 + 0]) / s, - (T)0.25 * s, - (rm[1 * 4 + 2] + rm[2 * 4 + 1]) / s}; - } else { - T s = (T)2.0 * - std::sqrt( - 1.0 + rm[2 * 4 + 2] - rm[0 * 4 + 0] - rm[1 * 4 + 1]); // S=4*qz - return {(rm[1 * 4 + 0] - rm[0 * 4 + 1]) / s, - (rm[0 * 4 + 2] + rm[2 * 4 + 0]) / s, - (rm[1 * 4 + 2] + rm[2 * 4 + 1]) / s, - (T)0.25 * s}; - } - } - - /** - * Returns a 3D, 4x4 rotation matrix. - * This is the "homogeneous" expression to convert to a rotation matrix, - * which works even when the Quaternion is not a unit Quaternion. - */ - inline std::array toRotationMatrix4x4() { - T a2 = a_ * a_, b2 = b_ * b_, c2 = c_ * c_, d2 = d_ * d_; - T ab = a_ * b_, ac = a_ * c_, ad = a_ * d_; - T bc = b_ * c_, bd = b_ * d_; - T cd = c_ * d_; - return {a2 + b2 - c2 - d2, - 2 * (bc - ad), - 2 * (bd + ac), - 0, - 2 * (bc + ad), - a2 - b2 + c2 - d2, - 2 * (cd - ab), - 0, - 2 * (bd - ac), - 2 * (cd + ab), - a2 - b2 - c2 + d2, - 0, - 0, - 0, - 0, - 1}; - } - - inline Quaternion normalize() const { - assert(abs() > 0); // or this is not normalizable - T factor = abs(); - return *this / (factor != 0 ? factor : 1); - } - - inline T dot(const Quaternion &other) { - return a_ * other.a_ + b_ * other.b_ + c_ * other.c_ + d_ * other.d_; - } - - /** - * The square of the norm of the Quaternion. - * (The square is sometimes useful, and it avoids paying for a sqrt). - */ - inline T norm_squared() const { - return a_ * a_ + b_ * b_ + c_ * c_ + d_ * d_; - } - - /** - * The norm of the Quaternion (the l2 norm). - */ - inline T abs() const { - return std::sqrt(norm_squared()); - } - - inline Quaternion operator/=(T y) { - a_ /= y; - b_ /= y; - c_ /= y; - d_ /= y; - return *this; - } - - inline Quaternion operator*=(T y) { - a_ *= y; - b_ *= y; - c_ *= y; - d_ *= y; - return *this; - } - - inline Quaternion operator+=(Quaternion const &other) { - a_ += other.a_; - b_ += other.b_; - c_ += other.c_; - d_ += other.d_; - return *this; - } - - inline Quaternion operator-=(Quaternion const &other) { - a_ -= other.a_; - b_ -= other.b_; - c_ -= other.c_; - d_ -= other.d_; - return *this; - } - - private: - T a_; // AKA w, qw - T b_; // AKA x, qx - T c_; // AKA y, qy - T d_; // AKA z, qz -}; - -template -inline Quaternion operator/(Quaternion const &lhs, T rhs) { - return Quaternion(lhs) /= rhs; -} - -template -inline Quaternion operator*(Quaternion const &lhs, T rhs) { - return Quaternion(lhs) *= rhs; -} - -template -inline Quaternion operator+( - Quaternion const &lhs, - Quaternion const &rhs) { - return Quaternion(lhs) += rhs; -} - -template -inline Quaternion operator-( - Quaternion const &lhs, - Quaternion const &rhs) { - return Quaternion(lhs) -= rhs; -} - -} // namespace react -} // namespace facebook diff --git a/ReactCommon/fabric/graphics/Transform.cpp b/ReactCommon/fabric/graphics/Transform.cpp index f1711f0418f..6caea8e9749 100644 --- a/ReactCommon/fabric/graphics/Transform.cpp +++ b/ReactCommon/fabric/graphics/Transform.cpp @@ -7,7 +7,6 @@ #include "Transform.h" -#include #include #include @@ -34,203 +33,211 @@ Transform Transform::Identity() { Transform Transform::Perspective(Float perspective) { auto transform = Transform{}; + transform.operations.push_back(TransformOperation{ + TransformOperationType::Perspective, perspective, 0, 0}); transform.matrix[11] = -1 / perspective; return transform; } -Transform Transform::Scale(Float factorX, Float factorY, Float factorZ) { +Transform Transform::Scale(Float x, Float y, Float z) { auto transform = Transform{}; - transform.matrix[0] = factorX; - transform.matrix[5] = factorY; - transform.matrix[10] = factorZ; + Float xprime = isZero(x) ? 0 : x; + Float yprime = isZero(y) ? 0 : y; + Float zprime = isZero(z) ? 0 : z; + if (xprime != 1 || yprime != 1 || zprime != 1) { + transform.operations.push_back(TransformOperation{ + TransformOperationType::Scale, xprime, yprime, zprime}); + transform.matrix[0] = xprime; + transform.matrix[5] = yprime; + transform.matrix[10] = zprime; + } return transform; } Transform Transform::Translate(Float x, Float y, Float z) { auto transform = Transform{}; - transform.matrix[12] = x; - transform.matrix[13] = y; - transform.matrix[14] = z; + Float xprime = isZero(x) ? 0 : x; + Float yprime = isZero(y) ? 0 : y; + Float zprime = isZero(z) ? 0 : z; + if (xprime != 0 || yprime != 0 || zprime != 0) { + transform.operations.push_back(TransformOperation{ + TransformOperationType::Translate, xprime, yprime, zprime}); + transform.matrix[12] = xprime; + transform.matrix[13] = yprime; + transform.matrix[14] = zprime; + } return transform; } Transform Transform::Skew(Float x, Float y) { auto transform = Transform{}; - transform.matrix[4] = std::tan(x); - transform.matrix[1] = std::tan(y); + Float xprime = isZero(x) ? 0 : x; + Float yprime = isZero(y) ? 0 : y; + transform.operations.push_back( + TransformOperation{TransformOperationType::Skew, xprime, yprime, 0}); + transform.matrix[4] = std::tan(xprime); + transform.matrix[1] = std::tan(yprime); return transform; } Transform Transform::RotateX(Float radians) { auto transform = Transform{}; - transform.matrix[5] = std::cos(radians); - transform.matrix[6] = std::sin(radians); - transform.matrix[9] = -std::sin(radians); - transform.matrix[10] = std::cos(radians); + if (!isZero(radians)) { + transform.operations.push_back( + TransformOperation{TransformOperationType::Rotate, radians, 0, 0}); + transform.matrix[5] = std::cos(radians); + transform.matrix[6] = std::sin(radians); + transform.matrix[9] = -std::sin(radians); + transform.matrix[10] = std::cos(radians); + } return transform; } Transform Transform::RotateY(Float radians) { auto transform = Transform{}; - transform.matrix[0] = std::cos(radians); - transform.matrix[2] = -std::sin(radians); - transform.matrix[8] = std::sin(radians); - transform.matrix[10] = std::cos(radians); + if (!isZero(radians)) { + transform.operations.push_back( + TransformOperation{TransformOperationType::Rotate, 0, radians, 0}); + transform.matrix[0] = std::cos(radians); + transform.matrix[2] = -std::sin(radians); + transform.matrix[8] = std::sin(radians); + transform.matrix[10] = std::cos(radians); + } return transform; } Transform Transform::RotateZ(Float radians) { auto transform = Transform{}; - transform.matrix[0] = std::cos(radians); - transform.matrix[1] = std::sin(radians); - transform.matrix[4] = -std::sin(radians); - transform.matrix[5] = std::cos(radians); + if (!isZero(radians)) { + transform.operations.push_back( + TransformOperation{TransformOperationType::Rotate, 0, 0, radians}); + transform.matrix[0] = std::cos(radians); + transform.matrix[1] = std::sin(radians); + transform.matrix[4] = -std::sin(radians); + transform.matrix[5] = std::cos(radians); + } return transform; } Transform Transform::Rotate(Float x, Float y, Float z) { auto transform = Transform{}; - if (x != 0) { + transform.operations.push_back( + TransformOperation{TransformOperationType::Rotate, x, y, z}); + if (!isZero(x)) { transform = transform * Transform::RotateX(x); } - if (y != 0) { + if (!isZero(y)) { transform = transform * Transform::RotateY(y); } - if (z != 0) { + if (!isZero(z)) { transform = transform * Transform::RotateZ(z); } return transform; } -Transform::SRT Transform::ExtractSRT(Transform const &t) { - // First we need to extract translation, rotation, and scale from both - // matrices, in that order. Matrices must be in this form: [a b c d] [e f g h] - // [i j k l] - // [0 0 0 1] - // We also assume that all scale factors are non-negative. - // TODO T68587989: If ViewProps retains the underlying transform props instead - // of just the matrix version of transforms, then we can use those properties - // directly instead of decomposing properties from a matrix which will always - // be lossy. Because of these assumptions, animations involving negative - // scale/rotation and anything involving skews will not look great. - // assert( - // t.matrix[12] == 0 && t.matrix[13] == 0 && t.matrix[14] == 0 && - // t.matrix[15] == 1 && "Last row of matrix must be [0,0,0,1]"); +Transform Transform::FromTransformOperation( + TransformOperation transformOperation) { + if (transformOperation.type == TransformOperationType::Perspective) { + return Transform::Perspective(transformOperation.x); + } + if (transformOperation.type == TransformOperationType::Scale) { + return Transform::Scale( + transformOperation.x, transformOperation.y, transformOperation.z); + } + if (transformOperation.type == TransformOperationType::Translate) { + return Transform::Translate( + transformOperation.x, transformOperation.y, transformOperation.z); + } + if (transformOperation.type == TransformOperationType::Skew) { + return Transform::Skew(transformOperation.x, transformOperation.y); + } + if (transformOperation.type == TransformOperationType::Rotate) { + return Transform::Rotate( + transformOperation.x, transformOperation.y, transformOperation.z); + } - // lhs: - // Translation: extract the values from the rightmost column - Float translationX = t.matrix[3]; - Float translationY = t.matrix[7]; - Float translationZ = t.matrix[11]; + // Identity or Arbitrary + return Transform::Identity(); +} - // Scale: the length of the first three column vectors - // TODO: do we need to do anything special for negative scale factors? - // the last element is a uniform scale factor - Float scaleX = t.matrix[15] * - sqrt(pow(t.matrix[0], 2) + pow(t.matrix[4], 2) + - pow(t.matrix[8], 2)); // sqrt(a^2 + e^2 + i^2) - Float scaleY = t.matrix[15] * - sqrt(pow(t.matrix[1], 2) + pow(t.matrix[5], 2) + - pow(t.matrix[9], 2)); // sqrt(b^2 + f^2 + j^2) - Float scaleZ = t.matrix[15] * - sqrt(pow(t.matrix[2], 2) + pow(t.matrix[6], 2) + - pow(t.matrix[10], 2)); // sqrt(c^2 + g^2 + k^2) - - Float rScaleFactorX = scaleX == 0 ? 1 : scaleX; - Float rScaleFactorY = scaleY == 0 ? 1 : scaleY; - Float rScaleFactorZ = scaleZ == 0 ? 1 : scaleZ; - - // Construct a rotation matrix and convert that to quaternions - auto rotationMatrix = std::array{t.matrix[0] / rScaleFactorX, - t.matrix[1] / rScaleFactorY, - t.matrix[2] / rScaleFactorZ, - 0, - t.matrix[4] / rScaleFactorX, - t.matrix[5] / rScaleFactorY, - t.matrix[6] / rScaleFactorZ, - 0, - t.matrix[8] / rScaleFactorX, - t.matrix[9] / rScaleFactorY, - t.matrix[10] / rScaleFactorZ, - 0, - 0, - 0, - 0, - 1}; - - Quaternion q = - Quaternion::fromRotationMatrix(rotationMatrix).normalize(); - - return Transform::SRT{ - translationX, translationY, translationZ, scaleX, scaleY, scaleZ, q}; +TransformOperation Transform::DefaultTransformOperation( + TransformOperationType type) { + switch (type) { + case TransformOperationType::Arbitrary: + return TransformOperation{TransformOperationType::Arbitrary, 0, 0, 0}; + case TransformOperationType::Perspective: + return TransformOperation{TransformOperationType::Perspective, 0, 0, 0}; + case TransformOperationType::Scale: + return TransformOperation{TransformOperationType::Scale, 1, 1, 1}; + case TransformOperationType::Translate: + return TransformOperation{TransformOperationType::Translate, 0, 0, 0}; + case TransformOperationType::Rotate: + return TransformOperation{TransformOperationType::Rotate, 0, 0, 0}; + case TransformOperationType::Skew: + return TransformOperation{TransformOperationType::Skew, 0, 0, 0}; + default: + case TransformOperationType::Identity: + return TransformOperation{TransformOperationType::Identity, 0, 0, 0}; + } } Transform Transform::Interpolate( float animationProgress, Transform const &lhs, Transform const &rhs) { - // Extract SRT for both sides - // This is extracted in the form: X,Y,Z coordinates for translations; X,Y,Z - // coordinates for scale; and a quaternion for rotation. - auto lhsSRT = ExtractSRT(lhs); - auto rhsSRT = ExtractSRT(rhs); + // Iterate through operations and reconstruct an interpolated resulting + // transform If at any point we hit an "Arbitrary" Transform, return at that + // point + Transform result = Transform::Identity(); + for (int i = 0, j = 0; + i < lhs.operations.size() || j < rhs.operations.size();) { + bool haveLHS = i < lhs.operations.size(); + bool haveRHS = j < rhs.operations.size(); - // Interpolate translation and scale terms linearly (LERP) - Float translateX = - (lhsSRT.translationX + - (rhsSRT.translationX - lhsSRT.translationX) * animationProgress); - Float translateY = - (lhsSRT.translationY + - (rhsSRT.translationY - lhsSRT.translationY) * animationProgress); - Float translateZ = - (lhsSRT.translationZ + - (rhsSRT.translationZ - lhsSRT.translationZ) * animationProgress); - Float scaleX = - (lhsSRT.scaleX + (rhsSRT.scaleX - lhsSRT.scaleX) * animationProgress); - Float scaleY = - (lhsSRT.scaleY + (rhsSRT.scaleY - lhsSRT.scaleY) * animationProgress); - Float scaleZ = - (lhsSRT.scaleZ + (rhsSRT.scaleZ - lhsSRT.scaleZ) * animationProgress); + if ((haveLHS && + lhs.operations[i].type == TransformOperationType::Arbitrary) || + (haveRHS && + rhs.operations[i].type == TransformOperationType::Arbitrary)) { + return result; + } + if (haveLHS && lhs.operations[i].type == TransformOperationType::Identity) { + i++; + continue; + } + if (haveRHS && rhs.operations[j].type == TransformOperationType::Identity) { + j++; + continue; + } - // Use the quaternion vectors to produce an interpolated rotation via SLERP - // dot: cos of the angle between the two quaternion vectors - Quaternion q1 = lhsSRT.rotation; - Quaternion q2 = rhsSRT.rotation; - Float dot = q1.dot(q2); - // Clamp dot between -1 and 1 - dot = (dot < -1 ? -1 : (dot > 1 ? 1 : dot)); - // There are two ways of performing an identical slerp: q1 and -q1. - // If the dot-product is negative, we can multiply q1 by -1 and our animation - // will take the "short way" around instead of the "long way". - if (dot < 0) { - q1 = q1 * (Float)-1; - dot = dot * -1; - } - // Interpolated angle - Float theta = acosf(dot) * animationProgress; + // Here we either set: + // 1. lhs = next left op, rhs = next right op (when types are identical and + // both exist) + // 2. lhs = next left op, rhs = default of type (if types unequal, or rhs + // doesn't exist) + // 3. lhs = default of type, rhs = next right op (if types unequal, or rhs + // doesn't exist) This guarantees that the types of both sides are equal, + // and that one or both indices moves forward. + TransformOperationType type = + (haveLHS ? lhs.operations[i] : rhs.operations[j]).type; + TransformOperation lhsOp = + (haveLHS ? lhs.operations[i++] + : Transform::DefaultTransformOperation(type)); + TransformOperation rhsOp = + (haveRHS && rhs.operations[j].type == type + ? rhs.operations[j++] + : Transform::DefaultTransformOperation(type)); + assert(type == lhsOp.type); + assert(type == rhsOp.type); - Transform rotation = Transform::Identity(); - - // Compute orthonormal basis - Quaternion orthonormalBasis = (q2 - q1 * dot); - - if (orthonormalBasis.abs() > 0) { - Quaternion orthonormalBasisNormalized = orthonormalBasis.normalize(); - - // Compute orthonormal basis - // Final quaternion result - slerp! - Quaternion resultingRotationVec = - (q1 * (Float)cos(theta) + - orthonormalBasisNormalized * (Float)sin(theta)) - .normalize(); - - // Convert quaternion to matrix - rotation.matrix = resultingRotationVec.toRotationMatrix4x4(); + result = result * + Transform::FromTransformOperation(TransformOperation{ + type, + lhsOp.x + (rhsOp.x - lhsOp.x) * animationProgress, + lhsOp.y + (rhsOp.y - lhsOp.y) * animationProgress, + lhsOp.z + (rhsOp.z - lhsOp.z) * animationProgress}); } - // Compose matrices and return - return (Scale(scaleX, scaleY, scaleZ) * rotation) * - Translate(translateX, translateY, translateZ); + return result; } bool Transform::operator==(Transform const &rhs) const { @@ -253,6 +260,20 @@ Transform Transform::operator*(Transform const &rhs) const { const auto &lhs = *this; auto result = Transform{}; + for (const auto &op : this->operations) { + if (op.type == TransformOperationType::Identity && + result.operations.size() > 0) { + continue; + } + result.operations.push_back(op); + } + for (const auto &op : rhs.operations) { + if (op.type == TransformOperationType::Identity && + result.operations.size() > 0) { + continue; + } + result.operations.push_back(op); + } auto lhs00 = lhs.matrix[0], lhs01 = lhs.matrix[1], lhs02 = lhs.matrix[2], lhs03 = lhs.matrix[3], lhs10 = lhs.matrix[4], lhs11 = lhs.matrix[5], diff --git a/ReactCommon/fabric/graphics/Transform.h b/ReactCommon/fabric/graphics/Transform.h index 357e2863d7d..7d14a635afb 100644 --- a/ReactCommon/fabric/graphics/Transform.h +++ b/ReactCommon/fabric/graphics/Transform.h @@ -8,11 +8,11 @@ #pragma once #include +#include #include #include #include -#include #ifdef ANDROID #include @@ -21,21 +21,38 @@ namespace facebook { namespace react { -struct ScaleRotationTranslation { - Float translationX; - Float translationY; - Float translationZ; - Float scaleX; - Float scaleY; - Float scaleZ; - Quaternion rotation; +inline bool isZero(Float n) { + // We use this ternary expression instead of abs, fabsf, etc, because + // Float can be double or float depending on compilation target. + return (n < 0 ? n * (-1) : n) < 0.00001; +} + +/** + * Defines operations used to construct a transform matrix. + * An "Arbitrary" operation means that the transform was seeded with some + * arbitrary initial result. + */ +enum class TransformOperationType { + Arbitrary, + Identity, + Perspective, + Scale, + Translate, + Rotate, + Skew +}; +struct TransformOperation { + TransformOperationType type; + Float x; + Float y; + Float z; }; /* * Defines transform matrix to apply affine transformations. */ struct Transform { - using SRT = ScaleRotationTranslation; + std::vector operations{}; std::array matrix{ {1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1}}; @@ -47,6 +64,14 @@ struct Transform { static void print(Transform const &t, std::string prefix); #endif + /* + * Given a TransformOperation, return the proper transform. + */ + static Transform FromTransformOperation( + TransformOperation transformOperation); + static TransformOperation DefaultTransformOperation( + TransformOperationType type); + /* * Returns the identity transform (`[1 0 0 0; 0 1 0 0; 0 0 1 0; 0 0 0 1]`). */ @@ -80,25 +105,6 @@ struct Transform { static Transform RotateZ(Float angle); static Transform Rotate(Float angleX, Float angleY, Float angleZ); - /** - * Extract SRT (scale, rotation, transformation) from a Transform matrix. - * - * CAVEATS: - * 1. The input matrix must not have Skew applied. - * 2. Scaling factors must be non-negative. Scaling by a negative factor is - * equivalent to a rotation, and though it is possible to detect if 1 or - * 3 of the scale signs are flipped (but not two), it is not possible - * to detect WHICH of the scales are flipped. Thus, any animation - * that involves a negative scale factor will not crash but will - * interpolate over nonsensical values. - * 3. Another caveat is that if the animation interpolates TO a 90º - * rotation in the X, Y, or Z axis, the View will appear to suddenly - * explode in size. Interpolating THROUGH 90º is fine as long as you don't end - * up at 90º or close to it (89.99). The same is true for 0±90 and 360n+90, - * etc. - */ - static SRT ExtractSRT(Transform const &transform); - /** * Perform an interpolation between lhs and rhs, given progress. * This first decomposes the matrices into translation, scale, and rotation,