Помните, когда мы в школе проходили все эти синусы, косинусы и углы, у всех был вопрос — а как нам это пригодится в жизни? Тогда казалось, что это нужно только учёным и математикам, но на самом деле всё трёхмерное моделирование и 3D-игры — это та самая школьная тригонометрия.
При чём тут 3D
В одной из статей мы рассказывали про 3D-игру Doom. Одной из особенностей этой игры было то, что у неё не было настоящей трёхмерной графики — движок оперировал двумерными моделями. Но на экране казалось, что это настоящая трёхмерность:
- можно было понять, что находится ближе, а что — дальше;
- если подойти к двери, она увеличивалась, а если отойти — уменьшалась;
- когда враги приближались, они становились больше, а на горизонте их было почти не видно и так далее.
Всё дело — в угле зрения. От него зависит, каким будет казаться объект, маленьким или большим, и как он изменит свой размер на разных расстояниях.
Угол зрения
Чтобы понять, что такое угол зрения, нам нужно знать всего две величины — высоту объекта и расстояние до него:
Сейчас наблюдателю наш красный объект кажется далёким, потому что угол зрения — маленький. Но если мы подвинем этот объект поближе к наблюдателю, то угол зрения станет больше:
Теперь наблюдателю кажется, что объект очень близко, потому что угол зрения стал гораздо больше, чем раньше. Получается, что угол зрения влияет на то, как мы воспринимаем предметы — близкими или далёкими.
И вот тут нам пригождается школьная тригонометрия — все эти синусы, косинусы и тангенсы. Благодаря им мы сможем рассчитывать нужный размер предметов на экране в зависимости от того, какой угол получится между нашей виртуальной камерой и разницей в высоте предмета. Это позволит нам смоделировать разное расстояние до предметов, как будто у двумерного плоского экрана появляется третье измерение — глубина.
Ненастоящее 3D в DOOM
Из школьной программы мы помним формулу тангенса:
tg α = высота / расстояние, где α — наш угол зрения.
Единственное, что отличает на экране далёкие предметы от близких, — это высота, поэтому мы можем регулировать её так:
высота = расстояние × tg α.
Если расстояние будет равно единице, то высота объекта — это просто будет тангенс угла зрения альфа.
А раз так, то мы можем это использовать для эффекта 3D:
- Берём любой объект
- Выясняем, какой будет угол зрения для этого объекта, если подойти к нему вплотную, насколько позволяет игровой движок.
- Теперь если нам нужно показать, что мы отходим от объекта, то мы просто уменьшаем угол зрения. С ним уменьшится и тангенс, и высота объекта на экране.
- То же самое и с приближением — чтобы показать на экране, что мы как будто подходим к объекту, мы просто увеличиваем угол зрения, а с ним увеличивается и высота. Кажется, что мы подошли поближе.
Как видите, тут нигде нет расстояния до объекта — только угол зрения, который создаёт эффект приближения или удаления. Чистая тригонометрия.
Настоящее 3D
В настоящем 3D синусы и косинусы нужны, чтобы посчитать новые координаты всех сторон движущегося объекта. Штука в том, что нам нужно перенести объёмный трёхмерный объект на плоский двумерный экран — сделать проекцию.
На плоской поверхности у нас есть только две координаты — X и Y, поэтому нам нужны формулы, которые помогут учесть третью координату Z и нарисовать объект так, чтобы он выглядел объёмным:
x':=x*sin(угол между плоскостью XOY и отрезком OZ) ;
y':=y*cos(угол между плоскостью XOY и отрезком OZ) ;
Чтобы трёхмерные объекты на экране можно было двигать, тоже используют тригонометрию. Например, если у нас есть трёхмерный кубик, у вершин которого есть координаты x, y и z, то, чтобы его повернуть на угол L по оси X, нужно сделать такое для каждой вершины:
x'=x;
y':=y*cos(L)+z*sin(L) ;
z':=-y*sin(L)+z*cos(L) ;
Здесь x', y' и z' — новые координаты вершины. Если мы нарисуем кубик с такими новыми координатами каждой вершины, то будет казаться, что мы его немножко повернули.
Для других координат это работает похожим образом — нужно просто знать угол поворота для каждой оси, чтобы правильно посчитать новые координаты.
Чтобы показать, как это работает, давайте сделаем HTML-страницу, которая нарисует нам вращающийся кубик. Мы прокомментировали каждую строку кода, чтобы вы тоже смогли понять, что там происходит. Это настоящее 3D, для которого тоже нужна школьная тригонометрия :-)
Сохраните себе этот код как HTML-документ и откройте в браузере, чтобы посмотреть на вращение кубика. Если не знаете, как это сделать, — вот подробный гайд в помощь:
Спасательный круг для тех, кто начинает писать на JavaScript
<!DOCTYPE html>
<html>
<head>
<title>3D-кубик</title>
</head>
<body>
<!-- пусть кубик вращается по центру страницы-->
<div align="center">
<!-- готовим область для рисования — 200 на 200 пикселей -->
<canvas id="cubeCanvas" width="200" height="200"></canvas>
</div>
<!-- скрипт, который нарисует нам кубик -->
<script type="text/javascript">
// весь скрипт — одна большая функция
(function () {
// переменная, через которую будем работать с областью для рисования
var canvas = document.getElementById("cubeCanvas");
// размер кубика — это минимальное значение высоты или ширины холста
var size = Math.min (canvas.width,canvas.height);
// холст для рисования — двухмерный
var g = canvas.getContext("2d");
// массив с координатами вершин кубика по осям X, Y и Z
var nodes =
[[-1, -1, -1], [-1, -1, 1], [-1, 1, -1], [-1, 1, 1],
[1, -1, -1], [1, -1, 1], [1, 1, -1], [1, 1, 1]];
// а эта переменная отвечает за грани — какие вершины нужно соединить между собой по номерам, чтобы в итоге получился кубик. [0,1] означает, что будет линия между нулевой и первой вершиной, [1,3] — линия между первой и третьей вершиной и так далее
var edges =
[[0, 1], [1, 3], [3, 2], [2, 0], [4, 5], [5, 7], [7, 6],
[6, 4], [0, 4], [1, 5], [2, 6], [3, 7]];
// если нужно сделать кубик больше или меньше — используем функцию масштабирования
function scale (factor0, factor1, factor2) {
// берём каждую грань
nodes.forEach(function (node) {
// и умножаем каждую её координату на размер масштаба
node[0] *= factor0; node[1] *= factor1; node[2] *= factor2;
});
}
// вращаем кубик и получаем новые координаты для каждой вершины, а в функцию передаём углы вращения по осям X и Y
function rotateCuboid (angleX, angleY) {
// запоминаем значения синусов и косинусов для каждого угла вращения
var sinX = Math.sin(angleX);
var cosX = Math.cos(angleX);
var sinY = Math.sin(angleY);
var cosY = Math.cos(angleY);
// для каждой вершины — пересчитываем координаты после поворота
nodes.forEach(function (node) {
// помещаем значения координат вершины в отдельные переменные
var x = node[0]; var y = node[1]; var z = node[2];
// а вот тут происходит сама магия вращения — мы с помощью синусов и косинусов получаем новые координаты для каждой вершины куба
node[0] = x * cosX - z * sinX;
node[2] = z * cosX + x * sinX;
z = node[2];
node[1] = y * cosY - z * sinY;
node[2] = z * cosY + y * sinY;
});
}
// эта функция отрисовывает кубик по текущим координатам вершин
function drawCuboid () {
// берём двухмерный холст, который мы заводили раньше
g.save();
// очищаем его
g.clearRect(0, 0, canvas.width, canvas.height);
// помещаем наш будущий кубик в центр координат
g.translate(canvas.width / 2, canvas.height / 2);
// рисовать будем чёрным
g.strokeStyle = "#000000";
// начинаем рисовать по линиям
g.beginPath();
// берём каждую грань
edges.forEach(function (edge) {
// запоминаем координаты, которые нужно отрисовать
var p1 = nodes[edge[0]];
var p2 = nodes[edge[1]];
// идём на начальную точку
g.moveTo(p1[0], p1[1]);
// и виртуально соединяем её линией со второй точкой и так делаем для каждой грани
g.lineTo(p2[0], p2[1]);
});
// нарисовали — выключаем режим рисования линий
g.closePath();
// отрисовываем полностью сразу весь кубик, который у нас получился с помощью виртуальных линий
g.stroke();
// восстанавливаем холст до начального состояния — убираем с него всё, чтобы подготовиться к рисованию следующего кадра
g.restore();
}
// выбираем масштаб — уменьшим кубик в 4 раза
scale (size/4, size/4, size/4);
// здесь задаём начальные углы наклона кубика по осям X и Y. Попробуйте их поменять и посмотреть, что получится
rotateCuboid (Math.PI / 3, Math.atan(Math.sqrt(10)));
// основной цикл, который отвечает за анимацию вращения
setInterval( function() {
// поворачиваем наш кубик на выбранные углы
rotateCuboid (0.02, 0.02);
// отрисовываем кубик
drawCuboid ();
// интервал между кадрами — 10 миллисекунд
}, 10);
// закончилась главная функция
})();
</script>
</body>
</html>
Что дальше
Попробуйте поменять параметры в скрипте — масштаб, скорость вращения или углы наклона. А ещё можно попробовать сделать каждую грань своего цвета или вообще залить их сплошным цветом, чтобы эффект 3D был сильнее.