Использование контроллеров VR с WebVR
Устарело: Эта функция больше не рекомендуется. Хотя некоторые браузеры могут ее все еще поддерживать, она может быть уже удалена из соответствующих веб-стандартов, может быть в процессе удаления или может поддерживаться только для совместимости. Избегайте ее использования и, при возможности, обновите существующий код; см. таблицу совместимости в нижней части этой страницы для принятия решения. Имейте в виду, что эта функция может перестать работать в любой момент.
Во многих наборах оборудования WebVR есть контроллеры, которые используются вместе с гарнитурой. Их можно использовать в приложениях WebVR через API Gamepad, а именно через API расширений Gamepad, который добавляет возможности API для доступа к позе контроллера, тактильным актуаторам и многому другому. Эта статья объясняет основы.
Примечание: API WebVR заменен на API WebXR. WebVR никогда не был ратифицирован как стандарт, был реализован и включен по умолчанию в очень небольшом количестве браузеров и поддерживал небольшое количество устройств.
API WebVR
API WebVR — это новая, но очень интересная функция веб-платформы, которая позволяет разработчикам создавать веб-приложения виртуальной реальности. Это делается путем предоставления доступа к гарнитурам VR, подключенным к вашему компьютеру, как VRDisplay объекта, которые можно использовать для запуска и остановки презентации на дисплее, запроса данных о движении (например, ориентации и позиции), которые могут быть использованы для обновления отображения в каждом кадре цикла анимации и многого другого.
Прежде чем читать эту статью, вы должны быть знакомы с основами API WebVR. Сначала прочитайте Использование API WebVR, если вы этого еще не сделали, в котором также подробно описана поддержка браузеров и необходимая настройка оборудования.
API Gamepad
API Gamepad — это API, достаточно хорошо поддерживаемый, который позволяет разработчикам получать доступ к геймпадам/контроллерам, подключенным к вашему компьютеру, и использовать их для управления веб-приложениями. Основной API Gamepad предоставляет доступ к подключенным контроллерам как к Gamepad объектам, которые затем можно запросить, чтобы узнать, какие кнопки нажаты, и как движутся джойстики (оси) в любой момент и т. д.
Дополнительную информацию об использовании основного API Gamepad можно найти в Использование API Gamepad и Реализация управления с помощью API Gamepad.
Однако в этой статье мы в основном будем сосредоточены на некоторых новых функциях, предоставляемых API расширений Gamepad, который позволяет получить доступ к расширенной информации о контроллере, такой как данные о позиции и ориентации, управление тактильными актуаторами (например, вибрация оборудования) и многое другое. Этот API очень новый и в настоящее время поддерживается и включен по умолчанию только в Firefox 55+ каналах Beta/Nightly.
Типы контроллеров
Существует два типа контроллеров, которые вы встретите при работе с оборудованием VR:
- Контроллеры 6DoF (шесть степеней свободы) предоставляют доступ к данным о положении и ориентации — они могут управлять VR-сценой и объектами в ней с помощью движения, а также поворота. Хорошим примером являются контроллеры HTC VIVE.
- Контроллеры 3DoF (три степени свободы) обеспечивают ориентацию, но не данные о положении. Примером является контроллер Google Daydream, который можно поворачивать для указания на разные объекты в 3D-пространстве, как лазерную указку, но нельзя перемещать внутри 3D-сцены.
Базовый доступ к контроллерам
Теперь перейдем к коду. Сначала рассмотрим основы того, как получить доступ к контроллерам VR с помощью API Gamepad. Здесь есть несколько нюансов, которые стоит учитывать.
Мы подготовили пример, чтобы продемонстрировать его — см. наш исходный код vr-controller-basic-info (посмотреть его в работе здесь). Эта демонстрация выводит информацию о VR-дисплеях и геймпадах, подключенных к вашему компьютеру.
Получение информации о дисплее
Первый примечательный код выглядит следующим образом:
let initialRun = true;
if (navigator.getVRDisplays && navigator.getGamepads) {
info.textContent = "WebVR API and Gamepad API supported.";
reportDisplays();
} else {
info.textContent =
"WebVR API and/or Gamepad API not supported by this browser.";
}
Здесь мы сначала используем переменную отслеживания, initialRun, чтобы отметить, что это первый раз, когда мы загрузили страницу. Вы узнаете больше об этом позже. Далее мы определяем, поддерживаются ли API WebVR и Gamepad, проверяя наличие методов Navigator.getVRDisplays() и Navigator.getGamepads(). Если да, то мы запускаем нашу reportDisplays() пользовательскую функцию, чтобы начать процесс. Эта функция выглядит так:
function reportDisplays() {
navigator.getVRDisplays().then((displays) => {
console.log(`${displays.length} displays`);
displays.forEach((display, i) => {
const cap = display.capabilities;
// cap is a VRDisplayCapabilities object
const listItem = document.createElement("li");
listItem.innerText = `
VR Display ID: ${display.displayId}
VR Display Name: ${display.displayName}
Display can present content: ${cap.canPresent}
Display is separate from the computer's main display: ${cap.hasExternalDisplay}
Display can return position info: ${cap.hasPosition}
Display can return orientation info: ${cap.hasOrientation}
Display max layers: ${cap.maxLayers}`;
listItem.insertBefore(
document.createElement("strong"),
listItem.firstChild,
).textContent = `Display ${i + 1}`;
list.appendChild(listItem);
});
setTimeout(reportGamepads, 1000);
// For VR, controllers will only be active after their corresponding headset is active
});
}
Эта функция сначала использует основанный на обещаниях метод Navigator.getVRDisplays(), который возвращает массив, содержащий VRDisplay объекты, представляющие подключенные дисплеи. Затем она выводит значения VRDisplay.displayId и VRDisplay.displayName каждого дисплея и ряд полезных значений, содержащихся в связанном объекте VRDisplayCapabilities дисплея. Наиболее полезными из них являются hasOrientation и hasPosition, которые позволяют обнаружить, может ли устройство вернуть данные об ориентации и позиции, и настроить ваше приложение соответственно.
Последняя строка в этой функции — это вызов setTimeout(), который запускает функцию reportGamepads() через 1 секунду. Зачем нам это нужно? Во-первых, контроллеры VR будут готовы только после активации связанной с ними VR-гарнитуры, поэтому нам нужно вызвать это после того, как getVRDisplays() была вызвана и вернула информацию о дисплее. Во-вторых, API Gamepad намного старше API WebVR и не основан на обещаниях. Как вы увидите позже, метод getGamepads() является синхронным и сразу же возвращает объекты Gamepad — он не ждет, пока контроллер будет готов сообщить информацию. Если вы не подождете немного, возвращаемая информация может быть неточной (по крайней мере, это то, что мы обнаружили в наших тестах).
Получение информации о геймпаде
Функция reportGamepads() выглядит так:
function reportGamepads() {
const gamepads = navigator.getGamepads();
console.log(`${gamepads.length} controllers`);
for (const gp of gamepads) {
const listItem = document.createElement("li");
listItem.classList = "gamepad";
listItem.innerText = `
Associated with VR Display ID: ${gp.displayId}
Gamepad associated with which hand: ${gp.hand}
Available haptic actuators: ${gp.hapticActuators.length}
Gamepad can return position info: ${gp.pose.hasPosition}
Gamepad can return orientation info: ${gp.pose.hasOrientation}`;
listItem.insertBefore(
document.createElement("strong"),
}),
listItem.firstChild,
).textContent = `Gamepad ${gp.index}`;
list.appendChild(listItem);
}
initialRun = false;
}
Она работает аналогично reportDisplays() — мы получаем массив объектов Gamepad с помощью не основанного на обещаниях метода getGamepads(), затем перебираем каждый из них и выводим информацию о каждом:
- Свойство
Gamepad.displayIdсовпадает сdisplayIdгарнитуры, с которой связан контроллер, и поэтому полезно для связывания информации о контроллере и гарнитуре. - Свойство
Gamepad.index— это уникальный числовой индекс, который идентифицирует каждый подключенный контроллер. -
Gamepad.handвозвращает, в какую руку ожидается держать контроллер. -
Gamepad.hapticActuatorsвозвращает массив доступных тактильных актуаторов контроллера. Здесь мы возвращаем его длину, чтобы увидеть, сколько каждого доступно. - Наконец, мы возвращаем
GamepadPose.hasPositionиGamepadPose.hasOrientation, чтобы показать, может ли контроллер вернуть данные о позиции и ориентации. Это работает так же, как и для дисплеев, за исключением того, что в случае с геймпадами эти значения доступны в объекте pose, а не в объекте capabilities.
Обратите внимание, что мы также добавили каждому пункту списка, содержащему информацию о контроллере, имя класса gamepad. Мы объясним, для чего это, позже.
Последнее, что нужно сделать, — установить переменную initialRun в false, поскольку начальный запуск завершен.
События Gamepad
Для завершения этого раздела рассмотрим связанные с геймпадом события. Нам нужно будет рассмотреть два — gamepadconnected и gamepaddisconnected — и их назначение довольно очевидно.
В конце нашего примера мы сначала включаем функцию removeGamepads():
function removeGamepads() {
const gpLi = document.querySelectorAll(".gamepad");
for (let i = 0; i < gpLi.length; i++) {
list.removeChild(gpLi[i]);
}
reportGamepads();
}
Эта функция извлекает ссылки на все элементы списка с именем класса gamepad, и удаляет их из DOM. Затем она повторно запускает reportGamepads(), чтобы заполнить список обновленным списком подключенных контроллеров.
removeGamepads() будет выполняться каждый раз, когда геймпад подключается или отключается, с помощью следующих обработчиков событий:
window.addEventListener("gamepadconnected", (e) => {
info.textContent = `Gamepad ${e.gamepad.index} connected.`;
if (!initialRun) {
setTimeout(removeGamepads, 1000);
}
});
window.addEventListener("gamepaddisconnected", (e) => {
info.textContent = `Gamepad ${e.gamepad.index} disconnected.`;
setTimeout(removeGamepads, 1000);
});
Мы включили вызовы setTimeout() — как мы делали с кодом инициализации в начале скрипта — чтобы убедиться, что геймпады готовы сообщить свою информацию, когда reportGamepads() вызывается в каждом случае.
Но есть еще одна важная деталь — вы заметите, что внутри обработчика gamepadconnected вызов таймаута выполняется только если initialRun равен false. Это связано с тем, что если ваши геймпады подключены при первой загрузке документа, событие gamepadconnected срабатывает один раз для каждого геймпада, поэтому removeGamepads()/reportGamepads() будут выполняться несколько раз. Это может привести к неточным результатам, поэтому мы хотим выполнить removeGamepads() внутри обработчика gamepadconnected только после начального запуска, а не во время него. Именно для этого предназначен initialRun.
Представление реальной демонстрации
Теперь давайте рассмотрим API Gamepad, используемый в реальной демонстрации WebVR. Вы можете найти эту демонстрацию по адресу raw-webgl-controller-example (посмотреть онлайн).
Точно так же, как и в нашей демонстрации raw-webgl-example (подробнее см. Использование API WebVR), она отображает вращающийся 3D куб, который вы можете выбрать для отображения в VR-дисплее. Единственное отличие состоит в том, что в режиме VR-отображения эта демонстрация позволяет перемещать куб, перемещая VR-контроллер (в исходной демонстрации куб перемещается при движении VR-гарнитуры).
Мы рассмотрим различия в коде в этой версии ниже — см. webgl-demo.js.
Получение данных джойстика
Внутри функции drawVRScene(), вы найдете этот фрагмент кода:
const gamepads = navigator.getGamepads();
const gp = gamepads[0];
if (gp) {
const gpPose = gp.pose;
const curPos = gpPose.position;
const curOrient = gpPose.orientation;
if (poseStatsDisplayed) {
displayPoseStats(gpPose);
}
}
Здесь мы получаем подключенные джойстики с помощью Navigator.getGamepads, затем сохраняем первый обнаруженный джойстик в переменной gp. Поскольку для этой демонстрации нам нужен только один джойстик, мы просто проигнорируем остальные.
Далее мы получаем объект GamepadPose для контроллера, сохранённого в gpPose (запросив Gamepad.pose), а также сохраняем текущие положение и ориентацию джойстика для этого кадра в переменных, чтобы их было легко получить позже. Мы также отображаем статистику кадра в DOM с помощью функции displayPoseStats(). Всё это выполняется только если gp имеет значение (если джойстик подключён), что предотвращает ошибку демонстрации, если у нас нет подключённого джойстика.
Немного позже в коде вы найдете этот блок:
if (gp && gpPose.hasPosition) {
mvTranslate([
0.0 + curPos[0] * 15 - curOrient[1] * 15,
0.0 + curPos[1] * 15 + curOrient[0] * 15,
-15.0 + curPos[2] * 25,
]);
} else if (gp) {
mvTranslate([0.0 + curOrient[1] * 15, 0.0 + curOrient[0] * 15, -15.0]);
} else {
mvTranslate([0.0, 0.0, -15.0]);
}
Здесь мы изменяем положение куба на экране в соответствии с данными position и orientation, полученными от подключённого контроллера. Эти значения (сохранённые в curPos и curOrient ) — Float32Array, содержащие значения X, Y и Z (здесь мы используем только [0], что соответствует X, и [1], что соответствует Y).
Если переменная gp содержит объект Gamepad и может возвращать значения позиции (gpPose.hasPosition), что указывает на контроллер с 6 степенями свободы, мы изменяем положение куба, используя значения положения и ориентации. Если верно только первое, что указывает на контроллер с 3 степенями свободы, мы изменяем положение куба, используя только значения ориентации. Если джойстик не подключён, мы не изменяем положение куба.
Отображение данных о положении джойстика
В функции displayPoseStats(), мы получаем все необходимые данные из объекта GamepadPose, переданного в него, а затем выводим их на панель пользовательского интерфейса, существующую в демонстрации для отображения таких данных:
function displayPoseStats(pose) {
const pos = pose.position;
const formatCoords = ([x, y, z]) =>
`x ${x.toFixed(3)}, y ${y.toFixed(3)}, z ${z.toFixed(3)}`;
posStats.textContent = pose.hasPosition
? `Position: ${formatCoords(pose.position)}`
: "Position not reported";
orientStats.textContent = pose.hasOrientation
? `Orientation: ${formatCoords(pose.orientation)}`
: "Orientation not reported";
linVelStats.textContent = `Linear velocity: ${formatCoords(
pose.linearVelocity,
)}`;
angVelStats.textContent = `Angular velocity: ${formatCoords(
pose.angularVelocity,
)}`;
linAccStats.textContent = pose.linearAcceleration
? `Linear acceleration: ${formatCoords(pose.linearAcceleration)}`
: "Linear acceleration not reported";
angAccStats.textContent = pose.angularAcceleration
? `Angular acceleration: ${formatCoords(pose.angularAcceleration)}`
: "Angular acceleration not reported";
}
Резюме
В этой статье вы получили очень базовое представление о том, как использовать расширения Gamepad для использования VR-контроллеров в приложениях WebVR. В реальном приложении, вероятно, будет гораздо более сложная система управления, с назначением контролей на кнопки VR-контроллеров, и отображение будет изменяться как отображением, так и позицией контроллеров одновременно. Однако здесь мы хотели выделить чисто части расширений Gamepad.
См. также
© 2005–2024 MDN contributors.
Licensed under the Creative Commons Attribution-ShareAlike License v2.5 or later.
https://developer.mozilla.org/en-US/docs/Web/API/WebVR_API/Using_VR_controllers_with_WebVR