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

Tout savoir sur la boucle de frames

Joe Medley
Joe Medley

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

Cet article décrit la boucle de frames, qui est une boucle infinie contrôlée par un user-agent dans laquelle le contenu est dessiné à l'écran de manière répétée. Le contenu est dessiné dans des blocs distincts appelés "frames". La succession d'images crée une illusion de mouvement.

Ce que n'est pas cet article

WebGL et WebGL2 sont les seuls moyens d'afficher du contenu lors d'une boucle de frames dans une application WebXR. Heureusement, de nombreux frameworks fournissent une couche d'abstraction en plus 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 sur un framework. Elle explique les principes de base d'une boucle de frame à l'aide de l'exemple de session de RV immersive de l'Immersive Web Working Group (démonstration, source). Si vous souhaitez vous plonger dans WebGL ou l'un de ses frameworks, vous trouverez sur Internet une liste d'articles qui ne cesse de s'allonger.

Les joueurs et le jeu

En essayant de comprendre la boucle de trame, je n'arrêtais pas de me perdre dans les détails. De nombreux objets sont en jeu, et certains d'entre eux ne sont nommés qu'à l'aide de propriétés de référence sur d'autres objets. Pour vous aider, je vais décrire les objets, que j'appelle les "joueurs". Ensuite, je décrirai comment ils interagissent, ce que j'appelle le « jeu ».

Les joueurs

XRViewerPose

Une posture correspond à la position et à l'orientation d'un objet dans un espace 3D. Le spectateur et les périphériques d'entrée ont une posture, mais c'est celle du spectateur qui nous intéresse ici. Les poses du lecteur et de l'appareil d'entrée comportent un attribut transform qui décrit leur 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 nécessitent un peu d'explication. Je les aborde en détail dans la section Réalité augmentée. L'exemple que j'utilise comme base pour cet article utilise un espace de référence 'local'. Cela signifie que l'origine est à la position de l'utilisateur au moment de la création de la session, sans valeur minimale bien définie, et que sa position exacte peut varier selon la plate-forme.

XRView

Une vue correspond à une caméra qui visualise une scène virtuelle. Une vue possède également un attribut transform qui décrit sa position en tant que vecteur et son orientation. Elles sont fournies à la fois sous forme de paire vecteur/quaternion et sous forme de matrice équivalente. Vous pouvez utiliser l'une ou l'autre des représentations en fonction de ce qui correspond le mieux à votre code. Chaque vue correspond à un écran ou à une partie d'é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 dispose d'une vue, qui peut recouvrir ou non l'écran de l'appareil. Les casques offrent généralement deux vues, une pour chaque œil.

XRWebGLLayer

Les calques fournissent une source d'images bitmap et des descriptions du rendu de ces images sur l'appareil. Cette description ne reflète pas tout à fait ce que fait ce joueur. Je considère qu'il s'agit d'un intermédiaire entre un appareil et un WebGLRenderingContext. MDN adopte un point de vue très similaire, indiquant qu'il "fournit une liaison" entre les deux. De ce fait, il permet d'accéder aux autres joueurs.

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

WebGLFramebuffer

Un framebuffer fournit des données d'image à WebGLRenderingContext. Après l'avoir récupéré à partir de XRWebGLLayer, il vous suffit de le transmettre au WebGLRenderingContext actuel. Hormis bindFramebuffer() (nous y reviendrons plus tard), vous n'accéderez jamais directement à cet objet. Il vous suffit de le transmettre de XRWebGLLayer à WebGLRenderingContext.

XRViewport

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

WebGLRenderingContext

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

Notez la relation entre XRWebGLLayer et WebGLRenderingContext. L'un correspond à l'appareil du lecteur et l'autre à la page Web. WebGLFramebuffer et XRViewport sont transmis du premier au second.

Relation entre XRWebGLLayer et WebGLRenderingContext
Relation entre XRWebGLLayer et WebGLRenderingContext

Le match

Maintenant que nous savons qui sont les joueurs, examinons le jeu auquel ils jouent. C'est un jeu qui recommence à chaque frame. Rappelez-vous que les frames font partie d'une boucle de frames qui se produit à un rythme qui dépend du matériel sous-jacent. Pour les applications de RV, le nombre d'images par seconde peut être compris entre 60 et 144. La RA pour Android s'exécute à 30 images par seconde. Votre code ne doit pas supposer une fréquence d'images particulière.

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

  1. Appelez XRSession.requestAnimationFrame(). En réponse, le user-agent appelle l'élément XRFrameRequestCallback, que vous avez défini.
  2. Dans votre fonction de rappel :
    1. Rappeler XRSession.requestAnimationFrame().
    2. Prenez la pose du spectateur.
    3. Transmettez ('lier') le WebGLFramebuffer de XRWebGLLayer à WebGLRenderingContext.
    4. Itérez chaque objet XRView, en récupérant son XRViewport à partir de XRWebGLLayer et en le transmettant à WebGLRenderingContext.
    5. Permet de dessiner un élément dans le framebuffer.

Comme les étapes 1 et 2a ont été présentées dans l'article précédent, je vais commencer par l'étape 2b.

Prenez la pose du spectateur

Cela va probablement de soi. Pour dessiner en RA ou RV, je dois savoir où se trouve le spectateur et où il regarde. La position et l'orientation de la visionneuse sont fournies par un objet XRViewerPose. Pour obtenir la position du spectateur, j'appelle XRFrame.getViewerPose() sur le frame d'animation actuel. Je lui transmets l'espace de référence que j'ai acquis lors de la configuration de 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 entamé 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.
  }
}

L'une des positions du spectateur représente la position globale de l'utilisateur (c'est-à-dire sa tête ou l'appareil photo du téléphone, dans le cas d'un smartphone). La pose indique à votre application où se trouve le spectateur. Le rendu réel de l'image utilise des objets XRView, dont nous parlerons 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ù se situent 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 pour le suivi. Par exemple, si les caméras du casque ou du téléphone sont utilisées pour suivre l'appareil, il est possible qu'il ne puisse plus déterminer où il se trouve en cas de faible luminosité ou qu'il n'y a pas de lumière, ou si les caméras sont couvertes.

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 temps. Cependant, j'ai déjà appelé XRSession.requestAnimationFrame() de sorte que si le système parvient à récupérer, la boucle de frames se poursuit. Sinon, le user-agent met fin à la session et appelle le gestionnaire d'événements end.

Un petit détour

L'étape suivante nécessite la création d'objets lors de la configuration de la session. Pour rappel, j'ai créé un canevas et lui ai demandé de créer un contexte de rendu Web GL compatible avec XR. J'ai obtenu cette information 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 comme 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 ("bind") le tampon WebGLFramebuffer

XRWebGLLayer fournit un tampon de frame pour WebGLRenderingContext fourni spécifiquement pour une utilisation avec WebXR et remplace le framebuffer par défaut du contexte de rendu. C'est ce qu'on appelle la "liaison" dans le 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 chaque objet XRView

Après avoir obtenu la pose et lié le framebuffer, vous pouvez obtenir les fenêtres d'affichage. XRViewerPose contient un tableau d'interfaces XRView, représentant chacune un écran ou une partie d'un écran. Elles contiennent les informations nécessaires pour afficher un contenu correctement positionné pour l'appareil et l'utilisateur, telles que le champ de vision, le décalage oculaire et d'autres propriétés optiques. Puisque je dessine pour deux yeux, j'ai deux vues, que je fais défiler et que je dessine une image distincte pour chacune.

Lors de l'implémentation pour la réalité augmentée basée sur un téléphone, je n'aurais qu'une seule vue, mais j'utiliserais toujours une boucle. Bien qu'il puisse sembler inutile d'effectuer une itération sur une seule vue, cela vous permet d'avoir un seul chemin de rendu pour un spectre 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
    }
  }
}

Transmettez l'objet XRViewport à WebGLRenderingContext.

Un objet XRView fait référence à ce qui est observable à l'écran. Mais 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 je 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 contexte webGLRenContext

En rédigeant cet article, j'ai eu un débat avec quelques collègues au sujet de la dénomination de l'objet webGLRenContext. Les exemples de scripts et la plupart des codes WebXR appellent simplement cette variable gl. Lorsque j'essayais de comprendre les exemples, j'oubliais sans cesse ce à quoi gl faisait référence. Je l'ai appelé webGLRenContext pour vous rappeler qu'il s'agit d'une instance de WebGLRenderingContext.

En effet, l'utilisation de gl permet aux noms de méthodes de ressembler à leurs équivalents dans l'API OpenGL ES 2.0, utilisée pour créer une RV dans des langages compilés. Cela est évident si vous avez écrit des applications de RV à l'aide d'OpenGL, mais cela peut être déroutant si vous débutez avec cette technologie.

Dessiner un élément dans le framebuffer

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

Conclusion

Ce n'est pas la fin des mises à jour ou des articles de WebXR. Vous trouverez une documentation de référence pour l'ensemble des interfaces et des membres WebXR sur MDN. Pour découvrir les améliorations à venir des interfaces elles-mêmes, suivez les fonctionnalités individuelles dans Chrome Status.

Photo de JESHOOTS.COM sur Unsplash