Positionner des objets virtuels dans des vues réelles

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

Joe Medley
Joe Medley

L'API WebXR Device a été disponible l'automne dernier dans Chrome 79. Comme indiqué précédemment, l'implémentation de l'API par Chrome est en cours de développement. Chrome a le plaisir de vous annoncer que certains travaux sont terminés. Dans Chrome 81, deux nouvelles fonctionnalités sont arrivées:

Cet article présente l'API WebXR Hit Test, 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, lisez les articles précédents de cette série.

Exemple de session de RA immersive

Le code présenté dans cet article est basé, mais pas identique, à celui qui figure dans l'exemple de test de positionnement de l'Immersive Web Working Group (démonstration, source). Cet exemple vous permet de placer des tournesols virtuels sur des surfaces réelles.

Lorsque vous ouvrez l'application pour la première fois, un cercle bleu s'affiche avec un point au milieu. Le point est l'intersection entre une ligne imaginaire entre votre appareil et le point dans l'environnement. Il se déplace lorsque vous déplacez l'appareil. Au fur et à mesure qu'il trouve des points d'intersection, il semble s'ancrer à des surfaces telles que des sols, des plateaux de table et des murs. En effet, les tests de positionnement fournissent la position et l'orientation du point d'intersection, mais rien concernant les surfaces elles-mêmes.

Ce cercle est appelé réticule. Il s'agit d'une image temporaire qui permet de placer un objet en réalité augmentée. Si vous appuyez sur l'écran, un tournesol est placé sur 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 affiché sur un mur, Lax ou Strict en fonction du 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 le dessinez pas directement à l'aide de WebGL ou WebGL2, consultez la documentation de votre framework. Pour cette raison, je ne détaillerai pas la façon dont le réticule est dessiné dans l'échantillon. Ci-dessous, une seule ligne est affichée pour une seule raison: afin que dans les exemples de code suivants, 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
});

Rejoindre une session

Dans les articles précédents, j'ai présenté le code permettant d'entrer dans une session XR. Vous trouverez ci-dessous une version 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é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 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 demande pas de frame d'animation (démarrant la boucle de frames) et pourquoi la boucle de frames ne semble pas impliquer de tests de positionnement. La source de la confusion était une incompré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 consultez cet exemple à l'aide d'un support autonome, et que vous disposez à la fois d'un casque et d'une manette. Pour mesurer les distances par rapport au contrôleur, vous devez utiliser une image de référence centrée 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, la visionneuse et la manette 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 est en mouvement.

Pour le dessin d'image, j'utilise l'espace de référence local, qui me donne de la stabilité en termes d'environnement. Ensuite, 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. Il est logique de considérer le contrôleur comme une visionneuse électronique. Ensuite, j'appelle xrSession.requestHitTestSource(), qui crée la source des données de test de positionnement que j'utiliserai pour le dessin.

Exécuter une boucle de frames

Le rappel requestAnimationFrame() obtient é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 pour essayer de trouver des surfaces. Pour créer l'illusion du mouvement, redessinez le réticule dans chaque image. En cas d'échec du test de positionnement, ne montrez pas le réticule. Par conséquent, 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 n'importe quoi en RA, j'ai besoin de savoir où se trouve le spectateur et où il le 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 l'élément hitTestSource comme argument et renvoie un tableau d'instances HitTestResult. Le test de positionnement peut détecter plusieurs surfaces. Le premier est le plus proche de l'appareil photo. 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 dirigée vers une boîte posée sur une table, sur un sol. Il est possible que le test de positionnement renvoie les trois surfaces du tableau. Dans la plupart des cas, ce boîtier est celui qui m'intéresse. Si la longueur du tableau renvoyé est de 0, autrement dit, si aucun test de positionnement n'est renvoyé, passez à la suite. 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 positionnement. Le processus de base est le suivant. Prenez une pose à partir du résultat du test de positionnement, transformez (déplacez) l'image du réticule jusqu'à la position du test de positionnement, puis définissez sa propriété visible sur "true". La posture représente la position 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).

À cette étape, il est important de savoir où le placer. Comme le réticule en mouvement vous permet d'effectuer en permanence des 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 d'y parvenir est de consulter l'exemple de code ou l'atelier de programmation. J'espère vous avoir fourni suffisamment d'informations pour comprendre ces deux aspects.

Nous n'avons pas fini de créer des API Web immersives, pas à long terme. Nous publierons de nouveaux articles ici au fur et à mesure de nos progrès.

Photo de Daniel Frank sur Unsplash