VR-тур на A-Frame + React
Всем привет! Меня зовут Егор Молчанов, я разработчик в команде 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-инспектор.
При выделении элемента появляется интерфейс с текущим позиционированием и вращением элемента, и оси координат над текущим элементом в виде стрелок, с помощью которых можно двигать текущий объект. В боковой панели будут отображаться координаты текущего местоположения в пространстве и все значения атрибутов.
Интерфейс с текущим позиционированием.
Таким образом можно наглядно перетаскивать объекты в пространстве, а потом переносить эти значения в код.
Создаём структуру проекта и базовую сцену
Для начала устанавливаем все необходимые зависимости:
"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‑Frame добавляются через префикс а‑
.
a‑scene — корневой объект, все объекты находятся внутри сцены.
cursor — с параметром
rayOrigin: mouse
разрешает нам клик по объектам с помощью мышки, без этого свойства клик может быть только через
, о нём поговорим позже.raycaster — указываем все кликабельные объекты, можно перечислять любые элементы через точку с запятой. Для простоты я указал класс
.clickable
который буду указывать на объектах , с которыми можно взаимодействовать.a‑light — управляет освещением и тенью. По умолчанию A‑Frame сам определяет источник света и создаёт тень на объектах в зависимости от расположения камеры. Так как мне это не нужно, с помощью
type='ambient'
сделал окружающий свет всегда постоянным, без теней.a‑sky — сфера, на которую мы натягиваем изображение 360 °.
Определяем radius
и position
нашей сферы, и указываем путь до нашей динамически меняющейся картинки в зависимости от комнаты.
Сфера с изображением 360 °.
Предварительная загрузка изображений
У меня в проекте будет три изображения, которые всегда будут показаны пользователю, и до начала старта приложения хотелось бы заранее загружать эти изображения.
В A‑Frame для перезагрузки изображения используется тег
:
a‑assets — внутри указываем наши изображения в теге
и обязательно проставляем ID, так как дальше мы будем вставлять изображения, используя этот индификатор.
timeout — указываем, сколько миллисекунд максимум ждём загрузки изображения, иначе показываем сцену как есть.
Реализация перемещения по комнатам
В файле Arrow.js создаём компонент стрелки:
const Arrow = props => {
return (
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 — добавляет плоское изображение.
onClick — передаём функцию, которая будет менять нам сцену.
class='clickable' — указываем, чтобы объект был кликабельный, о чём говорили ранее.
В файле App.js реализуем функцию, которая меняет сцену:
const eventHandler = event => {
setEvent(data[event])
}
Далее реализуем функцию, которая возвращает стрелки для навигации по комнатам:
const getArrow = () => {
return event.nextScense.map(e => {
return (
)
})
}
Внутри
вызываем {getArrow()}
, и теперь мы можем передвигаться по нашим комнатам.
Режим VR
Чтобы менять режим VR, нам нужно получить элемент
. Для этого создаём новый state isVrMode
и хук useRef
.
const [isVrMode, setVrMode] = useState(false)
const scenesRef = useRef(null)
Присваиваем атрибут ref
:
В useEffect
сделаем логику переключения режимов:
useEffect(() => {
isVrMode ? scenesRef?.current.enterVR() : scenesRef?.current.exitVR()
}, [isVrMode])
При изменениях флага isVrMode
будет срабатывать useEffect
и менять режим у .
Теперь нужно создать кнопку, которая будет менять флаг. Для удобства её нужно закрепить за камерой, чтобы не искать где‑то в пространстве. Пишем следующий код внутри
:
setVrMode(!isVrMode)}
src='#vr'
position='-1.5 2.7 -4'
rotation='0 0 0'
width='1'
height='1'
/>
a‑camera — определяет глаза пользователя. Всё, что положим внутрь, будет работать как
position: fixed
в CSS.a‑image — по аналогии, как делали в стрелках.
Добавляем VR-курсор: внутри
пишем
. Теперь у нас всегда посередине экрана отображается колечко, в основном оно нужно для VR-режима, чтобы взаимодействовать с объектами, но можно и всегда показывать. На мобильных устройствах VR-режим будет выглядеть так:
VR-режим на мобильных устройствах.
Реализация информационных табличек
Таблички реализуем так же, как и стрелки, но при клике на них будет открываться модальное окно, закреплённое на камере. В файле Info.js создаём компонент точно такой же, как и у стрелок:
const Info = props => {
return (
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 (
props.openModal()}
class='clickable'>
)
}
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('')
Создаём функцию, которая возвращает нам компонент
:
const getInfo = () => {
return event?.info?.map(e => {
return (
)
})
}
Вызываем рядом с getArrow()
новую функцию getInfo()
. Реализовываем функции смены текста и при его изменении меняем флаг isOpenInfo
:
const openModal = textModal => {
setTextModal(textModal)
}
useEffect(() => {
setOpenInfo(!!textModal)
}, [textModal])
Внутри
пишем:
{isOpenInfo ? : null}
У нас получилось такое окно с текстом:
Открытое модальное окно.
Заключение
A‑Frame — довольно простой инструмент для создания VR-приложения. Небольшими манипуляциями получилось создать простенький VR‑тур с перемещением по комнатам и небольшим интерактивом в виде информационных табличек.
Запись из приложения.
Также A‑Frame можно использовать для создания более сложных приложений, тех же игр. И использовать совместно с другими фреймворками, такими как Angular и Vue.
Проект можно посмотреть на GitHub.