mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Summary: In #7916 I moved transform matrix decomposition logic from JS to java. The next step is to accept list of transforms instead oftransform matrix as a transform ReactProp. This way there is no extra processing required on JS side for the transform param (at least for android now) and this on the other hand allow us to execute transform updates (through offloaded animation) solely on the UI thread. After this change there is a whole bunch of stuff from `Libraries/Utilities/MatrixMath.js` that can be deleted (methods like: determinant, inverse, transpose). Although astreet mentioned under one of my previous commits that the code is still being referenced internally at fb, so I decided not to delete it here. **Test plan (required)** Run UIExplorer Transform example before and after - compare the results Run android unit test: com.facebook.react.uimanager.MatrixMathHelperTest Closes https://github.com/facebook/react-native/pull/8892 Differential Revision: D3676017 Pulled By: astreet fbshipit-source-id: 5275e30805a85c12c89bea44e8b3a2b2ec7b33fa
460 lines
16 KiB
Java
460 lines
16 KiB
Java
package com.facebook.react.uimanager;
|
|
|
|
import com.facebook.infer.annotation.Assertions;
|
|
|
|
/**
|
|
* Provides helper methods for converting transform operations into a matrix and then into a list
|
|
* of translate, scale and rotate commands.
|
|
*/
|
|
public class MatrixMathHelper {
|
|
|
|
private static final double EPSILON = .00001d;
|
|
|
|
public static class MatrixDecompositionContext {
|
|
double[] perspective = new double[4];
|
|
double[] quaternion = new double[4];
|
|
double[] scale = new double[3];
|
|
double[] skew = new double[3];
|
|
double[] translation = new double[3];
|
|
double[] rotationDegrees = new double[3];
|
|
}
|
|
|
|
private static boolean isZero(double d) {
|
|
if (Double.isNaN(d)) {
|
|
return false;
|
|
}
|
|
return Math.abs(d) < EPSILON;
|
|
}
|
|
|
|
public static void multiplyInto(double[] out, double[] a, double[] b) {
|
|
double a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3],
|
|
a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7],
|
|
a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11],
|
|
a30 = a[12], a31 = a[13], a32 = a[14], a33 = a[15];
|
|
|
|
double b0 = b[0], b1 = b[1], b2 = b[2], b3 = b[3];
|
|
out[0] = b0*a00 + b1*a10 + b2*a20 + b3*a30;
|
|
out[1] = b0*a01 + b1*a11 + b2*a21 + b3*a31;
|
|
out[2] = b0*a02 + b1*a12 + b2*a22 + b3*a32;
|
|
out[3] = b0*a03 + b1*a13 + b2*a23 + b3*a33;
|
|
|
|
b0 = b[4]; b1 = b[5]; b2 = b[6]; b3 = b[7];
|
|
out[4] = b0*a00 + b1*a10 + b2*a20 + b3*a30;
|
|
out[5] = b0*a01 + b1*a11 + b2*a21 + b3*a31;
|
|
out[6] = b0*a02 + b1*a12 + b2*a22 + b3*a32;
|
|
out[7] = b0*a03 + b1*a13 + b2*a23 + b3*a33;
|
|
|
|
b0 = b[8]; b1 = b[9]; b2 = b[10]; b3 = b[11];
|
|
out[8] = b0*a00 + b1*a10 + b2*a20 + b3*a30;
|
|
out[9] = b0*a01 + b1*a11 + b2*a21 + b3*a31;
|
|
out[10] = b0*a02 + b1*a12 + b2*a22 + b3*a32;
|
|
out[11] = b0*a03 + b1*a13 + b2*a23 + b3*a33;
|
|
|
|
b0 = b[12]; b1 = b[13]; b2 = b[14]; b3 = b[15];
|
|
out[12] = b0*a00 + b1*a10 + b2*a20 + b3*a30;
|
|
out[13] = b0*a01 + b1*a11 + b2*a21 + b3*a31;
|
|
out[14] = b0*a02 + b1*a12 + b2*a22 + b3*a32;
|
|
out[15] = b0*a03 + b1*a13 + b2*a23 + b3*a33;
|
|
}
|
|
|
|
/**
|
|
* @param transformMatrix 16-element array of numbers representing 4x4 transform matrix
|
|
*/
|
|
public static void decomposeMatrix(double[] transformMatrix, MatrixDecompositionContext ctx) {
|
|
Assertions.assertCondition(transformMatrix.length == 16);
|
|
|
|
// output values
|
|
final double[] perspective = ctx.perspective;
|
|
final double[] quaternion = ctx.quaternion;
|
|
final double[] scale = ctx.scale;
|
|
final double[] skew = ctx.skew;
|
|
final double[] translation = ctx.translation;
|
|
final double[] rotationDegrees = ctx.rotationDegrees;
|
|
|
|
// create normalized, 2d array matrix
|
|
// and normalized 1d array perspectiveMatrix with redefined 4th column
|
|
if (isZero(transformMatrix[15])) {
|
|
return;
|
|
}
|
|
double[][] matrix = new double[4][4];
|
|
double[] perspectiveMatrix = new double[16];
|
|
for (int i = 0; i < 4; i++) {
|
|
for (int j = 0; j < 4; j++) {
|
|
double value = transformMatrix[(i * 4) + j] / transformMatrix[15];
|
|
matrix[i][j] = value;
|
|
perspectiveMatrix[(i * 4) + j] = j == 3 ? 0 : value;
|
|
}
|
|
}
|
|
perspectiveMatrix[15] = 1;
|
|
|
|
// test for singularity of upper 3x3 part of the perspective matrix
|
|
if (isZero(determinant(perspectiveMatrix))) {
|
|
return;
|
|
}
|
|
|
|
// isolate perspective
|
|
if (!isZero(matrix[0][3]) || !isZero(matrix[1][3]) || !isZero(matrix[2][3])) {
|
|
// rightHandSide is the right hand side of the equation.
|
|
// rightHandSide is a vector, or point in 3d space relative to the origin.
|
|
double[] rightHandSide = { matrix[0][3], matrix[1][3], matrix[2][3], matrix[3][3] };
|
|
|
|
// Solve the equation by inverting perspectiveMatrix and multiplying
|
|
// rightHandSide by the inverse.
|
|
double[] inversePerspectiveMatrix = inverse(
|
|
perspectiveMatrix
|
|
);
|
|
double[] transposedInversePerspectiveMatrix = transpose(
|
|
inversePerspectiveMatrix
|
|
);
|
|
multiplyVectorByMatrix(rightHandSide, transposedInversePerspectiveMatrix, perspective);
|
|
} else {
|
|
// no perspective
|
|
perspective[0] = perspective[1] = perspective[2] = 0d;
|
|
perspective[3] = 1d;
|
|
}
|
|
|
|
// translation is simple
|
|
for (int i = 0; i < 3; i++) {
|
|
translation[i] = matrix[3][i];
|
|
}
|
|
|
|
// Now get scale and shear.
|
|
// 'row' is a 3 element array of 3 component vectors
|
|
double[][] row = new double[3][3];
|
|
for (int i = 0; i < 3; i++) {
|
|
row[i][0] = matrix[i][0];
|
|
row[i][1] = matrix[i][1];
|
|
row[i][2] = matrix[i][2];
|
|
}
|
|
|
|
// Compute X scale factor and normalize first row.
|
|
scale[0] = v3Length(row[0]);
|
|
row[0] = v3Normalize(row[0], scale[0]);
|
|
|
|
// Compute XY shear factor and make 2nd row orthogonal to 1st.
|
|
skew[0] = v3Dot(row[0], row[1]);
|
|
row[1] = v3Combine(row[1], row[0], 1.0, -skew[0]);
|
|
|
|
// Compute XY shear factor and make 2nd row orthogonal to 1st.
|
|
skew[0] = v3Dot(row[0], row[1]);
|
|
row[1] = v3Combine(row[1], row[0], 1.0, -skew[0]);
|
|
|
|
// Now, compute Y scale and normalize 2nd row.
|
|
scale[1] = v3Length(row[1]);
|
|
row[1] = v3Normalize(row[1], scale[1]);
|
|
skew[0] /= scale[1];
|
|
|
|
// Compute XZ and YZ shears, orthogonalize 3rd row
|
|
skew[1] = v3Dot(row[0], row[2]);
|
|
row[2] = v3Combine(row[2], row[0], 1.0, -skew[1]);
|
|
skew[2] = v3Dot(row[1], row[2]);
|
|
row[2] = v3Combine(row[2], row[1], 1.0, -skew[2]);
|
|
|
|
// Next, get Z scale and normalize 3rd row.
|
|
scale[2] = v3Length(row[2]);
|
|
row[2] = v3Normalize(row[2], scale[2]);
|
|
skew[1] /= scale[2];
|
|
skew[2] /= scale[2];
|
|
|
|
// At this point, the matrix (in rows) is orthonormal.
|
|
// Check for a coordinate system flip. If the determinant
|
|
// is -1, then negate the matrix and the scaling factors.
|
|
double[] pdum3 = v3Cross(row[1], row[2]);
|
|
if (v3Dot(row[0], pdum3) < 0) {
|
|
for (int i = 0; i < 3; i++) {
|
|
scale[i] *= -1;
|
|
row[i][0] *= -1;
|
|
row[i][1] *= -1;
|
|
row[i][2] *= -1;
|
|
}
|
|
}
|
|
|
|
// Now, get the rotations out
|
|
quaternion[0] =
|
|
0.5 * Math.sqrt(Math.max(1 + row[0][0] - row[1][1] - row[2][2], 0));
|
|
quaternion[1] =
|
|
0.5 * Math.sqrt(Math.max(1 - row[0][0] + row[1][1] - row[2][2], 0));
|
|
quaternion[2] =
|
|
0.5 * Math.sqrt(Math.max(1 - row[0][0] - row[1][1] + row[2][2], 0));
|
|
quaternion[3] =
|
|
0.5 * Math.sqrt(Math.max(1 + row[0][0] + row[1][1] + row[2][2], 0));
|
|
|
|
if (row[2][1] > 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;
|
|
}
|
|
|
|
public static double[] createIdentityMatrix() {
|
|
double[] res = new double[16];
|
|
resetIdentityMatrix(res);
|
|
return res;
|
|
}
|
|
|
|
public static double degreesToRadians(double degrees) {
|
|
return degrees * Math.PI / 180;
|
|
}
|
|
|
|
public static void resetIdentityMatrix(double[] matrix) {
|
|
matrix[1] = matrix[2] = matrix[3] = matrix[4] = matrix[6] = matrix[7] = matrix[8] = matrix[9] =
|
|
matrix[11] = matrix[12] = matrix[13] = matrix[14] = 0;
|
|
matrix[0] = matrix[5] = matrix[10] = matrix[15] = 1;
|
|
}
|
|
|
|
public static void applyPerspective(double[] m, double perspective) {
|
|
m[11] = -1 / perspective;
|
|
}
|
|
|
|
public static void applyScaleX(double[] m, double factor) {
|
|
m[0] = factor;
|
|
}
|
|
|
|
public static void applyScaleY(double[] m, double factor) {
|
|
m[5] = factor;
|
|
}
|
|
|
|
public static void applyScaleZ(double[] m, double factor) {
|
|
m[10] = factor;
|
|
}
|
|
|
|
public static void applyTranslate2D(double[] m, double x, double y) {
|
|
m[12] = x;
|
|
m[13] = y;
|
|
}
|
|
|
|
public static void applyTranslate3D(double[] m, double x, double y, double z) {
|
|
m[12] = x;
|
|
m[13] = y;
|
|
m[14] = z;
|
|
}
|
|
|
|
public static void applySkewX(double[] m, double radians) {
|
|
m[4] = Math.sin(radians);
|
|
m[5] = Math.cos(radians);
|
|
}
|
|
|
|
public static void applySkewY(double[] m, double radians) {
|
|
m[0] = Math.cos(radians);
|
|
m[1] = Math.sin(radians);
|
|
}
|
|
|
|
public static void applyRotateX(double[] m, double radians) {
|
|
m[5] = Math.cos(radians);
|
|
m[6] = Math.sin(radians);
|
|
m[9] = -Math.sin(radians);
|
|
m[10] = Math.cos(radians);
|
|
}
|
|
|
|
public static void applyRotateY(double[] m, double radians) {
|
|
m[0] = Math.cos(radians);
|
|
m[2] = -Math.sin(radians);
|
|
m[8] = Math.sin(radians);
|
|
m[10] = Math.cos(radians);
|
|
}
|
|
|
|
// http://www.w3.org/TR/css3-transforms/#recomposing-to-a-2d-matrix
|
|
public static void applyRotateZ(double[] m, double radians) {
|
|
m[0] = Math.cos(radians);
|
|
m[1] = Math.sin(radians);
|
|
m[4] = -Math.sin(radians);
|
|
m[5] = Math.cos(radians);
|
|
}
|
|
}
|