Как разработчик программного обеспечения, я трачу время на создание веб-сайтов и веб-приложений, но в течение долгого времени меня также интересовала виртуальная реальность. Я назвал свой Oculus Go «Бетти» и у меня кружится голова от разговоров о поездках на гондоле по Венеции, прыжках на захватывающих дух американских горках и путешествиях по венам в человеческом теле. Поскольку я в основном использую React, я был взволнован, узнав, что могу разрабатывать опыт виртуальной реальности с помощью библиотеки, которую я уже знаю и люблю.

Чтобы попробовать свои силы в React VR, я недавно создал приложение виртуальной реальности под названием Найди свой Дзен, которое позволяет пользователю выбрать иммерсивную среду для медитации, каждая из которых имеет свою собственную мантру, вдохновленную прекрасным шоу. Хорошее место. В мае 2018 года, вскоре после того, как я создал свое приложение, Facebook выпустил обновленную и переименованную версию React VR под названием React 360 с множеством изменений и значительных улучшений.

Перенеся свое приложение на React 360, я обратил внимание на некоторые важные различия между React VR и React 360. Я написал следующую статью для разработчиков, которые обладают практическими знаниями React. Если вы не знакомы с библиотекой, я рекомендую сначала начать здесь.

Если вы хотите познакомиться с React VR (а также с Recompose, служебные функции которого помогают управлять состоянием моего приложения), вы можете найти их здесь и здесь.

Просмотр готового демонстрационного кода

$ git clone https://github.com/lilybarrett/find-your-zen.git
$ cd find-your-zen
$ npm i
$ npm start

Файловая структура

Базовая файловая структура React VR была следующей:

  • index.vr.js = точка входа для моего приложения
  • vr папка = хранит код, запускающий мое приложение, включая файлы index.htmlи client.js
  • static_assets = хранит изображения, аудиофайлы и другие внешние ресурсы

А вот новая файловая структура React 360:

  • index.js = точка входа для моего приложения
  • client.js = настраивает «среду выполнения», которая превращает мои компоненты React в трехмерные элементы в нашем ландшафте виртуальной реальности.
  • index.html = как в типичном приложении React, предоставляет мне место для монтирования моего кода React
  • static_assets = хранит изображения, аудиофайлы и другие внешние ресурсы

Я настроил остальную часть моей структуры папок следующим образом:

- components // shared components 
  - base-button
  - content
- consts
- providers // Recompose providers live here
- scenes
  - home-environment 
    - components
      - menu
      - title
      - zen-button
      - zens 
    - zen-environment
      - components
        - home-button
        - mantra 
- static-assets
  - images
  - sounds 

Общие компоненты находятся в папке верхнего уровня components. Хранится в scenes,my HomeEnvironment - первой загружаемой среде, где мой пользователь получает доступ к меню сред медитации для исследования - и ZenEnvironment сцены имеют свои собственные наборы соответствующих компонентов. Мое управление состоянием осуществляется с помощью Recompose providers и функционально объединяется в каждый компонент, которому требуется доступ к состоянию.

Установка приложения

В React VR мой client.js был довольно простым и не давал мне слишком много вариантов конфигурации:

// React VR application -- vr/client.js
// Auto-generated content.
// This file contains the boilerplate to set up your React app.
// If you want to modify your application, start in "index.vr.js"
import { VRInstance } from "react-vr-web";
function init(bundle, parent, options) {
   const vr = new VRInstance(bundle, "MeditationApp", parent, {
      cursorVisibility: "auto",
      // Add custom options here
      ...options,
   });
vr.render = function() {
      // Any custom behavior you want to perform on each frame goes  
      here
   };
   // Begin the animation loop
   vr.start();
   return vr;
}
window.ReactVR = {init};

В React 360 я могу монтировать контент моего приложения на поверхность или в другое место. Поверхности, как говорится в документации, позволяют добавлять 2D-интерфейсы в 3D-пространство, позволяя работать в пикселях, а не в физических измерениях. В моем случае я оборачиваю визуальный контент своего приложения в AppContent компонент, который монтирую на цилиндрическую поверхность React 360 по умолчанию. Эта поверхность проецирует содержимое на внутреннюю часть цилиндра, расположенного по центру перед пользователем, с радиусом 4 метра.

Я могу создавать свои собственные пользовательские поверхности в React 360, увеличивая или уменьшая радиус или делая поверхность плоской, а не цилиндрической.

Я также монтирую все приложение в расположение по умолчанию React 360, что позволяет моему приложению использовать все преимущества среды выполнения React 360.

Новая среда выполнения - одно из значительных преимуществ React 360 перед React VR. Почему? Отделение аспектов визуализации или «времени выполнения» приложения от кода приложения улучшает задержку: время между действием пользователя и временем обновления пикселей в представлении в ответ на это действие. Если передача данных идет слишком медленно, это приводит к прерывистому, дезориентирующему виду для пользователя - подобно буферизации видео на Youtube или статическим помехам на экране телевизора.

Как далее объясняется в документации React 360, веб-браузеры являются однопоточными, что означает, что в рамках незаметных обновлений приложения этот процесс может блокировать или препятствовать передаче данных. Это особенно проблематично для пользователей, просматривающих ваш 360-градусный вид на гарнитуре VR, где значительная задержка рендеринга может нарушить ощущение погружения, - говорится в документации. Запуская код вашего приложения в отдельном контексте, мы позволяем циклу рендеринга постоянно обновлять с высокой частотой кадров .

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

// components/content.js
import React from "react";
import { View } from "react-360";
import { HomeEnvironment, ZenEnvironment } from "../../scenes";
import { withAppContext } from "../../providers";
const AppContent = withAppContext(() => (
   <View>
      <HomeEnvironment />
      <ZenEnvironment />
   </View>
));
export default AppContent;
// index.js
import React from "react";
import {
   AppRegistry,
   View,
} from "react-360";
import { AppContent } from "./components";
import { withAppContext } from "./providers";
const MeditationApp = withAppContext(() => (
   <View style={{
      transform: [{ translate: [0, 0, -2] }]
   }}>
      <AppContent />
   </View>
));
AppRegistry.registerComponent("AppContent", () => AppContent);
AppRegistry.registerComponent("MeditationApp", () => MeditationApp);

Мой client.js занимается установкой моего компонента на места и поверхности:

// client.js
import { ReactInstance, Surface } from "react-360-web";
function init(bundle, parent, options = {}) {
   const r360 = new ReactInstance(bundle, parent, {
      fullScreen: true,
      // Add custom options here
      ...options,
   });
   r360.renderToSurface(
      r360.createRoot("AppContent", { /* initial props */ }),
      r360.getDefaultSurface()
   );
   r360.renderToLocation(
      r360.createRoot("MeditationApp", { /* initial props */ }),
      r360.getDefaultLocation(),
   );
   r360.compositor.setBackground(
      r360.getAssetURL("images/homebase.png")
   );
}
window.React360 = {init};

Воспроизведение аудио

В моей папке consts я создал файл zens.js, в котором можно быстро сохранить мои данные, включая правильный аудиофайл и изображение, для каждой среды:

const zens = [
   { id: 1,
     mantra: "Find your inner motherforking peace",
     image: "images/hawaii_beach.jpg",
     audio: "sounds/waves.mp3",
     text: "I'm feeling beachy keen",
   },
   { id: 2,
     mantra: "Breathe in peace, breathe out bullshirt",
     image: "images/horseshoe_bend.jpg",
     audio: "sounds/birds.mp3",
     text: "Ain't no mountain high enough",
   },
   { id: 3,
     mantra: "Benches will be benches",
     image: "images/sunrise_paris_2.jpg",
     audio: "sounds/chimes.mp3",
     text: "I want a baguette",
   },
   { id: 4,
     image: "images/homebase.png",
     text: "Home"
   }
]
export default zens;

Для воспроизведения звука в сценах React VR я использовал компонент Sound, который принимал URL-адрес звукового файла в папке static_assets в качестве source опоры. Чтобы предотвратить воспроизведение звука в средах, которым он не принадлежит, например, в домашней среде, я реализовал логику через Recompose для «скрытия» и «отображения» компонента Sound в зависимости от того, находимся ли мы в среде без аудиофайлов. связанные с ним.

// React VR -- components/audio.js
import React from "react";
import { Sound } from "react-vr";
import zens from "../consts/zens.js";
import { compose } from "recompose";
import { asset } from "react-vr";
import { hideIf, usingAppContext } from "../providers/index.js";
const hideIfNoAudioUrl = hideIf(({ selectedZen }) => {
   const zenAudio = zens[selectedZen - 1].audio;
   return zenAudio === null || zenAudio === undefined || zenAudio.length === 0;
});
export default compose(
   usingAppContext,
   hideIfNoAudioUrl,
)(({ selectedZen }) => {
   const zenAudio = zens[selectedZen - 1].audio;
   return (
      <Sound source={asset(zenAudio)} />
   )
});

React 360 значительно улучшает это. Для воспроизведения звука я использую модуль AudioModuleNative. Его playEnvironmental метод позволяет мне указать путь (к аудио в папке с ресурсами) и громкость, на которой будет воспроизводиться этот звук в циклическом темпе. Как только аудиофайл перестанет воспроизводиться, он начнется снова.

Попутно я понял, что мне нужно сообщить своему приложению, когда нужно остановить воспроизведение определенного аудиофайла при переключении сцен. (В противном случае, погружаясь в Find Your Zen, вы можете закончить прослушивание звука из вашей предыдущей среды, то есть церковных колоколов на городской площади в Париже, после того, как вы вернетесь в домашнюю среду). Я достигаю этого с помощью метода AudioModule stopEnvironmental.

Продолжайте читать, чтобы увидеть это в действии…

Использование изображений

В React VR я использовал компонент Pano для отображения фотографии на 360 градусов. Чтобы отобразить конкретное изображение, Pano, как и Audio, использовал URL-адрес ресурсов как sourceprop. В зависимости от того, какую среду выбрал пользователь, состояние приложения обновляется для отображения изображения для этой среды.

// React VR -- components/wrapped-pano.js
import React from "react";
import { Pano } from "react-vr";
import { usingAppContext } from "../providers/index.js";
import { Audio } from "../components/index.js";
import zens from "../consts/zens.js";
import { asset } from "react-vr";
export default usingAppContext(({ selectedZen }) => {
   return (
      <Pano source={asset(zens[selectedZen - 1].image)} >
         <Audio />
      </Pano>
   )
});

Возможно, вы заметили или не заметили, что в моем приложении React 360 client.js я пишу следующую строку после рендеринга компонентов моего приложения:

r360.compositor.setBackground(r360.getAssetURL("images/homebase.png"));

Эта строка кода, которая сразу устанавливает фоновое изображение при первом подключении приложения, использует утилиту asset из React 360 для автоматического поиска нужного изображения в папке mystatic_assets.

Это все хорошо, но я все же хочу изменить изображение в зависимости от того, какую среду выбирает пользователь. К счастью, я могу обрабатывать динамические изображения из события React с помощью модуля Environment React 360. Вот пример использования:

Environment.setBackgroundImage(asset(someImage));

Чтобы собрать все это воедино, вот как я динамически настраиваю фоновое изображение и звук в зависимости от среды, которую выбирает пользователь, с помощью функций Recompose withState и withHandlers:

// providers/withStateAndHandlers.js
import React from "react";
import { withState, withHandlers, compose } from "recompose";
import { Environment, asset, NativeModules } from "react-360";
const { AudioModule } = NativeModules;
import { zens } from "../consts";
const withStateAndHandlers = compose(
   withState("selectedZen", "zenClicked", 4),
   withHandlers({
      zenClicked: (props) => (id, evt) => {
         Environment.setBackgroundImage(asset(zens[id - 1].image));
         if (zens[id - 1].audio !== null && zens[id - 1].audio !== undefined) {
            AudioModule.playEnvironmental({
               source: asset(zens[id - 1].audio),
               volume: 0.3,
            });
           } else {
               AudioModule.stopEnvironmental();
           }
         props.zenClicked(selectedZen => id);
      }
   }),
)
export default withStateAndHandlers;

Стилизация приложения

React 360, как и React VR, использует Flexbox, чтобы легко адаптировать макет приложения к любому дисплею, будь то веб-браузер ноутбука, экран телефона или гарнитура VR. Однако для частей приложения, установленных в определенном месте - например, MeditationApp в моем случае - React 360 переключается с макета Flexbox на трехмерную систему координат, основанную на счетчике. Вот почему вы видите этот код в моем index.js:

// index.js
// other code goes here
const MeditationApp = withAppContext(() => (
   <View style={{
      transform: [{ translate: [0, 0, -2] }]
   }}>
      <AppContent />
   </View>
));
// other code goes here

В transform передаются значения x, y и z в указанном порядке. x представляет ориентацию объекта справа от пользователя; y представляет ориентацию вверх или вниз, а z представляет собой воспринимаемое расстояние от пользователя.

В приведенном выше примере View должен быть в центре и на 2 метра впереди пользователя.

Все трансформации расположены относительно своих родителей.

Практики, которые мне понравились

Таблицы стилей

StyleSheet из react-native позволил мне использовать JavaScript для стилизации моих компонентов React. Смотрите мой код ниже:

// scenes/home-environment/components/zen-button/style.js
import { StyleSheet } from "react-360";
export default StyleSheet.create({
   text: {
      backgroundColor: "#29ECCE",
      textAlign: "center",
      color: "white",
      marginTop: 30
   }
})

Здесь я создаю и экспортирую объект StyleSheet, который позволяет мне кратко, СУХИМ образом ссылаться на стили в самом моем компоненте.

// scenes/home-environment/components/zen-button/index.js
import React from "react";
import { BaseButton } from "../../../../components";
import style from "./style";
const ZenButton = ({ text, buttonClick, selectedZen }) => {
   return (
      <BaseButton
         text={text}
         selectedZen={selectedZen}
         buttonClick={buttonClick}
         textStyle={style.text}
      />
   )
}
export default ZenButton;

Управление государством

Поскольку, в конце концов, это все еще просто React, вы можете подойти к обработке состояния так же, как и в типичном приложении React: Redux, Recompose, Mobx и т. Д. Я решил использовать Перекомпонуйте, потому что мне нравится, как это позволяет мне создавать функциональные компоненты. Как упоминалось ранее, я написал несколько сообщений о Recompose в контексте React VR, которые вы можете найти здесь и здесь. Мне не нужно было ничего менять в моем подходе к управлению состоянием при переносе моего приложения с React VR на React 360.

Отладка React 360

Когда вы Inspect Element в приложении, вы увидите, что React 360 объединяет все свои файлы в один гигантский объект, который не так-то просто разобрать. К счастью, поскольку React 360 поддерживает исходные карты, мы все еще можем получить доступ к исходным файлам, использовать debugger и т. Д.