Umieszczanie wirtualnych obiektów w widoku rzeczywistego

Interfejs Hit Test API umożliwia umieszczanie wirtualnych obiektów w widoku rzeczywistym.

Joe Medley
Joe Medley

Interfejs WebXR Device API został wprowadzony jesienią ubiegłego roku w Chrome 79. Jak już wspomnieliśmy, wciąż pracujemy nad wdrażaniem interfejsu API w Chrome. Z przyjemnością informujemy, że część prac została już ukończona. W Chrome 81 pojawiły się 2 nowe funkcje:

W tym artykule omawiamy WebXR Hit Test API, który służy do umieszczania wirtualnych obiektów w widoku kamery w świecie rzeczywistym.

W tym artykule zakładam, że wiesz już, jak tworzyć sesje rozszerzonej rzeczywistości i jak uruchamiać pętlę klatki. Jeśli nie znasz tych pojęć, przeczytaj wcześniejsze artykuły z tej serii.

Przykład wciągającej sesji AR

Kod w tym artykule jest oparty na przykładowym teście trafień grupy roboczej Immersive Web (demo, źródło), ale nie jest z nim identyczny. W tym przykładzie możesz umieścić wirtualne słoneczniki na powierzchniach w rzeczywistym świecie.

Po pierwszym uruchomieniu aplikacji zobaczysz niebieskie kółko z kropką pośrodku. Punkt jest miejscem przecięcia wyimaginowanej linii od urządzenia do punktu w środowisku. Przesuwa się ona wraz z urządzeniem. Po znalezieniu punktów przecięcia powoduje wrażenie przyciągania do powierzchni takich jak podłogi, blaty stołu i ściany. Dzieje się tak, ponieważ testowanie trafień dostarcza informacji o pozycji i orientacji punktu przecięcia, ale nie o samych powierzchniach.

Ten okrąg nazywa się siatka. Jest to tymczasowy obraz, który pomaga umieszczać obiekty w rzeczywistości rozszerzonej. Gdy klikniesz ekran, na powierzchni w miejscu, w którym znajduje się celownik, pojawi się obrazek słońca, niezależnie od tego, gdzie klikniesz ekran. Celownik będzie się nadal poruszać wraz z urządzeniem.

siatki celownicze renderowane na ścianie, Lax lub Strict w zależności od kontekstu
Celownik to tymczasowy obraz, który ułatwia umieszczanie obiektów w rzeczywistości rozszerzonej.

Tworzenie siatki

Musisz samodzielnie utworzyć obraz siatki celowniczej, ponieważ nie jest on udostępniany przez przeglądarkę ani interfejs API. Sposób wczytywania i rysowania zależy od konkretnej platformy. Jeśli nie rysujesz go bezpośrednio za pomocą WebGL lub WebGL 2, zapoznaj się z dokumentacją platformy. Dlatego nie będę szczegółowo omawiać tego, jak wyznacza się siatkę w próbce. Poniżej podaję tylko jedną linię kodu z jednego powodu: aby w późniejszych przykładach kodu wiedzieć, do czego się odnoszę, gdy używam zmiennej reticle.

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

Prośba o sesję

Gdy żądasz sesji, musisz poprosić o 'hit-test' w tablicy requiredFeatures, jak pokazano poniżej.

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

Rozpoczynanie sesji

W poprzednich artykułach przedstawiłem kod wejścia w sesję XR. Poniżej przedstawiam wersję z kilkoma dodatkami. Najpierw dodałem listenera zdarzenia select. Gdy użytkownik kliknie ekran, w widoku aparatu pojawi się kwiat, który będzie widoczny w ramce celownika. Później opiszę ten detektor zdarzeń.

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

Wiele przestrzeni referencyjnych

Zwróć uwagę, że wyróżniony kod wywołuje funkcję XRSession.requestReferenceSpace() dwa razy. Początkowo było to dla mnie niejasne. Zapytanie o to, dlaczego kod testu kolizji nie prosi o ramkę animacji (rozpoczynającą pętlę ramek) i dlaczego pętla ramek wydaje się nie uwzględniać testów kolizji. Pomylenie wynikało z nieporozumienia pojęcia przestrzeni referencyjnych. Przestrzeń referencyjna wyraża relacje między punktem początkowym a światem.

Aby zrozumieć, do czego służy ten kod, udawaj, że wyświetlasz ten przykład za pomocą oddzielnego zestawu i masz zestaw słuchawkowy oraz kontroler. Aby mierzyć odległości od kontrolera, należy użyć ramki odniesienia z ośrodkiem w kontrolerze. Aby jednak coś narysować na ekranie, użyjesz współrzędnych skoncentrowanych na użytkowniku.

W tym przykładzie urządzenie wyświetlające i urządzenie sterujące to to samo urządzenie. Ale mam pewien problem. To, co rysuję, musi być stabilne w stosunku do otoczenia, ale „kontroler”, którym rysuję, się porusza.

Do rysowania obrazu używam przestrzeni odniesienia local, która zapewnia mi stabilność w zakresie środowiska. Po uzyskaniu tego uruchamiam pętlę klatek, wywołując funkcję requestAnimationFrame().

Do testów trafień korzystam z przestrzeni referencyjnej viewer, która określa pozycję urządzenia w momencie testu trafienia. Etykieta „przeglądający” jest w tym kontekście nieco myląca, bo mam na myśli kontroler. To ma sens, jeśli potraktujesz kontroler jako elektroniczny wyświetlacz. Potem nazywam się xrSession.requestHitTestSource(), co tworzy źródło danych testowych trafień, których użyję do rysowania.

Uruchamianie pętli ramek

Nowy kod do obsługi testowania trafień jest też przekazywany do funkcji wywołania zwrotnego requestAnimationFrame().

Gdy przenosisz urządzenie, celownik musi się poruszać razem z nim, aby znajdować powierzchnie. Aby stworzyć iluzję ruchu, narysuj siatkę w każdym ujęciu. Nie pokazuj jednak siatki, jeśli test trafienia się nie powiedzie. W przypadku siatki, którą wcześniej utworzyłem, ustawiłem właściwość visible na 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
}

Aby narysować coś w AR, muszę wiedzieć, gdzie znajduje się użytkownik i w jaką stronę patrzy. Sprawdzam, czy hitTestSourcexrViewerPose są nadal prawidłowe.

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
}

Teraz dzwonię pod numer getHitTestResults(). Przyjmuje jako argument element hitTestSource i zwraca tablicę instancji HitTestResult. Test kolizji może wykryć wiele powierzchni. Pierwszy element tablicy jest najbliżej aparatu. Zwykle jest ona używana, ale w zaawansowanych przypadkach użycia zwracana jest tablica. Wyobraź sobie na przykład, że kamera jest skierowana na pudełko na stole na podłodze. Możliwe, że test trafienia zwróci wszystkie 3 płaszczyzny w tablicy. W większości przypadków będzie to pole „Mnie interesuje”. Jeśli długość zwróconego tablicy wynosi 0, czyli jeśli nie zwrócono żadnego testu dopasowania, przejdź dalej. Spróbuj ponownie w następnym ujęciu.

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
}

Na koniec muszę przetworzyć wyniki testu uderzeń. Oto podstawowy proces. Uzyskaj pozę z wyniku testu uderzenia, przetransformuj (przesuń) obraz siatki do pozycji testu uderzenia, a potem ustaw jego właściwość visible na wartość true. Ta pozycja odzwierciedla położenie punktu na powierzchni.

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
}

Umieszczanie obiektu

Obiekt jest umieszczany w AR, gdy użytkownik kliknie ekran. Do sesji został już dodany element obsługi zdarzenia select. (patrz wyżej).

Ważne jest, aby wiedzieć, gdzie go umieścić. Ruchomy siatka celownika to stałe źródło testów trafień, dlatego najprostszym sposobem na umieszczenie obiektu jest narysowanie go w lokalizacji celownika podczas ostatniego testu trafienia.

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

Podsumowanie

Najlepszym sposobem na zapoznanie się z tą funkcją jest użycie przykładowego kodu lub skorzystanie z ćwiczenia z kodem. Mam nadzieję, że udało mi się zrozumieć oba te zagadnienia.

Nie skończyliśmy jeszcze tworzenia interfejsów API dla immersywnej sieci. W miarę postępów będziemy publikować tutaj nowe artykuły.

Zdjęcie: Daniel FrankUnsplash