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 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.