Хабрахабр

[Из песочницы] JavaScript в 3D: введение в Three.js

Привет, Хабр! Представляю Вашему вниманию перевод статьи «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 или импортировать другую из файла.

TorusKnotGeometry, мы можем создать сложные объекты одной строчкой кода. Используя функции как THREE. Мы скоро доберемся до этого, но сначала рассмотрим более простые формы.
Самая простая 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

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

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

const material = new THREE.MeshNormalMaterial();

Заметим, что если вы хотите поменять цвет материала, то можно использовать CSS фильтр и изменять насыщенность:

filter: hue-rotate(90deg) .

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

MeshBasicMaterial

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

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

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.

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

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

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

MeshStandartMaterial

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

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

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

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

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, по причине того, что формат “направлен на доставку ассетов в рантайме, компактный для передачи и быстрый для загрузки”.

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

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, есть очевидные преимущества пользования пакетом как этот – вы сохраняете структуру работы с компонентами, которая позволяет переиспользовать код.

Основываясь на моем опыте изучения, это может быть запутано и трудно изучать через пакет – например, вам придется транслировать Three.js объекты и методы на компоненты и пропсы. Для новичков я советую начать с обычного Vanila JS, потому что большинство онлайн материалов, написанных про Three.js относятся к Three.js на Vanila 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 я не мог найти ни одного ресурса как эта статья, поэтому я надеюсь я помог сделать эту технологию более доступной для начинающих.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть