Posizionamento di oggetti virtuali in viste del mondo reale

L'API Hit Test ti consente di posizionare gli 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 in precedenza, l'implementazione dell'API in Chrome è in fase di sviluppo. Chrome è felice di annunciare che parte del lavoro è stata completata. In Chrome 81 sono state introdotte due nuove funzionalità:

Questo articolo illustra l'API WebXR Hit Test, un mezzo per posizionare oggetti virtuali nella visuale della fotocamera del mondo reale.

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

L'esempio di sessione AR immersiva

Il codice riportato in questo articolo si basa, ma non è identico, a quello presente nell'esempio di hit test di Immmersive Web Working Group (demo, fonte). Questo esempio ti consente di posizionare girasoli virtuali su superfici del mondo reale.

Quando apri l'app per la prima volta, vedi un cerchio blu con un pallino al centro. Il punto è l'intersezione tra una linea immaginaria che parte dal dispositivo e il punto nell'ambiente. Si muove quando muovi il dispositivo. Quando trova punti di intersezione, sembra agganciarsi a superfici come pavimenti, piani di tavoli e pareti. Questo perché i test di corrispondenza forniscono la posizione e l'orientamento del punto di intersezione, ma non forniscono informazioni sulle superfici stesse.

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

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

Crea il mirino

Devi creare l'immagine del mirino autonomamente, poiché non è 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 nel sample. Di seguito ne mostro una riga per un solo motivo: in questo modo, negli esempi di codice successivi, saprai a cosa mi riferisco 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 ho presentato il codice per accedere a una sessione XR. Di seguito ho mostrato una versione di questo grafico con alcune aggiunte. Innanzitutto ho aggiunto l'ascoltatore di eventi select. Quando l'utente tocca lo schermo, un fiore viene inserito nell'inquadratura della fotocamera in base alla posizione del mirino. Descrivi questo listener di eventi più tardi.

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 l'ho trovata confuso. Ho chiesto perché il codice del test di corrispondenza non richiede un frame di animazione (avviando il loop dei frame) e perché il loop dei frame sembra non coinvolgere i test di corrispondenza. La fonte della confusione è stata un equivoco 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 sample utilizzando un impianto autonomo e di avere sia un auricolare che un controller. Per misurare le distanze dal controller, devi utilizzare un sistema di riferimento centrato sul controller. Tuttavia, per disegnare qualcosa sullo schermo, devi utilizzare coordinate centrate sull'utente.

In questo esempio, lo spettatore e il controller sono lo stesso dispositivo. Ma ho un problema. Ciò che disegno deve essere stabile per quanto riguarda l'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 aver capito, avvio il loop del frame chiamando requestAnimationFrame().

Per i test di hit, utilizzo lo spazio di riferimento viewer, che si basa sulla posizione del dispositivo al momento del test di hit. L'etichetta "viewer" è un po' confusa in questo contesto perché sto parlando di un controller. Ha senso se pensi al controller come a un visualizzatore elettronico. Dopo aver ricevuto questo valore, chiamo xrSession.requestHitTestSource(), che crea l'origine dei dati del test di hit che utilizzerò durante il disegno.

Eseguire un ciclo di frame

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

Quando muovi il dispositivo, il mirino deve muoversi con esso mentre cerca di trovare le superfici. Per creare l'illusione del movimento, ridisegna il mirino in ogni fotogramma. Ma non mostrare il reticolo se l'hit test non va a buon fine. Pertanto, per il mirino 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 qualcosa 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 potrebbe trovare più superfici. Il primo nell'array è quello più vicino alla fotocamera. La maggior parte delle volte lo utilizzerai, ma viene restituito un array per casi d'uso avanzati. Ad esempio, immagina che la videocamera sia rivolta verso una scatola su un tavolo sul pavimento. È possibile che il test degli hit restituisca tutte e tre le superfici nell'array. Nella maggior parte dei casi, è la scatola che mi interessa. Se la lunghezza dell'array restituito è pari a 0, in altre parole, se non viene restituito alcun hit test, prosegui 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 degli hit. Il processo di base è questo. Ottieni una posa dal risultato dell'hit test, trasforma (sposta) l'immagine del reticolo nella posizione dell'hit test, quindi imposta la 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)

In questa fase è importante sapere dove posizionarlo. Poiché il reticolo mobile fornisce una fonte costante di hit test, il modo più semplice per posizionare un oggetto è disegnarlo nella posizione del reticolo nell'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 comprendere questo aspetto è esaminare il codice di esempio o provare il codelab. Spero di averti fornito informazioni sufficienti per comprendere entrambi.

Non abbiamo ancora finito di creare API web immersive, anzi, siamo solo all'inizio. Pubblicheremo nuovi articoli man mano che procediamo.

Foto di Daniel Frank su Unsplash