Статьи

Внедрение виртуальной реальности в Интернет с помощью Google Cardboard и Three.js

Виртуальная реальность приближается. Вы знаете, что как разработчик — вы хотите участвовать. Oculus Rift, Gear VR, HTC Vive и многие другие делают волну, но многие разработчики не осознают, насколько велик потенциал в самом простом из них — Google Cardboard.

Я делал серию статей, связанных с IoT, здесь, в SitePoint, и изучал возможность подключения веб-API практически ко всему. До сих пор я рассматривал веб-API и игровой движок Unity , Jawbone Up API и Node.js, а также отображал данные веб-API на ЖК-дисплее Arduino через Node.js. В этой статье я хотел представить веб-API в мире виртуальной реальности таким образом, чтобы разработчики JavaScript могли легко приступить к работе. Google Cardboard и Three.js — идеальный первый шаг в этом направлении. Это также означает, что вашим пользователям не нужно устанавливать что-то конкретное, и вам не нужно тратить сотни долларов на гарнитуру VR. Просто возьмите совместимый смартфон, вставьте его в картонную гарнитуру и все готово к работе.

Фото предоставлено Google

Фото предоставлено Google

Где я могу получить один?

Существует множество различных производителей, которые производят гарнитуры, совместимые с Google Cardboard. У Google есть большой список на их странице Get Cardboard . Тот, который меня больше всего волнует, выйдет позже в этом году — обновленный View-Master® (эта замечательная игрушечная игрушка!). Новый View-Master® будет совместим с Google Cardboard!

Моя нынешняя гарнитура Google Cardboard принадлежит команде Dodocase . Эти ребята были просто великолепны. Их служба поддержки довольно дружелюбная и быстро реагирует. Если вы любитель творчества, вы можете найти все детали и сделать гарнитуру самостоятельно, следуя инструкциям, также доступным на странице « Получить картон» .

Что мы собираемся построить

Мы собираемся построить относительно простую (но все еще довольно симпатичную) сцену светящихся шаров света (мы будем называть их «частицами»), плавающих вокруг нашей головы. Эти частицы будут двигаться и менять цвет в зависимости от погоды в разных точках земного шара.

Здесь доступна рабочая демоверсия , весь исходный код и т. Д. Не минимизирован и готов для просмотра и использования по вашему желанию. Весь исходный код также доступен на GitHub .

Начинаем нашу сцену Three.js

Вся наша демонстрация будет работать на Three.js , фантастической 3D-библиотеке JavaScript, которая делает рендеринг 3D в браузере намного проще для понимания. Если вы не использовали его раньше, есть некоторая кривая обучения, но я постараюсь объяснить большинство вещей по ходу дела.

Мы начнем с добавления Three.js и нескольких ключевых модулей, которые также поставляются с Three.js. Эти модули включают функциональность, которую мы хотим.

<script src="./js/three.min.js"></script> <script src="./js/StereoEffect.js"></script> <script src="./js/DeviceOrientationControls.js"></script> <script src="./js/OrbitControls.js"></script> <script src="./js/helvetiker_regular.typeface.js"></script> 
  • three.min.js — Основная минимизированная библиотека для Three.js.
  • StereoEffect.js — позволяет нам превратить обычный дисплей Three.js в тот, который разделен на два, создавая иллюзию глубины («внеосевой стереоскопический эффект») для нашего опыта виртуальной реальности.
  • DeviceOrientationControls.js — предоставляет Three.js возможность определить, где находится наше устройство и куда оно перемещается. Это соответствует спецификации события W3 DeviceOrientation .
  • OrbitControls.js — позволяет нам управлять сценой, перетаскивая ее с помощью мыши или сенсорных событий, в тех случаях, когда события DeviceOrientation недоступны (обычно только во время тестирования на компьютере).
  • helvetiker_regular.typeface.js — шрифт, который мы будем использовать в Three.js для нашего текста.

В нашем JavaScript мы устанавливаем наши начальные глобальные переменные и вызываем функцию init() , которая запускает все.

Наша функция init() начинается с установки нашей переменной scene в качестве объекта Three.js Scene . Каждой визуализации Three.js нужна сцена, потому что именно там размещаются все остальные элементы.

 function init() { scene = new THREE.Scene(); 

Затем мы устанавливаем объект Three.js PerspectiveCamera который принимает следующие параметры: PerspectiveCamera(fov, aspect, near, far) . Они представляют:

  • fov — вертикальное поле зрения для камеры. Наш установлен на 90 градусов, что означает, что мы будем смотреть вверх и вниз примерно на 90 градусов, а смотреть вокруг.
  • aspect — соотношение сторон для камеры. Обычно это ширина, деленная на высоту области просмотра. Google установил его на 1 в одном из своих примеров, которые я видел, и это тоже сработало.
  • near и far — любые элементы, которые находятся между near и far значениями нашей камеры, отображаются.
 camera = new THREE.PerspectiveCamera(90, window.innerWidth / window.innerHeight, 0.001, 700); 

Мы устанавливаем исходное положение нашей камеры с помощью camera.position.set(x,y,z) . В основном мы хотим установить ось Y. Это определяет, насколько высоким мы будем в нашем виртуальном мире. Я нашел 15, чтобы быть разумной высоты.

 camera.position.set(0, 15, 0); 

Затем мы добавляем камеру на нашу сцену.

 scene.add(camera); 

Нам нужен элемент на странице, чтобы нарисовать все это, поэтому мы определяем наш рендерер и назначаем его элементу с идентификатором webglviewer . В Three.js у нас есть два типа средств визуализации, которые определяют, как Three.js будет отображать трехмерные объекты — CanvasRenderer и WebGLRenderer . CanvasRenderer использует 2D-контекст холста, а не WebGL. Мы не хотим этого, так как мы будем запускать это на Chrome для Android, который достаточно хорошо поддерживает WebGL. В связи с этим мы установили наш рендерер на Three.js WebGLRenderer .

 renderer = new THREE.WebGLRenderer(); element = renderer.domElement; container = document.getElementById('webglviewer'); container.appendChild(element); 

Чтобы получить наше стереоскопическое представление StereoEffect реальности, мы передаем наш рендерер через объект StereoEffect который мы импортировали ранее в StereoEffect.js .

 effect = new THREE.StereoEffect(renderer); 

Управление нашей камерой

Наши элементы управления для перемещения камеры с помощью мыши или сенсорных событий определены далее. Мы передаем нашу камеру и элемент DOM, к которому мы будем прикреплять наших слушателей событий. Мы устанавливаем целевую точку, вокруг которой мы вращаемся, на 0,15 больше, чем положение x камеры, но те же точки y и z.

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

 controls = new THREE.OrbitControls(camera, element); controls.target.set( camera.position.x + 0.15, camera.position.y, camera.position.z ); controls.noPan = true; controls.noZoom = true; 

Затем мы настроили наш прослушиватель событий DeviceOrientation, который позволит нам отслеживать движение телефона в нашем устройстве Google Cardboard. При этом используется модуль JS, который мы импортировали ранее в DeviceOrientationControls.js . Мы добавим слушателя немного ниже в нашем коде, например так:

 window.addEventListener('deviceorientation', setOrientationControls, true); 

Функция, которую мы будем прикреплять к нашему слушателю событий, это setOrientationControls() . Это определено чуть выше addEventListener для него. Слушатель события DeviceOrientation возвращает три значения, когда он обнаружил совместимое устройство — alpha , beta и gamma . Мы проверяем alpha значение в начале нашей функции, чтобы гарантировать, что данные о событиях поступают, как и ожидалось.

 function setOrientationControls(e) { if (!e.alpha) { return; } 

Если у нас есть устройство, которое поддерживает спецификацию DeviceOrientation (наш мобильный браузер Google Chrome), тогда мы берем переменную OrbitControls , в которой ранее находился наш объект OrbitControls , и заменяем его нашим объектом DeviceOrientationControls . Это переключает способ взаимодействия совместимых браузеров со сценой. Вместо событий мыши или касания они теперь будут перемещать устройство. Затем мы запускаем функции connect() и update() которые поставляются с объектом DeviceOrientationControls который все настраивает для нас.

 controls = new THREE.DeviceOrientationControls(camera, true); controls.connect(); controls.update(); 

Мы также добавляем событие для этих мобильных устройств, при котором наш браузер переключается в полноэкранный режим при щелчке, так как просмотр этого в Google Cardboard лучше всего выглядит без адресной строки.

 element.addEventListener('click', fullscreen, false); 

Наконец, после того, как мы настроили наш объект DeviceOrientationControls , мы можем удалить прослушиватель DeviceOrientation.

 window.removeEventListener('deviceorientation', setOrientationControls, true); 

Освещая нашу сцену

Я поместил довольно простое освещение в эту сцену только для того, чтобы пол (который мы определим далее) был виден, и у вас было ощущение глубины. У меня есть два точечных источника света одинаковой яркости и цвета, которые расположены под разными углами сцены. light находится под большим углом, в то время как lightScene указывает прямо вниз, чтобы осветить то место, где мы будем стоять. Освещение — хитрое искусство, и я уверен, что есть кто-то, кто мог бы сделать это освещение намного более захватывающим, чем в настоящее время!

 var light = new THREE.PointLight(0x999999, 2, 100); light.position.set(50, 50, 50); scene.add(light); var lightScene = new THREE.PointLight(0x999999, 2, 100); lightScene.position.set(0, 5, 0); scene.add(lightScene); 

Создание пола

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

Наш пол будет использовать повторяющуюся текстуру, хранящуюся в переменной floorTexture . Мы загружаем файл изображения с именем 'textures/wood.jpg' а затем настраиваем его на повторение в обоих направлениях на любом объекте, на котором он находится. new THREE.Vector2(50, 50) устанавливает размер этой текстуры, которую мы повторяем.

 var floorTexture = THREE.ImageUtils.loadTexture('textures/wood.jpg'); floorTexture.wrapS = THREE.RepeatWrapping; floorTexture.wrapT = THREE.RepeatWrapping; floorTexture.repeat = new THREE.Vector2(50, 50); 

По умолчанию текстуры получаются немного размытыми, чтобы ускорить процесс (а иногда слегка размытыми, и выглядят лучше), однако, поскольку у нас есть довольно детальная текстура половиц, которые мы предпочитаем выглядеть резкими, мы устанавливаем anisotropy для renderer.getMaxAnisotropy .

 floorTexture.anisotropy = renderer.getMaxAnisotropy(); 

Наш пол нуждается как в текстуре, так и в материале. Материал контролирует, как наш пол будет реагировать на освещение. Мы используем MeshPhongMaterial поскольку он заставляет наш объект реагировать на свет и выглядеть красиво и блестяще. В этом материале мы устанавливаем floorTexture мы определили ранее для использования.

 var floorMaterial = new THREE.MeshPhongMaterial({ color: 0xffffff, specular: 0xffffff, shininess: 20, shading: THREE.FlatShading, map: floorTexture }); 

Чтобы настроить форму, которую мы хотим, чтобы наш пол был, мы должны создать объект, определяющий, какую геометрию мы бы хотели иметь. Three.js имеет ряд геометрий, таких как куб, цилиндр, сфера, кольцо и многое другое. Мы будем придерживаться очень простой геометрии, плоскости. Стоит отметить, что я использовал тип плоскости PlaneBufferGeometry . Вы также можете использовать здесь PlaneGeometry , однако это может занять немного больше памяти (и нам действительно не нужно ничего слишком причудливого… это пол!). Мы определяем его с высотой и шириной 1000.

 var geometry = new THREE.PlaneBufferGeometry(1000, 1000); 

Наш пол должен иметь физическое представление, которое объединяет нашу геометрию и материал, который мы определили, в реальный объект, который мы можем добавить в нашу сцену. Мы делаем это с помощью Mesh . При добавлении меша он помещается в сцену в вертикальном положении (больше стены, чем пола), поэтому мы поворачиваем его так, чтобы он был плоским под нашими виртуальными ногами, прежде чем добавить его в нашу сцену.

 var floor = new THREE.Mesh(geometry, floorMaterial); floor.rotation.x = -Math.PI / 2; scene.add(floor); 

Собираем наши частицы

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

 particles = new THREE.Object3D(), totalParticles = 200, maxParticleSize = 200, particleRotationSpeed = 0, particleRotationDeg = 0, lastColorRange = [0, 0.3], currentColorRange = [0, 0.3], 

Давайте начнем смотреть на наш код частицы с обзора высокого уровня. Первоначально мы установили текстуру для наших частиц прозрачным png в 'textures/particle.png' . Затем мы перебираем количество частиц, которое мы определяем в totalParticles . Если вы хотите изменить количество частиц, появляющихся на сцене, вы можете увеличить это число, и оно будет генерировать больше и упорядочивать их для вас.

После того, как мы повторили их все и добавили к нашему объекту particles , мы поднимаем его так, чтобы он плавал вокруг нашей камеры. Затем мы добавляем объект нашей particles в нашу сцену.

 var particleTexture = THREE.ImageUtils.loadTexture('textures/particle.png'), spriteMaterial = new THREE.SpriteMaterial({ map: particleTexture, color: 0xffffff }); for (var i = 0; i < totalParticles; i++) { // Code setting up all our particles! } particles.position.y = 70; scene.add(particles); 

Теперь посмотрим, что именно происходит в нашем цикле for. Мы начнем с создания нового объекта Sprite Three.js и присвоения ему нашего spriteMaterial . Затем мы масштабируем его до 64 × 64 (того же размера, что и наша текстура) и размещаем его. Мы хотим, чтобы наши частицы находились в случайных позициях вокруг нас, поэтому мы устанавливаем для них значения x и y в Math.random() - 0.5 от -0,5 до 0,5, используя Math.random() - 0.5 и значения в Math.random() - 0.75 от -0,75 до 0,25, используя Math.random() - 0.75 . Почему эти ценности? После нескольких экспериментов я подумал, что они дают лучший эффект при плавании вокруг камеры.

 for (var i = 0; i < totalParticles; i++) { var sprite = new THREE.Sprite(spriteMaterial); sprite.scale.set(64, 64, 1.0); sprite.position.set(Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.75); 

Затем мы устанавливаем размер каждой частицы где-то между 0 и maxParticleSize мы установили ранее.

 sprite.position.setLength(maxParticleSize * Math.random()); 

Ключевой частью создания таких частиц, как светящиеся частицы, является THREE.AdditiveBlending смешивания THREE.AdditiveBlending в Three.js. Это добавляет цвет текстуры к цвету позади нее, давая нам больше эффекта свечения над другими частицами и полом. Мы применяем это и затем добавляем каждый спрайт к нашему объекту particles .

 sprite.material.blending = THREE.AdditiveBlending; particles.add(sprite); } 

API погоды

Все это до сих пор приводило нас в состояние, когда у нас есть статический набор частиц, подготовленных в сцене с полом и освещением. Давайте сделаем вещи немного интереснее, добавив веб-API для оживления сцены. Мы будем использовать OpenWeatherMap API для получения погодных условий в разных городах.

Функция, которую мы adjustToWeatherConditions() для подключения к API погоды, — adjustToWeatherConditions() . Мы рассмотрим код в целом и затем рассмотрим, что он делает.

OpenWeatherMap API работает лучше всего, если мы завершим наш вызов для нескольких городов одним HTTP-запросом. Для этого мы создаем новую строку с именем cityIDs которая начинается с пустой строки. Затем мы добавляем список идентификаторов городов, которые можно передать в запрос GET. Если вы хотите выбрать из списка городов, у них есть полный список городов по всему миру и связанные с ними идентификаторы в примерах загрузки по адресу http://78.46.48.103/sample/city.list.json.gz .

 function adjustToWeatherConditions() { var cityIDs = ''; for (var i = 0; i < cities.length; i++) { cityIDs += cities[i][1]; if (i != cities.length - 1) cityIDs += ','; } 

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

Чтобы иметь возможность вызывать этот API, вам потребуется ключ API для передачи в параметр APPID GET. Чтобы получить ключ API, создайте учетную запись на http://openweathermap.org и перейдите на свою страницу «Мой дом» .

Функция getURL() в нашем примере — это действительно очень простой вызов XMLHttpRequest. Если у вас есть ошибки перекрестного источника, вам может потребоваться переключить эту функцию на что-то, использующее JSONP. Судя по тому, что я видел в своих демонстрациях во время разработки, использование XMLHttpRequest, казалось, работало нормально с этими API.

После успешного выполнения нашего запроса GET у нас появляется функция обратного вызова, которая извлекает данные о погоде для всех городов в переменной cityWeather . Вся информация, которую мы хотим, находится в info.list в нашем возвращенном JSON.

 getURL('http://api.openweathermap.org/data/2.5/group?id=' + cityIDs + '&APPID=kj34723jkh23kj89dfkh2b28ey982hwm223iuyhe2c', function(info) { cityWeather = info.list; 

Далее мы будем искать время в каждом месте.

Местные Городские Времена Через TimeZoneDB

TimeZoneDB достаточно любезен, чтобы иметь аккуратную маленькую библиотеку JavaScript, которую мы будем использовать, чтобы все было легко и просто:

 <script src="timezonedb.js" type="text/javascript"></script> 

После того, как мы adjustToWeatherConditions() наши данные о погоде в adjustToWeatherConditions() , мы вызываем нашу следующую функцию lookupTimezones() которая будет извлекать, lookupTimezones() времени в каждом месте. Мы передаем ему нулевое значение, чтобы сказать, что мы хотим найти часовой пояс для первого города, и мы передаем длину нашего массива погоды, чтобы он знал, сколько еще городов мы хотим пройти после этого.

 lookupTimezones(0, cityWeather.length); 

Наша lookupTimezones() сама начинается с использования объекта TimeZoneDB которому у нас есть доступ из timezonedb.js . Затем мы объединяем функцию getJSON() TimeZoneDB с широтой и долготой каждого местоположения, которое мы получаем из серии данных массива cityWeather нашего API погоды. Он извлекает время в каждом месте, и мы сохраняем его в массиве с именем cityTimes . Мы запускаем его до тех пор, пока у нас будет больше городов для поиска ( t отслеживает, какой у нас индекс, а len имеет длину нашего массива данных о погоде). Как только мы applyWeatherConditions() их все, мы запускаем applyWeatherConditions() .

Обновление: спасибо Voycie в комментариях, которые заметили, что TimeZoneDB начал возвращать ошибку 503 из-за слишком большого количества вызовов в секунду. Чтобы исправить это, код ниже теперь окружает наш цикл lookupTimezones(t, len); в setTimeout() которая ждет 1200 миллисекунд, прежде чем снова использовать API.

 function lookupTimezones(t, len) { var tz = new TimeZoneDB; tz.getJSON({ key: "KH3KH239D1S", lat: cityWeather[t].coord.lat, lng: cityWeather[t].coord.lon }, function(timeZone){ cityTimes.push(new Date(timeZone.timestamp * 1000)); t++; if (t < len) { setTimeout(function() { lookupTimezones(t, len); }, 1200); } else { applyWeatherConditions(); } }); } 

Применение погодных условий

Теперь, когда у нас есть все необходимые данные, нам просто нужно применить эффекты и движение в ответ на эти данные. Функция applyWeatherConditions() довольно большая, поэтому мы рассмотрим ее шаг за шагом.

В начале нашего JavaScript в наших объявлениях переменных мы устанавливаем переменную следующим образом:

 currentCity = 0 

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

Мы запускаем функцию displayCurrentCityName() в начале нашей функции applyWeatherConditions() которая добавляет немного трехмерного текста, отображающего название нашего текущего города. Позднее мы объясним, как это работает. Я обнаружил, что лучше всего иметь его в начале этой функции, чтобы, если есть какие-либо задержки в обработке всех этих цветов, мы сначала получили как минимум несколько миллисекунд названия города в качестве ответа.

Затем мы присваиваем данные о погоде для текущего города info переменной, чтобы сделать ее более понятной для ссылок во всей нашей функции.

 function applyWeatherConditions() { displayCurrentCityName(cities[currentCity][0]); var info = cityWeather[currentCity]; 

Далее мы устанавливаем две наши переменные, которые относятся к ветру. particleRotationSpeed будет скоростью ветра в милях в секунду, деленной на два (чтобы немного замедлить его, чтобы мы могли видеть частицы), а particleRotationDeg будет представлять направление ветра в градусах.

 particleRotationSpeed = info.wind.speed / 2; // dividing by 2 just to slow things down particleRotationDeg = info.wind.deg; 

Мы получаем время суток в этом месте из нашего массива cityTimes . Время представлено в формате UTC, поэтому мы используем getUTCHours() чтобы извлечь только значение часа. Если по какой-либо причине там нет времени, мы просто используем 0.

 var timeThere = cityTimes[currentCity] ? cityTimes[currentCity].getUTCHours() : 0 

Чтобы показать день и ночь в этой демонстрации, мы будем использовать очень широкую оценку. Если время между 6 и 18 включительно, то это дневное время. В противном случае, это ночное время. Вы можете теоретически сделать кучу вычислений по положению солнца или найти другой API, который включает информацию о дне / ночи, если хотите, однако для целей базовой визуализации я подумал, что этого будет достаточно.

 isDay = timeThere >= 6 && timeThere <= 18; 

Если это дневное время, то мы корректируем цвета наших частиц в соответствии с нашими погодными данными. Мы используем оператор switch для просмотра main ключа наших данных о погоде. Это ряд значений из API OpenWeatherData, которые представляют общую категоризацию погоды в этом месте. Мы будем следить за «Облаками», «Дождем» или «Ясно». Я присматриваю за этими значениями и устанавливаю цветовую гамму наших частиц в зависимости от этого.

Наш диапазон цветов будет представлен в HSL, поэтому currentColorRange[0] представляет оттенок нашего цвета, а currentColorRange[1] представляет насыщенность. Когда облачно, мы устанавливаем оттенок равным 0, поэтому он белый. Когда идет дождь, мы устанавливаем оттенок на синий, но становимся темнее со значением насыщенности. Когда ясно, мы показываем это с хорошим голубым. Если это ночь, тогда мы устанавливаем оттенок и насыщенность светло-фиолетового цвета.

 if (isDay) { switch (info.weather[0].main) { case 'Clouds': currentColorRange = [0, 0.01]; break; case 'Rain': currentColorRange = [0.7, 0.1]; break; case 'Clear': default: currentColorRange = [0.6, 0.7]; break; } } else { currentColorRange = [0.69, 0.6]; } 

В конце нашей функции мы либо идем в следующий город, либо возвращаемся к первому. Затем мы устанавливаем тайм-аут, который через 5 секунд перезапустит нашу applyWeatherConditions() с новым значением currentCity . Это то, что настраивает нашу петлю через каждый город.

 if (currentCity < cities.length-1) currentCity++; else currentCity = 0; setTimeout(applyWeatherConditions, 5000); 

Отображение названия нашего текущего города

Чтобы отобразить наше текущее название города, мы удаляем любую предыдущую сетку Three.js, хранящуюся в переменной с именем currentCityTextMesh (в ситуации, когда это уже было выполнено), а затем воссоздаем ее с именем нашего нового города. Мы используем объект Three.js TextGeometry который позволяет нам передавать TextGeometry текст и устанавливать его размер и глубину.

 function displayCurrentCityName(name) { scene.remove(currentCityTextMesh); currentCityText = new THREE.TextGeometry(name, { size: 4, height: 1 }); 

Затем мы устанавливаем меш, который является простым, полностью непрозрачным белым. Мы позиционируем его, используя параметры position и rotation а затем добавляем его в нашу сцену.

 currentCityTextMesh = new THREE.Mesh(currentCityText, new THREE.MeshBasicMaterial({ color: 0xffffff, opacity: 1 })); currentCityTextMesh.position.y = 10; currentCityTextMesh.position.z = 20; currentCityTextMesh.rotation.x = 0; currentCityTextMesh.rotation.y = -180; scene.add(currentCityTextMesh); 

Держать время

Чтобы следить за временем в нашем работающем опыте Three.js, мы создаем переменную clock которая содержит объект Three.js Clock() . Это отслеживает время между каждым рендером. Мы установили это в конце нашей функции init() .

 clock = new THREE.Clock(); 

Анимация!

Наконец, мы хотим, чтобы все перемещалось и обновлялось в каждом кадре. Для этого мы запустим функцию, которую мы будем вызывать animate() . Сначала мы запускаем его в конце нашей функции init() . Наша функция animate() начинается с получения количества секунд, в течение которых была запущена сцена Three.js. Он хранит это в течение elapsedSeconds . Мы также решаем, в каком направлении должны вращаться наши частицы, если ветер меньше или равен 180, мы будем вращать их по часовой стрелке, если нет, мы будем вращать их против часовой стрелки.

 function animate() { var elapsedSeconds = clock.getElapsedTime(), particleRotationDirection = particleRotationDeg <= 180 ? -1 : 1; 

Чтобы фактически вращать их в каждом кадре нашей анимации Three.js, мы рассчитываем количество секунд, в течение которых работала наша анимация, умноженные на скорость, с которой мы хотим, чтобы наши частицы путешествовали, и направление, в котором мы хотим, чтобы они двигались. Это определяет значение у нашего вращения группы particles .

 particles.rotation.y = elapsedSeconds * particleRotationSpeed * particleRotationDirection; 

Мы также отслеживаем текущий и последний цвета, поэтому знаем, в каких кадрах нам нужно их изменить. Зная, что они были в последнем кадре, мы избегаем пересчета всего для кадров, в которых мы еще не изменили город. Если они отличаются, то мы устанавливаем значение HSL для каждой частицы в нашем объекте particles на этот новый цвет, но с рандомизированным значением для яркости, которое составляет от 0,2 до 0,7.

 if (lastColorRange[0] != currentColorRange[0] && lastColorRange[1] != currentColorRange[1]) { for (var i = 0; i < totalParticles; i++) { particles.children[i].material.color.setHSL(currentColorRange[0], currentColorRange[1], (Math.random() * (0.7 - 0.2) + 0.2)); } lastColorRange = currentColorRange; } 

Затем мы устанавливаем нашу функцию animate() для повторного запуска следующего кадра анимации:

 requestAnimationFrame(animate); 

И, наконец, мы запускаем две функции, которые обеспечивают бесперебойную работу.

update(clock.getDelta()) сохраняет наш рендерер, объект камеры и элементы управления, соответствующие размеру update(clock.getDelta()) просмотра браузера.

render(clock.getDelta()) рендерит нашу сцену каждый кадр. Внутри этой функции мы вызываем это для effect чтобы отобразить его, используя стереоскопический эффект, который мы установили ранее:

 effect.render(scene, camera); 

В бою!

Поместите это на общедоступный веб-сервер, загрузите его на свой телефон с помощью Google Chrome, коснитесь его, чтобы сделать его полноэкранным, а затем вставьте его в гарнитуру Google Cardboard. Во время всего этого бега у вас должно быть прекрасное зрение, которое контролируется вашими движениями головы:

particlesofsanfran

particlesofsydney

particlesoftokyo

Сравнивая это с погодой за окном в Сиднее, это выглядит точно!

В бою

Не стесняйтесь настраивать его в новых городах, менять цвета, скорости и все в соответствии с вашими предпочтениями или создавать совершенно новую визуализацию. Это все часть веселья!

Вывод

Теперь у вас должен быть довольно хороший уровень знаний о том, что требуется для работы с 3D VR в Google Cardboard и Three.js. Если вы делаете что-то на основе этого кода, оставляете заметку в комментариях или связываетесь со мной в Твиттере ( @thatpatrickguy ), я хотел бы проверить это!