Positionner des objets virtuels dans des vues réelles

L'API Hit Test vous permet de positionner des éléments virtuels dans une vue du monde réel.

Joe Medley
Joe Medley

L'API WebXR Device a été lancée l'automne dernier dans Chrome 79. Comme indiqué à l'époque, l'implémentation de l'API par Chrome est en cours de développement. Chrome est heureux d'annoncer que certains de ces travaux sont terminés. Deux nouvelles fonctionnalités ont été ajoutées à Chrome 81 :

Cet article traite de l'API WebXR Hit Test, qui permet de placer des objets virtuels dans une vue de caméra du monde réel.

Dans cet article, nous partons du principe que vous savez déjà créer une session de réalité augmentée et exécuter une boucle de frames. Si vous ne connaissez pas ces concepts, nous vous conseillons de lire les articles précédents de cette série.

Exemple de session de RA immersive

Le code de cet article est basé sur l'exemple de test de positionnement du groupe de travail Immersive Web (démo, source), mais n'est pas identique. Cet exemple vous permet de placer des tournesols virtuels sur des surfaces du monde réel.

Lorsque vous ouvrez l'application pour la première fois, un cercle bleu avec un point au milieu s'affiche. Le point est l'intersection entre une ligne imaginaire allant de votre appareil au point dans l'environnement. Il se déplace lorsque vous déplacez l'appareil. Lorsqu'il trouve des points d'intersection, il semble s'accrocher à des surfaces telles que les sols, les tables et les murs. En effet, le test de positionnement fournit la position et l'orientation du point d'intersection, mais aucune information sur les surfaces elles-mêmes.

Ce cercle est appelé réticule. Il s'agit d'une image temporaire qui aide à placer un objet en réalité augmentée. Si vous appuyez sur l'écran, un tournesol est placé sur la surface à l'emplacement et selon l'orientation du réticule, quel que soit l'endroit où vous avez appuyé sur l'écran. Le réticule continue de se déplacer avec votre appareil.

Un réticule affiché sur un mur, Lax ou Strict selon le contexte
Le réticule est une image temporaire qui aide à placer un objet en réalité augmentée.

Créer le réticule

Vous devez créer vous-même l'image du réticule, car elle n'est pas fournie par le navigateur ni par l'API. La méthode de chargement et de dessin est spécifique au framework. Si vous ne dessinez pas directement à l'aide de WebGL ou WebGL2, consultez la documentation de votre framework. C'est pourquoi je ne vais pas expliquer en détail comment le réticule est dessiné dans l'exemple. Ci-dessous, je n'en montre qu'une seule ligne pour une seule raison : pour que, dans les exemples de code ultérieurs, vous sachiez à quoi je fais référence lorsque j'utilise la variable reticle.

let reticle = new Gltf2Node({url: 'media/gltf/reticle/reticle.gltf'});

Demander une session

Lorsque vous demandez une session, vous devez demander 'hit-test' dans le tableau requiredFeatures, comme indiqué ci-dessous.

navigator.xr.requestSession('immersive-ar', {
  requiredFeatures: ['local', 'hit-test']
})
.then((session) => {
  // Do something with the session
});

Accéder à une session

Dans les articles précédents, j'ai présenté le code permettant d'accéder à une session XR. Vous trouverez ci-dessous une version de ce schéma avec quelques ajouts. Tout d'abord, j'ai ajouté l'écouteur d'événements select. Lorsque l'utilisateur appuie sur l'écran, une fleur est placée dans la vue de la caméra en fonction de la position du réticule. Je décrirai cet écouteur d'événements plus tard.

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

Plusieurs espaces de référence

Notez que le code mis en surbrillance appelle XRSession.requestReferenceSpace() deux fois. Au début, j'ai trouvé cela déroutant. J'ai demandé pourquoi le code du test de positionnement ne demandait pas d'image d'animation (démarrage de la boucle d'images) et pourquoi la boucle d'images ne semblait pas impliquer de tests de positionnement. La source de la confusion était une mauvaise compréhension des espaces de référence. Les espaces de référence expriment les relations entre une origine et le monde.

Pour comprendre ce que fait ce code, imaginez que vous visualisez cet exemple à l'aide d'un appareil autonome et que vous disposez d'un casque et d'un contrôleur. Pour mesurer les distances par rapport au contrôleur, vous devez utiliser un repère centré sur le contrôleur. Mais pour dessiner quelque chose à l'écran, vous devez utiliser des coordonnées centrées sur l'utilisateur.

Dans cet exemple, l'appareil de visionnage et le contrôleur sont le même appareil. Mais j'ai un problème. Ce que je dessine doit être stable par rapport à l'environnement, mais la "manette" avec laquelle je dessine est en mouvement.

Pour le dessin d'images, j'utilise l'espace de référence local, qui me donne de la stabilité en termes d'environnement. Après avoir obtenu cette valeur, je lance la boucle de frames en appelant requestAnimationFrame().

Pour les tests de positionnement, j'utilise l'espace de référence viewer, qui est basé sur la position de l'appareil au moment du test de positionnement. Le libellé "viewer" est quelque peu déroutant dans ce contexte, car je parle d'un contrôleur. Cela est logique si vous considérez le contrôleur comme un viseur électronique. Ensuite, j'appelle xrSession.requestHitTestSource(), qui crée la source des données de test de positionnement que j'utiliserai pour dessiner.

Exécuter une boucle de frames

Le rappel requestAnimationFrame() reçoit également un nouveau code pour gérer les tests de positionnement.

Lorsque vous déplacez votre appareil, le réticule doit se déplacer avec lui lorsqu'il essaie de trouver des surfaces. Pour créer l'illusion de mouvement, redessinez le réticule dans chaque frame. Mais n'affichez pas le réticule si le test de positionnement échoue. Pour le réticule que j'ai créé précédemment, j'ai défini sa propriété visible sur 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
}

Pour dessiner quoi que ce soit en RA, je dois savoir où se trouve l'utilisateur et où il regarde. Je vérifie donc que hitTestSource et xrViewerPose sont toujours valides.

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
}

J'appelle maintenant getHitTestResults(). Elle prend hitTestSource comme argument et renvoie un tableau d'instances HitTestResult. Le test de positionnement peut détecter plusieurs surfaces. Le premier élément du tableau est le plus proche de la caméra. La plupart du temps, vous l'utiliserez, mais un tableau est renvoyé pour les cas d'utilisation avancés. Par exemple, imaginez que votre caméra est orientée vers une boîte posée sur une table, elle-même posée sur le sol. Il est possible que le test de positionnement renvoie les trois surfaces du tableau. Dans la plupart des cas, c'est la boîte qui m'intéresse. Si la longueur du tableau renvoyé est égale à 0, c'est-à-dire si aucun test de positionnement n'est renvoyé, continuez. Réessayez dans l'image suivante.

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
}

Enfin, je dois traiter les résultats du test de positionnement. Le processus de base est le suivant. Obtenez une pose à partir du résultat du test de positionnement, transformez (déplacez) l'image du réticule vers la position du test de positionnement, puis définissez sa propriété visible sur "true". La pose représente la pose d'un point sur une surface.

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
}

Placer un objet

Un objet est placé en RA lorsque l'utilisateur appuie sur l'écran. J'ai déjà ajouté un gestionnaire d'événements select à la session. (Voir ci-dessus.)

L'important, à cette étape, est de savoir où le placer. Étant donné que le réticule mobile vous fournit une source constante de tests de positionnement, le moyen le plus simple de placer un objet est de le dessiner à l'emplacement du réticule lors du dernier test de positionnement.

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

Conclusion

Le meilleur moyen de comprendre ce concept est de parcourir l'exemple de code ou d'essayer l'atelier de programmation. J'espère vous avoir fourni suffisamment d'informations pour que vous compreniez les deux.

Nous n'avons pas fini de créer des API Web immersives, loin de là. Nous publierons de nouveaux articles ici au fur et à mesure de notre progression.