Matrix math for the web

현재 번역은 완벽하지 않습니다. 한국어로 문서 번역에 동참해주세요.

행렬은 공간에서 오브젝트들의 변환을 표현하기 위해 사용되고, 웹에서 시각화에 쓰이는 중요한 도구입니다.  이 문서에서는 행렬을 어떻게 생성하고 CSS3 transformsmatrix3d transform 타입과 함께 사용하는지 살펴보겠습니다.

이 문서에서는 설명을 보다 쉽게 하기 위해 CSS3를 이용하고 있지만, 행렬은 WebGL과 셰이더를 포함한 많은 기술에서 사용하는 개념입니다. 이 문서는 MDN content kit에서도 볼 수 있습니다. 라이브 예제들은 "MDN"이라는 전역 객체가 가지고 있는 utility functions들을 사용하고 있습니다.

변환 행렬이란?

 행렬에는 여러 종류가 있지만 우리가 관심 있는 것은 3D 변환 행렬입니다. 이 행렬들은 4x4 그리드에 채워넣은 16개의 값들로 이뤄져있습니다. JavaScript에서는 array를 이용해 행렬을 쉽게 표현할 수 있습니다. 전형적인 예제인 단위 행렬에서 시작해보겠습니다. 어떤 행렬을 점이나 다른 행렬에 곱했을 때 결과가 동일한 행렬을 단위행렬이라 부릅니다.

var identityMatrix = [
  1, 0, 0, 0,
  0, 1, 0, 0,
  0, 0, 1, 0,
  0, 0, 0, 1
];

잠시 행렬에서 곱셈이 어떻게 되는지 보고 넘어갈까요? 가장 쉬운 예제는 한 점에 행렬을 곱해보는 것입니다. 여러분들이 이미 3D공간상의 점이 4x4 행렬과 곱셈을 하기에 적합하지 않다는 걸 눈치채셨을 지도 모르겠습니다. 그래서 4번째 차원으로 W가 들어갑니다. 전형적인 예제에서는 이 값을 1로 두어도 계산할 수 있습니다. W가 특별하게 사용되는 경우도 있지만 이 문서에서 다루는 범주에서 벗어나기 때문에, 이 값이 얼마나 편리하게 쓰이는지 보시려면 WebGL model view projection 을 참고하시기 바랍니다.

행렬과 점이 어떻게 나열되어있는지 눈여겨 보세요.:

[1, 0, 0, 0,
 0, 1, 0, 0,
 0, 0, 1, 0,
 0, 0, 0, 1]

[4, 3, 2, 1]

곱셈 함수 정의하기

예제 코드에서 곱셈 함수를 다음과 같이 정의했습니다.  — multiplyMatrixAndPoint():

function multiplyMatrixAndPoint(matrix, point) {
  
  //행렬의 모든 원소에 행번호와 열번호로 이뤄진 간단한 변수 명을 부여.
  var c0r0 = matrix[ 0], c1r0 = matrix[ 1], c2r0 = matrix[ 2], c3r0 = matrix[ 3];
  var c0r1 = matrix[ 4], c1r1 = matrix[ 5], c2r1 = matrix[ 6], c3r1 = matrix[ 7];
  var c0r2 = matrix[ 8], c1r2 = matrix[ 9], c2r2 = matrix[10], c3r2 = matrix[11];
  var c0r3 = matrix[12], c1r3 = matrix[13], c2r3 = matrix[14], c3r3 = matrix[15];
  
  //점을 이루는 좌푯값들마다 간단한 변수명을 부여
  var x = point[0];
  var y = point[1];
  var z = point[2];
  var w = point[3];
  
  //각각의 좌푯값을 첫번째 열의 원소들과 곱한 뒤 더한다.
  var resultX = (x * c0r0) + (y * c0r1) + (z * c0r2) + (w * c0r3);
  
 //각각의 좌푯값을 두번째 열의 원소들과 곱한 뒤 더한다. 
  var resultY = (x * c1r0) + (y * c1r1) + (z * c1r2) + (w * c1r3);
  
 //각각의 좌푯값을 세번째 열의 원소들과 곱한 뒤 더한다.
  var resultZ = (x * c2r0) + (y * c2r1) + (z * c2r2) + (w * c2r3);
  
 //각각의 좌푯값을 네번째 열의 원소들과 곱한 뒤 더한다.
  var resultW = (x * c3r0) + (y * c3r1) + (z * c3r2) + (w * c3r3);
  
  return [resultX, resultY, resultZ, resultW];
}

이제 위의 함수를 이용해 점에 행렬을 곱할 수 있게 되었습니다. 단위 행렬을 이용한다면 정확히 같은 값이 나와야 합니다:

// sets identityResult to [4,3,2,1]
var identityResult = multiplyMatrixAndPoint(identityMatrix, [4, 3, 2, 1]);

같은 점을 반환하는 것은 그렇게 유용하지 않지만, 점에 대해 곱할 수 있는 다른 행렬들 중에는 유용한 것들이 많습니다. 다음 장에서 이런 행렬들을 알아보겠습니다.

Multiplying two matrices

In addition to multiplying a matrix and a point together, you can also multiply two matrices together. The function from above can be re-used to help out in this process:

function multiplyMatrices(matrixA, matrixB) {
  
  // Slice the second matrix up into columns
  var column0 = [matrixB[0], matrixB[4], matrixB[8], matrixB[12]];
  var column1 = [matrixB[1], matrixB[5], matrixB[9], matrixB[13]];
  var column2 = [matrixB[2], matrixB[6], matrixB[10], matrixB[14]];
  var column3 = [matrixB[3], matrixB[7], matrixB[11], matrixB[15]];
  
  // Multiply each column by the matrix
  var result0 = multiplyMatrixAndPoint(matrixA, column0);
  var result1 = multiplyMatrixAndPoint(matrixA, column1);
  var result2 = multiplyMatrixAndPoint(matrixA, column2);
  var result3 = multiplyMatrixAndPoint(matrixA, column3);
  
  // Turn the result columns back into a single matrix
  return [
    result0[0], result1[0], result2[0], result3[0],
    result0[1], result1[1], result2[1], result3[1],
    result0[2], result1[2], result2[2], result3[2],
    result0[3], result1[3], result2[3], result3[3]
  ];
}

Usage

Let's look at this function in action:

var someMatrix = [
  4, 0, 0, 0,
  0, 3, 0, 0,
  0, 0, 5, 0,
  4, 8, 4, 1
]

var identityMatrix = [
  1, 0, 0, 0,
  0, 1, 0, 0,
  0, 0, 1, 0,
  0, 0, 0, 1
];

// Returns a new array equivalent to someMatrix
var someMatrixResult = multiplyMatrices(identityMatrix, someMatrix);

Important: These matrix functions are written for clarity of explanation, not for speed or memory management. These functions create a lot of new arrays, which can be particularly expensive for real time operations due to garbage collection. In real production code it would be best to use optimized functions. glMatrix is an example of a library that has a large focus on speed and performance. The focus in the glMatrix library is to have target arrays that are allocated before the update loop.

Translation matrix

A translation matrix is based off the identity matrix. It moves the object in one of 3 directions, x, y, or z. The easiest way to think of a translation is like picking up a coffee cup. The coffee cup must be kept upright and oriented the same way so that no coffee is spilled. It can move up in the air off the table and around the air in space.

Now the coffee can't actually be drank with only a translation matrix because the cup cannot be tilted. In a later section, a new matrix will be discussed that will be able to handle that task.

var x = 50;
var y = 100;
var z = 0;

var translationMatrix = [
    1,    0,    0,   0,
    0,    1,    0,   0,
    0,    0,    1,   0,
    x,    y,    z,   1
];

Manipulating the DOM with a matrix

A really easy way to start using a matrix is to use the CSS3 matrix3d transform. First we'll set up a simple <div> with some content. The style is not shown. But it is set to a fixed width and height and centered on the page. The <div> has a transition set for the transform so that matrix is animated in making it easy to see what is being done.

<div id='move-me' class='transformable'>
  <h2>Move me with a matrix</h2>
  <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit...</p>
</div>

Finally for each of the examples we will generate a 4x4 matrix, then update the <div>'s style to have a transform applied to it, set to a matrix3d. Bear in mind that even though the matrix is made up of 4 rows and 4 columns, it collapses into a single line of 16 values.

// Create the matrix3d style property from a matrix array
function matrixArrayToCssMatrix(array) {
  return 'matrix3d(' + array.join(',') + ')';
}
	
// Grab the DOM element
var moveMe = document.getElementById('move-me');

// Returns a result like: "matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 50, 100, 0, 1);"
var matrix3dRule = matrixArrayToCssMatrix(translationMatrix);

// Set the transform
moveMe.style.transform = matrix3dRule;

JSFiddle에서보기

An example of matrix translation

Scale matrix

A scale matrix makes something larger or smaller in one of 3 dimension: width, height, and depth. In typical (cartesian) coordinates this would be stretching and shrinking in x, y, and z.

var w = 1.5; // width  (x)
var h = 0.7; // height (y)
var d = 1;   // depth  (z)

var scaleMatrix = [
    w,    0,    0,   0,
    0,    h,    0,   0,
    0,    0,    d,   0,
    0,    0,    0,   1
];

JSFiddle에서보기

An example of matrix scaling

Rotation matrix

Rotation matrices start looking a little bit more complicated than scaling and transform matrices. They use trigonometric functions to perform the rotation. While this section won't break the steps down into exhaustive detail, take this example for illustration.

// Manually rotating a point about the origin without matrices
var point = [10, 2];

// Calculate the distance from the origin
var distance = Math.sqrt(point[0] * point[0] + point[1] * point[1]);

// 60 degrees
var rotationInRadians = Math.PI / 3; 

var transformedPoint = [
  Math.cos(rotationInRadians) * distance,
  Math.sin(rotationInRadians) * distance
];

It is possible to encode these type of steps into a matrix, and do it each of the x, y, and z dimensions. Below is the representation of a rotation about the X axis:

var sin = Math.sin;
var cos = Math.cos;

// NOTE: There is no perspective in these transformations, so a rotation
//       at this point will only appear to only shrink the div

var a = Math.PI * 0.3; //Rotation amount

// Rotate around Z axis
var rotateZMatrix = [
  cos(a), -sin(a),    0,    0,
  sin(a),  cos(a),    0,    0,
       0,       0,    1,    0,
       0,       0,    0,    1
];

JSFiddle에서보기

Here are a set of functions that return the rotation matrices. One big note is that there is no perspective applied, so it might not feel very 3D yet. The flatness is equivalent to when a camera zooms in really close onto an object in the distance — the sense of perspective disappears.

function rotateAroundXAxis(a) {
  
  return [
       1,       0,        0,     0,
       0,  cos(a),  -sin(a),     0,
       0,  sin(a),   cos(a),     0,
       0,       0,        0,     1
  ];
}

function rotateAroundYAxis(a) {
  
  return [
     cos(a),   0, sin(a),   0,
          0,   1,      0,   0,
    -sin(a),   0, cos(a),   0,
          0,   0,      0,   1
  ];
}

function rotateAroundZAxis(a) {
  
  return [
    cos(a), -sin(a),    0,    0,
    sin(a),  cos(a),    0,    0,
         0,       0,    1,    0,
         0,       0,    0,    1
  ];
}

JSFiddle에서보기

Matrix Composition

The real power of matrices come from matrix composition. When matrices of a certain class are multiplied together they preserve the history of the transformations and are reversible. This means that if a translation, rotation, and scale matrix are all combined together, when the order of the matrices is reversed and re-applied then the original points are returned.

The order that matrices are multiplied in matters. When multiplying numbers a * b = c, and b * a = c are both true. For example 3 * 4 = 12, and 4 * 3 = 12. In math these numbers would be described as commutative. Matrices are not guaranteed to be the same if the order is switched, so matrices are non-commutative.

Another mind-bender is that matrix multiplication in WebGL and CSS3 needs to happen in the reverse order that the operations intuitively happen. For instance, to scale something down by 80%, move it down 200 pixels, and then rotate about the origin 90 degrees would look something like the following in pseudo-code.

  transformation = rotate * translate * scale

Composing multiple transformations

The function that we will be using is part of the utility functions. It takes an array of matrices and multiplies them together. In WebGL shader code, this is built into the language and the * operator can be used. Additionally this example uses a scale and translate function which return matrices as defined above.

var transformMatrix = MDN.multiplyArrayOfMatrices([
  rotateAroundZAxis(Math.PI * 0.5),    // Step 3: rotate around 90 degrees
  translate(0, 200, 0),                // Step 2: move down 100 pixels
  scale(0.8, 0.8, 0.8),                // Step 1: scale down
]);

JSFiddle에서보기

An example of matrix composition

Finally a fun step to show how matrices work is to reverse the steps to bring the matrix back to the original identity matrix.

var transformMatrix = MDN.multiplyArrayOfMatrices([
  scale(1.25, 1.25, 1.25),             // Step 6: scale back up
  translate(0, -200, 0),               // Step 5: move back up
  rotateAroundZAxis(-Math.PI * 0.5),   // Step 4: rotate back
  rotateAroundZAxis(Math.PI * 0.5),    // Step 3: rotate around 90 degrees
  translate(0, 200, 0),                // Step 2: move down 100 pixels
  scale(0.8, 0.8, 0.8),                // Step 1: scale down
]);

Why matrices are important

Matrices are important because they comprise a small set of numbers that can describe a wide range of transformations in space. They can easily be shared around in programs. Different coordinate spaces can be described with matrices, and some matrix multiplication will move one set of data from one coordinate space to another coordinate space. Matrices effectively remember every part of the previous transforms that were used to generate them.

For uses in WebGL, the graphics card is particularly good at multiplying a large number of points in space by matrices. Different operations like positioning points, calculating lighting, and posing animated characters all rely on this fundamental tool.

문서 태그 및 공헌자

 이 페이지의 공헌자: sangwoo
 최종 변경: sangwoo,