Umieszczanie wirtualnych obiektów w widoku rzeczywistego

Interfejs Hit Test API umożliwia pozycjonowanie wirtualnych przedmiotów w widoku rzeczywistym.

Jan Kowalski
Joe Medley

Interfejs WebXR Device API został wysłany jesienią zeszłego roku w Chrome 79. Jak już wspomnieliśmy, trwa wdrażanie tego interfejsu API w Chrome. Chrome z przyjemnością informuje, że niektóre prace zostały zakończone. W Chrome 81 pojawiły się 2 nowe funkcje:

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

Zakładam, że wiesz, jak utworzyć sesję w rzeczywistości rozszerzonej i jak uruchomić pętlę klatek. Jeśli nie znasz tych pojęć, przeczytaj wcześniejsze artykuły z tej serii.

Próbka sesji AR

Kod w tym artykule bazuje na kodzie, który podano w próbce działania grupy roboczej Immersive Web Working Group (demo, źródło). Ten przykład pozwala umieścić wirtualne słoneczniki na powierzchniach w świecie rzeczywistym.

Po pierwszym uruchomieniu aplikacji zobaczysz niebieskie kółko z kropką pośrodku. Kropka to punkt przecięcia między urojoną linią biegnącą od urządzenia a punktem w środowisku. Rusza się wraz z urządzeniem. Gdy znajdzie punkty przecięcia, przyciągnie je do powierzchni takich jak podłoga, blaty stołu i ściany. Dzieje się tak, ponieważ testowanie z wykorzystaniem trafień dostarcza informacje o położeniu i orientacji punktu przecięcia, ale nie o samych powierzchniach.

Takie okrąg jest nazywany siatką, czyli tymczasowym obrazem, który ułatwia umieszczanie obiektu w rzeczywistości rozszerzonej. Jeśli dotkniesz ekranu, słonecznik zostanie umieszczony na powierzchni w miejscu, w którym znajduje się siatka, i w jakiej jest orientacji, niezależnie od tego, w którym miejscu dotkniesz ekranu. Siatka srebra porusza się wraz z urządzeniem.

Siatka wyrenderowana na ścianie, luźna lub rygorystyczna (w zależności od kontekstu)
Siatka siatkowa to tymczasowy obraz, który pomaga umieścić obiekt w rzeczywistości rozszerzonej.

Tworzenie siatki

Obraz siatki musisz utworzyć samodzielnie, ponieważ nie jest on udostępniany przez przeglądarkę ani interfejs API. Metoda ładowania i rysowania jest zależna od platformy. Jeśli nie rysujesz jej bezpośrednio przy użyciu WebGL lub WebGL 2, zapoznaj się z dokumentacją platformy. Z tego powodu nie będziemy szczegółowo omawiać, jak siatka siatka jest narysowana w próbce. Poniżej przestawiam ten wiersz tylko z jednego powodu: aby w późniejszych przykładach kodu było to widać, gdy używam zmiennej reticle.

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

Poproś o sesję

Żądając sesji, musisz zażądać '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
});

Rozpoczęcie sesji

W poprzednich artykułach był przedstawiony kod umożliwiający udział w sesji XR. Poniżej znajdziesz jej nową wersję z dodatkami. Najpierw dodaję odbiornik zdarzenia select. Gdy użytkownik dotknie ekranu, w polu widzenia aparatu pojawi się kwiat w zależności od ułożenia siatki. Później opiszę ten odbiornik.

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 pokoi referencyjnych

Zwróć uwagę, że podświetlony kod wywołuje XRSession.requestReferenceSpace() dwukrotnie. Na początku było to dla mnie niejasne. Zapytałem, dlaczego kod testowy nie wysyła żądania ramki animacji (rozpoczyna pętlę klatki) i dlaczego pętla klatek nie zawiera testów trafień. Wynikało to z niezrozumienia pomieszczeń referencyjnych. Miejsca odniesienia odzwierciedlają relacje między pochodzeniem a światem.

Aby zrozumieć, jak działa ten kod, udawaj, że oglądasz ten przykład na samodzielnym sprzęcie, który zawiera zarówno zestaw słuchawkowy, jak i kontroler. Do pomiaru odległości od kontrolera użyj ramki referencyjnej skupionej na kontrolerze. Aby coś narysować na ekranie, potrzebuje się współrzędnych.

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 względem otoczenia, ale kontroler, którym rysuję, porusza się.

Do rysowania używam przestrzeni referencyjnej local, która zapewnia mi stabilność w zakresie środowiska. Następnie uruchamiam pętlę klatek, wywołując requestAnimationFrame().

Do testowania trafień używam przestrzeni referencyjnej viewer, która jest oparta na pozycji urządzenia w momencie testu. Etykieta „przeglądający” jest w tym kontekście trochę myląca, ponieważ mówię o kontrolerze. Ma sens, jeśli myślisz o kontrolerze jak o goglach elektronicznych. Po otrzymaniu go wywołuję funkcję xrSession.requestHitTestSource(), która tworzy źródło danych testowych trafień, których używam podczas rysowania.

Zapętlanie klatek

Wywołanie zwrotne requestAnimationFrame() otrzymuje też nowy kod do testowania trafień.

Gdy poruszasz urządzeniem, celownik musi poruszać się razem z nim, aby znaleźć powierzchnię. Aby stworzyć iluzję ruchu, ponownie narysuj prostokąt w każdej klatce. Nie pokazuj jednak siatki, jeśli test się nie powiedzie. Wcześniej ustawiłem jego 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. Testuję 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ę do: getHitTestResults(). Przyjmuje hitTestSource jako argument i zwraca tablicę wystąpień HitTestResult. Test trafienia może obejmować wiele platform. Pierwszy w tablicy to ten znajdujący się najbliżej kamery. Najczęściej będziesz go używać, ale w zaawansowanych przypadkach użycia zwracana jest tablica. Wyobraź sobie np., że kamera jest skierowana na pudło leżące na stole na podłodze. Test trafień może 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 to 0, czyli jeżeli nie zostanie zwrócony wynik testu, przejdź dalej. Spróbuj ponownie w następnej ramce.

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 musimy przetworzyć wyniki testu trafień. Podstawowy proces ten wygląda tak. Pobierz pozycję z wyniku testu działania, przekształć (przekształć) obraz prostokąta w pozycję testową, a następnie ustaw dla jego właściwości visible wartość „true” (prawda). Pozycja przedstawia 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

Gdy użytkownik kliknie ekran, w AR zostanie umieszczony obiekt. Już dodałem do sesji moduł obsługi zdarzeń select. (patrz wyżej).

Na tym etapie ważne jest, aby wiedzieć, gdzie go umieścić. Ruchoma siatka danych zapewnia stałe źródło testów trafień, więc najprostszym sposobem na umieszczenie obiektu jest jej położenie 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

Aby sobie z tym poradzić, najlepiej jest zapoznać się z przykładowym kodem lub skorzystać z ćwiczenia z programowania. Mam nadzieję, że przedstawiłem(-am) wystarczająco dużo informacji, żeby zrozumieć obydwa zagadnienia.

To jeszcze nie koniec tworzenia wciągających internetowych interfejsów API. W miarę postępów publikujemy tu nowe artykuły.

Zdjęcie: Daniel Frank, Unsplash