Основы

Это первая статья в серии статей о three.js. Three.js это 3D-библиотека, которая максимально упрощает создание 3D-контента на веб-странице.

Three.js часто путают с WebGL, поскольку чаще всего, но не всегда, three.js использует WebGL для рисования 3D. WebGL - это очень низкоуровневое api, рисующее только точки, линии и треугольники. Чтобы сделать что-нибудь полезное с WebGL, как правило, требуется немало кода, и именно здесь приходит Three.js. Он обрабатывает такие вещи, как сцены, источники света, тени, материалы, текстуры, 3D-математику, все, что вам нужно было бы написать самостоятельно, если бы вы использовали WebGL напрямую.

В этих руководствах предполагается, что вы уже знаете JavaScript, и по большей части они будут использовать стандарт ES6+. Смотрите здесь краткий список вещей, которые вы, как ожидается, уже знаете. Большинство браузеров, которые поддерживают three.js, обновляются автоматически, поэтому большинство пользователей должны иметь возможность запускать этот код. Если вы хотите, чтобы этот код запускался в действительно старых браузерах, посмотрите на транспайлер, такой как Babel. Конечно, пользователи, использующие действительно старые браузеры, вероятно, имеют машины, которые не могут запускать three.js.

При изучении большинства языков программирования первое, что делают люди, это заставляют компьютер напечатать "Hello World!". Для 3D одна из самых распространенных задач - создать 3D-куб, так что давайте начнем с "Hello Cube!"

Первое, что нам нужно, это тэг <canvas>:

<body>
  <canvas id="c"></canvas>
</body>

Three.js будет рисовать на этом холсте, так что нам нужно найти его и передать three.js.

<script type="module">
import * as THREE from '../../build/three.module.js';

function main() {
  const canvas = document.querySelector('#c');
  const renderer = new THREE.WebGLRenderer({canvas});
  ...
</script>

Обратите внимание, что здесь есть некоторые не явные детали. Если вы не передадите холст в three.js, библиотека создаст его за вас, но затем нужно будет добавить его в DOM. Место добавления может меняться в зависимости от вашего варианта использования, и вам придется изменить свой код, поэтому я считаю, что передача canvas в three.js выглядит немного более гибкой. Я могу поместить холст где угодно, и код найдет его там, как если бы у меня был код для вставки холста в документ, и мне, вероятно, пришлось бы изменить этот код, если бы изменился мой вариант использования.

Когда канвас найден, мы создаем WebGLRenderer. Renderer - это то, что отвечает за фактическое получение всех предоставленных вами данных и их отрисовку на холст. В прошлом были другие рендеры, такие как CSSRenderer, CanvasRenderer, а в будущем могут быть WebGL2Renderer или WebGPURenderer. На данный момент есть WebGLRenderer, который использует WebGL для рисования 3D на холсте.

Далее нам нужна камера.

const fov = 75;
const aspect = 2;  // значение для canvas по умолчанию
const near = 0.1;
const far = 5;
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);

fov сокращение от field of view, поле зрения. В этом случае 75 градусов в вертикальном измерении. Обратите внимание, что большинство углов в Three.js указаны в радианах, но по какой-то причине перспективная камера принимает градусы.

aspect это соотношение сторон холста (англ. aspect ratio). Мы рассмотрим детали в другой статье, но по умолчанию холст имеет размер 300x150 пикселей, значит соотношение сторон 300/150 или 2.

near и far представляют пространство перед камерой, которое будет отображаться. Все, что находится до или после этого диапазона, будет обрезано (не нарисовано).

Эти 4 параметра определяют усеченную пирамиду "frustum". Frustum это название 3D фигуры, напоминающей пирамиду с отсеченной верхушкой. Другими словами, думайте о слове "frustum" как о трехмерной фигуре, такой как сфера, куб и призма.

Высота ближней и дальней плоскостей определяется полем зрения (field of view). Ширина обеих плоскостей определяется полем зрения и соотношением сторон (aspect).

Все, что находится внутри определенного усеченного контура, будет нарисовано. Снаружи ничего не будет.

По умолчанию камера смотрит вниз по оси -Z и вверх по оси +Y. Мы поместим наш куб в начало координат (origin), поэтому нам нужно немного отодвинуть камеру назад, чтобы что-то увидеть.

camera.position.z = 2;

Вот как мы её направили.

На диаграмме выше мы видим, что наша камера находится в z = 2. И смотрит вниз по оси -Z. Усеченная пирамида начинается с 0.1 единицы спереди камеры и до 5 единиц перед камерой. Поскольку на этой диаграмме мы смотрим вниз, поле зрения (fov) зависит от отношения сторон (aspect). Так как ширина холста в 2 раза больше высоты, при просмотре поле обзора будет намного шире, чем указанные нами 75 градусов, которые являются вертикальным полем зрения.

Далее создадим Scene. Scene в three.js корень формы графа сцены. Все, что вы хотите нарисовать необходимо добавить на сцену. Мы рассмотрим подробнее, как работают сцены, в следующей статье.

const scene = new THREE.Scene();

Далее мы создаем BoxGeometry который содержит данные для прямоугольного параллелепипеда. Почти все, что мы хотим отобразить в Three.js, нуждается в геометрии, которая определяет вершины нашего трехмерного объекта.

const boxWidth = 1;
const boxHeight = 1;
const boxDepth = 1;
const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);

Затем мы создаем основной материал и устанавливаем его цвет. Цвета могут быть определены с использованием 6-значных шестнадцатеричных значений цвета, как в CSS.

const material = new THREE.MeshBasicMaterial({color: 0x44aa88});

Затем мы создаем полигональную сетку Mesh. Mesh в three.js представляет комбинацию формы объекта Geometry и Material (как нарисовать объект, блестящий или плоский, какой цвет, какую текстуру(ры) применить и т.д.) а также положение, ориентацию, и масштаб этого объекта в сцене.

const cube = new THREE.Mesh(geometry, material);

И, наконец, мы добавляем Mesh на сцену

scene.add(cube);

Затем мы можем отрендерить сцену, вызвав функцию render рендерера передав ей сцену и камеру.

renderer.render(scene, camera);

Вот рабочий пример

Трудно сказать, что это 3D-куб, так как мы видим его непосредственно по оси -Z, а сам куб выровнен по этой оси, поэтому мы видим только одну грань.

Давайте оживим его, и, надеюсь, это прояснит, что он рисуется в 3D. Для его анимации мы будем отрисовывать внутри цикла отрисовки, используя requestAnimationFrame.

Вот наш цикл

function render(time) {
  time *= 0.001;  // конвертировать время в секунды

  cube.rotation.x = time;
  cube.rotation.y = time;

  renderer.render(scene, camera);

  requestAnimationFrame(render);
}
requestAnimationFrame(render);

requestAnimationFrame это запрос к браузеру, что вы хотите что-то анимировать. Вы передаете ему функцию для вызова. В нашем случае эта функция render. Браузер вызовет вашу функцию, и если вы обновите что-либо, связанное с отображением страницы, браузер выполнит перерисовку страницы. В нашем случае мы вызываем renderer.render, которая нарисует нашу сцену.

requestAnimationFrame передает время с момента загрузки страницы в нашу функцию. Это время приходит в миллисекундах. Я считаю, что работать с секундами намного проще, поэтому здесь мы конвертируем время в секунды.

Затем мы устанавливаем вращение куба по X и Y на текущее время. Эти повороты в радианах. В круге 2 пи радиана, поэтому наш куб должен повернуться вокруг каждой оси примерно за 6.28 секунд.

Затем мы отрисовываем сцену и запрашиваем еще один кадр анимации, чтобы продолжить наш цикл.

Вне цикла мы вызываем requestAnimationFrame один раз, чтобы запустить цикл.

Это немного лучше, но все еще трудно увидеть 3d. Что может помочь, так это добавить немного освещения, поэтому давайте добавим источник света. В Three.js есть много разных источников света, о которых мы поговорим в следующей статье. А пока давайте создадим направленный свет.

{
  const color = 0xFFFFFF;
  const intensity = 1;
  const light = new THREE.DirectionalLight(color, intensity);
  light.position.set(-1, 2, 4);
  scene.add(light);
}

Направленные источники имеет положение и цель. Оба по умолчанию равны 0, 0, 0. В нашем случае мы устанавливаем положение источника света на -1, 2, 4 чтобы оно было немного слева, сверху и позади нашей камеры. Цель по-прежнему 0, 0, 0, поэтому они будут светить в направлении начала координат.

Нам также нужно изменить материал. MeshBasicMaterial не воспреимчив к свету. Давайте изменим его на MeshPhongMaterial, который отражает свет.

-const material = new THREE.MeshBasicMaterial({color: 0x44aa88});  // greenish blue
+const material = new THREE.MeshPhongMaterial({color: 0x44aa88});  // greenish blue

И вот оно работает.

Теперь должно быть довольно четко видно 3D.

Просто для удовольствия добавим еще 2 кубика.

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

Сначала мы сделаем функцию, которая создает новый материал с указанным цветом. Затем создает mesh, используя указанную геометрию, добавляет ее к сцене и устанавливает ей позицию X.

function makeInstance(geometry, color, x) {
  const material = new THREE.MeshPhongMaterial({color});

  const cube = new THREE.Mesh(geometry, material);
  scene.add(cube);

  cube.position.x = x;

  return cube;
}

Затем мы будем вызывать его 3 раза с 3 разными цветами и позициями X, сохраняя экземпляры Mesh в массив.

const cubes = [
  makeInstance(geometry, 0x44aa88,  0),
  makeInstance(geometry, 0x8844aa, -2),
  makeInstance(geometry, 0xaa8844,  2),
];

Наконец, мы закрутим все 3 куба в нашей функции отрисовки. Мы рассчитываем немного разные коэффициенты вращения для каждого.

function render(time) {
  time *= 0.001;  // конвертировать время в секунды

  cubes.forEach((cube, ndx) => {
    const speed = 1 + ndx * .1;
    const rot = time * speed;
    cube.rotation.x = rot;
    cube.rotation.y = rot;
  });

  ...

и вот оно.

Если вы сравните его с диаграммой сверху вниз, вы увидите, что она соответствует нашим ожиданиям. С кубами в X = -2 и X = +2 они частично находятся вне нашей усеченной пирамиды. Они также несколько искривлены, так как поле зрения на холсте очень велико.

Я надеюсь, что это короткое вступление поможет вам начать изучение. Далее мы рассмотрим, как сделать отзывчивый дизайн, чтобы код можно было применять к различным ситуациям.