在现实世界视图中定位虚拟对象

借助 Hit Test API,您可以在现实世界视图中放置虚拟物品。

Joe Medley
Joe Medley

WebXR Device API 于去年秋季在 Chrome 79 中发布。如当时所述,Chrome 对该 API 的实现仍在开发中。Chrome 很高兴地宣布,部分工作已完成。Chrome 81 中新增了两项功能:

本文介绍了 WebXR Hit Test API,该 API 可用于在现实世界的相机视图中放置虚拟对象。

在本文中,我假设您已经知道如何创建增强现实会话,并且知道如何运行帧循环。如果您不熟悉这些概念,建议您阅读本系列中的前几篇文章。

沉浸式 AR 会话示例

本文中的代码基于沉浸式 Web 工作组的命中测试示例(演示来源),但并不完全相同。在此示例中,您可以在现实世界中的表面上放置虚拟向日葵。

首次打开应用时,您会看到一个中间带圆点的蓝色圆圈。 圆点是设备到环境中某点的假想线之间的交点。它会随着设备的移动而移动。当它找到交点时,似乎会贴合到地面、桌面和墙壁等表面。之所以会这样,是因为命中测试会提供交点的位置和方向,但不会提供有关表面的任何信息。

此圆圈称为十字线,是一种临时图像,可帮助您在增强现实中放置对象。如果您点按屏幕,系统会在十字线位置和十字线点的方向上放置一朵向日葵,无论您点按屏幕的哪个位置。十字线会继续随设备移动。

根据上下文在墙上呈现的十字线,可以是宽松模式,也可以是严格模式
瞄准线是一种临时图片,可帮助在增强现实模式下放置对象。

创建网状线

您必须自行创建十字线图片,因为浏览器或 API 不会提供该图片。加载和绘制它的方法因框架而异。 如果您不是直接使用 WebGL 或 WebGL2 绘制,请参阅框架文档。因此,我不会详细介绍如何在示例中绘制十字线。下面我只展示了一行代码,原因只有一个:这样,在后面的代码示例中,当使用 reticle 变量时,您就知道我指的是什么了。

let reticle = new Gltf2Node({url: 'media/gltf/reticle/reticle.gltf'});

请求会话

请求会话时,您必须在 requiredFeatures 数组中请求 'hit-test',如下所示。

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 中绘制任何内容,我需要知道观看者的位置以及他们正在看哪里。因此,我测试了 hitTestSourcexrViewerPose 是否仍然有效。

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);
  }
}

总结

若要掌握此功能,最好的方法是逐步了解示例代码或尝试 Codelab。希望我已为您提供了足够的背景信息,以便您理解这两者。

我们还远未完成沉浸式 Web API 的构建。随着我们取得进展,我们会在此处发布新文章。

照片提供者:Daniel Frank;来源:Unsplash