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
Joe Medley

La API de WebXR Device se lanzó el otoño pasado en Chrome 79. Como se indicó en ese momento, la implementación de la API de Chrome está en curso. Chrome se complace en anunciar que ya se completó parte del trabajo. En Chrome 81, se agregaron 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, se supone 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, consulta los artículos anteriores de esta serie.

Ejemplo de la sesión de RA envolvente

El código de este artículo se basa en el que se encuentra en la muestra de prueba de impacto del grupo de trabajo de la Web Imersiva (demo, fuente), pero no es idéntico. 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 que va desde tu dispositivo hasta 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. Esto se debe a que la prueba de hit proporciona la posición y orientación del punto de intersección, pero no proporciona información sobre las 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 coloca un girasol en la superficie en la ubicación del retículo y la orientación del punto del retículo, independientemente de dónde presiones la pantalla. El retículo continúa moviéndose con tu dispositivo.

Un retículo renderizado en una pared, laxo o estricto según el contexto
El retículo es una imagen temporal que ayuda a colocar un objeto en la 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 lo dibujas directamente con WebGL o WebGL2, consulta la documentación de tu 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 de ella por un solo motivo: para que, en muestras de código posteriores, sepas a qué me refiero cuando uso la variable reticle.

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

Solicitar una sesión

Cuando solicites una sesión, debes 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 artículos anteriores, presenté código para ingresar a una sesión de XR. A continuación, muestro una versión de esto con algunas incorporaciones. 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. Más adelante, describiré ese objeto de escucha de eventos.

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

Varios espacios de referencia

Observa que el código destacado llama a XRSession.requestReferenceSpace() dos veces. Al principio, me pareció confuso. Le pregunté por qué el código de prueba de acierto no solicita un fotograma de animación (inicia el bucle de fotogramas) y por qué el bucle de fotogramas parece no incluir pruebas de acierto. La fuente de la confusión fue un malentendido sobre los espacios de referencia. Los espacios de referencia expresan las relaciones entre un origen y el mundo.

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

En este ejemplo, el visor y el controlador son el mismo dispositivo. Pero tengo un problema. Lo que dibujo debe ser estable en relación con el entorno, pero el “controlador” con el que dibujo se mueve.

Para dibujar imágenes, uso el espacio de referencia local, que me proporciona estabilidad en términos del entorno. Después de obtener esto, llamo a requestAnimationFrame() para iniciar el bucle de fotogramas.

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

Ejecuta un bucle de tramas

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

A medida que mueves el dispositivo, el retículo debe moverse con él mientras intenta encontrar superficies. Para crear una ilusión de movimiento, vuelve a dibujar el retículo en cada marco. Sin embargo, no muestres la retícula si falla la prueba de impacto. Por lo tanto, para el retículo que creé antes, configuré su 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 cualquier cosa en RA, necesito saber dónde está el usuario y dónde mira. Por lo tanto, pruebo que hitTestSource y xrViewerPose sigan 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 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 mostrará un array para casos de uso avanzados. Por ejemplo, imagina que tu cámara apunta a una caja sobre una mesa en un piso. Es posible que la prueba de hit muestre las tres plataformas del array. En la mayoría de los casos, será la casilla que me interesa. Si la longitud del array que se muestra es 0, es decir, si no se muestra ninguna prueba de hit, 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 hit. El proceso básico es el siguiente: Obtén una postura a partir 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, configura su propiedad visible como verdadera. La pose representa la posición 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

Se coloca un objeto en la RA cuando el usuario presiona la pantalla. Ya agregué un controlador de eventos select a la sesión. (Consulta más arriba).

Lo importante en este paso es saber dónde colocarlo. Dado que la retícula en movimiento te brinda una fuente constante de pruebas de acierto, la forma más sencilla de colocar un objeto es dibujarlo en la ubicación de la retícula en la última prueba de acierto.

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 comprender esto es analizar el código de muestra o probar el codelab. Espero haberte proporcionado suficientes antecedentes para que entiendas ambos.

No hemos terminado de crear APIs web envolventes, ni mucho menos. Publicaremos artículos nuevos aquí a medida que avancemos.

Foto de Daniel Frank en Unsplash