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é publiée l'automne dernier dans Chrome 79. Comme indiqué à l'époque, l'implémentation de l'API dans Chrome est en cours. Chrome est heureux d'annoncer que certains travaux sont terminés. Chrome 81 propose deux nouvelles fonctionnalités :

Cet article présente l'API de test de positionnement WebXR, qui permet de placer des objets virtuels dans une vue de caméra réelle.

Dans cet article, je suppose que vous savez déjà créer une session de réalité augmentée et que vous savez exécuter une boucle de frames. Si vous ne connaissez pas ces concepts, nous vous recommandons 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 celui de l'exemple de test de collision du groupe de travail sur le Web immersif, mais n'est pas identique (démo, source). 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 correspond à l'intersection entre une ligne imaginaire partant de votre appareil et le point de l'environnement. Il bouge à mesure que vous déplacez l'appareil. À mesure qu'il trouve des points d'intersection, il semble s'ancrer à des surfaces telles que les sols, les dessus de table et les murs. En effet, les tests de positionnement fournissent la position et l'orientation du point d'intersection, mais ne fournissent aucune information sur les surfaces elles-mêmes.

Ce cercle est appelé réticule, qui est une image temporaire qui aide à placer un objet en réalité augmentée. Si vous appuyez sur l'écran, un tournesol est placé à la surface à l'emplacement et à 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.

Réticule accroché à un mur, lax ou Strict en fonction du contexte
Le réticule est une image temporaire qui permet de 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 propre au framework. Si vous ne le dessinez pas directement à l'aide de WebGL ou WebGL2, consultez la documentation de votre framework. C'est pourquoi je ne vais pas entrer dans les détails de la façon dont le réticule est dessiné dans l'exemple. Ci-dessous, je n'en montre qu'une ligne pour une seule raison: pour que vous sachiez à quoi je fais référence lorsque j'utilise la variable reticle dans les exemples de code ultérieurs.

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

Lancer une session

Dans des articles précédents, j'ai présenté du code permettant d'accéder à une session XR. J'ai présenté une version ci-dessous avec quelques ajouts. J'ai d'abord ajouté l'écouteur d'événements select. Lorsque l'utilisateur appuie sur l'écran, une fleur est placée dans la vue de l'appareil photo en fonction de la position du réticule. Je décrirai cet écouteur d'événement 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. J'ai trouvé cela déroutant au début. Je me suis demandé pourquoi le code de test de collision ne demande pas de frame d'animation (démarrage de la boucle de frame) et pourquoi la boucle de frame ne semble pas impliquer de tests de collision. La source de la confusion était une malentendue des espaces de référence. Les espaces de référence expriment les relations entre un point d'origine et le monde.

Pour comprendre ce que fait ce code, supposons que vous affichez cet échantillon à l'aide d'un support autonome, et que vous disposez à la fois d'un casque et d'une manette. Pour mesurer les distances à partir du contrôleur, vous devez utiliser un cadre de référence 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, le lecteur 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 le "contrôleur" avec lequel je dessine bouge.

Pour le dessin d'images, j'utilise l'espace de référence local, qui me donne de la stabilité en termes d'environnement. Une fois que j'ai obtenu cela, je lance la boucle de frame 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é "lecteur" est quelque peu déroutant dans ce contexte, car je parle d'un contrôleur. Cela a du sens si vous considérez le contrôleur comme un visionneuse électronique. Après avoir obtenu cela, j'appelle xrSession.requestHitTestSource(), qui crée la source de données de test de collision que j'utiliserai lors du dessin.

Exécuter une boucle de frames

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

Lorsque vous déplacez votre appareil, le réticule doit le faire également pour tenter de trouver des surfaces. Pour créer l'illusion de mouvement, redessinez le réticule dans chaque frame. En revanche, 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 la 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 le spectateur 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(). Il utilise hitTestSource comme argument et renvoie un tableau d'instances HitTestResult. Le test de positionnement peut trouver plusieurs surfaces. Le premier élément du tableau est celui qui se trouve le plus près de l'appareil photo. Vous l'utiliserez la plupart du temps, mais un tableau est renvoyé pour les cas d'utilisation avancés. Par exemple, imaginez que votre caméra est dirigée vers une boîte sur une table au sol. Il est possible que le test de positionnement renvoie les trois surfaces du tableau. Dans la plupart des cas, il s'agit de la case qui m'intéresse. Si la longueur du tableau renvoyé est de 0, en d'autres termes, si aucun test de contact n'est renvoyé, continuez. Réessayez dans le frame suivant.

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 contact. Voici le processus de base. Obtenez une pose à partir du résultat du test de collision, transformez (déplacez) l'image du réticule vers la position du test de collision, 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é dans la 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 dans 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 consiste à 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 cela est de suivre l'exemple de code ou d'essayer l'atelier de programmation. J'espère vous avoir fourni suffisamment d'informations pour que vous puissiez comprendre les deux.

Nous ne sommes pas encore au bout de la création d'API Web immersives. Nous publierons de nouveaux articles ici au fur et à mesure de nos progrès.

Photo de Daniel Frank sur Unsplash