[Из песочницы] JavaScript в 3D: введение в Three.js28.11.2019 21:33
Привет, Хабр! Представляю Вашему вниманию перевод статьи «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 код:
Некоторые фреймворки имет предпочтительные пути обращения к узлам 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 (
<>
this.scene = element} />
>
);
}
}
Импортированная функция ThreeEntryPoint должна вызываться в методе componentDidMount и передавать новый div как аргумент, используя ссылки В качестве примера такого подхода в действии, можно склонировать репозиторий и попробовать самостоятельно: https://github.com/BretCameron/three-js-sample.
Заключение
Есть еще очень много, что я могу расскзать про Three.js, но я надеюсь что эта статья дала вам достатчно информации для того, чтобы начать использовать эту мощную технологию. Когда я только начал изучать Three.js я не мог найти ни одного ресурса как эта статья, поэтому я надеюсь я помог сделать эту технологию более доступной для начинающих.