Posizionamento di oggetti virtuali in viste del mondo reale

L'API Hit Test ti consente di posizionare elementi virtuali in una visualizzazione del mondo reale.

Joe Medley
Joe Medley

L'API WebXR Device è stata rilasciata lo scorso autunno in Chrome 79. Come affermato allora, l'implementazione dell'API da parte di Chrome è in fase di sviluppo. Chrome è felice di annunciare che alcuni lavori sono stati completati. In Chrome 81 sono state introdotte due nuove funzionalità:

Questo articolo tratta dell'API WebXR Hit Test, un mezzo per posizionare oggetti virtuali in una visualizzazione della videocamera del mondo reale.

In questo articolo presuppongo che tu sappia già come creare una sessione di realtà aumentata e come eseguire un ciclo di frame. Se non hai familiarità con questi concetti, ti consigliamo di leggere gli articoli precedenti di questa serie.

Il campione di sessione AR immersiva

Il codice di questo articolo si basa su quello presente nell'esempio di test di riscontro dell'Immersive Web Working Group, ma non è identico (demo, sorgente). Questo esempio ti consente di posizionare girasoli virtuali sulle superfici del 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 va dal dispositivo al punto nell'ambiente. Si muove quando sposti il dispositivo. Quando trova punti di intersezione, sembra agganciarsi a superfici come pavimenti, piani di tavoli e pareti. Lo fa perché il test di hit fornisce la posizione e l'orientamento del punto di intersezione, ma non informazioni sulle superfici stesse.

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

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

Crea il reticolo

Devi creare tu stesso l'immagine del reticolo, poiché non viene fornita dal browser o dall'API. Il metodo di caricamento e disegno è specifico per il framework. Se non lo disegni direttamente utilizzando WebGL o WebGL2, consulta la documentazione del framework. Per questo motivo, non entrerò nei dettagli su come viene disegnato il reticolo nell'esempio. Di seguito ne mostro una riga solo per un motivo: in modo che negli esempi di codice successivi tu sappia a cosa mi riferisco quando utilizzo la variabile reticle.

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

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

Partecipare a una sessione

Negli articoli precedenti ho presentato il codice per l'inserimento di una sessione XR. Di seguito mostro una versione di questo con alcune aggiunte. Innanzitutto, ho aggiunto il listener dell'evento select. Quando l'utente tocca lo schermo, viene posizionato un fiore nella visualizzazione della videocamera in base alla posa del reticolo. Descriverò questo listener di eventi più avanti.

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

Più spazi di riferimento

Tieni presente che il codice evidenziato chiama XRSession.requestReferenceSpace() due volte. All'inizio ho trovato questa cosa un po' strana. Ho chiesto perché il codice di test di rilevamento dei tocchi non richiede un frame di animazione (avviando il ciclo dei frame) e perché il ciclo dei frame sembra non coinvolgere i test di rilevamento dei tocchi. La fonte della confusione era un malinteso sugli spazi di riferimento. Gli spazi di riferimento esprimono le relazioni tra un'origine e il mondo.

Per capire cosa fa questo codice, immagina di visualizzare questo esempio utilizzando un rig autonomo e di avere sia un visore che un controller. Per misurare le distanze dal controller, utilizzeresti un sistema di riferimento centrato sul controller. Tuttavia, per disegnare qualcosa sullo schermo, utilizzeresti le coordinate centrate sull'utente.

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

Per il disegno delle immagini, utilizzo lo spazio di riferimento local, che mi offre stabilità in termini di ambiente. Dopo averlo ricevuto, avvio il ciclo dei frame chiamando requestAnimationFrame().

Per il test di rilevamento, utilizzo lo spazio di riferimento viewer, che si basa sulla posa del dispositivo al momento del test di rilevamento. L'etichetta "spettatore" è un po' fuorviante in questo contesto perché mi riferisco a un controller. Ha senso se pensi al controller come a un visualizzatore elettronico. Dopo averlo ottenuto, chiamo xrSession.requestHitTestSource(), che crea l'origine dei dati di test di hit che utilizzerò durante il disegno.

Esecuzione di un ciclo di frame

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

Quando sposti il dispositivo, il reticolo deve muoversi con esso mentre cerca le superfici. Per creare l'illusione del movimento, ridisegna il reticolo in ogni fotogramma. ma non mostrare il reticolo se il test di impatto non va a buon fine. 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. Quindi verifico 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. Il test di hit può trovare più superfici. 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 videocamera sia puntata su una scatola su un tavolo sul pavimento. È possibile che il test di hit restituisca tutte e tre le superfici nell'array. Nella maggior parte dei casi, si tratta della casella che mi interessa. Se la lunghezza dell'array restituito è 0, in altre parole, se non viene restituito alcun test di hit, continua. Riprova nel fotogramma 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 del test di hit. La procedura di base è la seguente. Ottieni una posa dal risultato del test di hit, trasforma (sposta) l'immagine del reticolo nella posizione del test di hit, quindi imposta la relativa proprietà visible su true. La posa rappresenta la posa 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
}

Posizionare 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.)

La cosa importante in questo passaggio è sapere dove posizionarlo. Poiché il reticolo in movimento fornisce una fonte costante di test di impatto, il modo più semplice per posizionare un oggetto è disegnarlo nella posizione del reticolo all'ultimo test di impatto.

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 capire come funziona è esaminare il codice di esempio o provare il codelab. Spero di averti fornito abbastanza informazioni di base per comprendere entrambi.

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

Foto di Daniel Frank su Unsplash