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ó entonces, estamos trabajando en la implementación de la API de Chrome. 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 prueba de posicionamiento de WebXR, 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, debes leer 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, que es una imagen temporal que ayuda a colocar un objeto en la 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 por tu cuenta, ya que el navegador ni la API no la proporcionan. 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, se muestra una línea por un solo motivo: para que en las 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'});

Cómo 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
});

Ingresa 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 posición 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);
  });
}

Varios espacios de referencia

Observa que el código destacado llama a XRSession.requestReferenceSpace() dos veces. Al principio, me pareció confuso. Pregunté por qué el código de la prueba de posicionamiento no solicita un fotograma de animación (que inicia el bucle de fotogramas) y por qué el bucle de fotogramas no incluye 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 entender lo que hace este código, imagina que estás viendo esta muestra con un rig independiente y que tienes un visor y un control. 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 esto, inicio el bucle de fotogramas llamando a requestAnimationFrame().

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 un poco confusa en este contexto porque estoy hablando 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 datos de prueba de colisiones que usaré cuando dibuje.

Ejecuta un bucle de tramas

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

A medida que mueves el dispositivo, el retículo debe moverse con él para intentar 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 algo en la RA, necesito saber dónde está el usuario y hacia dónde está mirando. Por lo tanto, reviso 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 arreglo de instancias de HitTestResult. La prueba de impacto 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 la cámara está apuntando a una caja en 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 casilla 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 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 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

Se coloca un objeto en 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 brindado suficiente información para entender ambos conceptos.

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