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

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

Джо Медли
Joe Medley

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

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

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

Пример иммерсивной сессии дополненной реальности.

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

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

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

Прицельная сетка, отображаемая на стене, 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
}

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

Заключение

Лучший способ разобраться в этом — пошагово просмотреть пример кода или попробовать выполнить задания из CodeLab . Надеюсь, я предоставил вам достаточно информации, чтобы вы поняли и то, и другое.

Мы ещё далеко не закончили разработку иммерсивных веб-API. По мере продвижения мы будем публиковать здесь новые статьи.

Фотография Даниэля Франка на Unsplash.