Pull to refresh

VR-тур на A-Frame + React

Level of difficultyEasy
Reading time7 min
Views2.5K

Всем привет! Меня зовут Егор Молчанов, я разработчик в команде CRM для менеджеров ипотечного кредитования в компании Домклик. Хочу поделиться своим опытом создания VR‑тура с помощью фреймворка A‑Frame и библиотеки React. Для этого написал свой небольшой pet‑проект, который мы сейчас разберём.

Подробнее про инструменты

A‑Frame — это фреймворк для разработки виртуальной реальности. Простой и эффективный инструмент разработки VR‑контента. Он основан на HTML, что упрощает работу. У него огромное количество заготовленных разнообразных компонентов, формирующих необыкновенную структуру с возможностью расширения
и модификации. Фреймворк создан на чистом WebGL, в его основе лежит библиотека Three.js. Он формирует трёхмерную сцену через комплект геометрических примитивов, таких как цилиндр, куб или сфера.

React — это библиотека для разработки пользовательских интерфейсов.

ТЗ проекта

Создать сайт, где можно пройтись по различным помещениям, покрутить головой на 360 градусов, разместить информационные таблички и реализовать вход и выход из VR-режима.

Позиционирование в трёхмерном пространстве

Сначала немного теории. На сайте с поддержкой виртуальной реальности мы находимся в трёхмерной системе координат. Для того, чтобы разместить в пространстве элемент в A‑Frame, мы используем атрибут position, в котором указываем координаты (x, y, z), отталкиваясь от изначальной точки (0, 0, 0), как показано на рисунке ниже.

Трёхмерная система координат.
Трёхмерная система координат.

Three.js и WebGL изначально не имеют единиц измерения, но принято считать одну единицу равной одному метру.

Следующим важным атрибутом является rotation, который отвечает за вращения объекта вокруг своей оси по координатам (x, y, z). Единица измерения вращения — градусы.

Вращение объекта вокруг своей оси.
Вращение объекта вокруг своей оси.

Визуальный 3D-инспектор A-Frame

При реализации проекта будут встречаться такие атрибуты как position, rotation, width, height, scale, radius и так далее. Сразу хочу рассказать, откуда берутся значения для этих атрибутов. Есть такой замечательный инструмент как визуальный 3D‑инспектор, который встроен в A‑Frame. При нажатии Ctrl + Alt + i откроется вот такое окно:

Визуальный 3D-инспектор.
Визуальный 3D-инспектор.

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

 Интерфейс с текущим позиционированием.
Интерфейс с текущим позиционированием.

Таким образом можно наглядно перетаскивать объекты в пространстве, а потом переносить эти значения в код.

Создаём структуру проекта и базовую сцену

Для начала устанавливаем все необходимые зависимости:

"aframe": "^1.4.2",
"react": "^18.2.0",
"react-dom": "^18.2.0"

В файле data.js создаём структуру наших комнат в виде объекта:

export default {
  room_1: {
    nameScense: 'room_1',
    nextScense: [
      {
        position: '-2.110 -0.500 -5.990',
        rotation: '0 0 0',
        next: 'room_2'
      }
    ],
    info: [
      {
        position: '-2.679 1.000 -1.201',
        rotation: '0 90 0',
        textModal: 'Здесь находится ресепшен'
    ]
  },
  room_2: {
...
  • nameScense — имя комнаты, потребуется для изображений 360 °.

  • nextScens — массив будущих стрелок для перехода в другие комнаты. В объекте указываем позиционирование нашей стрелки в пространстве и в какую комнату перейдём.

  • info — массив будущих информационных табличек, указываем позиционирование и текст.

В файле App.js определяем state нашей комнаты:

const [event, setEvent] = useState(data.room_1)

Создаём сцену:

<a-scene cursor='rayOrigin: mouse;' raycaster='objects: .clickable'>
  <a-light type='ambient' />
  <a-sky 
    position='0 1 -10' 
    rotation='0 0 0' 
    src={`assets/img/${event.nameScense}.jpg`}
    radius='50' />
</a-scene>

Все элементы A‑Frame добавляются через префикс а‑.

  • a‑scene — корневой объект, все объекты находятся внутри сцены.

  • cursor — с параметром rayOrigin: mouse разрешает нам клик по объектам с помощью мышки, без этого свойства клик может быть только через <a-cursor>, о нём поговорим позже.

  • raycaster — указываем все кликабельные объекты, можно перечислять любые элементы через точку с запятой. Для простоты я указал класс .clickable который буду указывать на объектах ,с которыми можно взаимодействовать.

  • a‑light — управляет освещением и тенью. По умолчанию A‑Frame сам определяет источник света и создаёт тень на объектах в зависимости от расположения камеры. Так как мне это не нужно, с помощью type='ambient' сделал окружающий свет всегда постоянным, без теней.

  • a‑sky — сфера, на которую мы натягиваем изображение 360 °.

Определяем radius и position нашей сферы, и указываем путь до нашей динамически меняющейся картинки в зависимости от комнаты.

Сфера с изображением 360 °.
Сфера с изображением 360 °.

Предварительная загрузка изображений

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

В A‑Frame для перезагрузки изображения используется тег <a-assets>:

<a-assets timeout='5000'>
  <img id='info' src='assets/img/info.png' />
  <img id='vr' src='assets/img/vr.png' />
  <img id='arrow' src='assets/img/arrow.svg' />
</a-assets>
  • a‑assets — внутри указываем наши изображения в теге <img> и обязательно проставляем ID, так как дальше мы будем вставлять изображения, используя этот индификатор.

  • timeout — указываем, сколько миллисекунд максимум ждём загрузки изображения, иначе показываем сцену как есть.

Реализация перемещения по комнатам

В файле Arrow.js создаём компонент стрелки:

const Arrow = props => {
  return (
    <a-image
      class='clickable'
      onClick={() => props.eventHandler(props.next)}
      src='#arrow'
      position={props.position}
      rotation={props.rotation}
      width='0.8'
      height='0.65'
      scale='1 1 1'
    />
  )
}

export default Arrow

Устанавливаем ширину, высоту и масштаб с помощью атрибутов width, height и scale.

  • a‑image — добавляет плоское изображение.

  • src — указываем ID, который указывали в <a‑assets>.

  • onClick — передаём функцию, которая будет менять нам сцену.

  • class='clickable' — указываем, чтобы объект был кликабельный, о чём говорили ранее.

В файле App.js реализуем функцию, которая меняет сцену:

const eventHandler = event => {
    setEvent(data[event])
  }

Далее реализуем функцию, которая возвращает стрелки для навигации по комнатам:

const getArrow = () => {
  return event.nextScense.map(e => {
    return (
      <Arrow 
        position={e.position} 
        rotation={e.rotation} 
        next={e.next} 
        eventHandler={eventHandler} 
        key={e.position} />
    )
  })
}

Внутри <a-scene> вызываем {getArrow()}, и теперь мы можем передвигаться по нашим комнатам.

Режим VR

Чтобы менять режим VR, нам нужно получить элемент <a-scene>. Для этого создаём новый state isVrMode и хук useRef .

const [isVrMode, setVrMode] = useState(false)
const scenesRef = useRef(null)

Присваиваем атрибут ref:

<a-scene ref={scenesRef} cursor='rayOrigin: mouse;' raycaster='objects: .clickable'>

В useEffect сделаем логику переключения режимов:

useEffect(() => {
    isVrMode ? scenesRef?.current.enterVR() : scenesRef?.current.exitVR()
}, [isVrMode])

При изменениях флага isVrMode будет срабатывать useEffect и менять режим у <a‑scene>.

Теперь нужно создать кнопку, которая будет менять флаг. Для удобства её нужно закрепить за камерой, чтобы не искать где‑то в пространстве. Пишем следующий код внутри <a-scene>:

<a-camera position='0 1 -1' rotation='0 0 0'>
  <a-image
    class='clickable'
    onClick={() => setVrMode(!isVrMode)}
    src='#vr'
    position='-1.5 2.7 -4'
    rotation='0 0 0'
    width='1'
    height='1'
  />
</a-camera>
  • a‑camera — определяет глаза пользователя. Всё, что положим внутрь, будет работать как position: fixed в CSS.

  • a‑image — по аналогии, как делали в стрелках.

Добавляем VR-курсор: внутри <a-camera> пишем <a-cursor />. Теперь у нас всегда посередине экрана отображается колечко, в основном оно нужно для VR-режима, чтобы взаимодействовать с объектами, но можно и всегда показывать. На мобильных устройствах VR-режим будет выглядеть так:

VR-режим на мобильных устройствах.
VR-режим на мобильных устройствах.

Реализация информационных табличек

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

const Info = props => {
  return (
    <a-image
      class='clickable'
      onClick={() => props.openModal(props.text)}
      src='#info'
      position={props.position}
      rotation={props.rotation}
      width='0.75'
      height='0.5'
    />
  )
}

export default Info

В файле Modal.js создаём окно:

const Modal = props => {
  return (
    <a-rounded
      position='-0.8 -0.3 -1.5'
      width='1.5'
      height='0.5'
      color='#fff'
      radius='0.07'
      onClick={() => props.openModal()}
      class='clickable'>
      <a-text
        align='left'
        position='0.1 0.3 0.007'
        width='0.7'
        wrap-count='40'
        scale='2 2 1'
        value={props.text}
        font='assets/fonts/Regular.fnt'
        color='black'
      />
    </a-rounded>
  )
}

export default Modal
  • a‑rounded — не встроен в A‑Frame, его загружал отдельно. Он реализует форму примитива, у которого можно закруглить углы c помощью атрибута radius. Фон устанавливается атрибутом color.

"aframe-rounded": "^1.0.3"
  • a‑text — компонент для вставки текста. Так как он вложен в a-rounded, позиционирование идёт от родителя.

  • align — выравнивает текст по левому краю.

  • wrap‑count — через какое количество символов делать перенос строки.

  • font — указываем шрифт.

В файле App.js устанавливаем state для показа модального окна и его текст:

const [isOpenInfo, setOpenInfo] = useState(false)
const [textModal, setTextModal] = useState('')

Создаём функцию, которая возвращает нам компонент <Info>:

const getInfo = () => {
  return event?.info?.map(e => {
    return (
      <Info 
        position={e.position} 
        rotation={e.rotation} 
        openModal={openModal} 
        text={e.textModal} 
        key={e.position} />
    )
  })
}

Вызываем рядом с getArrow() новую функцию getInfo(). Реализовываем функции смены текста и при его изменении меняем флаг isOpenInfo:

const openModal = textModal => {
  setTextModal(textModal)
}

useEffect(() => {
  setOpenInfo(!!textModal)
}, [textModal])

Внутри <a-camera> пишем:

{isOpenInfo ? <Modal text={textModal} openModal={openModal} /> : null}

У нас получилось такое окно с текстом:

Открытое модальное окно.
Открытое модальное окно.

Заключение

A‑Frame — довольно простой инструмент для создания VR-приложения. Небольшими манипуляциями получилось создать простенький VR‑тур с перемещением по комнатам и небольшим интерактивом в виде информационных табличек.

Запись из приложения.
Запись из приложения.

Также A‑Frame можно использовать для создания более сложных приложений, тех же игр. И использовать совместно с другими фреймворками, такими как Angular и Vue.

Проект можно посмотреть на GitHub.

Tags:
Hubs:
Total votes 19: ↑18 and ↓1+22
Comments2

Articles

Information

Website
domclick.ru
Registered
Founded
Employees
501–1,000 employees
Location
Россия
Representative
Евгения Макарова