API Hit Test позволяет размещать виртуальные элементы в реальном мире.
API устройств WebXR появился прошлой осенью в Chrome 79. Как было сказано тогда, реализация API в Chrome находится в стадии разработки. Chrome рад сообщить, что часть работы завершена. В Chrome 81 появились две новые функции:
В этой статье рассматривается API-интерфейс WebXR Hit Test — средство размещения виртуальных объектов в реальном виде с камеры.
В этой статье я предполагаю, что вы уже знаете, как создать сеанс дополненной реальности, и знаете, как запустить цикл кадров. Если вы не знакомы с этими понятиями, вам следует прочитать предыдущие статьи этой серии.
- Виртуальная реальность приходит в Интернет
- Виртуальная реальность приходит в Интернет, часть II
- Web AR: возможно, вы уже знаете, как им пользоваться
Пример иммерсивного сеанса AR
Код в этой статье основан, но не идентичен, коду, найденному в образце Hit Test рабочей группы Immersive Web ( демо , исходный код ). Этот пример позволяет разместить виртуальные подсолнухи на поверхностях реального мира.
Когда вы впервые откроете приложение, вы увидите синий круг с точкой посередине. Точка — это пересечение воображаемой линии, ведущей от вашего устройства к точке в окружающей среде. Он перемещается вместе с устройством. Когда он находит точки пересечения, кажется, что он привязывается к таким поверхностям, как полы, столешницы и стены. Это происходит потому, что тестирование на попадание определяет положение и ориентацию точки пересечения, но ничего не касается самих поверхностей.
Этот круг называется сеткой и представляет собой временное изображение, помогающее поместить объект в дополненную реальность. Если вы коснетесь экрана, подсолнух разместится на поверхности в месте расположения и ориентации точки прицела, независимо от того, где вы коснулись экрана. Сетка продолжает двигаться вместе с вашим устройством.
Создайте сетку
Вы должны создать изображение прицела самостоятельно, поскольку оно не предоставляется браузером или API. Метод загрузки и рисования зависит от платформы. Если вы не рисуете его напрямую с помощью WebGL или WebGL2, обратитесь к документации вашего фреймворка. По этой причине я не буду подробно останавливаться на том, как нарисована прицельная марка в образце. Ниже я показываю одну его строку только по одной причине: чтобы в последующих примерах кода вы знали, о чем я говорю, когда использую переменную reticle
.
let reticle = new Gltf2Node({url: 'media/gltf/reticle/reticle.gltf'});
Запросить сеанс
При запросе сеанса вы должны запросить 'hit-test'
в массиве requiredFeatures
, как показано ниже.
navigator.xr.requestSession('immersive-ar', {
requiredFeatures: ['local', 'hit-test']
})
.then((session) => {
// Do something with the session
});
Вход в сеанс
В предыдущих статьях я представил код для входа в сеанс XR. Ниже я показал версию этого с некоторыми дополнениями. Сначала я добавил прослушиватель событий select
. Когда пользователь коснется экрана, в поле зрения камеры появится цветок в зависимости от положения прицельной сетки. Я опишу этот прослушиватель событий позже.
function onSessionStarted(xrSession) {
xrSession.addEventListener('end', onSessionEnded);
xrSession.addEventListener('select', onSelect);
let canvas = document.createElement('canvas');
gl = canvas.getContext('webgl', { xrCompatible: true });
xrSession.updateRenderState({
baseLayer: new XRWebGLLayer(session, gl)
});
xrSession.requestReferenceSpace('viewer').then((refSpace) => {
xrViewerSpace = refSpace;
xrSession.requestHitTestSource({ space: xrViewerSpace })
.then((hitTestSource) => {
xrHitTestSource = hitTestSource;
});
});
xrSession.requestReferenceSpace('local').then((refSpace) => {
xrRefSpace = refSpace;
xrSession.requestAnimationFrame(onXRFrame);
});
}
Несколько ссылочных пространств
Обратите внимание, что выделенный код дважды вызывает XRSession.requestReferenceSpace()
. Сначала меня это смутило. Я спросил, почему код теста попадания не запрашивает кадр анимации (запускает цикл кадров) и почему цикл кадров, похоже, не включает тесты попадания. Источником путаницы было неправильное понимание эталонных пространств. Референтные пространства выражают отношения между началом и миром.
Чтобы понять, что делает этот код, представьте, что вы просматриваете этот пример с помощью автономного оборудования, и у вас есть и гарнитура, и контроллер. Для измерения расстояний от контроллера вы должны использовать систему отсчета, центрированную на контроллере. Но чтобы нарисовать что-то на экране, вам придется использовать координаты, ориентированные на пользователя.
В этом примере средство просмотра и контроллер — это одно и то же устройство. Но у меня есть проблема. То, что я рисую, должно быть стабильным по отношению к окружающей среде, но «контроллер», с помощью которого я рисую, движется.
Для рисования изображений я использую local
эталонное пространство, что дает мне стабильность с точки зрения окружающей среды. Получив это, я запускаю цикл кадра, вызывая requestAnimationFrame()
.
Для проверки нажатия я использую эталонное пространство viewer
, которое основано на позе устройства во время проверки нажатия. Ярлык «просмотрщик» в этом контексте несколько сбивает с толку, поскольку я говорю о контроллере. Это имеет смысл, если вы думаете о контроллере как об электронном средстве просмотра. Получив это, я вызываю xrSession.requestHitTestSource()
, который создает источник данных проверки попадания, которые я буду использовать при рисовании.
Запуск цикла кадров
Обратный вызов requestAnimationFrame()
также получает новый код для обработки проверки попадания.
Когда вы перемещаете устройство, прицельная марка должна двигаться вместе с ним, пытаясь найти поверхности. Чтобы создать иллюзию движения, перерисовывайте прицельную сетку в каждом кадре. Но не показывайте прицельную сетку, если проверка на попадание не удалась. Итак, для сетки, которую я создал ранее, я установил для visible
значение false
.
function onXRFrame(hrTime, xrFrame) {
let xrSession = xrFrame.session;
xrSession.requestAnimationFrame(onXRFrame);
let xrViewerPose = xrFrame.getViewerPose(xrRefSpace);
reticle.visible = false;
// Reminder: the hitTestSource was acquired during onSessionStart()
if (xrHitTestSource && xrViewerPose) {
let hitTestResults = xrFrame.getHitTestResults(xrHitTestSource);
if (hitTestResults.length > 0) {
let pose = hitTestResults[0].getPose(xrRefSpace);
reticle.visible = true;
reticle.matrix = pose.transform.matrix;
}
}
// Draw to the screen
}
Чтобы нарисовать что-либо в AR, мне нужно знать, где находится зритель и куда он смотрит. Итак, я проверяю, что hitTestSource
и xrViewerPose
все еще действительны.
function onXRFrame(hrTime, xrFrame) {
let xrSession = xrFrame.session;
xrSession.requestAnimationFrame(onXRFrame);
let xrViewerPose = xrFrame.getViewerPose(xrRefSpace);
reticle.visible = false;
// Reminder: the hitTestSource was acquired during onSessionStart()
if (xrHitTestSource && xrViewerPose) {
let hitTestResults = xrFrame.getHitTestResults(xrHitTestSource);
if (hitTestResults.length > 0) {
let pose = hitTestResults[0].getPose(xrRefSpace);
reticle.visible = true;
reticle.matrix = pose.transform.matrix;
}
}
// Draw to the screen
}
Теперь я вызываю getHitTestResults()
. Он принимает hitTestSource
в качестве аргумента и возвращает массив экземпляров HitTestResult
. Тест на попадание может обнаружить несколько поверхностей. Первый в массиве — тот, который находится ближе всего к камере. Большую часть времени вы будете использовать его, но для расширенных случаев использования возвращается массив. Например, представьте, что ваша камера направлена на коробку, стоящую на столе на полу. Вполне возможно, что проверка попадания вернет все три поверхности в массиве. В большинстве случаев меня волнует именно коробка. Если длина возвращаемого массива равна 0, другими словами, если проверка попадания не возвращается, продолжайте дальше. Попробуйте еще раз в следующем кадре.
function onXRFrame(hrTime, xrFrame) {
let xrSession = xrFrame.session;
xrSession.requestAnimationFrame(onXRFrame);
let xrViewerPose = xrFrame.getViewerPose(xrRefSpace);
reticle.visible = false;
// Reminder: the hitTestSource was acquired during onSessionStart()
if (xrHitTestSource && xrViewerPose) {
let hitTestResults = xrFrame.getHitTestResults(xrHitTestSource);
if (hitTestResults.length > 0) {
let pose = hitTestResults[0].getPose(xrRefSpace);
reticle.visible = true;
reticle.matrix = pose.transform.matrix;
}
}
// Draw to the screen
}
Наконец, мне нужно обработать результаты проверки попадания. Основной процесс таков. Получите позу из результата теста на попадание, преобразуйте (переместите) изображение прицела в положение теста на попадание, затем установите для его visible
значение true. Поза представляет собой положение точки на поверхности.
function onXRFrame(hrTime, xrFrame) {
let xrSession = xrFrame.session;
xrSession.requestAnimationFrame(onXRFrame);
let xrViewerPose = xrFrame.getViewerPose(xrRefSpace);
reticle.visible = false;
// Reminder: the hitTestSource was acquired during onSessionStart()
if (xrHitTestSource && xrViewerPose) {
let hitTestResults = xrFrame.getHitTestResults(xrHitTestSource);
if (hitTestResults.length > 0) {
let pose = hitTestResults[0].getPose(xrRefSpace);
reticle.matrix = pose.transform.matrix;
reticle.visible = true;
}
}
// Draw to the screen
}
Размещение объекта
Объект помещается в AR, когда пользователь касается экрана. Я уже добавил в сеанс обработчик событий select
. ( См. выше .)
На этом этапе важно знать, где его разместить. Поскольку движущаяся сетка дает вам постоянный источник проверок на попадание, самый простой способ разместить объект — нарисовать его в том месте, где находилась сетка во время последней проверки на попадание.
function onSelect(event) {
if (reticle.visible) {
// The reticle should already be positioned at the latest hit point,
// so we can just use its matrix to save an unnecessary call to
// event.frame.getHitTestResults.
addARObjectAt(reticle.matrix);
}
}
Заключение
Лучший способ разобраться с этим — просмотреть пример кода или попробовать работу с кодовой лабораторией . Надеюсь, я дал вам достаточно информации, чтобы понять и то, и другое.
Мы еще далеко не закончили создание иммерсивных веб-API. Мы будем публиковать здесь новые статьи по мере продвижения.
Фото Дэниела Франка на Unsplash