Матрицы

На этот раз мы познакомимся с матрицами WebGL и оценим их преимущества по сравнению с нашей собственной реализацией.

Поворот, перенос и масштабирование, которые мы выполняли ранее, отлично укладываются в логику матриц, каждую из которых мы рассмотрим отдельно. Но сначала мы рассмотрим единичную матрицу — аналог единицы при работе с обычными числами. Вспомните, что при масштабировании нам нужно было задать начальное значение коэффициента масштабирования, равное единице, чтобы объект не сжимался в пустоту, как это бы происходило при нулевом значении коэффициента. Так и в случае с матрицами существует единичная матрица, при умножении на которую ничего не меняется. Рассмотрим умножение подробнее.

При умножении матрицы 4х4 на вектор размерностью четыре (наши координаты) мы получим вектор размерностью четыре (о том, почему используется четыре компонента вместо трех — x, y и z — мы узнаем в следующий раз). Вот, как это происходит (вы ведь помните школьный курс, верно 😉 ?..):

Согласно этому правилу единичная матрица будет работать следующим образом:

То есть это матрица, которая не делает ничего и оставляет координаты в исходном виде.

Теперь матрица переноса, которая будет перемещать объект:

Как видите, матрица дает нам тот же результат, который мы моделировали самостоятельно в Двигаем объекты:

  • X’ = X + dX
  • Y’ = Y + dY

Матрица масштабирования:

И, наконец, матрица поворота по оси Z:

Отлично, теперь с помощью матриц мы можем делать все то же самое, что мы делали самостоятельно в предыдущих примерах. Только более сложным способом :). Так какое же преимущество есть у матриц? Первое преимущество заключается в том, что при умножении нескольких матриц мы получим одну матрицу, объединяющую в себе все трансформации. Например, вот как выглядит матрица, объединяющая масштабирование и перенос (увеличить объект в два раза в длину и ширину и переместить его на 10 единиц вправо и вверх):

В этом примере было рассмотрено объединение двух трансформаций. Но представьте, что их гораздо больше. Например, нам нужно увеличить объект в два раза, выполнить поворот по оси X, затем поворот по оси Y, а затем сместить его в новое положение. Представляете сложность вершинного шейдера и нагрузку на графический процессор? С матрицами же нам не важно, сколько трансформаций необходимо выполнить — вершинный шейдер будет выглядеть одинаково:


    attribute vec3 aVertexPosition;

    uniform mat4 uMVMatrix;

    void main(void) {
        gl_Position = uMVMatrix * vec4(aVertexPosition, 1.0);
    }

Из вышесказанного вытекает еще одно преимущество — нам не нужно менять вершинный шейдер при применении новой трансформации или изменении порядка применения трансформаций.

Естественно, если в одном месте упростилось, то в другом стало сложнее. Вершинный шейдер стал таким простым, потому что эта часть вычислений перекочевала в JavaScript. Рассмотрим все изменения по порядку.

В initShaders нам больше не нужны uTranslation, uScale и uAngle, вместо них мы будем использовать только uMVMatrix:


        shaderProgram.mvMatrix = gl.getUniformLocation(shaderProgram, "uMVMatrix");
        gl.uniformMatrix4fv(shaderProgram.mvMatrix, false, createIdentityMatrix());

Каждую трансформацию мы будем хранить в отдельной матрице, которым нужно задать какие-то начальные значения. Здесь нам и пригодится единичная матрица:


    var mTranslation = createIdentityMatrix();
    var mScale = createIdentityMatrix();;
    var mRotation = createIdentityMatrix();;

Функцию для создания единичной матрицы createIdentityMatrix, как и другие функции по работе с матрицами, мы рассмотрим немного позже.

Также изменилась функция обработки событий мыши. Теперь мы записываем каждую трансформацию в соответствующую матрицу, а в конце формируем итоговую матрицу перемножением всех матриц трансформаций:


    function onMouseMove(evt) {
        if (mousePressed) {
            var diffX = evt.clientX - dragStartX;
            var diffY = dragStartY - evt.clientY;
            if (currentEvent === eventType.drag) {
                dragOffset = [diffX * 2 / 500, diffY * 2 / 500];
                var finalTranslation = [translation[0] + dragOffset[0], translation[1] + dragOffset[1]];
                mTranslation = createTranslationMatrix(finalTranslation[0], finalTranslation[1]);
            } else if (currentEvent === eventType.scale) {
                dragOffset = [diffX / 100, diffY / 100];
                var finalScale = [scale[0] + dragOffset[0], scale[1] + dragOffset[1]]
                mScale = createScaleMatrix(finalScale[0], finalScale[1]);
            } else if (currentEvent === eventType.rotate) {
                dragOffset = diffX;
                mRotation = createRotationMatrix(rotation + dragOffset);
            }
            
            var mvMatrix = multiplyMatrices(mScale, mRotation);
            mvMatrix = multiplyMatrices(mvMatrix, mTranslation);
            gl.uniformMatrix4fv(shaderProgram.mvMatrix, false, mvMatrix);
        }
    }

За умножение матриц, как вы уже догадались, отвечает функция multiplyMatrices. И вот как она выглядит:


    function multiplyMatrices(m1, m2) {
        return [
            m1[0] * m2[0] + m1[1] * m2[4] + m1[2] * m2[8] + m1[3] * m2[12],
            m1[0] * m2[1] + m1[1] * m2[5] + m1[2] * m2[9] + m1[3] * m2[13],
            m1[0] * m2[2] + m1[1] * m2[6] + m1[2] * m2[10] + m1[3] * m2[14],
            m1[0] * m2[3] + m1[1] * m2[7] + m1[2] * m2[11] + m1[3] * m2[15],
            
            m1[4] * m2[0] + m1[5] * m2[4] + m1[6] * m2[8] + m1[7] * m2[12],
            m1[4] * m2[1] + m1[5] * m2[5] + m1[6] * m2[9] + m1[7] * m2[13],
            m1[4] * m2[2] + m1[5] * m2[6] + m1[6] * m2[10] + m1[7] * m2[14],
            m1[4] * m2[3] + m1[5] * m2[7] + m1[6] * m2[11] + m1[7] * m2[15],
            
            m1[8] * m2[0] + m1[9] * m2[4] + m1[10] * m2[8] + m1[11] * m2[12],
            m1[8] * m2[1] + m1[9] * m2[5] + m1[10] * m2[9] + m1[11] * m2[13],
            m1[8] * m2[2] + m1[9] * m2[6] + m1[10] * m2[10] + m1[11] * m2[14],
            m1[8] * m2[3] + m1[9] * m2[7] + m1[10] * m2[11] + m1[11] * m2[15],
            
            m1[12] * m2[0] + m1[13] * m2[4] + m1[14] * m2[8] + m1[15] * m2[12],
            m1[12] * m2[1] + m1[13] * m2[5] + m1[14] * m2[9] + m1[15] * m2[13],
            m1[12] * m2[2] + m1[13] * m2[6] + m1[14] * m2[10] + m1[15] * m2[14],
            m1[12] * m2[3] + m1[13] * m2[7] + m1[14] * m2[11] + m1[15] * m2[15]
        ];
    }

Вычислений довольно много, но функция очень простая. Она умножает одну матрицу 4х4 на другую, в результате чего также получается матрица 4х4. И теперь неосвещенным остался лишь код, который отвечает за создание матриц трансформаций. Вот он:


    // единичная матрица
    function createIdentityMatrix() {
        return [
            1, 0, 0, 0,
            0, 1, 0, 0,
            0, 0, 1, 0,
            0, 0, 0, 1
        ];
    }
    
    // матрица переноса
    function createTranslationMatrix(tx, ty) {
        return [
            1, 0, 0, 0,
            0, 1, 0, 0,
            0, 0, 1, 1,
            tx, ty, 0, 1
        ];
    }
    
    // матрица масштабирования
    function createScaleMatrix(sx, sy) {
        return [
            sx, 0,  0, 0,
            0,  sy, 0, 0,
            0,  0,  1, 0,
            0,  0,  0, 1
        ];
    }
    
    // матрица поворота по оси Z
    function createRotationMatrix(angle) {
        var angleRad = angle * Math.PI / 180;
        var c = Math.cos(angleRad);
        var s = Math.sin(angleRad);
        return [
            c, -s, 0, 0,
            s,  c, 0, 0,
            0,  0, 1, 0,
            0,  0, 0, 1
        ];
    }

Вот и все! Львиная доля кода ушла на логику по работе с матрицами. Именно поэтому ранее мы использовали готовую библиотеку gl-matrix — в ней есть весь необходимый функционал и она оптимизирована в плане быстродействия. Но сейчас важно было понять, как это все устроено, чтобы в дальнейшем со спокойной душой возложить всю работу на плечи библиотеки.

Я надеюсь, что теперь матрицы не кажутся вам чем-то сложным и непонятным. В дальнейшем матрицы еще не раз покажут свою силу и гибкость. Понимание их работы пригодится нам и в следующий раз, когда мы поговорим о камере и мировом пространстве.

Код как обычно можно найти на GitHub, демонстрацию онлайн можно найти здесь

Матрицы
Метки:

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *