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

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

Joe Medley
Joe Medley

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

本文介绍了 WebXR Hit Test 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 实例数组。点击测试可能会找到多个 Surface。数组中的第一个是离镜头最近的那个。大多数情况下,您会用到该数组,但对于高级用例,系统会返回数组。例如,假设摄像头对准地板上桌子上的一个盒子。点击测试可能会返回数组中的所有三个 Surface。在大多数情况下,我会关注这个框。如果返回的数组的长度为 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,来源:Unstone