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

利用 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 引用空间,该空间基于点击测试时设备的姿态。在此情况下,“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。数组中的第一个元素是距离相机最近的那个。大多数情况下,您需要使用该数据集,但对于高级用例,会返回一个数组。例如,假设您的摄像头对准了地板上桌子上的一个箱子。点击测试可能会返回数组中的所有三个表面。在大多数情况下,它是我关心的框。如果所返回数组的长度为 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 FrankUnsplash 用户提交