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

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

Joe Medley
Joe Medley

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

本文介绍了 WebXR Hit Test API,这是一种在真实世界相机视图中放置虚拟对象的方法。

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

沉浸式 AR 会话示例

本文中的代码基于 Immersive Web Working Group 的点击测试示例(演示、源代码),但与该示例不完全相同。 此示例可让您将虚拟向日葵放置在现实世界中的表面上。

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

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

在墙上渲染的十字线,根据其上下文,可以是 Lax 或 Strict
标线是一种临时图片,有助于在增强现实模式下放置对象。

创建标线

您必须自行创建标线图片,因为浏览器或 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 中绘制任何内容,我需要知道观看者所在的位置以及他们正在观看的位置。因此,我会测试 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);
  }
}

总结

掌握此内容的最佳方法是逐步完成示例代码或试用 Codelab。希望我提供的背景信息足以让您理解这两者。

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