Расположение виртуальных объектов в реальном мире

API Hit Test позволяет размещать виртуальные элементы в реальном мире.

Джо Медли
Joe Medley

API устройств WebXR появился прошлой осенью в Chrome 79. Как было сказано тогда, реализация API в Chrome находится в стадии разработки. Chrome рад сообщить, что часть работы завершена. В Chrome 81 появились две новые функции:

В этой статье рассматривается API-интерфейс WebXR Hit Test — средство размещения виртуальных объектов в реальном виде с камеры.

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

Пример иммерсивного сеанса AR

Код в этой статье основан, но не идентичен, коду, найденному в образце Hit Test рабочей группы Immersive Web ( демо , исходный код ). Этот пример позволяет разместить виртуальные подсолнухи на поверхностях реального мира.

Когда вы впервые откроете приложение, вы увидите синий круг с точкой посередине. Точка — это пересечение воображаемой линии, ведущей от вашего устройства к точке в окружающей среде. Он перемещается вместе с устройством. Когда он находит точки пересечения, кажется, что он привязывается к таким поверхностям, как полы, столешницы и стены. Это происходит потому, что тестирование на попадание определяет положение и ориентацию точки пересечения, но ничего не касается самих поверхностей.

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

Сетка, отображаемая на стене, Lax или Strict в зависимости от контекста.
Прицельная марка — это временное изображение, которое помогает поместить объект в дополненную реальность.

Создайте сетку

Вы должны создать изображение прицела самостоятельно, поскольку оно не предоставляется браузером или 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