La realidad virtual llega a la Web (parte II)

Todo sobre el bucle de fotogramas

Joe Medley
Joe Medley

Recientemente, publiqué La realidad virtual llega a la Web, un artículo que presenta conceptos básicos detrás de la API de WebXR Device. También proporcioné instrucciones para solicitar, ingresar y finalizar una sesión de 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 de forma repetida en la pantalla. El contenido se dibuja en bloques discretos llamados fotogramas. La sucesión de fotogramas crea la ilusión de movimiento.

WebGL y WebGL2 son los únicos medios para renderizar contenido durante un bucle de fotogramas en una app de WebXR. Afortunadamente, muchos frameworks proporcionan una capa de abstracción sobre WebGL y WebGL2. Entre estos frameworks, se 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 framework. En él, se explican los aspectos básicos de un bucle de fotogramas con el ejemplo de sesión de VR inmersiva del Grupo de trabajo de la Web Inmersiva (demo, fuente). Si quieres sumergirte en WebGL o en uno de los frameworks, Internet proporciona 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 por propiedades de referencia en otros objetos. Para ayudarte a mantener la claridad, describiré los objetos, a los que llamaré "jugadores". Luego, describiré cómo interactúan, lo que llamaré “el juego”.

Los jugadores

XRViewerPose

Una pose es la posición y la orientación de algo en el espacio 3D. Tanto los usuarios como los dispositivos de entrada tienen una pose, pero lo que nos interesa aquí es la pose del usuario. Las posiciones del visor y del dispositivo de entrada tienen un atributo transform que describe su posición como un vector y su orientación como un cuaternion en relación con el origen. El origen se especifica según el tipo de espacio de referencia solicitado cuando se llama a XRSession.requestReferenceSpace().

Los espacios de referencia son un poco difíciles de explicar. Las abordo en detalle en Realidad. La muestra que uso como base para este artículo usa un espacio de referencia 'local', lo que significa que el origen está en la posición del usuario en el momento de la creación de la sesión sin un piso bien definido, y su posición precisa puede variar según la plataforma.

XRView

Una vista corresponde a una cámara que ve la escena virtual. Una vista también tiene un atributo transform que describe su posición como un vector y su orientación. Se proporcionan como un par de vectores/cuaterniones y como una matriz equivalente. Puedes usar cualquiera de las representaciones según la que mejor se adapte a tu código. Cada vista corresponde a una pantalla o a una parte de una pantalla que usa un dispositivo 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 o no la pantalla del dispositivo. Por lo general, los auriculares tienen dos vistas, una para cada ojo.

XRWebGLLayer

Las capas proporcionan una fuente de imágenes de mapa 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 el jugador. Lo veo como un intermediario entre un dispositivo y un WebGLRenderingContext. MDN tiene una opinión similar y afirma que "proporciona una vinculación" entre los dos. Por lo tanto, proporciona acceso a los otros jugadores.

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

WebGLFramebuffer

Un búfer de trama proporciona datos de imagen a WebGLRenderingContext. Después de recuperarlo de XRWebGLLayer, simplemente debes pasarlo al WebGLRenderingContext actual. Aparte de llamar a bindFramebuffer() (hablaremos de esto más adelante), nunca accederás a este objeto directamente. Solo lo pasarás de XRWebGLLayer a WebGLRenderingContext.

XRViewport

Un viewport proporciona las coordenadas y 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 segundo.

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 el juego que juegan. Es un juego que comienza de nuevo con cada fotograma. Recuerda que los fotogramas forman parte de un bucle de fotogramas que se produce a una velocidad que depende del hardware subyacente. En el caso de las aplicaciones de RV, los fotogramas por segundo pueden variar de 60 a 144. La RA para Android se ejecuta a 30 fotogramas por segundo. Tu código no debe suponer ninguna velocidad de fotogramas en particular.

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

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

Como los pasos 1 y 2a se abordaron en el artículo anterior, comenzaré en el paso 2b.

Obtén la pose del usuario

No creo que haga falta decirlo. Para dibujar algo en RA o RV, necesito saber dónde está el usuario y hacia dónde está mirando. La posición y orientación del usuario se proporcionan a través de un objeto XRViewerPose. 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 son relativos al espacio de referencia que solicité cuando ingresé a la sesión actual. Como tal vez recuerdes, debo pasar el espacio de referencia actual cuando solicito la pose.

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 del usuario que representa la posición general del usuario, es decir, la cabeza del usuario o la cámara del teléfono en el caso de un smartphone. La pose le indica a tu aplicación dónde está el usuario. La renderización de imágenes real usa objetos XRView, de los que hablaré más adelante.

Antes de continuar, pruebo si se muestra la pose del usuario 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 de saber dónde se encuentra o dónde se encuentran sus dispositivos de entrada en relación con el entorno. El seguimiento se puede perder de varias maneras y varía según el método que se use para el seguimiento. Por ejemplo, si se usan cámaras en los auriculares o el teléfono para hacer un seguimiento, es posible que el dispositivo pierda su capacidad de determinar dónde se encuentra en situaciones con poca luz o sin luz, o si las cámaras están cubiertas.

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

Un desvío breve

El siguiente paso requiere objetos creados durante la configuración de la sesión. Recuerda que creé un lienzo y le indiqué que creara un contexto de renderización de WebGL 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 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 ('vincula') el WebGLFramebuffer

XRWebGLLayer proporciona un búfer de trama para el WebGLRenderingContext que se proporciona específicamente para usar con WebXR y reemplazar el búfer de trama predeterminado de los contextos de renderización. Esto se denomina "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 sobre cada objeto XRView

Después de obtener la pose y vincular el framebuffer, es hora de obtener los viewports. XRViewerPose contiene un array de interfaces XRView, cada una de las cuales representa una pantalla o una parte de una pantalla. Contienen información necesaria para renderizar contenido que está posicionado correctamente para el dispositivo y el visor, como el campo de visión, el desplazamiento del ojo y otras propiedades ópticas. Como estoy dibujando para dos ojos, tengo dos vistas, por las que realizo un bucle y dibujo una imagen independiente para cada una.

Cuando implemente la realidad aumentada basada en teléfonos, solo tendría una vista, pero aún 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 immersivos.

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 al WebGLRenderingContext

Un objeto XRView hace referencia a lo que se puede observar en una pantalla. Sin embargo, para dibujar en esa vista, necesito coordenadas y dimensiones específicas de mi dispositivo. Al igual que con el búfer de trama, 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 webGLRenContext

Mientras escribía este artículo, tuve un debate con algunos colegas sobre el nombre del objeto webGLRenContext. Las secuencias de comandos de muestra y la mayoría del código de WebXR simplemente llaman a esta variable gl. Cuando trabajaba para comprender los ejemplos, olvidaba a qué se refería gl. La llamé webGLRenContext para recordarte mientras aprendes que esta es una instancia de WebGLRenderingContext.

El motivo es que usar gl permite que los nombres de los métodos se vean como sus counterparts en la API de OpenGL ES 2.0, que se usa para crear VR en lenguajes compilados. Este hecho es obvio si escribiste apps de realidad virtual con OpenGL, pero es confuso si es la primera vez que usas esta tecnología.

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

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

Conclusión

Esto no es el final de las actualizaciones ni 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 Chrome Status.

Foto de JESHOOTS.COM en Unsplash