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, prace nad implementacją interfejsu API w Chrome są w toku. Z przyjemnością informujemy, że część prac jest już ukończona. W Chrome 81 pojawiły się 2 nowe funkcje:

Z tego artykułu dowiesz się, jak korzystać z interfejsu API WebXR Hit Test, który umożliwia umieszczanie wirtualnych obiektów w widoku z kamery.

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 został opracowany na podstawie przykładowego testu trafienia z grupy Immersive WebWork Group (demo, source), lecz nie jest identyczny. W tym przykładzie możesz umieścić wirtualne słoneczniki na powierzchniach w rzeczywistym świecie.

Po pierwszym uruchomieniu aplikacji zobaczysz niebieski okrąg z kropką w środku. Punkt jest miejscem przecięcia wyimaginowanej linii od urządzenia do punktu w środowisku. Przesuwa się ona wraz z urządzeniem. Gdy znajdzie punkty przecięcia, wydaje się, że przykleja się do powierzchni, takich jak podłoga, blaty stołów czy ś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. Siatka siatkowa cały czas przesuwa się razem z urządzeniem.

siatka sieciowa wyrenderowana na ścianie, w standardzie 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. Z tego powodu nie będę szczegółowo omawiać sposobu, w jaki siatkówka jest narysowana w próbce. Poniżej pokazujemy jeden wiersz tego kodu tylko z jednego powodu. Aby w późniejszych przykładach kodu uświadomiłem Ci, do czego mam odniesienia, używając 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 do otwierania sesji XR. Poniżej przedstawiam wersję z kilkoma dodatkami. Najpierw dodałem listenera zdarzenia select. Gdy użytkownik dotknie ekranu, w widoku aparatu zostanie umieszczony kwiat dopasowany do pozycji siatki. Opiszę ten odbiorca 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 wywołuje 2 razy XRSession.requestReferenceSpace(). 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. Mam jednak 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 testowania trafień używam przestrzeni referencyjnej viewer, która jest określana na podstawie pozycji urządzenia w momencie testu trafień. Etykieta „widz” jest w tym kontekście nieco myląca, ponieważ mówię o kontrolerze. 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.

Wykonywanie pętli klatki

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

Gdy ruszasz urządzeniem, siatka celownika musi przemieszczać się razem z nim, szukając powierzchni. 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. Test trafienia może zwrócić wszystkie 3 powierzchnie 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 działań. 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

Gdy użytkownik kliknie ekran, obiekt zostanie umieszczony w rozszerzonej rzeczywistości. Do sesji został już dodany element obsługi zdarzenia select. (patrz powyżej).

Ważne jest, aby wiedzieć, gdzie go umieścić. Ponieważ poruszający się celownik zapewnia stałe źródło testów kolizji, najprostszym sposobem umieszczenia obiektu jest narysowanie go w miejscu celownika w miejscu ostatniego testu kolizji.

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