La réalité virtuelle arrive sur le Web, 2e partie

Tout savoir sur la boucle de frame

Joe Medley
Joe Medley

Récemment, j'ai publié La réalité virtuelle arrive sur le Web, un article qui présente les concepts de base de l'API WebXR Device. J'ai également fourni des instructions pour demander, accéder et terminer une session XR.

Cet article décrit la boucle de frame, qui est une boucle infinie contrôlée par l'agent utilisateur dans laquelle le contenu est dessiné à plusieurs reprises à l'écran. Le contenu est dessiné dans des blocs distincts appelés "frames". La succession d'images crée l'illusion de mouvement.

WebGL et WebGL2 sont les seuls moyens de générer du contenu lors d'une boucle de frames dans une application WebXR. Heureusement, de nombreux frameworks fournissent une couche d'abstraction au-dessus de WebGL et WebGL2. Ces frameworks incluent three.js, babylonjs et PlayCanvas, tandis que A-Frame et React 360 ont été conçus pour interagir avec WebXR.

Cet article n'est ni un tutoriel WebGL ni un tutoriel sur un framework. Il explique les principes de base d'une boucle de frame à l'aide de l'exemple de session de réalité virtuelle immersive du groupe de travail sur le Web immersif (démo, source). Si vous souhaitez vous plonger dans WebGL ou l'un des frameworks, Internet propose une liste croissante d'articles.

Les joueurs et le match

Lorsque j'ai essayé de comprendre la boucle de frame, je me suis perdu dans les détails. De nombreux objets sont en jeu, et certains ne sont nommés que par des propriétés de référence sur d'autres objets. Pour vous aider à vous y retrouver, je vais décrire les objets, que j'appelle "joueurs". Je vais ensuite décrire leur interaction, que j'appelle "le jeu".

Les joueurs

XRViewerPose

Une pose correspond à la position et à l'orientation d'un objet dans l'espace 3D. Les spectateurs et les appareils d'entrée ont une pose, mais c'est la pose du spectateur qui nous intéresse ici. Les poses du lecteur et de l'appareil d'entrée ont un attribut transform décrivant sa position en tant que vecteur et son orientation en tant que quaternion par rapport à l'origine. L'origine est spécifiée en fonction du type d'espace de référence demandé lors de l'appel de XRSession.requestReferenceSpace().

Les espaces de référence prennent un certain temps à expliquer. Je les aborde en détail dans Réalité augmentée. L'exemple que j'utilise comme base de cet article utilise un espace de référence 'local', ce qui signifie que l'origine se trouve à la position du spectateur au moment de la création de la session, sans plancher bien défini, et que sa position précise peut varier selon la plate-forme.

XRView

Une vue correspond à une caméra qui regarde la scène virtuelle. Une vue comporte également un attribut transform qui décrit sa position en tant que vecteur et son orientation. Ils sont fournis à la fois sous forme de paire vecteur/quaternion et sous forme de matrice équivalente. Vous pouvez utiliser l'une ou l'autre représentation, en fonction de celle qui convient le mieux à votre code. Chaque vue correspond à un écran ou à une partie d'un écran utilisé par un appareil pour présenter des images à l'utilisateur. Les objets XRView sont renvoyés dans un tableau à partir de l'objet XRViewerPose. Le nombre de vues dans le tableau varie. Sur les appareils mobiles, une scène de RA comporte une seule vue, qui peut ou non recouvrir l'écran de l'appareil. Les casques comportent généralement deux vues, une pour chaque œil.

XRWebGLLayer

Les calques fournissent une source d'images bitmap et des descriptions de la façon dont ces images doivent être affichées sur l'appareil. Cette description ne décrit pas vraiment ce que fait ce lecteur. Je considère cela comme un intermédiaire entre un appareil et un WebGLRenderingContext. MDN adopte une approche similaire, affirmant qu'il "fournit un lien" entre les deux. Il permet ainsi d'accéder aux autres joueurs.

En général, les objets WebGL stockent des informations d'état pour le rendu des graphiques 2D et 3D.

WebGLFramebuffer

Un framebuffer fournit des données d'image à WebGLRenderingContext. Après l'avoir récupéré dans XRWebGLLayer, il vous suffit de le transmettre à l'WebGLRenderingContext actuelle. En dehors de l'appel de bindFramebuffer() (nous y reviendrons plus tard), vous n'accéderez jamais directement à cet objet. Vous le transmettrez simplement de XRWebGLLayer à WebGLRenderingContext.

XRViewport

Une fenêtre d'affichage fournit les coordonnées et les dimensions d'une région rectangulaire dans WebGLFramebuffer.

WebGLRenderingContext

Un contexte de rendu est un point d'accès programmatique pour un canevas (l'espace sur lequel nous dessinons). Pour ce faire, il a besoin à la fois d'un WebGLFramebuffer et d'un XRViewport.

Notez la relation entre XRWebGLLayer et WebGLRenderingContext. L'une correspond à l'appareil de l'utilisateur et l'autre à la page Web. WebGLFramebuffer et XRViewport sont transmis de l'ancien à l'autre.

Relation entre XRWebGLLayer et WebGLRenderingContext
La relation entre XRWebGLLayer et WebGLRenderingContext

Le jeu

Maintenant que nous connaissons les acteurs, examinons le jeu qu'ils jouent. Il s'agit d'un jeu qui recommence à chaque frame. Rappelez-vous que les trames font partie d'une boucle de trame qui se produit à une fréquence qui dépend du matériel sous-jacent. Pour les applications de réalité virtuelle, le nombre d'images par seconde peut varier de 60 à 144. La RA pour Android s'exécute à 30 images par seconde. Votre code ne doit pas supposer de fréquence d'images particulière.

Le processus de base de la boucle de frame se présente comme suit:

  1. Appelez XRSession.requestAnimationFrame(). En réponse, l'agent utilisateur appelle le XRFrameRequestCallback, que vous avez défini.
  2. Dans votre fonction de rappel :
    1. Appelez à nouveau XRSession.requestAnimationFrame().
    2. Obtenez la position du spectateur.
    3. Transmettez ('liez') le WebGLFramebuffer du XRWebGLLayer au WebGLRenderingContext.
    4. Itérez sur chaque objet XRView, récupérez son XRViewport à partir de XRWebGLLayer et transmettez-le à WebGLRenderingContext.
    5. Dessinez quelque chose dans le framebuffer.

Étant donné que les étapes 1 et 2a ont été abordées dans l'article précédent, je vais commencer par l'étape 2b.

Obtenir la pose du spectateur

Cela va sans doute de soi. Pour dessiner quoi que ce soit en RA ou en RV, je dois savoir où se trouve le spectateur et où il regarde. La position et l'orientation du spectateur sont fournies par un objet XRViewerPose. J'obtiens la pose du spectateur en appelant XRFrame.getViewerPose() sur le frame d'animation actuel. Je lui transmets l'espace de référence que j'ai acquis lorsque j'ai configuré la session. Les valeurs renvoyées par cet objet sont toujours relatives à l'espace de référence que j'ai demandé lorsque j'ai accédé à la session en cours. Comme vous vous en souvenez peut-être, je dois transmettre l'espace de référence actuel lorsque je demande 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.
 
}
}

Une seule pose de spectateur représente la position globale de l'utilisateur, c'est-à-dire la tête du spectateur ou la caméra du téléphone dans le cas d'un smartphone. La pose indique à votre application où se trouve le spectateur. Le rendu d'image réel utilise des objets XRView, que je vais aborder un peu plus tard.

Avant de continuer, je vérifie si la pose du spectateur a été renvoyée au cas où le système perdrait le suivi ou bloquerait la pose pour des raisons de confidentialité. Le suivi est la capacité de l'appareil XR à savoir où il se trouve et/ou où se trouvent ses périphériques d'entrée par rapport à l'environnement. Le suivi peut être perdu de plusieurs manières et varie en fonction de la méthode utilisée. Par exemple, si les caméras du casque ou du téléphone sont utilisées pour le suivi, l'appareil peut perdre la possibilité de déterminer où il se trouve dans des situations où la lumière est faible ou inexistante, ou si les caméras sont recouvertes.

Par exemple, si le casque affiche une boîte de dialogue de sécurité telle qu'une invite d'autorisation, le navigateur peut cesser de fournir des poses à l'application pendant ce processus. Toutefois, j'ai déjà appelé XRSession.requestAnimationFrame() afin que, si le système peut se rétablir, la boucle de frame continue. Sinon, l'agent utilisateur met fin à la session et appelle le gestionnaire d'événements end.

Un petit détour

L'étape suivante nécessite des objets créés lors de la configuration de la session. Rappelez-vous que j'ai créé un canevas et lui ai demandé de créer un contexte de rendu Web GL compatible avec XR, que j'ai obtenu en appelant canvas.getContext(). Tout le dessin est effectué à l'aide de l'API WebGL, de l'API WebGL2 ou d'un framework basé sur WebGL tel que Three.js. Ce contexte a été transmis à l'objet de session via updateRenderState(), avec une nouvelle instance 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)
 
});

Transmettre (lire) le WebGLFramebuffer

XRWebGLLayer fournit un framebuffer pour le WebGLRenderingContext fourni spécifiquement pour être utilisé avec WebXR et remplacer le framebuffer par défaut des contextes de rendu. C'est ce que l'on appelle un "liage" en langage 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
 
}
}

Itérer sur chaque objet XRView

Après avoir obtenu la pose et lié le framebuffer, il est temps d'obtenir les vues. XRViewerPose contient un tableau d'interfaces XRView, chacune représentant un écran ou une partie d'un écran. Ils contiennent des informations nécessaires pour afficher un contenu correctement positionné pour l'appareil et le spectateur, telles que le champ de vision, le décalage des yeux et d'autres propriétés optiques. Comme je dessine pour deux yeux, j'ai deux vues, que je parcours en boucle et que je dessine une image distincte pour chacune.

Lors de l'implémentation pour la réalité augmentée sur téléphone, je n'aurais qu'une seule vue, mais j'utiliserais quand même une boucle. Bien qu'il puisse sembler inutile d'itérer sur une seule vue, cela vous permet d'avoir un seul chemin de rendu pour un large éventail d'expériences immersives. Il s'agit d'une différence importante entre WebXR et les autres systèmes immersifs.

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

Transmettre l'objet XRViewport au WebGLRenderingContext

Un objet XRView fait référence à ce qui est observable à l'écran. Toutefois, pour dessiner dans cette vue, j'ai besoin de coordonnées et de dimensions spécifiques à mon appareil. Comme pour le framebuffer, je les demande à XRWebGLLayer et les transmets à 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
   
}
 
}
}

Le webGLRenContext

En rédigeant cet article, j'ai eu un débat avec quelques collègues sur le nom de l'objet webGLRenContext. Les exemples de scripts et la plupart du code WebXR appellent simplement cette variable gl. Lorsque j'essayais de comprendre les exemples, j'oubliais constamment à quoi gl faisait référence. Je l'ai appelé webGLRenContext pour vous rappeler que vous apprenez qu'il s'agit d'une instance de WebGLRenderingContext.

En effet, l'utilisation de gl permet aux noms de méthode de ressembler à leurs homologues dans l'API OpenGL ES 2.0, utilisée pour créer de la RV dans des langages compilés. Ce fait est évident si vous avez écrit des applications de RV à l'aide d'OpenGL, mais peut prêter à confusion si vous ne connaissez pas du tout cette technologie.

Dessiner quelque chose dans le framebuffer

Si vous vous sentez vraiment ambitieux, vous pouvez utiliser WebGL directement, mais je ne le recommande pas. Il est beaucoup plus simple d'utiliser l'un des frameworks listés en haut.

Conclusion

Ce n'est pas la fin des mises à jour ni des articles sur WebXR. Vous trouverez une documentation de référence sur toutes les interfaces et tous les membres de WebXR sur MDN. Pour en savoir plus sur les améliorations à venir des interfaces elles-mêmes, suivez les fonctionnalités individuelles sur État de Chrome.

Photo de JESHOOTS.COM sur Unsplash