Pull to refresh

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

Reading time5 min
Views38K
Original author: Vlad-Ștefan Harbuz


Обучающий материал с ресурса 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 на основании разрешения экрана.

Дымка и alpha-blending



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



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

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

  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). Живой пример.



Создание трёхмерного заголовка было очень увлекательным занятием, и мы впечатлены мощью three.js. Надеемся, что эта статья поможет вам создавать похожие вещи.
Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
+23
Comments10

Articles

Change theme settings