Эта статья является продолжением серии статей о WebGL. Первая началась с основ, а предыдущая была о 3D перспективной проекции. Если вы их не читали, пожалуйста, сначала ознакомьтесь с ними.
В последней статье нам пришлось переместить F перед усеченной пирамидой, потому что функция m4.perspective
ожидает, что она будет находиться в начале координат (0, 0, 0), и что объекты в усеченной пирамиде находятся от -zNear
до -zFar
перед ней.
Перемещение вещей перед видом не кажется правильным подходом, не так ли? В реальном мире вы обычно перемещаете камеру, чтобы сфотографировать здание.
Вы обычно не перемещаете здания, чтобы они были перед камерой.
Но в нашей последней статье мы придумали проекцию, которая требует, чтобы вещи были перед началом координат на оси -Z. Чтобы достичь этого, что мы хотим сделать, это переместить камеру в начало координат и переместить все остальное на правильное количество, чтобы оно все еще было в том же месте относительно камеры.
Нам нужно эффективно переместить мир перед камерой. Самый простой способ сделать это - использовать “обратную” матрицу. Математика для вычисления обратной матрицы в общем случае сложна, но концептуально это легко. Обратная - это значение, которое вы бы использовали, чтобы отрицать какое-то другое значение. Например, обратная матрица, которая перемещает по X на 123, - это матрица, которая перемещает по X на -123. Обратная матрица, которая масштабирует на 5, - это матрица, которая масштабирует на 1/5 или 0.2. Обратная матрица, которая поворачивает на 30° вокруг оси X, была бы той, которая поворачивает на -30° вокруг оси X.
До этого момента мы использовали перемещение, поворот и масштабирование, чтобы влиять на позицию и ориентацию нашей ‘F’. После умножения всех матриц вместе у нас есть одна матрица, которая представляет, как переместить ‘F’ от начала координат к месту, размеру и ориентации, которые мы хотим. Мы можем сделать то же самое для камеры. Как только у нас есть матрица, которая говорит нам, как перемещать и поворачивать камеру от начала координат туда, где мы хотим, мы можем вычислить ее обратную, которая даст нам матрицу, которая говорит нам, как перемещать и поворачивать все остальное в противоположном количестве, что эффективно сделает так, что камера будет в (0, 0, 0), и мы переместили все перед ней.
Давайте сделаем 3D сцену с кругом ‘F’, как на диаграммах выше.
Вот код.
function drawScene() {
var numFs = 5;
var radius = 200;
...
// Вычисляем матрицу
var aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
var zNear = 1;
var zFar = 2000;
var projectionMatrix = m4.perspective(fieldOfViewRadians, aspect, zNear, zFar);
var cameraMatrix = m4.yRotation(cameraAngleRadians);
cameraMatrix = m4.translate(cameraMatrix, 0, 0, radius * 1.5);
// Делаем матрицу вида из матрицы камеры.
var viewMatrix = m4.inverse(cameraMatrix);
// перемещаем проекционное пространство в пространство вида (пространство перед
// камерой)
var viewProjectionMatrix = m4.multiply(projectionMatrix, viewMatrix);
// Рисуем 'F' в круге
for (var ii = 0; ii < numFs; ++ii) {
var angle = ii * Math.PI * 2 / numFs;
var x = Math.cos(angle) * radius;
var z = Math.sin(angle) * radius;
// добавляем перемещение для этой F
var matrix = m4.translate(viewProjectionMatrix, x, 0, z);
// Устанавливаем матрицу.
gl.uniformMatrix4fv(matrixLocation, false, matrix);
// Рисуем геометрию.
var primitiveType = gl.TRIANGLES;
var offset = 0;
var count = 16 * 6;
gl.drawArrays(primitiveType, offset, count);
}
}
Сразу после того, как мы вычисляем нашу проекционную матрицу, вы можете видеть, что мы вычисляем камеру, которая ходит вокруг ‘F’, как на диаграмме выше.
// Вычисляем матрицу камеры
var cameraMatrix = m4.yRotation(cameraAngleRadians);
cameraMatrix = m4.translate(cameraMatrix, 0, 0, radius * 1.5);
Затем мы вычисляем “матрицу вида” из матрицы камеры. “Матрица вида”
// Делаем матрицу вида из матрицы камеры.
var viewMatrix = m4.inverse(cameraMatrix);
Затем мы комбинируем (умножаем) их, чтобы сделать матрицу viewProjection.
// создаем матрицу viewProjection. Это будет применять перспективу
// И перемещать мир так, что камера эффективно является началом координат
var viewProjectionMatrix = m4.multiply(projectionMatrix, viewMatrix);
Наконец, мы используем это пространство как начальное пространство для размещения каждой `F’
var x = Math.cos(angle) * radius;
var z = Math.sin(angle) * radius;
var matrix = m4.translate(viewProjectionMatrix, x, 0, z);
Другими словами, viewProjection одинаков для каждой F
. Та же перспектива,
та же камера.
И вуаля! Камера, которая ходит вокруг круга ‘F’. Перетащите слайдер cameraAngle
,
чтобы переместить камеру вокруг.
Это все хорошо, но использование поворота и перемещения для перемещения камеры туда, где вы хотите, и указания на то, что вы хотите видеть, не всегда легко. Например, если бы мы хотели, чтобы камера всегда указывала на конкретную одну из ‘F’, потребовалась бы довольно сумасшедшая математика, чтобы вычислить, как повернуть камеру, чтобы указать на эту ‘F’, пока она ходит вокруг круга ‘F’.
К счастью, есть более простой способ. Мы можем просто решить, где мы хотим камеру и на что мы хотим, чтобы она указывала, и затем вычислить матрицу, которая поместит камеру туда. Основываясь на том, как работают матрицы, это удивительно легко.
Сначала нам нужно знать, где мы хотим камеру. Мы назовем это
cameraPosition
. Затем нам нужно знать позицию вещи, на которую мы хотим
смотреть или целиться. Мы назовем это target
. Если мы вычтем
cameraPosition
из target
, у нас будет вектор, который указывает в
направлении, в котором нам нужно идти от камеры, чтобы добраться до цели. Давайте
назовем это zAxis
. Поскольку мы знаем, что камера указывает в направлении -Z, мы
можем вычесть другим способом cameraPosition - target
. Мы нормализуем
результаты и копируем их прямо в z
часть матрицы.
+----+----+----+----+ | | | | | +----+----+----+----+ | | | | | +----+----+----+----+ | Zx | Zy | Zz | | +----+----+----+----+ | | | | | +----+----+----+----+
Эта часть матрицы представляет ось Z. В этом случае ось Z камеры. Нормализация вектора означает создание вектора, который представляет 1.0. Если вы вернетесь к статье о 2D повороте, где мы говорили о единичных окружностях и о том, как они помогали с 2D поворотом. В 3D нам нужны единичные сферы, и нормализованный вектор представляет точку на единичной сфере.
Но этого недостаточно информации. Просто один вектор дает нам точку на единичной сфере, но какую ориентацию от этой точки использовать для ориентации вещей? Нам нужно заполнить другие части матрицы. Конкретно части оси X и оси Y. Мы знаем, что в общем эти 3 части перпендикулярны друг другу. Мы также знаем, что “в общем” мы не направляем камеру прямо вверх. Учитывая это, если мы знаем, какой путь вверх, в этом случае (0,1,0), Мы можем использовать это и что-то, называемое “векторным произведением”, чтобы вычислить ось X и ось Y для матрицы.
Я не имею представления, что означает векторное произведение в математических терминах. Что я знаю, так это то, что если у вас есть 2 единичных вектора, и вы вычисляете векторное произведение из них, вы получите вектор, который перпендикулярен этим 2 векторам. Другими словами, если у вас есть вектор, указывающий на юго-восток, и вектор, указывающий вверх, и вы вычисляете векторное произведение, вы получите вектор, указывающий либо на юго-запад, либо на северо-восток, поскольку это 2 вектора, которые перпендикулярны юго-востоку и вверх. В зависимости от того, в каком порядке вы вычисляете векторное произведение, вы получите противоположный ответ.
В любом случае, если мы вычислим векторное произведение нашего zAxis
и
up
, мы получим xAxis для камеры.
И теперь, когда у нас есть xAxis
, мы можем вычислить векторное произведение zAxis
и xAxis
,
что даст нам yAxis
камеры
Теперь все, что нам нужно сделать, это вставить 3 оси в матрицу. Это дает нам
матрицу, которая будет ориентировать что-то, что указывает на target
из
cameraPosition
. Нам просто нужно добавить 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 = normalize(cross(up, zAxis));
var yAxis = normalize(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, 50, radius * 1.5);
// Получаем позицию камеры из матрицы, которую мы вычислили
var cameraPosition = [
cameraMatrix[12],
cameraMatrix[13],
cameraMatrix[14],
];
var up = [0, 1, 0];
// Вычисляем матрицу камеры, используя look at.
var cameraMatrix = m4.lookAt(cameraPosition, fPosition, up);
// Делаем матрицу вида из матрицы камеры.
var viewMatrix = m4.inverse(cameraMatrix);
...
И вот результат.
Перетащите слайдер и обратите внимание, как камера отслеживает одну ‘F’.
Обратите внимание, что вы можете использовать математику “lookAt” не только для камер. Общие применения - заставить голову персонажа
следовать за кем-то. Заставить башню целиться в цель. Заставить объект следовать по пути. Вы вычисляете,
где на пути находится цель. Затем вы вычисляете, где на пути цель будет через несколько мгновений
в будущем. Подставьте эти 2 значения в вашу функцию lookAt
, и вы получите матрицу, которая заставляет
ваш объект следовать по пути и ориентироваться к пути тоже.
Прежде чем вы продолжите, вы можете захотеть проверить эту короткую заметку о названиях матриц.
Иначе давайте изучим анимацию дальше.