Posicionamiento de objetos virtuales en vistas del mundo real

La API de Hit Test te permite posicionar elementos virtuales en una vista del mundo real.

Joe Medley
José Medley

La API de WebXR Device se envió el otoño pasado en Chrome 79. Como se indicó en ese momento, la implementación de la API por parte de Chrome está en desarrollo. Chrome se complace en anunciar que parte del trabajo está terminado. En Chrome 81, llegaron dos funciones nuevas:

En este artículo, se explica la API de WebXR Hit Test, un medio para colocar objetos virtuales en una vista de cámara del mundo real.

En este artículo, supongo que ya sabes cómo crear una sesión de realidad aumentada y que sabes cómo ejecutar un bucle de fotogramas. Si no estás familiarizado con estos conceptos, te recomendamos que leas los artículos anteriores de esta serie.

La muestra de la sesión de RA envolvente

El código de este artículo se basa, aunque no es idéntico, en el que se encuentra en la muestra de la prueba de posicionamiento del grupo de trabajo web de Immersive (demostración, fuente). Este ejemplo te permite colocar girasoles virtuales en superficies del mundo real.

Cuando abras la app por primera vez, verás un círculo azul con un punto en el medio. El punto es la intersección entre una línea imaginaria de tu dispositivo y el punto del entorno. Se mueve a medida que mueves el dispositivo. A medida que encuentra puntos de intersección, parece ajustarse a superficies como pisos, mesas y paredes. Lo hace porque las pruebas de posicionamiento proporcionan la posición y la orientación del punto de intersección, pero nada de las propias superficies.

Este círculo se denomina retículo y es una imagen temporal que ayuda a colocar un objeto en realidad aumentada. Si presionas la pantalla, se colocará un girasol en la superficie, en la ubicación y la orientación del retículo, independientemente del lugar en el que hayas presionado la pantalla. El retículo sigue moviéndose con tu dispositivo.

Un retículo renderizado en una pared, Laxo o Estricto según su contexto
El retículo es una imagen temporal que ayuda a colocar un objeto en realidad aumentada.

Crea el retículo

Debes crear la imagen del retículo, ya que no la proporcionan el navegador ni la API. El método de carga y dibujo es específico del framework. Si no la dibujas directamente con WebGL o WebGL2, consulta la documentación del framework. Por este motivo, no entraré en detalles sobre cómo se dibuja el retículo en la muestra. A continuación, muestro una línea solo por un motivo: de modo que, en las muestras de código posteriores, sabrás a qué me refiero cuando use la variable reticle.

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

Solicita una sesión

Cuando solicites una sesión, deberás solicitar 'hit-test' en el array requiredFeatures, como se muestra a continuación.

navigator.xr.requestSession('immersive-ar', {
  requiredFeatures: ['local', 'hit-test']
})
.then((session) => {
  // Do something with the session
});

Cómo ingresar a una sesión

En los artículos anteriores, expliqué el código para ingresar a una sesión XR. He mostrado una versión de esto a continuación con algunas adiciones. Primero, agregué el objeto de escucha de eventos select. Cuando el usuario presione la pantalla, se colocará una flor en la vista de la cámara según la pose del retículo. Describiré ese objeto de escucha de eventos más adelante.

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

Múltiples espacios de referencia

Ten en cuenta que el código destacado llama a XRSession.requestReferenceSpace() dos veces. Al principio, me pareció confuso. Te pregunté por qué el código de prueba de posicionamiento no solicita un fotograma de animación (iniciando el bucle de fotogramas) y por qué el bucle de fotogramas parece no implicar pruebas de posicionamiento. La fuente de la confusión fue un malentendido de los espacios de referencia. Los espacios de referencia expresan relaciones entre un origen y el mundo.

Para comprender lo que hace este código, imagina que estás viendo este ejemplo con un rig independiente y que tienes auriculares y un controlador. Para medir las distancias desde el controlador, usarías un marco de referencia centrado en el controlador. Sin embargo, para dibujar algo en la pantalla, usarás coordenadas centradas en el usuario.

En este ejemplo, el visor y el control son el mismo dispositivo. Pero tengo un problema. Lo que dibujo debe ser estable con respecto al entorno, pero el "controlador" con el que estoy dibujando se está moviendo.

Para el dibujo de imágenes, uso el espacio de referencia local, que me brinda estabilidad en términos del entorno. Después de obtener esto, empiezo el bucle de fotogramas con una llamada a requestAnimationFrame().

Para las pruebas de posicionamiento, uso el espacio de referencia viewer, que se basa en la posición del dispositivo en el momento de la prueba de posicionamiento. La etiqueta "visor" es un poco confusa en este contexto porque hablo de un controlador. Tiene sentido si el controlador es como un visor electrónico. Después de obtener esto, llamo a xrSession.requestHitTestSource(), que crea la fuente de los datos de prueba de posicionamiento que usaré cuando dibuje.

Cómo ejecutar un bucle de fotogramas

La devolución de llamada requestAnimationFrame() también obtiene código nuevo para controlar las pruebas de posicionamiento.

A medida que mueves el dispositivo, el retículo debe moverse con él mientras intenta encontrar superficies. Para crear la ilusión de movimiento, vuelve a dibujar el retículo en cada marco. Pero no muestres el retículo si falla la prueba de posicionamiento. Por lo tanto, para el retículo que creé antes, establezco la propiedad visible en 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
}

Para dibujar algo en RA, necesito saber dónde está el usuario y dónde está mirando. Por lo tanto, pruebo que hitTestSource y xrViewerPose siguen siendo válidos.

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
}

Ahora llamo a getHitTestResults(). Toma el objeto hitTestSource como argumento y muestra un array de instancias de HitTestResult. La prueba de posicionamiento puede encontrar varias superficies. El primero del array es el más cercano a la cámara. La mayoría de las veces lo usarás, pero se muestra un array para casos de uso avanzados. Por ejemplo, imagina que tu cámara apunta a una caja ubicada sobre una mesa en el piso. Es posible que la prueba de posicionamiento muestre las tres superficies del array. En la mayoría de los casos, será la caja que me interesa. Si la longitud del array que se muestra es 0, en otras palabras, si no se muestra ninguna prueba de posicionamiento, continúa. Vuelve a intentarlo en el siguiente fotograma.

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
}

Por último, debo procesar los resultados de la prueba de posicionamiento. El proceso básico es el siguiente: Toma una postura del resultado de la prueba de posicionamiento, transforma (mueve) la imagen del retículo a la posición de la prueba de posicionamiento y, luego, establece la propiedad visible como verdadera. La pose representa la pose de un punto en una superficie.

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
}

Coloca un objeto

Un objeto se coloca en RA cuando el usuario presiona la pantalla. Ya agregué un controlador de eventos select a la sesión. (Consulta la sección anterior).

Lo importante en este paso es saber dónde colocarlo. Dado que el retículo en movimiento te proporciona una fuente constante de pruebas de posicionamiento, la forma más simple de colocar un objeto es dibujarlo en la ubicación del retículo en la última prueba de posicionamiento.

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

Conclusión

La mejor manera de hacerlo es revisar el código de muestra o probar el codelab. Espero haberte brindado suficiente experiencia para entenderlos.

No hemos terminado de compilar APIs web inmersivas, por poco tiempo. Publicaremos nuevos artículos aquí a medida que avancemos.

Foto de Daniel Frank en Unsplash