├── .gitignore ├── .npmignore ├── LICENSE.md ├── README.md ├── index.js ├── normalize.js ├── package.json ├── test-decompose.js └── test-normalize.js /.gitignore: -------------------------------------------------------------------------------- 1 | bower_components 2 | node_modules 3 | *.log 4 | .DS_Store 5 | bundle.js 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | bower_components 2 | node_modules 3 | *.log 4 | .DS_Store 5 | bundle.js 6 | test 7 | test.js 8 | demo/ 9 | .npmignore 10 | LICENSE.md -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2014 Matt DesLauriers 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 18 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 19 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 20 | OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mat4-decompose 2 | 3 | [![stable](http://badges.github.io/stability-badges/dist/stable.svg)](http://github.com/badges/stability-badges) 4 | 5 | Decomposes a 3D matrix, useful for animations. Code ported from [W3 CSS Spec](http://www.w3.org/TR/css3-transforms/#decomposing-a-3d-matrix). PRs for more tests/robustness/optimizations welcome. 6 | 7 | Order: 8 | 9 | - first isolates perspective 10 | - then determines translation 11 | - then determines X scale, XY shear, Y scale, XZ and YZ shear, and Z scale 12 | - then determines quaternion rotation 13 | 14 | You may also be interested in [mat4-interpolate](https://www.npmjs.com/package/mat4-interpolate), [mat4-recompose](https://www.npmjs.com/package/mat4-recompose), and [css-mat4](https://www.npmjs.com/package/css-mat4). 15 | 16 | ## Usage 17 | 18 | [![NPM](https://nodei.co/npm/mat4-decompose.png)](https://nodei.co/npm/mat4-decompose/) 19 | 20 | #### `valid = decompose(matrix[, translation, scale, skew, perspective, quaternion])` 21 | 22 | Decomposes the given `matrix` (an array of 16 floats, like those gl-matrix operates on), storing the results into the specified optional vectors. 23 | 24 | - `translation` [x, y, z] 25 | - `scale` [x, y, z] 26 | - `skew` [xy, xz, yz] skew factors 27 | - `perspective` [x, y, z, w] 28 | - `quaternion` [x, y, z, w] 29 | 30 | Returns `false` is this matrix cannot be decomposed, `true` otherwise. 31 | 32 | ## License 33 | 34 | MIT, see [LICENSE.md](http://github.com/mattdesl/mat4-decompose/blob/master/LICENSE.md) for details. 35 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /*jshint unused:true*/ 2 | /* 3 | Input: matrix ; a 4x4 matrix 4 | Output: translation ; a 3 component vector 5 | scale ; a 3 component vector 6 | skew ; skew factors XY,XZ,YZ represented as a 3 component vector 7 | perspective ; a 4 component vector 8 | quaternion ; a 4 component vector 9 | Returns false if the matrix cannot be decomposed, true if it can 10 | 11 | 12 | References: 13 | https://github.com/kamicane/matrix3d/blob/master/lib/Matrix3d.js 14 | https://github.com/ChromiumWebApps/chromium/blob/master/ui/gfx/transform_util.cc 15 | http://www.w3.org/TR/css3-transforms/#decomposing-a-3d-matrix 16 | */ 17 | 18 | var normalize = require('./normalize') 19 | 20 | var create = require('gl-mat4/create') 21 | var clone = require('gl-mat4/clone') 22 | var determinant = require('gl-mat4/determinant') 23 | var invert = require('gl-mat4/invert') 24 | var transpose = require('gl-mat4/transpose') 25 | var vec3 = { 26 | length: require('gl-vec3/length'), 27 | normalize: require('gl-vec3/normalize'), 28 | dot: require('gl-vec3/dot'), 29 | cross: require('gl-vec3/cross') 30 | } 31 | 32 | var tmp = create() 33 | var perspectiveMatrix = create() 34 | var tmpVec4 = [0, 0, 0, 0] 35 | var row = [ [0,0,0], [0,0,0], [0,0,0] ] 36 | var pdum3 = [0,0,0] 37 | 38 | module.exports = function decomposeMat4(matrix, translation, scale, skew, perspective, quaternion) { 39 | if (!translation) translation = [0,0,0] 40 | if (!scale) scale = [0,0,0] 41 | if (!skew) skew = [0,0,0] 42 | if (!perspective) perspective = [0,0,0,1] 43 | if (!quaternion) quaternion = [0,0,0,1] 44 | 45 | //normalize, if not possible then bail out early 46 | if (!normalize(tmp, matrix)) 47 | return false 48 | 49 | // perspectiveMatrix is used to solve for perspective, but it also provides 50 | // an easy way to test for singularity of the upper 3x3 component. 51 | clone(perspectiveMatrix, tmp) 52 | 53 | perspectiveMatrix[3] = 0 54 | perspectiveMatrix[7] = 0 55 | perspectiveMatrix[11] = 0 56 | perspectiveMatrix[15] = 1 57 | 58 | // If the perspectiveMatrix is not invertible, we are also unable to 59 | // decompose, so we'll bail early. Constant taken from SkMatrix44::invert. 60 | if (Math.abs(determinant(perspectiveMatrix) < 1e-8)) 61 | return false 62 | 63 | var a03 = tmp[3], a13 = tmp[7], a23 = tmp[11], 64 | a30 = tmp[12], a31 = tmp[13], a32 = tmp[14], a33 = tmp[15] 65 | 66 | // First, isolate perspective. 67 | if (a03 !== 0 || a13 !== 0 || a23 !== 0) { 68 | tmpVec4[0] = a03 69 | tmpVec4[1] = a13 70 | tmpVec4[2] = a23 71 | tmpVec4[3] = a33 72 | 73 | // Solve the equation by inverting perspectiveMatrix and multiplying 74 | // rightHandSide by the inverse. 75 | // resuing the perspectiveMatrix here since it's no longer needed 76 | var ret = invert(perspectiveMatrix, perspectiveMatrix) 77 | if (!ret) return false 78 | transpose(perspectiveMatrix, perspectiveMatrix) 79 | 80 | //multiply by transposed inverse perspective matrix, into perspective vec4 81 | vec4multMat4(perspective, tmpVec4, perspectiveMatrix) 82 | } else { 83 | //no perspective 84 | perspective[0] = perspective[1] = perspective[2] = 0 85 | perspective[3] = 1 86 | } 87 | 88 | // Next take care of translation 89 | translation[0] = a30 90 | translation[1] = a31 91 | translation[2] = a32 92 | 93 | // Now get scale and shear. 'row' is a 3 element array of 3 component vectors 94 | mat3from4(row, tmp) 95 | 96 | // Compute X scale factor and normalize first row. 97 | scale[0] = vec3.length(row[0]) 98 | vec3.normalize(row[0], row[0]) 99 | 100 | // Compute XY shear factor and make 2nd row orthogonal to 1st. 101 | skew[0] = vec3.dot(row[0], row[1]) 102 | combine(row[1], row[1], row[0], 1.0, -skew[0]) 103 | 104 | // Now, compute Y scale and normalize 2nd row. 105 | scale[1] = vec3.length(row[1]) 106 | vec3.normalize(row[1], row[1]) 107 | skew[0] /= scale[1] 108 | 109 | // Compute XZ and YZ shears, orthogonalize 3rd row 110 | skew[1] = vec3.dot(row[0], row[2]) 111 | combine(row[2], row[2], row[0], 1.0, -skew[1]) 112 | skew[2] = vec3.dot(row[1], row[2]) 113 | combine(row[2], row[2], row[1], 1.0, -skew[2]) 114 | 115 | // Next, get Z scale and normalize 3rd row. 116 | scale[2] = vec3.length(row[2]) 117 | vec3.normalize(row[2], row[2]) 118 | skew[1] /= scale[2] 119 | skew[2] /= scale[2] 120 | 121 | 122 | // At this point, the matrix (in rows) is orthonormal. 123 | // Check for a coordinate system flip. If the determinant 124 | // is -1, then negate the matrix and the scaling factors. 125 | vec3.cross(pdum3, row[1], row[2]) 126 | if (vec3.dot(row[0], pdum3) < 0) { 127 | for (var i = 0; i < 3; i++) { 128 | scale[i] *= -1; 129 | row[i][0] *= -1 130 | row[i][1] *= -1 131 | row[i][2] *= -1 132 | } 133 | } 134 | 135 | // Now, get the rotations out 136 | quaternion[0] = 0.5 * Math.sqrt(Math.max(1 + row[0][0] - row[1][1] - row[2][2], 0)) 137 | quaternion[1] = 0.5 * Math.sqrt(Math.max(1 - row[0][0] + row[1][1] - row[2][2], 0)) 138 | quaternion[2] = 0.5 * Math.sqrt(Math.max(1 - row[0][0] - row[1][1] + row[2][2], 0)) 139 | quaternion[3] = 0.5 * Math.sqrt(Math.max(1 + row[0][0] + row[1][1] + row[2][2], 0)) 140 | 141 | if (row[2][1] > row[1][2]) 142 | quaternion[0] = -quaternion[0] 143 | if (row[0][2] > row[2][0]) 144 | quaternion[1] = -quaternion[1] 145 | if (row[1][0] > row[0][1]) 146 | quaternion[2] = -quaternion[2] 147 | return true 148 | } 149 | 150 | //will be replaced by gl-vec4 eventually 151 | function vec4multMat4(out, a, m) { 152 | var x = a[0], y = a[1], z = a[2], w = a[3]; 153 | out[0] = m[0] * x + m[4] * y + m[8] * z + m[12] * w; 154 | out[1] = m[1] * x + m[5] * y + m[9] * z + m[13] * w; 155 | out[2] = m[2] * x + m[6] * y + m[10] * z + m[14] * w; 156 | out[3] = m[3] * x + m[7] * y + m[11] * z + m[15] * w; 157 | return out; 158 | } 159 | 160 | //gets upper-left of a 4x4 matrix into a 3x3 of vectors 161 | function mat3from4(out, mat4x4) { 162 | out[0][0] = mat4x4[0] 163 | out[0][1] = mat4x4[1] 164 | out[0][2] = mat4x4[2] 165 | 166 | out[1][0] = mat4x4[4] 167 | out[1][1] = mat4x4[5] 168 | out[1][2] = mat4x4[6] 169 | 170 | out[2][0] = mat4x4[8] 171 | out[2][1] = mat4x4[9] 172 | out[2][2] = mat4x4[10] 173 | } 174 | 175 | function combine(out, a, b, scale1, scale2) { 176 | out[0] = a[0] * scale1 + b[0] * scale2 177 | out[1] = a[1] * scale1 + b[1] * scale2 178 | out[2] = a[2] * scale1 + b[2] * scale2 179 | } -------------------------------------------------------------------------------- /normalize.js: -------------------------------------------------------------------------------- 1 | module.exports = function normalize(out, mat) { 2 | var m44 = mat[15] 3 | // Cannot normalize. 4 | if (m44 === 0) 5 | return false 6 | var scale = 1 / m44 7 | for (var i=0; i<16; i++) 8 | out[i] = mat[i] * scale 9 | return true 10 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mat4-decompose", 3 | "version": "1.0.4", 4 | "description": "decomposes a 3D matrix", 5 | "main": "index.js", 6 | "license": "MIT", 7 | "author": { 8 | "name": "Matt DesLauriers", 9 | "email": "dave.des@gmail.com", 10 | "url": "https://github.com/mattdesl" 11 | }, 12 | "dependencies": { 13 | "gl-mat4": "^1.0.1", 14 | "gl-vec3": "^1.0.2" 15 | }, 16 | "devDependencies": { 17 | "tape": "^3.0.3" 18 | }, 19 | "scripts": { 20 | "test": "tape *.js" 21 | }, 22 | "keywords": [ 23 | "4x4", 24 | "decompose", 25 | "gl-mat4", 26 | "mat4", 27 | "decomposition", 28 | "interpolation", 29 | "lerp", 30 | "linear", 31 | "tween", 32 | "animate", 33 | "css", 34 | "matrix" 35 | ], 36 | "repository": { 37 | "type": "git", 38 | "url": "git://github.com/mattdesl/mat4-decompose.git" 39 | }, 40 | "homepage": "https://github.com/mattdesl/mat4-decompose", 41 | "bugs": { 42 | "url": "https://github.com/mattdesl/mat4-decompose/issues" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /test-decompose.js: -------------------------------------------------------------------------------- 1 | var test = require('tape').test 2 | 3 | var mat = require('gl-mat4') 4 | var decompose = require('./') 5 | 6 | test("decompose a 3D matrix", function(t) { 7 | var m = mat.create() 8 | var translate = [0,0,0], 9 | scale = [0,0,0], 10 | skew = [0,0,0], 11 | perspective = [0,0,0,1], 12 | quaternion = [0,0,0,1] 13 | 14 | var valid = decompose(m) 15 | t.equal(valid, true, 'can be decomposed') 16 | 17 | mat.identity(m) 18 | mat.translate(m, m, [-5, 2, 10]) 19 | decompose(m, translate) 20 | t.deepEqual(translate, [-5, 2, 10], 'extracts translation') 21 | 22 | mat.identity(m) 23 | mat.scale(m, m, [1, 0, 5]) 24 | decompose(m, translate, scale) 25 | t.deepEqual(scale, [1, 0, 5], 'extracts scale') 26 | 27 | mat.identity(m) 28 | mat.rotateX(m, m, Math.PI) 29 | decompose(m, translate, scale, skew, perspective, quaternion) 30 | t.deepEqual(quaternion, [1, 0, 0, 0], 'extracts rotation x') 31 | 32 | mat.identity(m) 33 | mat.rotateY(m, m, -Math.PI) 34 | decompose(m, translate, scale, skew, perspective, quaternion) 35 | t.deepEqual(quaternion, [0, -1, 0, 0], 'extracts rotation y') 36 | 37 | mat.identity(m) 38 | mat.rotateZ(m, m, -Math.PI) 39 | decompose(m, translate, scale, skew, perspective, quaternion) 40 | t.deepEqual(quaternion, [0, 0, -1, 0], 'extracts rotation z') 41 | t.deepEqual(skew, [0,0,0], 'extracts skew') 42 | 43 | mat.identity(m) 44 | mat.translate(m, m, [10, 5, -50]) 45 | mat.scale(m, m, [0.25, 0.5, -0.5]) 46 | mat.rotateZ(m, m, -Math.PI) 47 | decompose(m, translate, scale, skew, perspective, quaternion) 48 | t.deepEqual(translate, [10,5,-50], 'extracts translation') 49 | t.deepEqual(perspective, [0,0,0,1], 'extracts perspective') 50 | t.deepEqual(scale, [-0.25,-0.5,-0.5], 'extracts scale') 51 | t.deepEqual(quaternion, [0,0,0,1], 'extracts rotation') 52 | 53 | t.end() 54 | }) -------------------------------------------------------------------------------- /test-normalize.js: -------------------------------------------------------------------------------- 1 | var decomposeMat4 = require('./') 2 | var test = require('tape').test 3 | 4 | var mat = require('gl-mat4') 5 | var normalize = require('./normalize') 6 | 7 | test("normalize a 3D matrix", function(t) { 8 | var m = Array.prototype.slice.call(mat.create()) 9 | mat.scale(m, m, [2, 2, 2]) 10 | mat.translate(m, m, [4, 5, 2]) 11 | m[15] = 2 12 | 13 | var out = [] 14 | var n = normalize(out, m) 15 | t.equal(n, true, 'can normalize') 16 | t.deepEqual(out, [ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 4, 5, 2, 1 ]) 17 | 18 | m[15] = 0 19 | n = normalize(out, m) 20 | t.equal(n, false, 'cannot normalize') 21 | 22 | t.end() 23 | }) --------------------------------------------------------------------------------