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

Создание React VR-приложения, работающего в реальном времени

Создание React VR-приложения, работающего в реальном времени - 1

Библиотека React VR позволяет писать для веба приложения виртуальной реальности с использованием JavaScript и React поверх WebVR API [1]. Эта спецификация поддерживается последними (в некоторых случаях — экспериментальными) версиями браузеров Chrome, Firefox и Edge. И для этого вам не нужны очки VR.

WebVR Experiments [2] — это сайт-витрина, демонстрирующий возможности WebVR. Моё внимание привлёк проект The Musical Forest [3], созданный замечательным человеком из Google Creative Lab, который использовал A-Frame [4], веб-фреймворк для WebVR, разработанный командой Mozilla VR [5].

В Musical Forest благодаря WebSockets пользователи могут в реальном времени играть вместе музыку, нажимая на геометрические фигуры. Но из-за имеющихся возможностей и используемых технологий приложение получилось достаточно сложным (исходный код [6]). Так почему бы не создать аналогичное приложение, работающее в реальном времени, на React VR с многопользовательской поддержкой на базе Pusher?

Вот как выглядит React VR/Pusher-версия:

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

Для публикации событий возьмём Node.js-бэкенд, поэтому вам нужно иметь какой-то опыт работы с JavaScript и React. Если вы плохо знакомы с React VR и используемыми в VR концепциями, то для начала изучите этот материал [7].

Ссылки на скачивание (чтобы просто попробовать):
React VR-проект [8].
Node.js-бэкенд [9].

Настраиваем VR-проект

Начнём с установки (или обновления) инструмента React VR CLI:

npm install -g react-vr-cli

Создадим новый React VR-проект:

react-vr init musical-exp-react-vr-pusher

Идём в созданную им директорию и исполняем команду для запуска сервера разработки:

cd musical-exp-react-vr-pusher
npm start

В браузере идём по адресу http://localhost:8081/vr/ [10]. Должно появиться такое:

image

Если вы используете совместимый браузер (вроде Firefox Nightly [11] под Windows), то должны увидеть ещё и кнопку View in VR, позволяющую просматривать приложение в очках VR:

image

Перейдём к программированию.

Создаём фон

Для фона возьмём эквидистантное изображение (equirectangular image [12]). Главной особенностью таких изображений является то, что ширина должна быть ровно вдвое больше высоты. Так что откройте любимый графический редактор и создайте изображение 4096×2048 с градиентной заливкой. Цвет — на ваш вкус.

image

Внутри директории static_assets в корне приложения создаём новую папку images, и сохраняем туда картинку. Теперь откроем файл index.vr.js и заменим содержимое метода render на:

    render() {
      return (
        <View>
          <Pano source={asset('images/background.jpg')} />
        </View>
      );
    }

Перезагрузим страницу (или активируем горячую перезагрузку [13]), и увидим это:

image

Для эмулирования дерева воспользуемся Cylinder [14]. По факту нам их потребуется сотня, чтобы получился лес вокруг пользователя. В оригинальной Musical Forest в файле js/components/background-objects.js [15] можно найти алгоритм, генерирующий деревья. Если адаптировать код под React-компонент нашего проекта, получим:

    import React from 'react';
    import {
      View,
      Cylinder,
    } from 'react-vr';

    export default ({trees, perimeter, colors}) => {
      const DEG2RAD = Math.PI / 180;

      return (
        <View>
          {Array.apply(null, {length: trees}).map((obj, index) => {
              const theta = DEG2RAD * (index / trees) * 360;
              const randomSeed = Math.random();
              const treeDistance = randomSeed * 5 + perimeter;
              const treeColor = Math.floor(randomSeed * 3);
              const x = Math.cos(theta) * treeDistance;
              const z = Math.sin(theta) * treeDistance;

              return (
                <Cylinder
                  key={index}
                  radiusTop={0.3}
                  radiusBottom={0.3}
                  dimHeight={10}
                  segments={10}
                  style={{
                    color: colors[treeColor],
                    opacity: randomSeed,
                    transform: [{scaleY : 2 + Math.random()}, {translate: [x, 3, z]},],
                  }}
                />
              );
          })}
        </View>
      );
    }

Функциональный компонент берёт три параметра:

  • trees — количество деревьев, которое должно получиться в лесу;
  • perimeter — значение, позволяющее управлять дальностью отрисовки деревьев от пользователя;
  • colors — массив значений цветов [16] деревьев.

С помощью Array.apply(null, {length: trees}) можно создать массив пустых значений, к которому применим map-функцию, чтобы отрисовать массив цилиндров случайных цветов, прозрачности и позиций внутри компонента View [17].

Можно сохранить код в файле Forest.js внутри директории компонента и использовать его внутри index.vr.js:

    ...
    import Forest from './components/Forest';

    export default class musical_exp_react_vr_pusher extends React.Component {
      render() {
        return (
          <View>
            <Pano source={asset('images/background.jpg')} />

            <Forest trees={100} perimeter={15} colors={['#016549', '#87b926', '#b1c96b']} 
            />
          </View>
        );
      }
    };

    ...

В браузере увидим это [18]. Отлично, фон готов, создадим 3D-объекты, которые будут создавать звуки.

Создаём 3D-формы

Нужно создать шесть 3D-форм, при касании каждая будет проигрывать шесть разных звуков. Также пригодится маленькая анимация, когда курсор помещается и убирается с объекта.

Для создания форм нам нужны VrButton [19], Animated.View [20], Box [21], Cylinder [14] и Sphere [22]. Но поскольку все формы будут отличаться, просто инкапсулируем в компонент, это будет то же самое. Сохраните следующий код в файл components/SoundShape.js:

    import React from 'react';
    import {
      VrButton,
      Animated,
    } from 'react-vr';

    export default class SoundShape extends React.Component {

      constructor(props) {
        super(props);
        this.state = {
          bounceValue: new Animated.Value(0),
        };
      }

      animateEnter() {
        Animated.spring(  
          this.state.bounceValue, 
          {
            toValue: 1, 
            friction: 4, 
          }
        ).start(); 
      }

      animateExit() {
        Animated.timing(
          this.state.bounceValue,
          {
            toValue: 0,
            duration: 50,
          }
        ).start();
      }

      render() {
        return (
          <Animated.View
            style={{
              transform: [
                {rotateX: this.state.bounceValue},
              ],
            }}
          >
            <VrButton
              onEnter={()=>this.animateEnter()}
              onExit={()=>this.animateExit()}
            >
              {this.props.children}
            </VrButton>
          </Animated.View>
        );
      }
    };

Когда курсор попадает в область кнопки, Animated.spring меняет значение this.state.bounceValue с 0 на 1 и показывает эффект подпрыгивания. Когда курсор уходит из области кнопки, Animated.timing меняет значение this.state.bounceValue с 1 на 0 в течение 50 миллисекунд. Чтобы это работало, обернём VrButton в компонент Animated.View, который будет менять rotateX-преобразование View при каждом изменении состояния.

В index.vr.js можно добавить SpotLight (можете выбрать любой другой тип источника света и изменить его свойства) и использовать компонент SoundShape, тем самым сделав цилиндр:

    ...
    import {
      AppRegistry,
      asset,
      Pano,
      SpotLight,
      View,
      Cylinder,
    } from 'react-vr';
    import Forest from './components/Forest';
    import SoundShape from './components/SoundShape';

    export default class musical_exp_react_vr_pusher extends React.Component {
      render() {
        return (
          <View>
            ...

            <SpotLight intensity={1} style={{transform: [{translate: [1, 4, 4]}],}} />

            <SoundShape>
              <Cylinder
                radiusTop={0.2}
                radiusBottom={0.2}
                dimHeight={0.3}
                segments={8}
                lit={true}
                style={{
                  color: '#96ff00', 
                  transform: [{translate: [-1.5,-0.2,-2]}, {rotateX: 30}],
                }}
              />
            </SoundShape>
          </View>
        );
      }
    };
    ...

Конечно, можно менять свойства 3D-форм, и даже заменять их на 3D-модели [23].

Теперь добавим пирамиду (цилиндр с нулевым радиусом op radius и четырьмя сегментами):

    <SoundShape>
      <Cylinder
        radiusTop={0}
        radiusBottom={0.2}
        dimHeight={0.3}
        segments={4}
        lit={true}
        style={{
          color: '#96de4e',
          transform: [{translate: [-1,-0.5,-2]}, {rotateX: 30}],
        }}
      />
    </SoundShape>

Куб:

    <SoundShape>
      <Box
        dimWidth={0.2}
        dimDepth={0.2}
        dimHeight={0.2}
        lit={true}
        style={{
          color: '#a0da90', 
          transform: [{translate: [-0.5,-0.5,-2]}, {rotateX: 30}],
        }}
      />
    </SoundShape>

Параллелепипед:

    <SoundShape>
      <Box
        dimWidth={0.4}
        dimDepth={0.2}
        dimHeight={0.2}
        lit={true}
        style={{
          color: '#b7dd60',
          transform: [{translate: [0,-0.5,-2]}, {rotateX: 30}],
        }}
      />
    </SoundShape>

Сфера:

    <SoundShape>
      <Sphere
        radius={0.15}
        widthSegments={20}
        heightSegments={12}
        lit={true}
        style={{
          color: '#cee030',
          transform: [{translate: [0.5,-0.5,-2]}, {rotateX: 30}],
        }}
      />
    </SoundShape>

И треугольная призма:

    <SoundShape>
      <Cylinder
        radiusTop={0.2}
        radiusBottom={0.2}
        dimHeight={0.3}
        segments={3}
        lit={true}
        style={{
          color: '#e6e200',
          transform: [{translate: [1,-0.2,-2]}, {rotateX: 30}],
        }}
      />
    </SoundShape>

После импорта сохраняем файл и обновляем браузер. Должно получиться такое:

image

Теперь добавим звуки!

Добавляем звук

Помимо прочего, React VR поддерживает wav, mp3 и ogg-файлы. Полный список есть здесь [24].
Можно взять сэмплы с Freesound [25] или другого подобного сайта. Скачайте, какие вам нравятся, и поместите в директорию static_assets/sounds. Для нашего проекта возьмём звуки шести животных, птицу [26], другую птицу [27], ещё одну птицу [28], кошку [29], собаку [29] и сверчка [30] (последний файл пришлось пересохранить, чтобы уменьшить битрейт, иначе React VR его не проигрывал).

React VR предоставляет три опции проигрывания звука:

Однако 3D/объёмный звук поддерживает только компонент Sound, так что баланс левого и правого каналов будет меняться при перемещении слушателя по сцене или при повороте головы. Добавим его в компонент SoundShape, как и событие onClick в VrButton:

    ...
    import {
      ...
      Sound,
    } from 'react-vr';

    export default class SoundShape extends React.Component {
      ...
      render() {
        return (
          <Animated.View
            ...
          >
            <VrButton
              onClick={() => this.props.onClick()}
              ...
            >
              ...
            </VrButton>
            <Sound playerState={this.props.playerState} source={this.props.sound} />
          </Animated.View>
        );
      }
    }

Для управления проигрыванием воспользуемся MediaPlayerState [34]. Они будут передаваться как свойства компонента.

С помощью информации из index.vr.js определим массив:

    ...
    import {
      ...
      MediaPlayerState,
    } from 'react-vr';
    ...

    export default class musical_exp_react_vr_pusher extends React.Component {

      constructor(props) {
        super(props);

            this.config = [
              {sound: asset('sounds/bird.wav'), playerState: new MediaPlayerState({})},
              {sound: asset('sounds/bird2.wav'), playerState: new MediaPlayerState({})},
              {sound: asset('sounds/bird3.wav'), playerState: new MediaPlayerState({})},
              {sound: asset('sounds/cat.wav'), playerState: new MediaPlayerState({})},
              {sound: asset('sounds/cricket.wav'), playerState: new MediaPlayerState({})},
              {sound: asset('sounds/dog.wav'), playerState: new MediaPlayerState({})},
            ];
      }

      ...
    }
And a method to play a sound using the MediaPlayerState object when the right index is passed:
    ...

    export default class musical_exp_react_vr_pusher extends React.Component {

      ...

      onShapeClicked(index) {
        this.config[index].playerState.play();
      }

      ...
    }

Осталось только передать всю эту информацию в компонент SoundShape. Сгруппируем наши 3D-формы в массив и воспользуемся map-функцией для генерирования компонентов:

    ...

    export default class musical_exp_react_vr_pusher extends React.Component {

      ...

      render() {
        const shapes = [
          <Cylinder
            ...
          />,
          <Cylinder
            ...
          />,
          <Box
            ...
          />,
          <Box
            ...
          />,
          <Sphere
            ...
          />,
          <Cylinder
            ...
          />
        ];

        return (
          <View>
            ...

            {shapes.map((shape, index) => {
              return (       
                <SoundShape 
                  onClick={() => this.onShapeClicked(index)} 
                  sound={this.config[index].sound} 
                  playerState={this.config[index].playerState}>
                    {shape}
                </SoundShape>
              );
            })}

          </View>
        );
      }

      ...
    }

Перезапустите браузер и попробуйте понажимать на объекты, вы услышите разные звуки.
С помощью Pusher добавим в React VR-приложение многопользовательскую поддержку в реальном времени.

Настраиваем Pusher

Создадим бесплатный аккаунт на https://pusher.com/signup [35]. Когда вы создаёте приложение, вас попросят кое-что сконфигурировать:

image

Введите название, выберите в качестве фронтенда React, а в качестве бэкенда — Node.js. Пример кода для начала:

image

Не переживайте, вас не заставляют придерживаться конкретного набора технологий, вы всегда сможете их изменить. С Pusher можно использовать любые комбинации библиотек.

Копируем ID кластера (идёт после названия приложения, в этом примере — mt1), ID приложения, ключ и секретную информацию, они нам понадобятся. Всё это можно найти также во вкладке App Keys.

Публикуем событие

React VR работает как Web Worker [36] (подробнее об архитектуре React VR в видео [37]), так что нам надо включить скрипт Pusher-воркера в index.vr.js:

    ...
    importScripts('https://js.pusher.com/4.1/pusher.worker.min.js');

    export default class musical_exp_react_vr_pusher extends React.Component {
      ...
    }

Есть два условия, которые надо соблюсти. Во-первых, надо иметь возможность передавать идентификатор посредством URL (вроде http://localhost:8081/vr/?channel=1234 [38]), чтобы пользователи могли выбирать, в какие каналы заходить и делиться ими с друзьями.

Для этого нам надо считывать URL. К счастью, React VR идёт с нативным модулем [39] Location [40], который делает свойства объекта window.location доступными для контекста React.

Теперь нужно обратиться к серверу, который опубликует Pusher-событие, чтобы все подключённые клиенты тоже могли его проиграть. Но нам не нужно, чтобы клиент, сгенерировавший событие, тоже получил его, потому что в этом случае звук будет проигрываться дважды. Да и какой смысл ждать события для проигрывания звука, если это можно сделать немедленно, как только пользователь кликнул на объект.

Каждому Pusher-соединению присваивается уникальный ID сокета. Чтобы получатели не принимали события в Pusher, нужно передавать серверу socket_id клиента, которого нужно исключить при срабатывании события (подробнее об этом здесь [41]).

Таким образом, немного адаптировав функцию getParameterByName для чтения параметров URL [42] и сохранив socketId при успешном подключении к Pusher, мы можем соблюсти оба требования:

  ...
    import {
      ...
      NativeModules,
    } from 'react-vr';
    ...
    const Location = NativeModules.Location;

    export default class musical_exp_react_vr_pusher extends React.Component {
      componentWillMount() {
        const pusher = new Pusher('<INSERT_PUSHER_APP_KEY>', {
          cluster: '<INSERT_PUSHER_APP_CLUSTER>',
          encrypted: true,
        });
        this.socketId = null;
        pusher.connection.bind('connected', () => {
          this.socketId = pusher.connection.socket_id;
        });
        this.channelName = 'channel-' + this.getChannelId();
        const channel = pusher.subscribe(this.channelName);
        channel.bind('sound_played',  (data) => {
          this.config[data.index].playerState.play();
        });
      }

      getChannelId() {
        let channel = this.getParameterByName('channel', Location.href);
        if(!channel) {
          channel = 0;
        }

        return channel;
      }

      getParameterByName(name, url) {
        const regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)");
        const results = regex.exec(url);
        if (!results) return null;
        if (!results[2]) return '';
        return decodeURIComponent(results[2].replace(/+/g, " "));
      }

      ...
    }

Если в URL нет параметров канал, то по умолчанию присваивается ID 0. Этот ID будет добавляться к Pusher-каналу, чтобы сделать его уникальным.

Наконец, нам нужно вызвать endpoint на серверной стороне, которая опубликует событие, передав ID сокета клиента и канал, в котором будут публиковаться события:

    ...
    export default class musical_exp_react_vr_pusher extends React.Component {
      ...
      onShapeClicked(index) {
        this.config[index].playerState.play();
        fetch('http://<INSERT_YOUR_SERVER_URL>/pusher/trigger', {
          method: 'POST',
          headers: {
            'Accept': 'application/json',
            'Content-Type': 'application/json',
          },
          body: JSON.stringify({
            index: index,
            socketId: this.socketId,
            channelName: this.channelName,
          })
        });
      }
      ...
    }

Вот и весь код для React-части. Теперь разберёмся с сервером.

Создаём Node.js-бэкенд

С помощью команды генерируем файл package.json:

npm init -y

Добавляем зависимости:

    npm install --save body-parser express pusher

И сохраняем в файл этот код:

    const express = require('express');
    const bodyParser = require('body-parser');
    const Pusher = require('pusher');

    const app = express();
    app.use(bodyParser.json());
    app.use(bodyParser.urlencoded({ extended: false }));
    /*
      Эти заголовки необходимы, потому что сервер разработки React VR запущен на другом порту. Когда финальный проект будет опубликован, нужда в middleware может отпасть
    */
    app.use((req, res, next) => {
      res.header("Access-Control-Allow-Origin", "*")
      res.header("Access-Control-Allow-Headers", 
                 "Origin, X-Requested-With, Content-Type, Accept")
      next();
    });

    const pusher = new Pusher({
      appId: '<INSERT_PUSHER_APP_ID>',
      key: '<INSERT_PUSHER_APP_KEY>',
      secret: '<INSERT_PUSHER_APP_SECRET>',
      cluster: '<INSERT_PUSHER_APP_CLUSTER>',
      encrypted: true,
    });

    app.post('/pusher/trigger', function(req, res) {
      pusher.trigger(req.body.channelName, 
                     'sound_played', 
                     { index: req.body.index },
                     req.body.socketId );
      res.send('ok');
    });

    const port = process.env.PORT || 5000;
    app.listen(port, () => console.log(`Running on port ${port}`));

Как видите, мы настроили Express-сервер, Pusher-объект и route/pusher/trigger, который просто запускает событие с индексом звука для проигрывания и socketID для исключения получателя события.

Всё готово. Давайте тестировать.

Тестируем

Выполним Node.js-бэкенд с помощью команды:

node server.js

Обновим серверный URL в index.vr.js (с использованием вашего IP вместо localhost) и в двух браузерных окнах откроем адрес вроде http://localhost:8081/vr/?channel=1234 [38]. При клике на 3D-форму вы услышите дважды проигранный звук (это куда веселее делать с друзьями на разных компьютерах):

Заключение

React VR — превосходная библиотека, позволяющая легко создавать VR-проекты, особенно если вы уже знаете React/React Native. Если присовокупить к этому Pusher, то получится мощный комплекс разработки веб-приложений нового поколения.

Вы можете собрать production-релиз этого проекта для развёртывания на любом веб-сервере: https://facebook.github.io/react-vr/docs/publishing.html [43].

Также можете изменить цвета, формы, звуки, добавить больше функций из оригинальной Musical Forest [6].

Скачать код проекта можно из репозитория GitHub [8].

Автор: AloneCoder

Источник [44]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/javascript/259279

Ссылки в тексте:

[1] WebVR API: https://webvr.info/

[2] WebVR Experiments: https://webvrexperiments.com/

[3] The Musical Forest: https://forest.webvrexperiments.com/

[4] A-Frame: https://aframe.io/

[5] Mozilla VR: https://mozvr.com/

[6] исходный код: https://github.com/googlecreativelab/webvr-musicalforest

[7] этот материал: https://www.pluralsight.com/guides/front-end-javascript/getting-started-with-react-vr

[8] React VR-проект: https://github.com/eh3rrera/reactvr_musical

[9] Node.js-бэкенд: https://github.com/eh3rrera/server_pusher_reactvr_musical

[10] http://localhost:8081/vr/: http://localhost:8081/vr/

[11] Firefox Nightly: https://www.mozilla.org/en-US/firefox/channel/desktop/

[12] equirectangular image: https://www.flickr.com/groups/equirectangular/

[13] горячую перезагрузку: https://facebook.github.io/react-vr/docs/dev-tools.html#hot-reloading

[14] Cylinder: https://facebook.github.io/react-vr/docs/cylinder.html

[15] js/components/background-objects.js: https://github.com/googlecreativelab/webvr-musicalforest/blob/master/js/components/background-objects.js

[16] цветов: https://facebook.github.io/react-vr/docs/colors.html

[17] View: https://facebook.github.io/react-vr/docs/view.html

[18] это: https://blog.pusher.com/wp-content/uploads/2017/05/building-a-realtime-react-vr-app-trees.gif

[19] VrButton: https://facebook.github.io/react-vr/docs/vrbutton.html

[20] Animated.View: https://facebook.github.io/react-vr/docs/animated.html

[21] Box: https://facebook.github.io/react-vr/docs/box.html

[22] Sphere: https://facebook.github.io/react-vr/docs/sphere.html

[23] 3D-модели: https://facebook.github.io/react-vr/docs/model.html

[24] здесь: https://github.com/facebook/react-vr/blob/master/ReactVR/js/Audio/getSupportedFormats.js

[25] Freesound: https://www.freesound.org/

[26] птицу: https://www.freesound.org/people/funkymuskrat/sounds/217050/

[27] другую птицу: https://www.freesound.org/people/dobroide/sounds/34162/

[28] ещё одну птицу: https://www.freesound.org/people/dobroide/sounds/59367/

[29] кошку: https://www.freesound.org/people/NoiseCollector/sounds/4911/

[30] сверчка: https://www.freesound.org/people/Adam_N/sounds/164482/

[31] VrSoundEffects: https://github.com/facebook/react-vr/blob/master/Libraries/Utilities/VrSoundEffects.js

[32] onClickSound: https://facebook.github.io/react-vr/docs/vrbutton.html#onclicksound

[33] Sound: https://facebook.github.io/react-vr/docs/sound.html

[34] MediaPlayerState: https://facebook.github.io/react-vr/docs/mediaplayerstate.html

[35] https://pusher.com/signup: https://pusher.com/signup

[36] Web Worker: https://www.w3schools.com/HTML/html5_webworkers.asp

[37] видео: https://www.youtube.com/watch?v=KHx-32m50c4

[38] http://localhost:8081/vr/?channel=1234: http://localhost:8081/vr/?channel=1234

[39] нативным модулем: https://facebook.github.io/react-vr/docs/native-modules.html

[40] Location: https://github.com/facebook/react-vr/blob/master/ReactVR/js/Modules/Location.js

[41] здесь: https://pusher.com/docs/server_api_guide/server_excluding_recipients

[42] чтения параметров URL: http://stackoverflow.com/a/901144/3593852

[43] https://facebook.github.io/react-vr/docs/publishing.html: https://facebook.github.io/react-vr/docs/publishing.html

[44] Источник: https://habrahabr.ru/post/331816/