оглавление

WebGLFundamentals.org

WebGL 3D - Камеры

Эта статья продолжает серию статей о WebGL. В первой из них мы начали с основ WebGL, а в предыдущей рассмотрели перспективу в 3D. Если вы их ещё не читали, рекомендую ознакомиться сначала с ними.

В последней статье нам пришлось сдвинуть F в переднюю часть фрустума, потому что именно там её ожидает функция m4.perspective, а объекты во фрустуме находятся от -zNear до -zFar впереди.

Перемещать объекты, чтобы они оказались впереди наблюдателя, кажется неправильным, не так ли? В реальной жизни вы обычно перемещаете камеру, чтобы снять здание.

перемещение камеры относительно объектов

Вы ведь не двигаете здания перед неподвижной камерой, верно?

перемещение объектов относительно камеры

Но в последней статье мы пришли к проекции, для которой необходимо, чтобы объекты находились перед началом координат по оси -Z. Для достижения этого нам нужно сместить камеру в начало координат, а затем сместить всё остальное на заданное значение, чтобы их положение относительно камеры осталось неизменным.

перемещение объектов относительно вида

По сути, нам нужно перемещать мир перед камерой. Простейший способ сделать это - использовать "обратную" матрицу. Математика вычисления обратной матрицы довольно сложна, но по сути всё просто. Обратным будем называть значение, которое является инверсией другого значение. Например, обратным значением 123 будет -123. Обратной для матрицы масштабирования на 5 будет матрица масштабирования на 1/5 или 0.2. Обратной для матрицы поворота на 30° по X будет матрица поворота на -30° по X.

До этого момента мы использовали перенос, поворот и масштабирование для изменения положения и направленности нашей буквы 'F'. После умножения всех матриц мы получим одну матрицу, определяющую, как переместить 'F' из исходного положение в нужное нам место, размер и направленность. То же самое мы можем сделать и в случае с камерой. Если у нас есть матрица, которая должна перенести камеру в нужное нам место, нам нужно лишь найти обратную ей матрицу, чтобы переместить всё остальное на обратную величину, и в итоге камера останется в координатах (0, 0, 0), а все остальные объекты примут необходимые нам положения перед камерой.

Сделаем 3D-сцену с кругом из букв 'F', как показано на анимациях выше.

Так как мы отрисовываем 5 объектов, и все они используют одну и ту же проекционную матрицу, мы инициируем её за пределами цикла.


// Задаём проекционную матрицу
var aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
var zNear = 1;
var zFar = 2000;
var projectionMatrix = m4.perspective(fieldOfViewRadians, aspect, zNear, zFar);

Затем вычислим матрицу камеры, которая задаёт положение и направление камеры в мировом пространстве. Код ниже создаёт матрицу, которая вращает камеру вокруг начала координат по радиусу radius * 1.5, при этом камера направлена в центр координат.

Движение камеры

var numFs = 5;
var radius = 200;

// Вычисление матрицы камеры
var cameraMatrix = m4.yRotation(cameraAngleRadians);
cameraMatrix = m4.translate(cameraMatrix, 0, 0, radius * 1.5);

Далее вычисляем "матрицу вида" на основе матрицы камеры. "Матрица вида" - это матрица, которая перемещает всё в противоположном камере направлении, таким образом всё вращается относительно камеры, словно она находится в начале координат (0,0,0). Это возможно благодаря функции inverse, которая вычисляет обратную матрицу (матрица, которая делает всё с точностью наоборот по сравнению с исходной матрицей). Другими словами, если исходная матрица переносила бы камеру в определённое положение и ориентацию, то обратная матрица вращает всё кроме камеры, словно камера находится в начале координат.

// Создаём обратную матрицу камеры
var viewMatrix = m4.inverse(cameraMatrix);

Теперь объединим матрицу вида и проекционную матрицу в одну.

// Получаем видо-проекционную матрицу
var viewProjectionMatrix = m4.multiply(projectionMatrix, viewMatrix);

Наконец, отрисовываем круг из букв F. Каждая F начинается с видо-проекционной матрицы, затем поворачивается и смещается на значение радиуса.

for (var ii = 0; ii < numFs; ++ii) {
  var angle = ii * Math.PI * 2 / numFs;
  var x = Math.cos(angle) * radius;
  var y = Math.sin(angle) * radius

  // Начинаем с видо-проекционной матрицы.
  // Вычисляем матрицу для F
  var matrix = m4.translate(viewProjectionMatrix, x, 0, y);

  // Задаём матрицу
  gl.uniformMatrix4fv(matrixLocation, false, matrix);

  // Отрисовываем геометрию
  var primitiveType = gl.TRIANGLES;
  var offset = 0;
  var count = 16 * 6;
  gl.drawArrays(primitiveType, offset, count);
}

И вуаля! Камера вращается вокруг кольца из букв 'F'. Потяните за слайдер cameraAngle для вращения камеры.

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

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

Для начала нам нужно определиться, где расположить камеру. Назовём эту переменную cameraPosition. Ещё нам нужно знать положение объекта, на который смотрит камера. Назовём его target. Если вычесть target из cameraPosition, мы получим вектор, указывающий в нужном нам направлении от камеры до объекта. Назовём это zAxis. Мы знаем, что камера направлена в сторону -Z, поэтому мы можем отнять cameraPosition - target. Затем нормализуем результат и копируем его в z матрицы.

+----+----+----+----+
|    |    |    |    |
+----+----+----+----+
|    |    |    |    |
+----+----+----+----+
| Zx | Zy | Zz |    |
+----+----+----+----+
|    |    |    |    |
+----+----+----+----+

Эта часть матрицы представляет ось Z. Именно в этом случае - ось Z камеры. После нормализации мы получим единичный вектор - то есть вектор представляет значение 1.0. В статье про 2D-поворот мы говорили о единичной окружности, и как она полезна в 2D-повороте. В 3D нам нужна уже единичная сфера, а нормализованный вектор представляет точку на этой сфере.

ось z

Однако, этого недостаточно. Просто вектор даёт нам точку на единичной сфере, но от какой и до какой точки проходит этот вектор? Нам нужно заполнить и другие части матрицы. В частности, ось X и ось Y. Мы знаем, что эти три оси перпендикулярны друг другу. Мы также знаем, что обычно мы не направляем камеру прямо вверх. А это значит, что если нам известно, где верх (в нашем случае это 0,1,0), мы можем использовать "векторное произведение" для вычисления осей X и Y.

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

Так или иначе при вычислении векторного произведения наших zAxis и up мы получим xAxis для камеры.

zAxis умножить на up = xAxis

И теперь, когда у нас есть xAxis, мы можем умножить zAxis и xAxis, что даст нам в итоге yAxis камеры.

zAxis умножить на xAxis = yAxis

Теперь нам остаётся объединить 3 оси в матрицу. Это даст нам матрицу, которая направляет некий объект от cameraPosition до target. Нам осталось лишь добавить position.

+----+----+----+----+
| Xx | Xy | Xz |  0 |  <- ось x
+----+----+----+----+
| Yx | Yy | Yz |  0 |  <- ось y
+----+----+----+----+
| Zx | Zy | Zz |  0 |  <- ось z
+----+----+----+----+
| Tx | Ty | Tz |  1 |  <- положение камеры
+----+----+----+----+

Вот код для вычисления векторного произведения 2 векторов.

function cross(a, b) {
  return [a[1] * b[2] - a[2] * b[1],
          a[2] * b[0] - a[0] * b[2],
          a[0] * b[1] - a[1] * b[0]];
}

Вот код разницы двух векторов.

function subtractVectors(a, b) {
  return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
}

Код нормализации вектора (делает нормализованный вектор).

function normalize(v) {
  var length = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
  // проверяем, что мы не делим на 0
  if (length > 0.00001) {
    return [v[0] / length, v[1] / length, v[2] / length];
  } else {
    return [0, 0, 0];
  }
}

Код для создания матрицы "lookAt" ("смотреть на" - прим.пер.)

var m4 = {
  lookAt: function(cameraPosition, target, up) {
    var zAxis = normalize(
        subtractVectors(cameraPosition, target));
    var xAxis = cross(up, zAxis);
    var yAxis = cross(zAxis, xAxis);

    return [
       xAxis[0], xAxis[1], xAxis[2], 0,
       yAxis[0], yAxis[1], yAxis[2], 0,
       zAxis[0], zAxis[1], zAxis[2], 0,
       cameraPosition[0],
       cameraPosition[1],
       cameraPosition[2],
       1,
    ];
  }

А вот как мы можем использовать это, чтобы камера указывала на определённую 'F', перемещаясь вокруг кольца букв.

  ...

  // Вычисление положения первой буквы F
  var fPosition = [radius, 0, 0];

  // Используем матрицу для вычисления положения
  // на кольце букв, где находится камера
  var cameraMatrix = m4.yRotation(cameraAngleRadians);
  cameraMatrix = m4.translate(cameraMatrix, 0, 0, radius * 1.5);

  // Получаем положение камеры из вычисленной матрицы
  var cameraPosition = [
    cameraMatrix[12],
    cameraMatrix[13],
    cameraMatrix[14],
  ];

  var up = [0, 1, 0];

  // Вычисляем матрицу камеры, используя функцию "смотреть на"
  var cameraMatrix = m4.lookAt(cameraPosition, fPosition, up);

  // Создаём матрицу вида из матрицы камеры
  var viewMatrix = m4.inverse(cameraMatrix);

  ...

И вот результат.

Потяните за слайдер и посмотрите, как камера следит за одной буквой 'F'.

Замечу, что вы можете использовать функцию "lookAt" не только для камер. Например, чтобы голова персонажа следовала взглядом за кем-то. Или чтобы турель следовала за целью. То есть следование объекта своему пути. Вы вычисляете, где находится цель на пути объекта. Затем вычисляете, где цель будет на пути через несколько мгновений в будущем. Далее помещаете эти два значения в функцию lookAt, и получаете матрицу, которая направляет объект по заданному пути и направлению.

Узнайте больше об анимации в этой статье.

Стандарт lookAt

Большинство 3D-библиотек имеют функцию lookAt. Часто она создана, чтобы получать "матрицу вида", а не "матрицу камеры". Другими словами, она создаёт матрицу, которая вращает всё впереди камеры, вместо того, чтобы оперировать матрицей изменения самой камеры.

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

Вопросы? Спросите на stackoverflow.
Нашли ошибку? Создайте задачу на github.
comments powered by Disqus