La realidad virtual llega a la Web (parte II)

Todo sobre el bucle de marcos

Joe Medley
Joe Medley

Hace poco, publiqué la realidad virtual llega a la Web, un artículo en el que se presentan conceptos básicos de la API de WebXR Device. También proporcioné instrucciones para solicitar, ingresar y finalizar una sesión XR.

En este artículo, se describe el bucle de fotogramas, que es un bucle infinito controlado por el usuario-agente en el que el contenido se dibuja repetidamente en la pantalla. El contenido se dibuja en bloques discretos llamados marcos. La sucesión de marcos crea la ilusión del movimiento.

Qué no es este artículo

WebGL y WebGL2 son los únicos medios de renderizar contenido durante un bucle de fotogramas en una app de WebXR. Afortunadamente, muchos frameworks proporcionan una capa de abstracción además de WebGL y WebGL2. Tales frameworks incluyen three.js, babylonjs y PlayCanvas, mientras que A-Frame y React 360 se diseñaron para interactuar con WebXR.

Este artículo no es un instructivo de WebGL ni de un framework. Explica los conceptos básicos de un bucle de fotogramas con la muestra de la sesión de RV inmersiva de Immersive Web Working Group (demostración, fuente). Si quieres explorar WebGL o uno de los frameworks, Internet ofrece una lista creciente de artículos.

Los jugadores y el partido

Cuando intenté comprender el bucle de fotogramas, me perdía en los detalles. Hay muchos objetos en juego y algunos solo se nombran según las propiedades de referencia de otros objetos. Para ayudarte a hacerlo, describiré los objetos, a los que llamo "jugadores". Luego describiré cómo interactúan, a lo que llamo "el juego".

Los jugadores

XRViewerPose

Una pose es la posición y la orientación de algo en un espacio 3D. Tanto los visores como los dispositivos de entrada tienen una pose, pero lo que nos preocupa aquí es la pose del usuario. Tanto las posturas del visualizador como la del dispositivo de entrada tienen un atributo transform que describe su posición como un vector y su orientación como un cuaternión relativo al origen. El origen se especifica según el tipo de espacio de referencia solicitado cuando se llama a XRSession.requestReferenceSpace().

Los espacios de referencia tardan un poco en explicarse. Los abordamos en detalle en Realidad aumentada. En la muestra que uso como base de este artículo, se usa un espacio de referencia 'local', lo que significa que el origen se encuentra en la posición del usuario en el momento de la creación de la sesión sin un precio mínimo bien definido, y su posición precisa puede variar según la plataforma.

XRView

Una vista corresponde a una cámara que mira la escena virtual. Una vista también tiene un atributo transform que describe su posición como vector y su orientación. Se proporcionan como un par vector/cuaternión y como una matriz equivalente. Puedes usar cualquiera de las representaciones en función de la que mejor se adapte a tu código. Cada vista corresponde a una pantalla o a una parte de ella que un dispositivo usa para presentar imágenes al usuario. Los objetos XRView se muestran en un array del objeto XRViewerPose. La cantidad de vistas en el array varía. En los dispositivos móviles, una escena de RA tiene una vista, que puede cubrir la pantalla del dispositivo o no. Los auriculares suelen tener dos vistas, una para cada ojo.

XRWebGLLayer

Las capas proporcionan una fuente de imágenes de mapas de bits y descripciones de cómo se renderizarán esas imágenes en el dispositivo. Esta descripción no captura exactamente lo que hace este reproductor. Llegué a pensarlo como un intermediario entre un dispositivo y un WebGLRenderingContext. MDN tiene prácticamente la misma vista, lo que indica que "proporciona una vinculación" entre los dos. Por lo tanto, proporciona acceso a los otros jugadores.

En general, los objetos de WebGL almacenan información de estado para renderizar gráficos en 2D y 3D.

WebGLFramebuffer

Un búfer de fotogramas proporciona datos de imagen a WebGLRenderingContext. Después de recuperarlo del XRWebGLLayer, solo debes pasarlo al WebGLRenderingContext actual. Aparte de llamar a bindFramebuffer() (más adelante, obtendrás más información sobre esto), nunca accederás a este objeto directamente. Solo la pasarás de XRWebGLLayer a WebGLRenderingContext.

XRViewport

Un viewport proporciona las coordenadas y las dimensiones de una región rectangular en WebGLFramebuffer.

WebGLRenderingContext

Un contexto de renderización es un punto de acceso programático para un lienzo (el espacio en el que dibujamos). Para ello, necesita un WebGLFramebuffer y un XRViewport.

Observa la relación entre XRWebGLLayer y WebGLRenderingContext. Uno corresponde al dispositivo del usuario y el otro a la página web. WebGLFramebuffer y XRViewport se pasan del primero al último.

La relación entre XRWebGLLayer y WebGLRenderingContext
La relación entre XRWebGLLayer y WebGLRenderingContext

El juego

Ahora que sabemos quiénes son los jugadores, veamos qué juegan. Es un juego que comienza de nuevo con cada fotograma. Recuerda que los fotogramas son parte de un bucle de fotogramas que ocurre a una velocidad que depende del hardware subyacente. Para las aplicaciones de RV, los fotogramas por segundo pueden ser de entre 60 y 144. La RA para Android se ejecuta a 30 fotogramas por segundo. Tu código no debe asumir una velocidad de fotogramas en particular.

El proceso básico para el bucle de fotogramas se ve de la siguiente manera:

  1. Llamar a XRSession.requestAnimationFrame() En respuesta, el usuario-agente invoca el XRFrameRequestCallback, que definiste tú.
  2. Dentro de la función de devolución de llamada:
    1. Vuelve a llamar a XRSession.requestAnimationFrame().
    2. Toma la pose del espectador.
    3. Pasa (vincula) el WebGLFramebuffer de XRWebGLLayer a WebGLRenderingContext.
    4. Itera en cada objeto XRView, recupera su XRViewport de XRWebGLLayer y pásalo a WebGLRenderingContext.
    5. Dibuja algo en el búfer de fotogramas.

Debido a que los pasos 1 y 2a se trataron en el artículo anterior, comenzaré por el paso 2b.

Apuesta por la pose del usuario.

Probablemente no hace falta decirlo. Para dibujar algo en RA o RV, necesito saber dónde está el espectador y dónde está mirando. Un objeto XRViewerPose proporciona la posición y la orientación del usuario. Para obtener la pose del usuario, llamo a XRFrame.getViewerPose() en el fotograma de animación actual. Le paso el espacio de referencia que adquirí cuando configuré la sesión. Los valores que muestra este objeto siempre están relacionados con el espacio de referencia que solicité cuando ingresé a la sesión actual. Como recordarás, debo pasar el espacio de referencia actual cuando solicito la postura.

function onXRFrame(hrTime, xrFrame) {
  let xrSession = xrFrame.session;
  xrSession.requestAnimationFrame(onXRFrame);
  let xrViewerPose = xrFrame.getViewerPose(xrRefSpace);
  if (xrViewerPose) {
    // Render based on the pose.
  }
}

Hay una pose de usuario que representa la posición general del usuario, es decir, su cabeza o la cámara del teléfono en el caso de un smartphone. La pose le indica a la aplicación dónde está el visualizador. La renderización de imágenes real usa objetos XRView, a los que hablaremos en breve.

Antes de continuar, pruebo si la pose del visualizador se devolvió en caso de que el sistema pierda el seguimiento o bloquee la pose por motivos de privacidad. El seguimiento es la capacidad del dispositivo XR para saber dónde se encuentran sus dispositivos de entrada o sus dispositivos de entrada en relación con el entorno. El seguimiento puede perderse de varias maneras y varía según el método que se utilice para ello. Por ejemplo, si las cámaras de los auriculares o del teléfono se usan para rastrear el dispositivo, es posible que ya no puedan determinar dónde se encuentra en situaciones con poca luz o sin ella, o si las cámaras están cubiertas.

Un ejemplo de cómo bloquear la pose por motivos de privacidad es que, si los auriculares muestran un diálogo de seguridad, como un mensaje de permiso, el navegador podría dejar de proporcionar posturas a la aplicación mientras esto sucede. Sin embargo, ya llamé a XRSession.requestAnimationFrame() para que, si el sistema se puede recuperar, el bucle de fotogramas continúe. De lo contrario, el usuario-agente finalizará la sesión y llamará al controlador de eventos end.

Un pequeño desvío

En el siguiente paso, se requieren objetos creados durante la configuración de la sesión. Recuerda que creé un lienzo y le indiqué un contexto de renderización de Web GL compatible con XR, que obtuve llamando a canvas.getContext(). Todo el dibujo se realiza con la API de WebGL, la API de WebGL2 o un framework basado en WebGL, como Three.js. Este contexto se pasó al objeto de la sesión a través de updateRenderState(), junto con una instancia nueva de XRWebGLLayer.

let canvas = document.createElement('canvas');
// The rendering context must be based on WebGL or WebGL2
let webGLRenContext = canvas.getContext('webgl', { xrCompatible: true });
xrSession.updateRenderState({
    baseLayer: new XRWebGLLayer(xrSession, webGLRenContext)
  });

Pasa ('bind') el búfer de marco de WebGL

El XRWebGLLayer proporciona un búfer de fotogramas para el WebGLRenderingContext que se proporciona específicamente para su uso con WebXR y reemplaza el búfer de fotogramas predeterminado de los contextos de renderización. Esto se llama "vinculación" en el lenguaje de WebGL.

function onXRFrame(hrTime, xrFrame) {
  let xrSession = xrFrame.session;
  xrSession.requestAnimationFrame(onXRFrame);
  let xrViewerPose = xrFrame.getViewerPose(xrRefSpace);
  if (xrViewerPose) {
    let glLayer = xrSession.renderState.baseLayer;
    webGLRenContext.bindFramebuffer(webGLRenContext.FRAMEBUFFER, glLayer.framebuffer);
    // Iterate over the views
  }
}

Itera en cada objeto XRView

Después de obtener la posición y vincular el búfer de fotogramas, es hora de obtener los viewports. El objeto XRViewerPose contiene un array de interfaces XRView, cada una de las cuales representa una pantalla o una parte de ella. Contienen información necesaria para renderizar contenido que esté posicionado correctamente para el dispositivo y el visor, como el campo visual, el desplazamiento ocular y otras propiedades ópticas. Como estoy dibujando para dos ojos, tengo dos vistas que recorro y dibujo una imagen separada para cada una.

Cuando se implementa para la realidad aumentada basada en teléfonos, solo tendría una vista, pero igual usaría un bucle. Aunque puede parecer inútil iterar a través de una vista, hacerlo te permite tener una sola ruta de renderización para un espectro de experiencias envolventes. Esta es una diferencia importante entre WebXR y otros sistemas envolventes.

function onXRFrame(hrTime, xrFrame) {
  let xrSession = xrFrame.session;
  xrSession.requestAnimationFrame(onXRFrame);
  let xrViewerPose = xrFrame.getViewerPose(xrRefSpace);
  if (xrViewerPose) {
    let glLayer = xrSession.renderState.baseLayer;
    webGLRenContext.bindFramebuffer(webGLRenContext.FRAMEBUFFER, glLayer.framebuffer);
    for (let xrView of xrViewerPose.views) {
      // Pass viewports to the context
    }
  }
}

Pasa el objeto XRViewport a WebGLRenderingContext

Un objeto XRView hace referencia a lo que es observable en una pantalla. Pero para dibujar en esa vista, necesito coordenadas y dimensiones que son específicas de mi dispositivo. Al igual que con el búfer de fotogramas, los solicito desde XRWebGLLayer y los paso a WebGLRenderingContext.

function onXRFrame(hrTime, xrFrame) {
  let xrSession = xrFrame.session;
  xrSession.requestAnimationFrame(onXRFrame);
  let xrViewerPose = xrFrame.getViewerPose(xrRefSpace);
  if (xrViewerPose) {
    let glLayer = xrSession.renderState.baseLayer;
    webGLRenContext.bindFramebuffer(webGLRenContext.FRAMEBUFFER, glLayer.framebuffer);
    for (let xrView of xrViewerPose.views) {
      let viewport = glLayer.getViewport(xrView);
      webGLRenContext.viewport(viewport.x, viewport.y, viewport.width, viewport.height);
      // Draw something to the framebuffer
    }
  }
}

El contexto de webGLRenContext

Cuando escribí este artículo, tuve un debate con algunos colegas sobre la denominación del objeto webGLRenContext. Las secuencias de comandos de muestra y la mayoría del código WebXR llaman a esta variable gl. Cuando trabajaba para comprender las muestras, seguía olvidando a qué se refería gl. Lo llamé webGLRenContext para recordarte, mientras aprendes, que esta es una instancia de WebGLRenderingContext.

El motivo es que el uso de gl permite que los nombres de los métodos se vean como sus equivalentes en la API de OpenGL ES 2.0, que se usa para crear RV en idiomas compilados. Esto es evidente si escribiste apps de RV con OpenGL, pero es confuso si eres completamente nuevo en esta tecnología.

Cómo dibujar algo en el búfer de fotogramas

Si te sientes muy ambicioso, puedes usar WebGL directamente, pero no te recomiendo eso. Es mucho más sencillo usar uno de los frameworks que se mencionan en la parte superior.

Conclusión

Este no es el final de las actualizaciones o los artículos de WebXR. Puedes encontrar una referencia para todas las interfaces y miembros de WebXR en MDN. Para conocer las próximas mejoras en las interfaces, sigue las funciones individuales en Estado de Chrome.

Foto de JESHOOTS.COM en Unsplash