Umieszczanie wirtualnych obiektów w widoku rzeczywistego

Interfejs Hit Test API umożliwia umieszczanie wirtualnych elementów w widoku świata rzeczywistego.

Joe Medley
Joe Medley

Interfejs WebXR Device API został udostępniony jesienią ubiegłego roku w Chrome 79. Jak już wspomnieliśmy, implementacja tego interfejsu API w Chrome jest w trakcie opracowywania. Chrome z przyjemnością informuje, ż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 z kamery w świecie rzeczywistym.

W tym artykule zakładam, ż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 wciągającej rzeczywistości rozszerzonej

Kod w tym artykule jest oparty na kodzie z przykładowego testu trafień Immersive Web Working Group, ale nie jest z nim identyczny (demo, źródło). Ten przykład pozwala umieszczać wirtualne słoneczniki na powierzchniach w rzeczywistym świecie.

Po pierwszym otwarciu aplikacji zobaczysz niebieskie kółko z kropką na środku. Kropka to punkt przecięcia wyimaginowanej linii łączącej urządzenie z punktem w środowisku. Porusza się wraz z urządzeniem. Gdy znajdzie punkty przecięcia, wydaje się, że przyciąga je do powierzchni, takich jak podłogi, blaty stołów i ściany. Dzieje się tak, ponieważ testowanie trafień zapewnia pozycję i orientację punktu przecięcia, ale nie dostarcza żadnych informacji o samych powierzchniach.

Ten okrąg to siatka celownicza, czyli tymczasowy obraz, który pomaga umieścić obiekt w rzeczywistości rozszerzonej. Jeśli dotkniesz ekranu, słonecznik zostanie umieszczony na powierzchni w miejscu i orientacji siatki celowniczej, niezależnie od tego, gdzie dotkniesz ekranu. Celownik nadal będzie się poruszać wraz z urządzeniem.

siatka celownicza renderowana na ścianie, w trybie Lax lub Strict w zależności od kontekstu;
Celownik to tymczasowy obraz, który pomaga umieścić obiekt w rzeczywistości rozszerzonej.

Tworzenie siatki celowniczej

Obraz siatki musisz utworzyć samodzielnie, ponieważ nie jest on udostępniany przez przeglądarkę ani interfejs API. Metoda wczytywania i rysowania jest specyficzna dla danego frameworka. Jeśli nie rysujesz go bezpośrednio za pomocą WebGL lub WebGL2, zapoznaj się z dokumentacją frameworka. Z tego powodu nie będę szczegółowo opisywać, jak w przykładzie rysowany jest celownik. Poniżej pokazuję tylko jeden wiersz tego kodu. Robię to tylko po to, aby w późniejszych przykładach kodu było wiadomo, do czego się odnoszę, gdy używam zmiennej reticle.

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

Wysyłanie prośby o sesję

Podczas wysyłania żądania sesji musisz wysłać żądanie '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
});

Wchodzenie do sesji

W poprzednich artykułach przedstawiłem kod umożliwiający rozpoczęcie sesji XR. Poniżej znajdziesz wersję z kilkoma dodatkami. Najpierw dodaję selectdetektor zdarzeń. Gdy użytkownik dotknie ekranu, w widoku z kamery pojawi się kwiat w miejscu, w którym znajduje się celownik. Opiszę go 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 odniesienia

Zwróć uwagę, że wyróżniony kod wywołuje funkcję XRSession.requestReferenceSpace() 2 razy. Początkowo było to dla mnie niejasne. Zapytałem, dlaczego kod testu trafienia nie żąda klatki animacji (rozpoczynając pętlę klatek) i dlaczego pętla klatek nie wydaje się obejmować testów trafienia. Źródłem nieporozumienia było błędne zrozumienie przestrzeni odniesienia. Przestrzenie odniesienia wyrażają relacje między punktem początkowym a światem.

Aby zrozumieć, co robi ten kod, wyobraź sobie, że wyświetlasz ten przykład za pomocą samodzielnego urządzenia, a także masz zestaw słuchawkowy i kontroler. Aby mierzyć odległości od kontrolera, używasz układu odniesienia wyśrodkowanego na kontrolerze. Aby jednak narysować coś na ekranie, użyjesz współrzędnych zorientowanych na użytkownika.

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

Do rysowania obrazów używam local przestrzeni odniesienia, która zapewnia mi stabilność w zakresie środowiska. Po otrzymaniu tej wartości rozpoczynam pętlę klatek, wywołując funkcję requestAnimationFrame().

Do testowania trafień używam viewer przestrzeni odniesienia, która jest oparta na pozycji urządzenia w momencie testu trafień. Etykieta „przeglądarka” jest w tym kontekście nieco myląca, ponieważ mówię o kontrolerze. Ma to sens, jeśli kontroler potraktujesz jako elektroniczny wizjer. Następnie wywołuję funkcję xrSession.requestHitTestSource(), która tworzy źródło danych testu trafienia, których będę używać podczas rysowania.

Uruchamianie pętli klatek

requestAnimationFrame() Wywołanie zwrotne otrzymuje też nowy kod do obsługi testowania kliknięć.

Podczas przesuwania urządzenia celownik musi się poruszać wraz z nim, aby znaleźć powierzchnie. Aby stworzyć iluzję ruchu, przekształcaj siatkę w każdej klatce. Nie wyświetlaj jednak siatki celowniczej, jeśli test trafienia się nie powiedzie. W przypadku utworzonego wcześniej celownika ustawiłem wartość właściwości 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ć cokolwiek w AR, muszę wiedzieć, gdzie znajduje się widz i w którą stronę patrzy. Sprawdzam więc, 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ę pod numer getHitTestResults(). Przyjmuje hitTestSource jako argument i zwraca tablicę instancji HitTestResult. Test trafienia może wykryć wiele powierzchni. Pierwszy element tablicy to ten, który znajduje się najbliżej aparatu. Zazwyczaj będziesz z niej korzystać, ale w przypadku zaawansowanych zastosowań zwracana jest tablica. Wyobraź sobie, ż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, na którym mi zależy. Jeśli długość zwróconej tablicy wynosi 0, czyli nie zwrócono żadnego testu trafienia, 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 trafień. Podstawowy proces wygląda tak: Pobierz pozę z wyniku testu trafienia, przekształć (przesuń) obraz celownika do pozycji testu trafienia, a następnie ustaw jego 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 select program obsługi zdarzeń. (Patrz wyżej).

Ważne jest, aby wiedzieć, gdzie umieścić ten element. Ponieważ ruchomy celownik zapewnia stałe źródło testów trafień, najprostszym sposobem umieszczenia obiektu jest narysowanie go w miejscu 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

Najlepiej zapoznać się z tym, analizując przykładowy kod lub wykonując ćwiczenia z programowania. Mam nadzieję, że podałem wystarczająco dużo informacji, aby wyjaśnić obie te kwestie.

Nie skończyliśmy jeszcze tworzyć interfejsów API do obsługi internetu immersyjnego. W miarę postępów będziemy publikować tutaj nowe artykuły.

Zdjęcie: Daniel Frank, Unsplash