Posizionamento di oggetti virtuali in viste del mondo reale

L'API Hit Test consente di posizionare gli elementi virtuali in una visualizzazione reale.

Mario Medley
Mario Medley

L'API WebXR Device è stata fornita lo scorso autunno in Chrome 79. Come indicato in precedenza, l'implementazione dell'API da parte di Chrome è ancora in fase di sviluppo. Chrome è felice di annunciare che parte del lavoro è terminata. In Chrome 81 sono disponibili due nuove funzionalità:

Questo articolo descrive l'API WebXR Hit Test, che permette di posizionare oggetti virtuali nell'area inquadrata dalla videocamera nel mondo reale.

In questo articolo presumo che tu sappia già come creare una sessione di realtà aumentata e che sappia come eseguire un loop di frame. Se non hai dimestichezza con questi concetti, dovresti leggere i primi articoli di questa serie.

Esempio di sessione AR immersiva

Il codice in questo articolo si basa, ma non è identico, a quello riportato nell'esempio di Hit Test di Immersive Web Working Group (demo, fonte). Questo esempio ti consente di posizionare girasoli virtuali sulle superfici nel mondo reale.

Quando apri l'app per la prima volta, vedrai un cerchio blu con un punto al centro. Il punto è l'intersezione tra una linea immaginaria che collega il dispositivo al punto nell'ambiente. Si muove mentre muovi il dispositivo. Trovando i punti di incrocio, sembra agganciarsi a superfici come pavimenti, piani di lavoro e pareti. Ciò avviene perché gli hit test forniscono la posizione e l'orientamento del punto di intersezione, ma nulla sulle superfici stesse.

Questo cerchio è chiamato reticolo, che è un'immagine temporanea che aiuta a posizionare un oggetto in realtà aumentata. Se tocchi lo schermo, viene posizionato un girasole sulla superficie nella posizione del reticolo e nell'orientamento del punto del reticolo, indipendentemente dal punto in cui hai toccato lo schermo. Il reticolo continua a muoversi insieme al dispositivo.

Un reticolo reso su una parete, Lax o Strict a seconda del loro contesto
Il reticolo è un'immagine temporanea che aiuta a posizionare un oggetto in realtà aumentata.

Crea il reticolo

Devi creare l'immagine del reticolo autonomamente poiché non viene fornita dal browser o dall'API. Il metodo di caricamento e disegno è specifico del framework. Se non stai disegnando direttamente con WebGL o WebGL2, consulta la documentazione del framework. Per questo motivo, non entrerò in dettaglio su come viene disegnato il reticolo nel campione. Di seguito ne mostro una sola per un motivo: negli esempi di codice successivi, saprai a cosa faccio riferimento quando utilizzo la variabile reticle.

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

Richiedi una sessione

Quando richiedi una sessione, devi richiedere 'hit-test' nell'array requiredFeatures, come mostrato di seguito.

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

Accesso a una sessione

Negli articoli precedenti abbiamo presentato un codice per accedere a una sessione XR. Ne ho riportata una versione qui sotto con alcune aggiunte. Innanzitutto ho aggiunto l'ascoltatore di eventi select. Quando l'utente tocca lo schermo, nella visualizzazione della fotocamera viene posizionato un fiore in base alla posa del reticolo. Più avanti descriverò il listener di eventi.

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

Spazi di riferimento multipli

Tieni presente che il codice evidenziato chiama XRSession.requestReferenceSpace() due volte. All'inizio l'ho trovato poco chiara. Ho chiesto perché il codice dell'hit test non richiede un frame dell'animazione (con l'avvio del loop dei frame) e perché il loop dei frame sembra non includere test degli hit. La causa della confusione era il equivoco degli spazi di riferimento. Gli spazi di riferimento esprimono le relazioni tra un'origine e il mondo.

Per capire il funzionamento del codice, immagina di guardare l'esempio utilizzando un supporto indipendente e di avere sia un paio di cuffie sia un controller. Per misurare le distanze dal controller, si utilizza un frame di riferimento incentrato sul controller. Per disegnare qualcosa sullo schermo, però, dovresti usare le coordinate basate sull'utente.

In questo esempio, il visualizzatore e il controller sono lo stesso dispositivo. Ma ho un problema. Ciò che disegni deve essere stabile rispetto all'ambiente, ma il "controller" con cui disegno si muove.

Per il disegno di immagini, uso lo spazio di riferimento local, che mi offre stabilità in termini di ambiente. Dopo averlo fatto, avvio il loop di frame chiamando requestAnimationFrame().

Per gli hit test utilizzo lo spazio di riferimento viewer, che si basa sulla posa del dispositivo al momento dell'hit test. L'etichetta "viewer" è un po' confusa in questo contesto perché parlo di un controller. Ha senso se pensiamo al controller come a un visualizzatore elettronico. Dopo averlo ottenuto, chiamo xrSession.requestHitTestSource(), che crea l'origine dei dati dell'hit test da utilizzare durante il disegno.

Esecuzione di un loop di frame

Il callback requestAnimationFrame() riceve anche un nuovo codice per gestire gli hit test.

Quando muovi il dispositivo, il reticolo deve muoversi per cercare di trovare le superfici. Per creare l'illusione del movimento, ridisegna il reticolo in ogni fotogramma. Ma non mostrare il reticolo se il test ha esito negativo. Quindi, per il reticolo che ho creato in precedenza, ho impostato la proprietà visible su 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
}

Per disegnare qualsiasi cosa in AR, devo sapere dove si trova lo spettatore e dove sta guardando. Ho quindi verificato che hitTestSource e xrViewerPose siano ancora validi.

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
}

Ora chiamo getHitTestResults(). Prende hitTestSource come argomento e restituisce un array di istanze HitTestResult. L'hit test può individuare più piattaforme. Il primo nell'array è quello più vicino alla videocamera. La maggior parte delle volte lo utilizzerai, ma viene restituito un array per i casi d'uso avanzati. Ad esempio, immagina che la tua videocamera sia rivolta verso una scatola su un tavolo appoggiato al pavimento. È possibile che l'hit test restituisca tutte e tre le superfici nell'array. Nella maggior parte dei casi, sarà la scatola che mi interessa. Se la lunghezza dell'array restituito è pari a 0, in altre parole, se non viene restituito alcun hit test, continua. Riprova nel frame successivo.

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
}

Infine, devo elaborare i risultati dell'hit test. Il processo di base è questo. Ottieni una posa dal risultato dell'hit test, trasforma (sposta) l'immagine del reticolo nella posizione dell'hit test e imposta la relativa proprietà visible su true. La posa rappresenta la posizione di un punto su una superficie.

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
}

Posizionamento di un oggetto

Un oggetto viene posizionato in AR quando l'utente tocca lo schermo. Ho già aggiunto un gestore di eventi select alla sessione. (Vedi sopra.)

L'importante è sapere dove posizionarlo. Poiché il retico mobile offre una fonte costante di hit test, il modo più semplice per posizionare un oggetto è disegnarlo nella posizione del reticolo all'ultimo hit test.

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

Conclusione

Il modo migliore per ottenere un handle è analizzare il codice di esempio o provare il codelab. Spero di averti fornito informazioni sufficienti per comprendere meglio entrambe le cose.

Non abbiamo finito di creare API web immersive, non abbiamo finito. Pubblicheremo nuovi articoli qui man mano che procediamo.

Foto di Daniel Frank su Unsplash