Umieszczanie wirtualnych obiektów w widoku rzeczywistego

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

Joe Medley
Joe Medley

Interfejs WebXR Device API został wprowadzony jesienią zeszłego roku w Chrome 79. Jak wspomnieliśmy, implementacja interfejsu API w Chrome jest w toku. Z przyjemnością informujemy, że część prac została zakończona. W Chrome 81 pojawiły się 2 nowe funkcje:

W tym artykule omawiamy interfejs WebXR Hit Test API, który umożliwia umieszczanie wirtualnych obiektów w widoku kamery rzeczywistej.

W tym artykule zakładamy, że wiesz już, jak utworzyć sesję rzeczywistości rozszerzonej i jak uruchomić pętlę klatek. Jeśli nie znasz tych pojęć, przeczytaj wcześniejsze artykuły z tej serii.

Przykładowa sesja AR w trybie immersyjnym

Kod w tym artykule jest oparty na kodzie z przykładu testu pozycji wskaźnika grupy roboczej Immersive Web (wersja demonstracyjna, źródło), ale nie jest z nim identyczny. Ten przykład pozwala umieścić wirtualne słoneczniki na powierzchniach w prawdziwym świecie.

Gdy po raz pierwszy otworzysz aplikację, zobaczysz niebieskie kółko z kropką w środku. Kropka to punkt przecięcia wyimaginowanej linii od urządzenia do punktu w otoczeniu. Porusza się, gdy poruszasz urządzeniem. Gdy znajdzie punkty przecięcia, wydaje się, że przyciąga do powierzchni takich jak podłogi, blaty stołów i ściany. Dzieje się tak, ponieważ test pozycji wskaźnika podaje pozycję i orientację punktu przecięcia, ale nie podaje żadnych informacji o samych powierzchniach.

To kółko to celownik, czyli tymczasowy obraz, który pomaga umieścić obiekt w rzeczywistości rozszerzonej. Jeśli klikniesz ekran, słonecznik zostanie umieszczony na powierzchni w miejscu i orientacji celownika, niezależnie od tego, gdzie klikniesz ekran. Siatka będzie się poruszać wraz z urządzeniem.

Siatka celownicza renderowana na ścianie, tryb swobodny lub ścisły w zależności od kontekstu
Siatka to tymczasowy obraz, który pomaga umieścić obiekt w rzeczywistości rozszerzonej.

Tworzenie siatki

Obraz celownika musisz utworzyć samodzielnie, ponieważ nie jest on dostarczany przez przeglądarkę ani interfejs API. Sposób wczytywania i rysowania zależy od platformy. Jeśli nie rysujesz bezpośrednio za pomocą WebGL lub WebGL2, zapoznaj się z dokumentacją platformy. Z tego powodu nie będę szczegółowo omawiać, jak siatka jest rysowana w przykładzie. Poniżej pokazuję jedną linijkę kodu tylko z jednego powodu: aby w późniejszych przykładowych kodach było wiadomo, do czego się odnoszę, gdy używam zmiennej reticle.

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

Poproś o sesję

Podczas wysyłania żądania 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 umożliwiający rozpoczęcie sesji XR. Poniżej przedstawiam wersję tego schematu z kilkoma dodatkami. Najpierw dodaję detektor zdarzeń select. Gdy użytkownik kliknie ekran, w widoku kamery pojawi się kwiat, którego położenie będzie zależeć od pozycji celownika. Opiszę ten detektor zdarzeń później.

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 zaznaczony kod dwukrotnie wywołuje XRSession.requestReferenceSpace(). Początkowo było to dla mnie mylące. Zapytałem, dlaczego kod testu pozycji wskaźnika nie żąda klatki animacji (rozpoczynając pętlę klatek) i dlaczego pętla klatek nie wydaje się obejmować testów pozycji wskaźnika. Źródłem nieporozumienia było niezrozumienie przestrzeni referencyjnych. Przestrzenie referencyjne wyrażają relacje między punktem początkowym a światem.

Aby zrozumieć, jak działa ten kod, wyobraź sobie, że wyświetlasz ten przykład za pomocą samodzielnego zestawu, który składa się z gogli i kontrolera. Aby zmierzyć odległości od kontrolera, użyj układu odniesienia, którego środkiem jest kontroler. Aby jednak narysować coś na ekranie, użyjesz współrzędnych zorientowanych na użytkownika.

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

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

Do testu pozycji wskaźnika używam przestrzeni referencyjnej viewer, która jest oparta na pozycji urządzenia w momencie testu. Etykieta „viewer” (przeglądający) jest w tym kontekście nieco myląca, ponieważ mówię o kontrolerze. Ma to sens, jeśli pomyślisz o kontrolerze jako o elektronicznym wizjerze. Po uzyskaniu tego wywołuję xrSession.requestHitTestSource(), co tworzy źródło danych testu pozycji wskaźnika, którego użyję podczas rysowania.

Uruchamianie pętli klatek

Wywołanie zwrotne requestAnimationFrame() również otrzymuje nowy kod do obsługi testu pozycji wskaźnika.

Gdy poruszasz urządzeniem, celownik powinien poruszać się wraz z nim, próbując znaleźć powierzchnie. Aby uzyskać złudzenie ruchu, przerysuj siatkę w każdej klatce. Jeśli test pozycji wskaźnika się nie powiedzie, nie wyświetlaj celownika. Dla utworzonego wcześniej celownika 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ę widz i gdzie patrzy. Sprawdzam, czy hitTestSource i xrViewerPose 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ę do: getHitTestResults(). Przyjmuje hitTestSource jako argument i zwraca tablicę instancji HitTestResult. Test pozycji wskaźnika może wykryć wiele powierzchni. Pierwszy element w tablicy jest najbliżej aparatu. W większości przypadków będziesz używać tego elementu, ale w bardziej zaawansowanych przypadkach zwracana jest tablica. Wyobraź sobie na przykład, że aparat jest skierowany na pudełko na stole na podłodze. Możliwe, że test pozycji wskaźnika zwróci wszystkie 3 powierzchnie w tablicy. W większości przypadków będzie to pole, które mnie interesuje. Jeśli długość zwróconej tablicy wynosi 0, czyli nie zwrócono żadnego testu pozycji wskaźnika, przejdź dalej. Spróbuj ponownie w następnej klatce.

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 pozycji wskaźnika. Podstawowy proces wygląda tak: Pobierz pozę z wyniku testu pozycji wskaźnika, przekształć (przenieś) obraz celownika do pozycji testu pozycji wskaźnika, a następnie ustaw właściwość visible na wartość true. Pozycja reprezentuje pozycję 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 moduł obsługi zdarzeń select. (patrz wyżej).

W tym kroku ważne jest, aby wiedzieć, gdzie go umieścić. Ponieważ ruchomy celownik zapewnia stałe źródło testów pozycji wskaźnika, najprostszym sposobem na umieszczenie obiektu jest narysowanie go w miejscu celownika podczas ostatniego testu pozycji wskaźnika.

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 zrozumienie tego jest przeanalizowanie przykładowego kodu lub wykonanie ćwiczenia z programowania. Mam nadzieję, że udało mi się przekazać wystarczająco dużo informacji, aby wyjaśnić obie te kwestie.

Nie skończyliśmy jeszcze tworzyć interfejsów API do immersyjnych stron internetowych. W miarę postępów będziemy tu publikować nowe artykuły.