jlmakes / rematrix

Matrix transformations made easy.
https://rematrix.now.sh/
MIT License
430 stars 13 forks source link

Decomposing a 3D matrix #1

Open davidkpiano opened 7 years ago

davidkpiano commented 7 years ago

First of all, great job with this library - the code is well-documented and clear. I was up late last night looking for a good library for manipulating CSS transforms, and was very happy to find this one!

I was wondering if you would be open to the following (small) features? I can definitely help with some of them, let me know if you are willing to accept PRs on any of these.

Thanks so much for this library!

jlmakes commented 7 years ago

Thanks for the kind words @davidkpiano, I appreciate it!

"Decomposition - probably a bigger feature ask, but a way to decompose a matrix and grab specific values such as translateX, skewY, rotateZ, scaleY, etc. from it?"

Now this is 🔥 and strikes me as a very powerful addition to the API. Some cursory research has revealed it should be possible using inverse trigonometric functions.

I think this would add the most value for developers. If you’re up for it, I think it would make an outstanding contribution.

Can you show how you imagine the decomposition API, and what the method(s) return?

davidkpiano commented 7 years ago

Sure, and for reference the W3C has a decomposing algorithm.

Potential APIs:

Calling decompose

// From an array (2d or 3d)
Rematrix.decompose([0.5, -1, 1, 0.5, 10, -20]);
Rematrix.decompose([1.4, 0, -1.4, 0, 0.3, 2, 0, 0, 0.7, 0, 0.7, 0, 11.3, 0, -11.3, 1]);

// From a string
Rematrix.decompose("matrix(0.5, -1, 1, 0.5, 10, -20)")
Rematrix.decompose("matrix3d(1.4, 0, -1.4, 0, 0.3, 2, 0, 0, 0.7, 0, 0.7, 0, 11.3, 0, -11.3, 1)")

Potential return value shapes

// hierarchical
{
  translate: { x: 10, y: 0, z: 0 },
  rotate: { x: 40, y: -25, z: 16 },
  skew: { x: 0, y: 0 },
  scale: { x: 1, y: 1, z: 1 },
  perspective: 0
}

// as 3d vectors
{
  translate: [10, 0, 0],
  rotate: [40, -25, 16],
  // ... etc.
}

// verbose
{
  translateX: 10,
  translateY: 0,
  translateZ: 0,
  // ... etc.
}

From the above, I prefer either { x, y, z } or [x, y, z] format. I think hierarchical is more explicit but vectors is probably more useful to use in a wider range of use cases that can just accept a vector, e.g., for translate.

jlmakes commented 7 years ago

Looks great. I think I’m learning towards the verbose model, because object keys then correspond to Rematrix method names (and facilitate an elegant compose() method)


var decomposed = Rematrix.decompose(...)

/**
 * decomposed === {
 *  translateX: 10,
 *  translateY: 0,
 *  translateZ: 0,
 *  ... etc.
 * }
 */

function compose(decomposed) {
    return Object.keys(decomposed)
        .map(key => Rematrix[key](decomposed[key]))
        .reduce(Rematrix.multiply)
}

What’s your impression of this?

davidkpiano commented 7 years ago

I think verbose is fine, you make a good point of it mapping directly to CSS property names and Rematrix methods.

jlmakes commented 7 years ago

Wow, I took a quick look at the W3C algorithm, and it was frightening. Also, I'm rather curious about the order of operations for composition. I'll keep looking.

luckydonald commented 6 years ago

The CSS string would be handy too. I thought format(array) was about it, but it seems it isn't.

jlmakes commented 6 years ago

I noticed the W3C CSS Transform Module document no longer included the 3D matrix decomposition sections. But thanks to WayBackMachine....

The pseudo code below is based upon the "unmatrix" method in "Graphics Gems II, edited by Jim Arvo", but modified to use Quaternions instead of Euler angles to avoid the problem of Gimbal Locks. ``` Input: matrix ; a 4x4 matrix Output: translation ; a 3 component vector scale ; a 3 component vector skew ; skew factors XY,XZ,YZ represented as a 3 component vector perspective ; a 4 component vector quaternion ; a 4 component vector Returns false if the matrix cannot be decomposed, true if it can ``` ```js // Normalize the matrix. if (matrix[3][3] == 0) return false for (i = 0; i < 4; i++) for (j = 0; j < 4; j++) matrix[i][j] /= matrix[3][3] // perspectiveMatrix is used to solve for perspective, but it also provides // an easy way to test for singularity of the upper 3x3 component. perspectiveMatrix = matrix for (i = 0; i < 3; i++) perspectiveMatrix[i][3] = 0 perspectiveMatrix[3][3] = 1 if (determinant(perspectiveMatrix) == 0) return false // First, isolate perspective. if (matrix[0][3] != 0 || matrix[1][3] != 0 || matrix[2][3] != 0) // rightHandSide is the right hand side of the equation. rightHandSide[0] = matrix[0][3] rightHandSide[1] = matrix[1][3] rightHandSide[2] = matrix[2][3] rightHandSide[3] = matrix[3][3] // Solve the equation by inverting perspectiveMatrix and multiplying // rightHandSide by the inverse. inversePerspectiveMatrix = inverse(perspectiveMatrix) transposedInversePerspectiveMatrix = transposeMatrix4(inversePerspectiveMatrix) perspective = multVecMatrix(rightHandSide, transposedInversePerspectiveMatrix) else // No perspective. perspective[0] = perspective[1] = perspective[2] = 0 perspective[3] = 1 // Next take care of translation for (i = 0; i < 3; i++) translate[i] = matrix[3][i] // Now get scale and shear. 'row' is a 3 element array of 3 component vectors for (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] = length(row[0]) row[0] = normalize(row[0]) // Compute XY shear factor and make 2nd row orthogonal to 1st. skew[0] = dot(row[0], row[1]) row[1] = combine(row[1], row[0], 1.0, -skew[0]) // Now, compute Y scale and normalize 2nd row. scale[1] = length(row[1]) row[1] = normalize(row[1]) skew[0] /= scale[1]; // Compute XZ and YZ shears, orthogonalize 3rd row skew[1] = dot(row[0], row[2]) row[2] = combine(row[2], row[0], 1.0, -skew[1]) skew[2] = dot(row[1], row[2]) row[2] = combine(row[2], row[1], 1.0, -skew[2]) // Next, get Z scale and normalize 3rd row. scale[2] = length(row[2]) row[2] = normalize(row[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. pdum3 = cross(row[1], row[2]) if (dot(row[0], pdum3) < 0) for (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 * sqrt(max(1 + row[0][0] - row[1][1] - row[2][2], 0)) quaternion[1] = 0.5 * sqrt(max(1 - row[0][0] + row[1][1] - row[2][2], 0)) quaternion[2] = 0.5 * sqrt(max(1 - row[0][0] - row[1][1] + row[2][2], 0)) quaternion[3] = 0.5 * sqrt(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] return true ``` ☠️
ievgennaida commented 4 years ago

Example of the W3 implementation: https://github.com/facebook/react-native/blob/master/Libraries/Utilities/MatrixMath.js#L572

But I think the Facebook implementation has bugs (line duplicates? double check quaternions convetation) and also it's not optimized. There are a lot of unrequired arrays spawns.

Also, probably, perspective calc can be skipped?

And algorithm for the 3d matrix decomposition is moved here: https://www.w3.org/TR/css-transforms-2/