[Перевод] Трёхмерный фон для сайта в реальном времени на JavaScript при помощи three.js

1889d437b4f040e28ca36af6bc89b8d2.pngОбучающий материал с ресурса Phyramid, у которых именно такая шапка сайта.

16160cb34a7342ae84ffa48a39b8a298.png

Обновив в 2014 свой сайт, мы сделали трёхмерный фон в шапке, состоящий из геометрических фигур в 3D Max. Но потом мы подумали, что было бы гораздо круче генерить его в реальном времени на JS. Сказано — сделано, и при помощи замечательного фреймворка three.js мы сделали простенькую сценку. И вот, как это было.

Замечание по стилю кода: мы сначала хотели использовать только функциональный стиль, но из-за особенностей веба и работы алгоритма переключились на ООП.

Создаём поверхностьПервым шагом было создание основной части сцены. Для этого мы создали плоскость с сегментами 100×100, и потом сместили вершины случайным образом. Важный момент — необходимо задать geometry.dynamic = true и geometry.normalsNeedUpdate = true, чтобы three.js знал, что вершины поменяются и что ему надо будет пересчитать освещение. var makePlaneGeometry = function (width, height, widthSegments, heightSegments) { var geometry = new THREE.PlaneGeometry (width, height, widthSegments, heightSegments); var X_OFFSET_DAMPEN = 0.5; var Y_OFFSET_DAMPEN = 0.1; var Z_OFFSET_DAMPEN = 0.1; var randSign = function () { return (Math.random () > 0.5) ? 1: -1; }; for (var vertIndex = 0; vertIndex < geometry.vertices.length; vertIndex++) { geometry.vertices[vertIndex].x += Math.random() / X_OFFSET_DAMPEN * randSign(); geometry.vertices[vertIndex].y += Math.random() / Y_OFFSET_DAMPEN * randSign(); geometry.vertices[vertIndex].z += Math.random() / Z_OFFSET_DAMPEN * randSign(); } geometry.dynamic = true; geometry.computeFaceNormals(); geometry.computeVertexNormals(); geometry.normalsNeedUpdate = true; return geometry; }; var makePlane = function(geometry) { var material = new THREE.MeshBasicMaterial({color: 0x00576b, wireframe: true}); var plane = new THREE.Mesh(geometry, material); return plane; }; var init = function(container, viewWidth, viewHeight) { var scene = makeScene(); // (...) var plane = makePlane(makePlaneGeometry(400, 400, 100, 100)); scene.add(plane); // (...) }; Играемся с каркасом Простой материал для каркаса помог визуализации модели: var material = new THREE.MeshBasicMaterial({color: 0x00576b, wireframe: true}); TrackballControls.js был использован для перемещения по сцене. И вот, что у нас в результате получилось:

610e8044cda24b839569886a43fe5455.png

Круто, но ещё не отполировано. Добавим настоящий материал и свет.

Добавление материала и света Для достижения нужного внешнего вида потребовалась модель затенения ambient occlusion. Кроме того, нужно сделать видимыми рёбра модели без сглаживания. Поэтому материал lambert с плоским затенением подошёл идеально: var material = new THREE.MeshLambertMaterial ({color: 0xffffff, shading: THREE.FlatShading}); Использовалось два источника света. Первый — ambient, был размещён для равномерного освещения. Второй, направленный, создавал все эти крутые тени, которые придают модели полигональный вид.

var makeLights = function () { var ambientLight = new THREE.AmbientLight (0×1a1a1a); this.scene.add (ambientLight); var dirLight = new THREE.DirectionalLight (0xdfe8ef, 0.09); dirLight.position.set (5, 2, 1); this.scene.add (dirLight); }; Размещение камеры Мы хотели разместить камеру, смотрящую на плоскость примерно с угла в 45 градусов, что довольно просто. Поигравшись с камерой, мы выбрали угол в 75 градусов, который даёт эффект наблюдения «с вершины горы». var camera = new THREE.PerspectiveCamera (fov, aspectRatio, 0.1, 1000); camera.up = new THREE.Vector3(0, 1, 0); camera.rotation.x = 75 * Math.PI / 180; camera.position.z = zPos; Поле зрения доставило проблем, потому что на широких холстах сцена смотрелась странно, примерно как при настройке FOV в Quake на 180 градусов. Мы написали код для грубого подсчёта FOV на основании разрешения экрана.

Дымка и alpha-blending Картинка уже начинает напоминать нашу цель, но есть одна проблема. Чётко видны границы плоскости. Вот яркий пример этого, с камерой, смотрящей вниз.

2884924b684640e2876bb15d062d8051.png

Сначала мы хотели преобразовать плоскость в сферу, а камеру разместить внутри сферы, в центре. Подход вроде бы решал проблему, но поверхность уже выглядела не так, и собиралась в складки на полюсах.

Решением было добавить экспоненциальную дымку, которая получилась очень круто. После включения альфа-блендинга, дымка плавно переходила в фон заставки и давала крутой эффект.

var renderer = new THREE.WebGLRenderer ({antialiasing: true, alpha: true}); (…) scene.fog = new THREE.FogExp2(0×222228, 0.003); Вот картинка с усиленным эффектом дымки:

c96e3c48c64f410dbe990a35677d5471.png

Интерактивность (часть первая — мышь) Наконец, сцена начала выглядеть правильно, но управление ещё было неидеальным. TrackballControls позволяет свободно двигаться по сцене, но нам надо было разрешить только повороты относительно оси Z. Мы решили написать управление с нуля, основываясь на демке с вращающимся кубом от three.jsКогда пользователь двигает мышью, необходимо выключать авторотацию, и запоминать дистанцию, на которую была подвинута мышь, чтобы добавить её к повороту вокруг Z в следующем кадре.

var registerMouseMove = function (event) { this.autorotation = false; var mouseXOnMouseMove = event.clientX — (this.width / 2); var MOUSE_MOVE_DAMPENING = 0.0075; this.targetRotation = this.targetRotationOnMouseDown + (mouseXOnMouseMove — this.mouseXOnMouseDown) * MOUSE_MOVE_DAMPENING; }; Также необходим обработчик нажатий, чтобы перемещения были зарегистрированы только, если пользователь зажал кнопку мыши (и запомнить первоначальное расположение мыши для подсчёта дистанции).

var registerMouseDown = function (event) { startMouseMovementDetection (); this.mouseXOnMouseDown = event.clientX — (this.width / 2); this.targetRotationOnMouseDown = this.targetRotation; }; Всё, что остаётся — сделать сам поворот.

if (this.autorotation) { this.object.rotation.z += OBJECT_AUTOROTATION_AMOUNT; } else { this.object.rotation.z -= (this.targetRotation + this.object.rotation.z) * TARGET_ROTATION_DAMPENING; } Мы добавили ещё ограничение на движение — если объект двигают слишком медленно, мы считаем, что это шум или остаточные явления с последнего протаскивания, поэтому мы возвращаем метод поворота в состояние авторотации.

if (Math.abs (this.targetRotation + this.object.rotation.z) < OBJECT_ROTATION_THRESHOLD) { this.autorotation = true; } Интерактивность (часть вторая – касания)

Почти закончили! Ешё нам нужно сделать управление при помощи касаний. Работает примерно так же, как управление мышью.

var registerTouchDown = function (event) { if (event.touches.length === 1) { this.mouseXOnMouseDown = event.touches[0].pageX — (this.width / 2); this.mouseYOnMouseDown = event.touches[0].pageY — (this.height / 2); this.targetRotationOnMouseDown = this.targetRotation; } } Но есть проблема. На устройствах с сенсорным экраном жест, отвечающий за перемещение сцены, также отвечает и за прокрутку страницы. Это плохо влияло на управляемость, потому что практически мы отключали прокрутку.

Из-за этого нам пришлось проверять направление протаскивания. Если оно по большей части горизонтальное, тогда мы вращаем плоскость. Если преимущественно вертикальное, мы ничего не делали и разрешали случаться событиям по умолчанию.

function registerTouchMove (event) { if (event.touches.length === 1) { var MOUSE_MOVE_DAMPENING = 0.01; this.autorotation = false; var mouseXOnMouseMove = event.touches[0].pageX — (this.width / 2); var mouseYOnMouseMove = event.touches[0].pageY — (this.height / 2); var xDiff = mouseXOnMouseMove — this.mouseXOnMouseDown; var yDiff = mouseYOnMouseMove — this.mouseYOnMouseDown; if (Math.abs (xDiff) > Math.abs (yDiff)) { event.preventDefault (); this.targetRotation = this.targetRotationOnMouseDown + xDiff * MOUSE_MOVE_DAMPENING; } } } Обновление разрешения при изменении размера Последнее по очереди, но не по значимости — возможность динамически обновлять всю картинку при изменении размера окна браузера. var updateDimensions = function () { this.width = this.container.offsetWidth; this.height = this.container.offsetHeight; var aspectRatio = this.width / this.height; var fov = fovForAspectRatio (aspectRatio); var zPos = cameraZPositionForFov (fov); this.camera.aspect = aspectRatio; this.camera.fov = fov; this.camera.position.z = zPos; this.camera.updateProjectionMatrix (); this.renderer.setSize (this.width, this.height); }; Результаты Готово! Вот, как оно выходит при просмотре на весь экран (у нас полноэкранный просмотр выдаётся на странице 404). Живой пример.31a604ed6206478b8f2a7d5928d27e91.png

Создание трёхмерного заголовка было очень увлекательным занятием, и мы впечатлены мощью three.js. Надеемся, что эта статья поможет вам создавать похожие вещи.

© Habrahabr.ru