From a59afb98d5aba8c4826eda349a4432457aab1d4d Mon Sep 17 00:00:00 2001 From: Krzysztof Magiera Date: Tue, 21 Jun 2016 11:24:31 -0700 Subject: [PATCH] Decompose transform matrix in native (for android). Summary: This diff translates implementation of transform matrix decomposition from JS to java. This is to support offloading animations of transform property, in which case it is required that we can calculate decomposed transform in the UI thread. Since the matrix decomposition code is not being used for other platform I went ahead and deleted parts that are no longer being used. **Test plan** Run UIExplorer Transform example before and after - compare the results Closes https://github.com/facebook/react-native/pull/7916 Reviewed By: ritzau Differential Revision: D3398393 Pulled By: astreet fbshipit-source-id: 9881c3f565e2050e415849b0f76a0cefe11c6afb --- Examples/UIExplorer/UIExplorerList.android.js | 4 + Libraries/StyleSheet/processTransform.js | 7 - .../react/uimanager/BaseViewManager.java | 57 ++- .../react/uimanager/MatrixMathHelper.java | 353 ++++++++++++++++++ .../react/uimanager/MatrixMathHelperTest.java | 156 ++++++++ 5 files changed, 534 insertions(+), 43 deletions(-) create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/MatrixMathHelper.java create mode 100644 ReactAndroid/src/test/java/com/facebook/react/uimanager/MatrixMathHelperTest.java diff --git a/Examples/UIExplorer/UIExplorerList.android.js b/Examples/UIExplorer/UIExplorerList.android.js index 4fdfd4afb..c1410d023 100644 --- a/Examples/UIExplorer/UIExplorerList.android.js +++ b/Examples/UIExplorer/UIExplorerList.android.js @@ -189,6 +189,10 @@ const APIExamples = [ key: 'ToastAndroidExample', module: require('./ToastAndroidExample'), }, + { + key: 'TransformExample', + module: require('./TransformExample'), + }, { key: 'VibrationExample', module: require('./VibrationExample'), diff --git a/Libraries/StyleSheet/processTransform.js b/Libraries/StyleSheet/processTransform.js index 6978efdcf..fc44b5f8c 100644 --- a/Libraries/StyleSheet/processTransform.js +++ b/Libraries/StyleSheet/processTransform.js @@ -81,13 +81,6 @@ function processTransform(transform: Object): Object { } }); - // Android does not support the direct application of a transform matrix to - // a view, so we need to decompose the result matrix into transforms that can - // get applied in the specific order of (1) translate (2) scale (3) rotate. - // Once we can directly apply a matrix, we can remove this decomposition. - if (Platform.OS === 'android') { - return MatrixMath.decomposeMatrix(result); - } return result; } diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java index 0bcefba58..c74651755 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java @@ -6,6 +6,7 @@ import android.graphics.Color; import android.os.Build; import android.view.View; +import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.uimanager.annotations.ReactProp; @@ -17,14 +18,6 @@ public abstract class BaseViewManager { private static final String PROP_BACKGROUND_COLOR = ViewProps.BACKGROUND_COLOR; - private static final String PROP_DECOMPOSED_MATRIX = "decomposedMatrix"; - private static final String PROP_DECOMPOSED_MATRIX_ROTATE = "rotate"; - private static final String PROP_DECOMPOSED_MATRIX_ROTATE_X = "rotateX"; - private static final String PROP_DECOMPOSED_MATRIX_ROTATE_Y = "rotateY"; - private static final String PROP_DECOMPOSED_MATRIX_SCALE_X = "scaleX"; - private static final String PROP_DECOMPOSED_MATRIX_SCALE_Y = "scaleY"; - private static final String PROP_DECOMPOSED_MATRIX_TRANSLATE_X = "translateX"; - private static final String PROP_DECOMPOSED_MATRIX_TRANSLATE_Y = "translateY"; private static final String PROP_TRANSFORM = "transform"; private static final String PROP_OPACITY = "opacity"; private static final String PROP_ELEVATION = "elevation"; @@ -46,28 +39,21 @@ public abstract class BaseViewManager row[1][2]) { + quaternion[0] = -quaternion[0]; + } + if (row[0][2] > row[2][0]) { + quaternion[1] = -quaternion[1]; + } + if (row[1][0] > row[0][1]) { + quaternion[2] = -quaternion[2]; + } + + // correct for occasional, weird Euler synonyms for 2d rotation + + if (quaternion[0] < 0.001 && quaternion[0] >= 0 && + quaternion[1] < 0.001 && quaternion[1] >= 0) { + // this is a 2d rotation on the z-axis + rotationDegrees[0] = rotationDegrees[1] = 0d; + rotationDegrees[2] = roundTo3Places(Math.atan2(row[0][1], row[0][0]) * 180 / Math.PI); + } else { + quaternionToDegreesXYZ(quaternion, rotationDegrees); + } + } + + public static double determinant(double[] matrix) { + double m00 = matrix[0], m01 = matrix[1], m02 = matrix[2], m03 = matrix[3], m10 = matrix[4], + m11 = matrix[5], m12 = matrix[6], m13 = matrix[7], m20 = matrix[8], m21 = matrix[9], + m22 = matrix[10], m23 = matrix[11], m30 = matrix[12], m31 = matrix[13], m32 = matrix[14], + m33 = matrix[15]; + return ( + m03 * m12 * m21 * m30 - m02 * m13 * m21 * m30 - + m03 * m11 * m22 * m30 + m01 * m13 * m22 * m30 + + m02 * m11 * m23 * m30 - m01 * m12 * m23 * m30 - + m03 * m12 * m20 * m31 + m02 * m13 * m20 * m31 + + m03 * m10 * m22 * m31 - m00 * m13 * m22 * m31 - + m02 * m10 * m23 * m31 + m00 * m12 * m23 * m31 + + m03 * m11 * m20 * m32 - m01 * m13 * m20 * m32 - + m03 * m10 * m21 * m32 + m00 * m13 * m21 * m32 + + m01 * m10 * m23 * m32 - m00 * m11 * m23 * m32 - + m02 * m11 * m20 * m33 + m01 * m12 * m20 * m33 + + m02 * m10 * m21 * m33 - m00 * m12 * m21 * m33 - + m01 * m10 * m22 * m33 + m00 * m11 * m22 * m33 + ); + } + + /** + * Inverse of a matrix. Multiplying by the inverse is used in matrix math + * instead of division. + * + * Formula from: + * http://www.euclideanspace.com/maths/algebra/matrix/functions/inverse/fourD/index.htm + */ + public static double[] inverse(double[] matrix) { + double det = determinant(matrix); + if (isZero(det)) { + return matrix; + } + double m00 = matrix[0], m01 = matrix[1], m02 = matrix[2], m03 = matrix[3], m10 = matrix[4], + m11 = matrix[5], m12 = matrix[6], m13 = matrix[7], m20 = matrix[8], m21 = matrix[9], + m22 = matrix[10], m23 = matrix[11], m30 = matrix[12], m31 = matrix[13], m32 = matrix[14], + m33 = matrix[15]; + return new double[] { + (m12 * m23 * m31 - m13 * m22 * m31 + m13 * m21 * m32 - m11 * m23 * m32 - m12 * m21 * m33 + m11 * m22 * m33) / det, + (m03 * m22 * m31 - m02 * m23 * m31 - m03 * m21 * m32 + m01 * m23 * m32 + m02 * m21 * m33 - m01 * m22 * m33) / det, + (m02 * m13 * m31 - m03 * m12 * m31 + m03 * m11 * m32 - m01 * m13 * m32 - m02 * m11 * m33 + m01 * m12 * m33) / det, + (m03 * m12 * m21 - m02 * m13 * m21 - m03 * m11 * m22 + m01 * m13 * m22 + m02 * m11 * m23 - m01 * m12 * m23) / det, + (m13 * m22 * m30 - m12 * m23 * m30 - m13 * m20 * m32 + m10 * m23 * m32 + m12 * m20 * m33 - m10 * m22 * m33) / det, + (m02 * m23 * m30 - m03 * m22 * m30 + m03 * m20 * m32 - m00 * m23 * m32 - m02 * m20 * m33 + m00 * m22 * m33) / det, + (m03 * m12 * m30 - m02 * m13 * m30 - m03 * m10 * m32 + m00 * m13 * m32 + m02 * m10 * m33 - m00 * m12 * m33) / det, + (m02 * m13 * m20 - m03 * m12 * m20 + m03 * m10 * m22 - m00 * m13 * m22 - m02 * m10 * m23 + m00 * m12 * m23) / det, + (m11 * m23 * m30 - m13 * m21 * m30 + m13 * m20 * m31 - m10 * m23 * m31 - m11 * m20 * m33 + m10 * m21 * m33) / det, + (m03 * m21 * m30 - m01 * m23 * m30 - m03 * m20 * m31 + m00 * m23 * m31 + m01 * m20 * m33 - m00 * m21 * m33) / det, + (m01 * m13 * m30 - m03 * m11 * m30 + m03 * m10 * m31 - m00 * m13 * m31 - m01 * m10 * m33 + m00 * m11 * m33) / det, + (m03 * m11 * m20 - m01 * m13 * m20 - m03 * m10 * m21 + m00 * m13 * m21 + m01 * m10 * m23 - m00 * m11 * m23) / det, + (m12 * m21 * m30 - m11 * m22 * m30 - m12 * m20 * m31 + m10 * m22 * m31 + m11 * m20 * m32 - m10 * m21 * m32) / det, + (m01 * m22 * m30 - m02 * m21 * m30 + m02 * m20 * m31 - m00 * m22 * m31 - m01 * m20 * m32 + m00 * m21 * m32) / det, + (m02 * m11 * m30 - m01 * m12 * m30 - m02 * m10 * m31 + m00 * m12 * m31 + m01 * m10 * m32 - m00 * m11 * m32) / det, + (m01 * m12 * m20 - m02 * m11 * m20 + m02 * m10 * m21 - m00 * m12 * m21 - m01 * m10 * m22 + m00 * m11 * m22) / det + }; + } + + /** + * Turns columns into rows and rows into columns. + */ + public static double[] transpose(double[] m) { + return new double[] { + m[0], m[4], m[8], m[12], + m[1], m[5], m[9], m[13], + m[2], m[6], m[10], m[14], + m[3], m[7], m[11], m[15] + }; + } + + /** + * Based on: http://tog.acm.org/resources/GraphicsGems/gemsii/unmatrix.c + */ + public static void multiplyVectorByMatrix(double[] v, double[] m, double[] result) { + double vx = v[0], vy = v[1], vz = v[2], vw = v[3]; + result[0] = vx * m[0] + vy * m[4] + vz * m[8] + vw * m[12]; + result[1] = vx * m[1] + vy * m[5] + vz * m[9] + vw * m[13]; + result[2] = vx * m[2] + vy * m[6] + vz * m[10] + vw * m[14]; + result[3] = vx * m[3] + vy * m[7] + vz * m[11] + vw * m[15]; + } + + /** + * From: https://code.google.com/p/webgl-mjs/source/browse/mjs.js + */ + public static double v3Length(double[] a) { + return Math.sqrt(a[0]*a[0] + a[1]*a[1] + a[2]*a[2]); + } + + /** + * Based on: https://code.google.com/p/webgl-mjs/source/browse/mjs.js + */ + public static double[] v3Normalize(double[] vector, double norm) { + double im = 1 / (isZero(norm) ? v3Length(vector) : norm); + return new double[] { + vector[0] * im, + vector[1] * im, + vector[2] * im + }; + } + + /** + * The dot product of a and b, two 3-element vectors. + * From: https://code.google.com/p/webgl-mjs/source/browse/mjs.js + */ + public static double v3Dot(double[] a, double[] b) { + return a[0] * b[0] + + a[1] * b[1] + + a[2] * b[2]; + } + + /** + * From: + * http://www.opensource.apple.com/source/WebCore/WebCore-514/platform/graphics/transforms/TransformationMatrix.cpp + */ + public static double[] v3Combine(double[] a, double[] b, double aScale, double bScale) { + return new double[]{ + aScale * a[0] + bScale * b[0], + aScale * a[1] + bScale * b[1], + aScale * a[2] + bScale * b[2] + }; + } + + /** + * From: + * http://www.opensource.apple.com/source/WebCore/WebCore-514/platform/graphics/transforms/TransformationMatrix.cpp + */ + public static double[] v3Cross(double[] a, double[] b) { + return new double[]{ + a[1] * b[2] - a[2] * b[1], + a[2] * b[0] - a[0] * b[2], + a[0] * b[1] - a[1] * b[0] + }; + } + + /** + * Based on: + * http://www.euclideanspace.com/maths/geometry/rotations/conversions/quaternionToEuler/ + * and: + * http://quat.zachbennett.com/ + * + * Note that this rounds degrees to the thousandth of a degree, due to + * floating point errors in the creation of the quaternion. + * + * Also note that this expects the qw value to be last, not first. + * + * Also, when researching this, remember that: + * yaw === heading === z-axis + * pitch === elevation/attitude === y-axis + * roll === bank === x-axis + */ + public static void quaternionToDegreesXYZ(double[] q, double[] result) { + double qx = q[0], qy = q[1], qz = q[2], qw = q[3]; + double qw2 = qw * qw; + double qx2 = qx * qx; + double qy2 = qy * qy; + double qz2 = qz * qz; + double test = qx * qy + qz * qw; + double unit = qw2 + qx2 + qy2 + qz2; + double conv = 180 / Math.PI; + + if (test > 0.49999 * unit) { + result[0] = 0; + result[1] = 2 * Math.atan2(qx, qw) * conv; + result[2] = 90; + return; + } + if (test < -0.49999 * unit) { + result[0] = 0; + result[1] = -2 * Math.atan2(qx, qw) * conv; + result[2] = -90; + return; + } + + result[0] = roundTo3Places(Math.atan2(2 * qx * qw - 2 * qy * qz, 1 - 2 * qx2 - 2 * qz2) * conv); + result[1] = roundTo3Places(Math.atan2(2 * qy * qw - 2 * qx * qz, 1 - 2 * qy2 - 2 * qz2) * conv); + result[2] = roundTo3Places(Math.asin(2 * qx * qy + 2 * qz * qw) * conv); + } + + public static double roundTo3Places(double n) { + return Math.round(n * 1000d) * 0.001; + } +} diff --git a/ReactAndroid/src/test/java/com/facebook/react/uimanager/MatrixMathHelperTest.java b/ReactAndroid/src/test/java/com/facebook/react/uimanager/MatrixMathHelperTest.java new file mode 100644 index 000000000..66e9a2144 --- /dev/null +++ b/ReactAndroid/src/test/java/com/facebook/react/uimanager/MatrixMathHelperTest.java @@ -0,0 +1,156 @@ +package com.facebook.react.uimanager; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +import static org.fest.assertions.api.Assertions.assertThat; + +/** + * Test for {@link MatrixMathHelper} + */ +@RunWith(RobolectricTestRunner.class) +public class MatrixMathHelperTest { + + private void verifyZRotatedMatrix(double degrees, double rotX, double rotY, double rotZ) { + MatrixMathHelper.MatrixDecompositionContext ctx = + new MatrixMathHelper.MatrixDecompositionContext(); + double[] matrix = createRotateZ(degreesToRadians(degrees)); + MatrixMathHelper.decomposeMatrix(matrix, ctx); + assertThat(ctx.rotationDegrees).containsSequence(rotX, rotY, rotZ); + } + + private void verifyYRotatedMatrix(double degrees, double rotX, double rotY, double rotZ) { + MatrixMathHelper.MatrixDecompositionContext ctx = + new MatrixMathHelper.MatrixDecompositionContext(); + double[] matrix = createRotateY(degreesToRadians(degrees)); + MatrixMathHelper.decomposeMatrix(matrix, ctx); + assertThat(ctx.rotationDegrees).containsSequence(rotX, rotY, rotZ); + } + + private void verifyXRotatedMatrix(double degrees, double rotX, double rotY, double rotZ) { + MatrixMathHelper.MatrixDecompositionContext ctx = + new MatrixMathHelper.MatrixDecompositionContext(); + double[] matrix = createRotateX(degreesToRadians(degrees)); + MatrixMathHelper.decomposeMatrix(matrix, ctx); + assertThat(ctx.rotationDegrees).containsSequence(rotX, rotY, rotZ); + } + + @Test + public void testDecomposing4x4MatrixToProduceAccurateZaxisAngles() { + + MatrixMathHelper.MatrixDecompositionContext ctx = + new MatrixMathHelper.MatrixDecompositionContext(); + + MatrixMathHelper.decomposeMatrix( + new double[]{1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1}, + ctx); + + assertThat(ctx.rotationDegrees).containsSequence(0d, 0d, 0d); + + double[] angles = new double[]{30, 45, 60, 75, 90, 100, 115, 120, 133, 167}; + for (double angle : angles) { + verifyZRotatedMatrix(angle, 0d, 0d, angle); + verifyZRotatedMatrix(-angle, 0d, 0d, -angle); + } + + verifyZRotatedMatrix(180d, 0d, 0d, 180d); + + // all values are between 0 and 180; + // change of sign and direction in the third and fourth quadrant + verifyZRotatedMatrix(222, 0d, 0d, -138d); + + verifyZRotatedMatrix(270, 0d, 0d, -90d); + + // 360 is expressed as 0 + verifyZRotatedMatrix(360, 0d, 0d, 0d); + + verifyZRotatedMatrix(33.33333333, 0d, 0d, 33.333d); + + verifyZRotatedMatrix(86.75309, 0d, 0d, 86.753d); + + verifyZRotatedMatrix(42.00000000001, 0d, 0d, 42d); + + verifyZRotatedMatrix(42.99999999999, 0d, 0d, 43d); + + verifyZRotatedMatrix(42.99999999999, 0d, 0d, 43d); + + verifyZRotatedMatrix(42.49999999999, 0d, 0d, 42.5d); + + verifyZRotatedMatrix(42.55555555555, 0d, 0d, 42.556d); + } + + @Test + public void testDecomposing4x4MatrixToProduceAccurateYaxisAngles() { + double[] angles = new double[]{30, 45, 60, 75, 90, 100, 110, 120, 133, 167}; + for (double angle : angles) { + verifyYRotatedMatrix(angle, 0d, angle, 0d); + verifyYRotatedMatrix(-angle, 0d, -angle, 0d); + } + + // all values are between 0 and 180; + // change of sign and direction in the third and fourth quadrant + verifyYRotatedMatrix(222, 0d, -138d, 0d); + + verifyYRotatedMatrix(270, 0d, -90d, 0d); + + verifyYRotatedMatrix(360, 0d, 0d, 0d); + } + + @Test + public void testDecomposing4x4MatrixToProduceAccurateXaxisAngles() { + double[] angles = new double[]{30, 45, 60, 75, 90, 100, 110, 120, 133, 167}; + for (double angle : angles) { + verifyXRotatedMatrix(angle, angle, 0d, 0d); + verifyXRotatedMatrix(-angle, -angle, 0d, 0d); + } + + // all values are between 0 and 180; + // change of sign and direction in the third and fourth quadrant + verifyXRotatedMatrix(222, -138d, 0d, 0d); + + verifyXRotatedMatrix(270, -90d, 0d, 0d); + + verifyXRotatedMatrix(360, 0d, 0d, 0d); + } + + private static double[] createIdentityMatrix() { + return new double[] { + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 + }; + } + + private static double degreesToRadians(double degrees) { + return degrees * Math.PI / 180; + } + + private static double[] createRotateZ(double radians) { + double[] mat = createIdentityMatrix(); + mat[0] = Math.cos(radians); + mat[1] = Math.sin(radians); + mat[4] = -Math.sin(radians); + mat[5] = Math.cos(radians); + return mat; + } + + private static double[] createRotateY(double radians) { + double[] mat = createIdentityMatrix(); + mat[0] = Math.cos(radians); + mat[2] = -Math.sin(radians); + mat[8] = Math.sin(radians); + mat[10] = Math.cos(radians); + return mat; + } + + private static double[] createRotateX(double radians) { + double[] mat = createIdentityMatrix(); + mat[5] = Math.cos(radians); + mat[6] = Math.sin(radians); + mat[9] = -Math.sin(radians); + mat[10] = Math.cos(radians); + return mat; + } +}