- PVSM.RU - https://www.pvsm.ru -

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

Обновив в 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 был использован для перемещения по сцене. И вот, что у нас в результате получилось:

Круто, но ещё не отполировано. Добавим настоящий материал и свет.
Для достижения нужного внешнего вида потребовалась модель затенения ambient occlusion. Кроме того, нужно сделать видимыми рёбра модели без сглаживания. Поэтому материал lambert с плоским затенением подошёл идеально:
var material = new THREE.MeshLambertMaterial({color: 0xffffff, shading: THREE.FlatShading});
Использовалось два источника света. Первый – ambient, был размещён для равномерного освещения. Второй, направленный, создавал все эти крутые тени, которые придают модели полигональный вид.
var makeLights = function() {
var ambientLight = new THREE.AmbientLight(0x1a1a1a);
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 на основании разрешения экрана.
Картинка уже начинает напоминать нашу цель, но есть одна проблема. Чётко видны границы плоскости. Вот яркий пример этого, с камерой, смотрящей вниз.

Сначала мы хотели преобразовать плоскость в сферу, а камеру разместить внутри сферы, в центре. Подход вроде бы решал проблему, но поверхность уже выглядела не так, и собиралась в складки на полюсах.
Решением было добавить экспоненциальную дымку, которая получилась очень круто. После включения альфа-блендинга, дымка плавно переходила в фон заставки и давала крутой эффект.
var renderer = new THREE.WebGLRenderer({antialiasing: true, alpha: true});
(...)
scene.fog = new THREE.FogExp2(0x222228, 0.003);
Вот картинка с усиленным эффектом дымки:

Наконец, сцена начала выглядеть правильно, но управление ещё было неидеальным. 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). Живой пример [1].

Создание трёхмерного заголовка было очень увлекательным занятием, и мы впечатлены мощью three.js. Надеемся, что эта статья поможет вам создавать похожие вещи.
Автор: SLY_G
Источник [2]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/79469
Ссылки в тексте:
[1] Живой пример: http://www.phyramid.com/
[2] Источник: http://habrahabr.ru/post/247811/
Нажмите здесь для печати.