Информационный портал по безопасности » Программирование » Веб-разработка » Создание React VR-приложения, работающего в реальном времени

 

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

Автор: admin от 30-06-2017, 17:35, посмотрело: 528

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

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



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



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



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





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



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



Ссылки на скачивание (чтобы просто попробовать):

React VR-проект.

Node.js-бэкенд.



Настраиваем 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/. Должно появиться такое:



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



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



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



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



Создаём фон



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



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



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



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


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



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



Для эмулирования дерева воспользуемся Cylinder. По факту нам их потребуется сотня, чтобы получился лес вокруг пользователя. В оригинальной Musical Forest в файле js/components/background-objects.js можно найти алгоритм, генерирующий деревья. Если адаптировать код под 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 — массив значений цветов деревьев.



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



Можно сохранить код в файле 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>
        );
      }
    };

    ...


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



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



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



Для создания форм нам нужны VrButton, Animated.View, Box, Cylinder и Sphere. Но поскольку все формы будут отличаться, просто инкапсулируем в компонент, это будет то же самое. Сохраните следующий код в файл 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 (
          [leech=https://facebook.github.io/react-vr/docs/model.html]3D-модели[/leech].</p>

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

[code]    <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>


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



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



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



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



Помимо прочего, React VR поддерживает wav, mp3 и ogg-файлы. Полный список есть здесь.

Можно взять сэмплы с Freesound или другого подобного сайта. Скачайте, какие вам нравятся, и поместите в директорию static_assets/sounds. Для нашего проекта возьмём звуки шести животных, птицу, другую птицу, ещё одну птицу, кошку, собаку и сверчка (последний файл пришлось пересохранить, чтобы уменьшить битрейт, иначе React VR его не проигрывал).



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





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



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

    export default class SoundShape extends React.Component {
      ...
      render() {
        return (
          [leech=https://facebook.github.io/react-vr/docs/mediaplayerstate.html]MediaPlayerState[/leech]. Они будут передаваться как свойства компонента.</p>

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

[code]    ...
    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. Когда вы создаёте приложение, вас попросят кое-что сконфигурировать:



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



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



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



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



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



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



React VR работает как Web Worker (подробнее об архитектуре React VR в видео), так что нам надо включить скрипт 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), чтобы пользователи могли выбирать, в какие каналы заходить и делиться ими с друзьями.



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



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



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



Таким образом, немного адаптировав функцию getParameterByName для чтения параметров URL и сохранив 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. При клике на 3D-форму вы услышите дважды проигранный звук (это куда веселее делать с друзьями на разных компьютерах):





Заключение



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



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



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



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



Источник: Хабрахабр

Категория: Программирование / Веб-разработка

Уважаемый посетитель, Вы зашли на сайт как незарегистрированный пользователь.
Мы рекомендуем Вам зарегистрироваться либо войти на сайт под своим именем.

Добавление комментария

Имя:*
E-Mail:
Комментарий:
  • bowtiesmilelaughingblushsmileyrelaxedsmirk
    heart_eyeskissing_heartkissing_closed_eyesflushedrelievedsatisfiedgrin
    winkstuck_out_tongue_winking_eyestuck_out_tongue_closed_eyesgrinningkissingstuck_out_tonguesleeping
    worriedfrowninganguishedopen_mouthgrimacingconfusedhushed
    expressionlessunamusedsweat_smilesweatdisappointed_relievedwearypensive
    disappointedconfoundedfearfulcold_sweatperseverecrysob
    joyastonishedscreamtired_faceangryragetriumph
    sleepyyummasksunglassesdizzy_faceimpsmiling_imp
    neutral_faceno_mouthinnocent