Размещение виртуальных объектов в реальном мире

API проверки нажатия позволяет размещать виртуальные предметы в реальном мире.

Joe Medley
Joe Medley

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

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

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

Пример сеанса дополненной реальности с эффектом присутствия

Код в этой статье основан на примере проверки нажатия от Immersive Web Working Group (демо, исходный код), но не идентичен ему. В этом примере вы можете размещать виртуальные подсолнухи на поверхностях в реальном мире.

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

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

Прицел, отображаемый на стене, 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, которое основано на положении устройства в момент проверки. Ярлык "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
}

Чтобы нарисовать что-либо в дополненной реальности, мне нужно знать, где находится зритель и куда он смотрит. Поэтому я проверяю, действительны ли 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
}

Размещение объекта

Объект размещается в дополненной реальности, когда пользователь нажимает на экран. Я уже добавил в сеанс обработчик событий 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 для иммерсивного веба. По мере развития проекта мы будем публиковать здесь новые статьи.