VR-тур на A-Frame + React
- пятница, 9 февраля 2024 г. в 00:00:18

Всем привет! Меня зовут Егор Молчанов, я разработчик в команде 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). Единица измерения вращения — градусы.

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

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

Таким образом можно наглядно перетаскивать объекты в пространстве, а потом переносить эти значения в код.
Для начала устанавливаем все необходимые зависимости:
"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 нашей сферы, и указываем путь до нашей динамически меняющейся картинки в зависимости от комнаты.

У меня в проекте будет три изображения, которые всегда будут показаны пользователю, и до начала старта приложения хотелось бы заранее загружать эти изображения.
В 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, нам нужно получить элемент <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-режим будет выглядеть так:

Таблички реализуем так же, как и стрелки, но при клике на них будет открываться модальное окно, закреплённое на камере. В файле 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 Modala‑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.