Привет, Хабр! Представляю Вашему вниманию перевод статьи «JavaScript in 3D: an Introduction to Three.js» автора Брета Кемерона (Bret Cameron).

Введение


Three.js это мощный инструмент. Он помогает использовать 3D дизайн в браузере с приемлемой производительностью. По началу Three.js может быть сложным, особенно если вы никогда не погружались в мир 3D программирования ранее.

У меня есть базовый опыт работы с игровым движком Unity и C#, но все равно многие концепции оказались новыми для меня. Я пришел к выводу, что сейчас совсем мало ресурсов для начинающих разработчиков, поэтому я и решил написать эту статью. В ней мы рассмотрим основные элементы Three.js сцены от полигональных сеток и материалов до геометрии, загрузчиков и много другого.

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

Three.js примеры от Ben Houston, Thomas Diewald and StrykerDoesAnimation.

Векторы и контейнеры – основные строительные блоки


Зачастую выделяют два основных класса в Three.js – Vector3 и Box3. Если вы новичок в 3D, то это может звучать немного абстрактно, но вы встретите их еще очень много раз.

Vector3


Самый основной 3D класс, содержащий три числа: x,y и z. Числа представляют собой координаты точки в 3D пространстве или направление и длину. Например:

const vect = new THREE.Vector3(1, 1, 1);

Большая часть конструкторов в Three.js принимают объекты типа Vector3 в качестве входных аргументов, например Box3

Box3


Этот класс представляет кубойд (3д контейнер). Его главная задача – создать контейнер вокруг других объектов – и все, наименьший кубойд в который поместится 3D объект. Каждый Box3 выравнивается про осям x, y и z.Пример, как создать контейнер, используя Vector3:

const vect = new THREE.Vector3(1, 1, 1);
const box = new THREE.Box3(vect);

Пример как создать контейнер вокруг уже имеющегося 3D объекта:

const box = new THREE.Box3();
box.setFromObject(object);

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

Полигональная сетка


В Three.js основной визуальный элемент на сцене это Mesh. Это 3D объект, составленный из треугольных прямоугольников (полигональная сетка). Он строится при помощи двух обектов:
Geometry – определяет его форму, Material – определяет внешний вид.

Их определения могут показаться немного запутанно (например, класс Geometry может содержать информацию про цвет), но главное отличие именно такое.

Geometry


Основываясь на задаче, которую вы хотите достигнуть, возможно вам захочется определить геометрию внутри Three.js или импортировать другую из файла.

Используя функции как THREE.TorusKnotGeometry, мы можем создать сложные объекты одной строчкой кода. Мы скоро доберемся до этого, но сначала рассмотрим более простые формы.
Самая простая 3D фигура, кубойд или контейнер, может быть задан параметрами width, height и depth.

const geometry = new THREE.BoxGeometry( 20, 20, 20 );


Для сферы минимально нужно значение параметров radius, widthSegments и heightSegments. Две последние переменные указывают сколько треугольников модель должна использовать, чтобы представить сферу: больше количество – более гладко будет выглядеть.

const geometry = new THREE.SphereGeometry( 20, 64, 64 );


Если мы хотим сделать острые или треугольные формы, то можно использовать конус. Его аргументы это сочетание аргументов двух предыдущих фигур. Ниже, мы прописываем radius, widthSegments и radialSegments.

 const geometry = new THREE.ConeBufferGeometry( 5, 20, 32 );


Это лишь часть самых распространенных фигур. Three.js имеет внутри очень много фигур из коробки, которые можно посмотреть в документации. В этой статье, мы расмотрим более интересные формы, построенные на основе метода TorusKnotGeometry.

Почему эти фигуры выглядят именно так как они выглядят? Этот вопрос выходит за рамки этой статьи, но я призываю вас экспериментировать со значениями параметров, потому что вы можете получить очень интересные фигуры одной строчкой кода!

const geometry = new THREE.TorusKnotGeometry(10, 1.3, 500, 6, 6, 20);

https://codepen.io/BretCameron/pen/gOYqORg

Материалы


Геометрия задает форму наших 3D объектов, но не их внешний вид. Чтобы это исправить, нам нужны материалы.

Three.js предлагает из коробки 10 материалов, каждый из них имеет свои плюсы и настраиваемые параметры. Мы рассмотрим лишь часть самых полезных.



MeshNormalMaterial


Полезен при быстром старте и запуске

Мы начнем с MeshNormalMaterial, многоцветный материал, который мы использовали в примерах выше. Он соответствует нормальным векторам в панели RGB, другими словами, используются цвета для определения позиции вектора в 3D пространстве.

const material = new THREE.MeshNormalMaterial();

Заметим, что если вы хотите поменять цвет материала, то можно использовать CSS фильтр и изменять насыщенность:
 filter: hue-rotate(90deg) .


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

MeshBasicMaterial


Полезен при отображении только скелета

Если вы хотите придать фигуре единый цвет, то можно использовать MeshBasicMaterial, только если не применяется освещение. Я нашел полезным применения того материала в отрисовке скелета модели. Для отрисовки только скелета нужно передать { wireframe: true } как параметр.

const material = new THREE.MeshBasicMaterial({ 
  wireframe: true, 
  color: 0xdaa520
});

Главный недостаток этого материала в том, что абсолютно пропадает информация о глубине материала. Каждый материал имеет опцию для отображения только скелета, но только один материал решает проблему отсутствие глубины — MeshDepthMaterial


MeshLambertMaterial


Полезен при высокая производительность, но низкой точности

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

const scene = new THREE.Scene();
const frontSpot = new THREE.SpotLight(0xeeeece);
frontSpot.position.set(1000, 1000, 1000);
scene.add(frontSpot);
const frontSpot2 = new THREE.SpotLight(0xddddce);
frontSpot2.position.set(-500, -500, -500);
scene.add(frontSpot2);

Теперь добавим материал для нашей фигуры. Так как наша фигура похожа на украшение, я предлагаю добавить более золотистый цвет. Другой параметр, emissive, это цвет объекта исходящий от самого объекта (без источника света). Часто это лучше работает как темный цвет – например как темные тени серого, как в примере ниже

const material = new THREE.MeshLambertMaterial({
  color: 0xdaa520,
  emissive: 0x111111,
});


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

MeshPhongMaterial


Полезен при средней производительности и средней точности

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

Сейчас мы можем изменять свойство specular которое влияет на яркость и цвет отражения поверхности. Если свойство emissive обычно темное, то specular лучше работает для светлых цветов. Ниже мы используем светлый серый.

const material = new THREE.MeshPhongMaterial({
  color: 0xdaa520,
  emissive: 0x000000,
  specular: 0xbcbcbc,
});


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

MeshStandartMaterial


Полезен при высокой точности, но низкой производительности

Это самый точный материал из всех, хотя его использование повлечет за собой издержки использования большей мощности. MeshStandartMaterial используется с дополнительными параметрами metalness и roughness, каждый из которых принимает значение между 0 и 1.

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

Roughness добавляет дополнительный слой для кастомизации. Можно представить его как как противоположность глянцевости: 0 – очень глянцевый, 1 – очень матовый.

const material = new THREE.MeshStandardMaterial({
  color: 0xfcc742,
  emissive: 0x111111,
  specular: 0xffffff,
  metalness: 1,
  roughness: 0.55,
});


Это самый реалистичный материал из всех представленных в Three.js, но и самый ресурсозатратный

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

Загрузчики


Как мы уже обсудили выше, можно вручную определять геометрию и полигональные сетки. На практике люди чаще загружают свои геометрии из файлов. К счастью, Three.js имеет немного поддерживаемых загрузчиков, поддерживающих многие 3D форматы.

Основной ObjectLoader загружает JSON файл, используя JSON Object/Scene format. Большинство загрузчиков нужно импортировать вручную. Вы можете найти полный список поддерживаемых загрузчиков тут и импортировать их. Ниже небольшой список того что можно импортровать.

// GLTF
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
// OBJ
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js';
// STL
import { STLLoader } from 'three/examples/jsm/loaders/STLLoader.js';
// FBX
import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader.js';
// 3MF
import { 3MFLoader } from 'three/examples/jsm/loaders/3MFLoader.js';

Рекомендуемый формат для онлайн просмотра – GLTF, по причине того, что формат “направлен на доставку ассетов в рантайме, компактный для передачи и быстрый для загрузки”.

Кончено, может быть очень много причин предпочитать определенный тип файлов (например, если качество в приоритете или нужно точность для 3D печати). Лучшая же производительность онлайн будет, при импорте GLTF.

import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
import model from '../models/sample.gltf';
let loader = new GLTFLoader();
loader.load(model, function (geometry) {
  // if the model is loaded successfully, add it to your scene here
}, undefined, function (err) {
  console.error(err);
});

Соединяем все вместе


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

То, как вы организуете свой код решаете только вы. В более простых примерах, таких как в этой статье, есть смысл писать весь код в одном месте. Но на практике, полезно разделять отдельные элементы для возможности расширения кодовой базы и ее управления.

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

// Import dependencies
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';

// Создаем сцену
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x282c34);

// Определяем камеру, устанавливаем ее на заполнения окна браузера
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 10000);
camera.position.z = 5;

// Определеяем "рисовальщика" и устанавливаем на окно браузера
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);

// Берем элемент DOM и прикрепляем renderer.domElement к нему
document.getElementById('threejs').appendChild(renderer.domElement);

// Добавляем управление, устанавливаем как цель тот же DOM элемент
let controls = new OrbitControls(camera, document.getElementById('threejs'));
controls.target.set(0, 0, 0);
controls.rotateSpeed = 0.5;
controls.update();

// Определяем (или импортируем) геометрию объекта
const geometry = new THREE.TorusKnotGeometry(10, 1.3, 500, 6, 6, 20);

// Определяем материал объекта
const material = new THREE.MeshStandardMaterial({
  color: 0xfcc742,
  emissive: 0x111111,
  specular: 0xffffff,
  metalness: 1,
  roughness: 0.55,
});

// Создаем полигональную сеть, масштабируем ее и добавляем на сцену
const mesh = new THREE.Mesh(geometry, material);

mesh.scale.x = 0.1;
mesh.scale.y = 0.1;
mesh.scale.z = 0.1;

scene.add(mesh);

// Добавляем освещение, устанавливаем его и добавляем на сцену
const frontSpot = new THREE.SpotLight(0xeeeece);
const frontSpot2 = new THREE.SpotLight(0xddddce);

frontSpot.position.set(1000, 1000, 1000);
frontSpot2.position.set(-500, -500, -500);

scene.add(frontSpot);
scene.add(frontSpot2);

// Создаем функцию анимации, которая позволит вам отрисовать Вашу сцену и определить любое движение
const animate = function () {
  requestAnimationFrame(animate);

  mesh.rotation.x += 0.005;
  mesh.rotation.y += 0.005;
  mesh.rotation.z += 0.005;

  renderer.render(scene, camera);
};

// Зовем функцию анимации
animate();

Нужно ли использовать фреймворк?


Наконец то, пришло время обсудить стоит ли использовать Three.js со своим любимым фреймворком? На текущий момент, есть хороший пакет react-three-fiber для React. Для пользователей React, есть очевидные преимущества пользования пакетом как этот – вы сохраняете структуру работы с компонентами, которая позволяет переиспользовать код.

Для новичков я советую начать с обычного Vanila JS, потому что большинство онлайн материалов, написанных про Three.js относятся к Three.js на Vanila JS. Основываясь на моем опыте изучения, это может быть запутано и трудно изучать через пакет – например, вам придется транслировать Three.js объекты и методы на компоненты и пропсы. (как только вы освоите Three.js можете использовать любой пакет).

Как добавить Three.js в фреймворк


Three.js дает HTML объект (чаще всего называется он renderer.domElement) который может быть добавлен к любому HTML объекту в вашем приложении. Например, если у вас есть div с id=”threejs” вы можете просто включит следующий код в ваш Three.js код:

document.getElementById('threejs').appendChild(renderer.domElement);

Некоторые фреймворки имет предпочтительные пути обращения к узлам DOM дерева. Например, ref в React, $ref в Vue или ngRef в Angular и это выглядит как массивный плюс на фоне прямого обращения к элементам DOM. Как пример, давайте рассмотрим быструю реализацию для React.

Стратегия для React


Если вы используете React, то существует один путь внедрения Three.js файлов в один из ваших компонентов. В файле ThreeEntryPoint.js мы напишем следующий код:

export default function ThreeEntryPoint(sceneRef) {
  let renderer = new THREE.WebGLRenderer(); 
  // ...
  sceneRef.appendChild(renderer.domElement);
}

Мы экспортируем это как функцию, которая принимает один аргумент: ссылку на элемент в нашем компоненте. Теперь мы можем создать наш компонент

import React, { Component } from 'react';
import ThreeEntryPoint from './threejs/ThreeEntryPoint';
export default class ThreeContainer extends Component {
componentDidMount() {
    ThreeEntryPoint(this.scene);
  }
render() {
    return (
      <>
        <div ref={element => this.scene = element} />
      </>
    );
  }
}

Импортированная функция ThreeEntryPoint должна вызываться в методе componentDidMount и передавать новый div как аргумент, используя ссылки
В качестве примера такого подхода в действии, можно склонировать репозиторий и попробовать самостоятельно: https://github.com/BretCameron/three-js-sample.

Заключение


Есть еще очень много, что я могу расскзать про Three.js, но я надеюсь что эта статья дала вам достатчно информации для того, чтобы начать использовать эту мощную технологию. Когда я только начал изучать Three.js я не мог найти ни одного ресурса как эта статья, поэтому я надеюсь я помог сделать эту технологию более доступной для начинающих.

Комментарии (10)


  1. salkat
    28.11.2019 21:19
    -1

    Офигеть. Спасибо огромное. Как раз начинаю её использовать и информации не хватает


  1. addewyd
    28.11.2019 21:41

    Треугольный прямоугольник — это даже круче, чем кубойд.


  1. alsii
    28.11.2019 22:43
    +1

    Дочитайл до кубойда, дайльше не смойг. Пройстите...


    1. diogen4212
      29.11.2019 05:30

      АндроЙд всех приЙучил, спасиЙбо Гуглу(


      1. Metotron0
        30.11.2019 01:30
        +1

        Разве Гугл где-то настайвал на таком напйсании? Это не ему спасйбо, а гуманойдам с астеройдов, которые завезлй такую моду.


      1. bopoh13
        01.12.2019 16:15

        Давайте сначала разберёмся со словом "пойнт", ведь придирки к написанию брендов на английском важнее содержания (своих слов в великий и могучий не завезли).
        ЗЫ: Понимаю, что муки поиска буквы "Ё" в словах, где её заменили, беспокоят меня одного, но мучится будут все. Возмущайтесь, или курите regex.


        1. Metotron0
          01.12.2019 23:24

          Дичайше извиняюсь, но «мучиться».


  1. customizer
    29.11.2019 08:34

    zanudaModeOn

    Самый основной 3D класс, содержащий три числа: x,y и z.

    Не смотрится, кроме трех чисел у вас также выделен курсивом союз «и». В этом виде он похож на латинскую букву «u». Конечно это мелочь, но… некрасиво.

    Этот класс представляет кубойд (3д контейнер).

    Вообще-то в русском языке есть слово — параллелепипед. Мне кажется, что все люди, учившиеся в школе, должны его знать. Конечно, оно «длиннее», чем английское обозначение «Box3», но по смыслу правильное. Можно сделать так, при первом появлении Box3 в тексте, назвать его параллелепипедом и дать пояснение, что далее в статье будем называть его Box3. И будет все в пределах разумного. А «кубойд» — что-то совсем дикое, может быть вы хотели написать «кубоид», т.е. «кубообразная» фигура. Но тогда будет неверно по смыслу, Box3 не обязательно куб, т.е. параллелепипед с равными сторонами.

    Это 3D объект, составленный из треугольных прямоугольников (полигональная сетка).

    Треугольные прямоугольники — это конечно круто, но надеюсь это просто ошибка. Сетка (mesh) представляет собой сетку с треугольными ячейками.

    Для сферы минимально нужно значение параметров radius, widthSegments и heightSegments.

    На мой взгляд, давая английские названия свойств следует давать и их перевод на русском, т.е. radius (радиус), widthSegments (сегменты по ширине) и heightSegments(сегменты по высоте). Ведь это же перевод и в нем все слова должны быть понятны всем, даже начинающим. Автор оригинальной статьи в первых двух абзацах прямо говорит, что статья предназначена для начинающих разработчиков. А вы, с одной стороны упрощаете понимание — переводя эту статью на русский язык, а с другой стороны усложняете — не приводя перевод терминов и обозначений. А ведь согласитесь, что проще понять свойство предмета зная его название. И получается, что англоязычный читатель понимает, что это за свойство, а для русскоязычного — это просто набор букв, который порой очень трудно произнести.
    И далее из этого же ряда:
    emissive — светимость
    specular — отраженный свет, отблеск
    metalness — «металличность», как сильно материал походит на металл
    roughness — шероховатость.
    Рекомендуемый формат для онлайн просмотра – GLTF, по причине того, что формат “направлен на доставку ассетов в рантайме, ...

    Я так понимаю, что ассет — это asset — ресурс, средство, имущество, а рантайм — это runtime т.е. время выполнения. Извините, а что должен понять из этого предложения начинающий разработчик с недостаточным знанием английского языка? Вроде того, что он должен сам разбираться в этом и знание, вернее незнание английского языка это его собственная проблема? Тогда зачем вы сделали этот «перевод»? Поймите, это не в укор, а «понимания для».
    А вот это:
    … например, вам придется транслировать Three.js объекты и методы на компоненты и пропсы...

    Что такое пропсы? Возможно, это от англ. properties — свойства, но уж как-то «смесь французского английского с нижегородским», корень от английского слова, а окончание по склонению от множественного числа — по русски.
    zanudaModeOff

    Всякие совсем уж мелкие ошибки вроде
    Их определения могут показаться немного запутанно (например, класс Geometry может содержать информацию про цвет), но главное отличие именно такое.

    или
    Есть еще очень много, что я могу расскзать про Three.js, ...

    рассматривать не хочется, думаю это просто плохо вычитанный текст.
    Вообще-то справка по Three.js на русском языке существует — вот здесь.
    А это ссылка на проект перевода на Гитхабе.
    Правда я давно его не обновлял, понял что ошибся с «деревом» справки (слишком объёмными получились некоторые статьи, более 500 килобайт), а значит и система ссылок стала неверна и поэтому все переделываю.


    1. alsii
      29.11.2019 14:04

      Справедливости ради, "кубоид" — это синоним прямоугольного параллелепипеда. Редко, но употребляемый.


      "Треугольные прямоугольники" — в оригинале "triangular polygons", что-то вроде "триангулированные многоугольники" (непонятно), или "многоугольники, составленные из треугольников" (длинно).


      1. diogen4212
        29.11.2019 14:21

        там ещё уши есть))