diff --git a/ReactCommon/fabric/graphics/Quaternion.h b/ReactCommon/fabric/graphics/Quaternion.h new file mode 100644 index 00000000000..4233a2c6cb4 --- /dev/null +++ b/ReactCommon/fabric/graphics/Quaternion.h @@ -0,0 +1,208 @@ +/* + * 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 46575fec31c..34e8f5ca61b 100644 --- a/ReactCommon/fabric/graphics/Transform.cpp +++ b/ReactCommon/fabric/graphics/Transform.cpp @@ -7,18 +7,34 @@ #include "Transform.h" +#include #include +#include + namespace facebook { namespace react { +#ifdef RN_DEBUG_STRING_CONVERTIBLE +void Transform::print(Transform const &t, std::string prefix) { + LOG(ERROR) << prefix << "[ " << t.matrix[0] << " " << t.matrix[1] << " " + << t.matrix[2] << " " << t.matrix[3] << " ]"; + LOG(ERROR) << prefix << "[ " << t.matrix[4] << " " << t.matrix[5] << " " + << t.matrix[6] << " " << t.matrix[7] << " ]"; + LOG(ERROR) << prefix << "[ " << t.matrix[8] << " " << t.matrix[9] << " " + << t.matrix[10] << " " << t.matrix[11] << " ]"; + LOG(ERROR) << prefix << "[ " << t.matrix[12] << " " << t.matrix[13] << " " + << t.matrix[14] << " " << t.matrix[15] << " ]"; +} +#endif + Transform Transform::Identity() { return {}; } Transform Transform::Perspective(Float perspective) { auto transform = Transform{}; - transform.matrix[11] = -1.0 / perspective; + transform.matrix[11] = -1 / perspective; return transform; } @@ -86,16 +102,130 @@ Transform Transform::Rotate(Float x, Float y, Float 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, because in + 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]"); + + // lhs: + // Translation: extract the values from the rightmost column + Float translationX = t.matrix[3]; + Float translationY = t.matrix[7]; + Float translationZ = t.matrix[11]; + + // 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}; +} + Transform Transform::Interpolate( float animationProgress, Transform const &lhs, Transform const &rhs) { - auto result = Transform{}; - for (size_t i = 0; i < 16; i++) { - result.matrix[i] = - lhs.matrix[i] + (rhs.matrix[i] - lhs.matrix[i]) * animationProgress; + // 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); + + // 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); + + // 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; } - return result; + // Interpolated angle + Float theta = acosf(dot) * animationProgress; + + 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(); + } + + // Compose matrices and return + return (Scale(scaleX, scaleY, scaleZ) * rotation) * + Translate(translateX, translateY, translateZ); } bool Transform::operator==(Transform const &rhs) const { diff --git a/ReactCommon/fabric/graphics/Transform.h b/ReactCommon/fabric/graphics/Transform.h index 18e568f5471..357e2863d7d 100644 --- a/ReactCommon/fabric/graphics/Transform.h +++ b/ReactCommon/fabric/graphics/Transform.h @@ -12,6 +12,7 @@ #include #include #include +#include #ifdef ANDROID #include @@ -20,13 +21,32 @@ namespace facebook { namespace react { +struct ScaleRotationTranslation { + Float translationX; + Float translationY; + Float translationZ; + Float scaleX; + Float scaleY; + Float scaleZ; + Quaternion rotation; +}; + /* * Defines transform matrix to apply affine transformations. */ struct Transform { + using SRT = ScaleRotationTranslation; + std::array matrix{ {1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1}}; + /** + * For debugging only. Prints out the matrix. + */ +#ifdef RN_DEBUG_STRING_CONVERTIBLE + static void print(Transform const &t, std::string prefix); +#endif + /* * Returns the identity transform (`[1 0 0 0; 0 1 0 0; 0 0 1 0; 0 0 0 1]`). */ @@ -61,10 +81,29 @@ struct Transform { static Transform Rotate(Float angleX, Float angleY, Float angleZ); /** - * Perform a simple interpolation between lhs and rhs, given "progress" - * between the two assuming that we are "moving" from lhs to rhs. This is a - * simple linear interpolation between each matrix index and will only work - * for simple scaling or translation; this will not work for rotation. + * 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, + * performs slerp between the two rotations, and a linear interpolation + * of scale and translation. * * @param progress * @param lhs