Содержание

WebGL2Fundamentals.org

Fix, Fork, Contribute

WebGL2 Реализация Стекла Матриц

Эта статья является продолжением WebGL 2D DrawImage. Если вы не читали её, я рекомендую начать оттуда.

В той последней статье мы реализовали WebGL эквивалент функции drawImage из Canvas 2D, включая её способность указывать как исходный прямоугольник, так и прямоугольник назначения.

Что мы ещё не сделали - это позволить нам вращать и/или масштабировать изображение из любой произвольной точки. Мы могли бы сделать это, добавив больше аргументов, как минимум нам нужно было бы указать центральную точку, поворот и масштаб по x и y. К счастью, есть более универсальный и полезный способ. Способ, которым Canvas 2D API делает это - это стек матриц. Функции стека матриц Canvas 2D API: save, restore, translate, rotate и scale.

Стек матриц довольно прост в реализации. Мы создаём стек матриц. Мы создаём функции для умножения верхней матрицы стека на матрицу перевода, поворота или масштабирования, используя функции, которые мы создали ранее.

Вот реализация

Сначала конструктор и функции save и restore

function MatrixStack() {
  this.stack = [];

  // поскольку стек пуст, это поместит начальную матрицу в него
  this.restore();
}

// Извлекает верхний элемент стека, восстанавливая ранее сохранённую матрицу
MatrixStack.prototype.restore = function() {
  this.stack.pop();
  // Никогда не позволяем стеку быть полностью пустым
  if (this.stack.length < 1) {
    this.stack[0] = m4.identity();
  }
};

// Помещает копию текущей матрицы в стек
MatrixStack.prototype.save = function() {
  this.stack.push(this.getCurrentMatrix());
};

Нам также нужны функции для получения и установки верхней матрицы

// Получает копию текущей матрицы (верх стека)
MatrixStack.prototype.getCurrentMatrix = function() {
  return this.stack[this.stack.length - 1].slice();  // создаёт копию
};

// Позволяет нам установить текущую матрицу
MatrixStack.prototype.setCurrentMatrix = function(m) {
  return this.stack[this.stack.length - 1] = m;
};

Наконец, нам нужно реализовать translate, rotate и scale, используя наши предыдущие матричные функции.

// Переводит текущую матрицу
MatrixStack.prototype.translate = function(x, y, z) {
  if (z === undefined) {
    z = 0;
  }
  var m = this.getCurrentMatrix();
  this.setCurrentMatrix(m4.translate(m, x, y, z));
};

// Вращает текущую матрицу вокруг Z
MatrixStack.prototype.rotateZ = function(angleInRadians) {
  var m = this.getCurrentMatrix();
  this.setCurrentMatrix(m4.zRotate(m, angleInRadians));
};

// Масштабирует текущую матрицу
MatrixStack.prototype.scale = function(x, y, z) {
  if (z === undefined) {
    z = 1;
  }
  var m = this.getCurrentMatrix();
  this.setCurrentMatrix(m4.scale(m, x, y, z));
};

Обратите внимание, что мы используем 3D матричные математические функции. Мы могли бы просто использовать 0 для z при переводе и 1 для z при масштабировании, но я обнаружил, что я так привык использовать 2D функции из Canvas 2D, что часто забываю указать z, и тогда код ломается, поэтому давайте сделаем z необязательным

// Переводит текущую матрицу
MatrixStack.prototype.translate = function(x, y, z) {
  if (z === undefined) {
    z = 0;
  }
  var m = this.getCurrentMatrix();
  this.setCurrentMatrix(m4.translate(m, x, y, z));
};

...

// Масштабирует текущую матрицу
MatrixStack.prototype.scale = function(x, y, z) {
  if (z === undefined) {
    z = 1;
  }
  var m = this.getCurrentMatrix();
  this.setCurrentMatrix(m4.scale(m, x, y, z));
};

Используя наш drawImage из предыдущего урока, у нас были эти строки

// эта матрица будет конвертировать из пикселей в пространство отсечения
var matrix = m4.orthographic(
    0, gl.canvas.clientWidth, gl.canvas.clientHeight, 0, -1, 1);

// переводим наш четырёхугольник в dstX, dstY
matrix = m4.translate(matrix, dstX, dstY, 0);

// масштабируем наш четырёхугольник размером в 1 единицу
// от 1 единицы до dstWidth, dstHeight единиц
matrix = m4.scale(matrix, dstWidth, dstHeight, 1);

Нам просто нужно создать стек матриц

var matrixStack = new MatrixStack();

и умножить на верхнюю матрицу из нашего стека в

// эта матрица будет конвертировать из пикселей в пространство отсечения
var matrix = m4.orthographic(
    0, gl.canvas.clientWidth, gl.canvas.clientHeight, 0, -1, 1);

// Стек матриц находится в пикселях, поэтому он идёт после проекции
// выше, которая конвертировала наше пространство из пространства отсечения в пространство пикселей
matrix = m4.multiply(matrix, matrixStack.getCurrentMatrix());

// переводим наш четырёхугольник в dstX, dstY
matrix = m4.translate(matrix, dstX, dstY, 0);

// масштабируем наш четырёхугольник размером в 1 единицу
// от 1 единицы до dstWidth, dstHeight единиц
matrix = m4.scale(matrix, dstWidth, dstHeight, 1);

И теперь мы можем использовать это так же, как мы использовали бы это с Canvas 2D API.

Если вы не знаете, как использовать стек матриц, вы можете думать об этом как о перемещении и ориентации начала координат холста. Так, например, по умолчанию в 2D холсте начало координат (0,0) находится в левом верхнем углу.

Например, если мы переместим начало координат в центр холста, то рисование изображения в точке 0,0 будет рисовать его, начиная с центра холста

Давайте возьмём наш предыдущий пример и просто нарисуем одно изображение

var textureInfo = loadImageAndCreateTextureInfo('resources/star.jpg');

function draw(time) {
  gl.clear(gl.COLOR_BUFFER_BIT);

  matrixStack.save();
  matrixStack.translate(gl.canvas.width / 2, gl.canvas.height / 2);
  matrixStack.rotateZ(time);

  drawImage(
    textureInfo.texture,
    textureInfo.width,
    textureInfo.height,
    0, 0);

  matrixStack.restore();
}

И вот это.

вы можете видеть, что хотя мы передаём 0, 0 в drawImage, поскольку мы используем matrixStack.translate для перемещения начала координат в центр холста, изображение рисуется и вращается вокруг этого центра.

Давайте переместим центр вращения в центр изображения

matrixStack.translate(gl.canvas.width / 2, gl.canvas.height / 2);
matrixStack.rotateZ(time);
matrixStack.translate(textureInfo.width / -2, textureInfo.height / -2);

И теперь оно вращается вокруг центра изображения в центре холста

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

matrixStack.translate(gl.canvas.width / 2, gl.canvas.height / 2);
matrixStack.rotateZ(time);

matrixStack.save();
{
  matrixStack.translate(textureInfo.width / -2, textureInfo.height / -2);

  drawImage(
    textureInfo.texture,
    textureInfo.width,
    textureInfo.height,
    0, 0);

}
matrixStack.restore();

matrixStack.save();
{
  // Мы находимся в центре центрального изображения, поэтому переходим в левый верхний угол
  matrixStack.translate(textureInfo.width / -2, textureInfo.height / -2);
  matrixStack.rotateZ(Math.sin(time * 2.2));
  matrixStack.scale(0.2, 0.2);
  // Теперь мы хотим правый нижний угол изображения, которое мы собираемся нарисовать
  matrixStack.translate(-textureInfo.width, -textureInfo.height);

  drawImage(
    textureInfo.texture,
    textureInfo.width,
    textureInfo.height,
    0, 0);

}
matrixStack.restore();

matrixStack.save();
{
  // Мы находимся в центре центрального изображения, поэтому переходим в правый верхний угол
  matrixStack.translate(textureInfo.width / 2, textureInfo.height / -2);
  matrixStack.rotateZ(Math.sin(time * 2.3));
  matrixStack.scale(0.2, 0.2);
  // Теперь мы хотим левый нижний угол изображения, которое мы собираемся нарисовать
  matrixStack.translate(0, -textureInfo.height);

  drawImage(
    textureInfo.texture,
    textureInfo.width,
    textureInfo.height,
    0, 0);

}
matrixStack.restore();

matrixStack.save();
{
  // Мы находимся в центре центрального изображения, поэтому переходим в левый нижний угол
  matrixStack.translate(textureInfo.width / -2, textureInfo.height / 2);
  matrixStack.rotateZ(Math.sin(time * 2.4));
  matrixStack.scale(0.2, 0.2);
  // Теперь мы хотим правый верхний угол изображения, которое мы собираемся нарисовать
  matrixStack.translate(-textureInfo.width, 0);

  drawImage(
    textureInfo.texture,
    textureInfo.width,
    textureInfo.height,
    0, 0);

}
matrixStack.restore();

matrixStack.save();
{
  // Мы находимся в центре центрального изображения, поэтому переходим в правый нижний угол
  matrixStack.translate(textureInfo.width / 2, textureInfo.height / 2);
  matrixStack.rotateZ(Math.sin(time * 2.5));
  matrixStack.scale(0.2, 0.2);
  // Теперь мы хотим левый верхний угол изображения, которое мы собираемся нарисовать
  matrixStack.translate(0, 0);  // 0,0 означает, что эта строка на самом деле ничего не делает

  drawImage(
    textureInfo.texture,
    textureInfo.width,
    textureInfo.height,
    0, 0);

}
matrixStack.restore();

И вот это

Если вы думаете о различных функциях стека матриц, translate, rotateZ и scale как о перемещении начала координат, то способ, которым я думаю об установке центра вращения, это куда мне нужно переместить начало координат, чтобы когда я вызываю drawImage, определённая часть изображения была в предыдущем начале координат?

Другими словами, допустим, на холсте 400x300 я вызываю matrixStack.translate(210, 150). В этот момент начало координат находится в точке 210, 150, и всё рисование будет относительно этой точки. Если мы вызовем drawImage с 0, 0, это то место, где будет нарисовано изображение.

Допустим, мы хотим, чтобы центром вращения был правый нижний угол. В этом случае куда нам нужно переместить начало координат, чтобы когда мы вызываем drawImage, точка, которую мы хотим сделать центром вращения, была в текущем начале координат? Для правого нижнего угла текстуры это было бы -textureWidth, -textureHeight, так что теперь когда мы вызываем drawImage с 0, 0, текстура будет нарисована здесь, и её правый нижний угол находится в предыдущем начале координат.

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

Вы можете заметить, что стек матриц очень похож на граф сцены, который мы рассматривали ранее. Граф сцены имел дерево узлов, и когда мы проходили по дереву, мы умножали каждый узел на узел его родителя. Стек матриц - это эффективно другая версия того же процесса.

Есть предложения или замечания? Создайте issue на GitHub.
comments powered by Disqus