Rzeczywistość wirtualna w internecie pojawia się w części II

Wszystko o pętli ramek

Joe Medley
Joe Medley

Niedawno opublikowałem artykuł Wirtualna rzeczywistość w internecie, w którym przedstawiłem podstawowe zagadnienia związane z interfejsem WebXR Device API. Przekazałem też instrukcje dotyczące żądania, rozpoczęcia i zakończenia sesji XR.

W tym artykule opisano pętlę ramki, czyli pętlę nieskończoną kontrolowaną przez użytkownika, w której treści są wielokrotnie wyświetlane na ekranie. Treści są wyświetlane w oddzielnych blokach zwanych klatkami. Kolejność klatek tworzy iluzję ruchu.

Co nie jest omawiane w tym artykule

WebGL i WebGL2 to jedyne sposoby renderowania treści podczas pętli klatek w aplikacji WebXR. Na szczęście wiele frameworków zapewnia warstwę abstrakcji nad WebGL i WebGL2. Do takich frameworków należą three.js, babylonjs i PlayCanvas, a A-Frame i React 360 zostały zaprojektowane do interakcji z WebXR.

Ten artykuł nie jest samouczkiem WebGL ani samouczkiem dotyczącym frameworka. Wyjaśnia on podstawy pętli ramki za pomocą przykładowej sesji w wirtualnej rzeczywistości przygotowanej przez grupę roboczą Immersive Web Working Group (prezentacja, źródło). Jeśli chcesz dowiedzieć się więcej o WebGL lub jednej z ramek, w internecie znajdziesz stale rosnącą listę artykułów.

Zawodnicy i gra

Podczas próby zrozumienia pętli ramek ciągle gubiłem się w szczegółach. W grze występuje wiele obiektów, a niektóre z nich są tylko nazywane przez odwołanie do właściwości innych obiektów. Aby Ci pomóc, opiszę obiekty, które nazywam „graczami”. Następnie opiszę, jak one ze sobą współdziałają, czyli „grę”.

Gracze

XRViewerPose

Poza to pozycja i orientacja czegoś w przestrzeni 3D. Zarówno widzowie, jak i urządzenia wejściowe mają pozę, ale w tym przypadku interesuje nas tylko pozycja widza. Zarówno pozy widza, jak i urządzenia wejściowego mają atrybut transform opisujący jego położenie jako wektor i orientację jako wektor kwadratowy względem punktu wyjścia. Podczas wywoływania funkcji XRSession.requestReferenceSpace() pochodzenie jest określane na podstawie żądanego typu przestrzeni odniesienia.

Pokoje referencyjne wymagają nieco wyjaśnień. Omawiam je szczegółowo w artykule o rozszerzonej rzeczywistości. Przykład, którego używam w tym artykule, korzysta z przestrzeni odniesienia 'local', co oznacza, że punkt początkowy znajduje się w miejscu, w którym znajdował się widz w momencie tworzenia sesji, bez wyraźnie określonej podłogi. Dokładna pozycja może się różnić w zależności od platformy.

XRView

Widok odpowiada kamerze, która rejestruje wirtualną scenę. Widok ma też atrybut transform opisujący jego położenie jako wektora i jego orientację. Są one podawane jako para wektorów lub kwaternionów oraz jako macierz równoważna. Możesz użyć dowolnej reprezentacji w zależności od tego, która lepiej pasuje do Twojego kodu. Każdy widok odpowiada wyświetlaczowi lub części wyświetlacza używanego przez urządzenie do wyświetlania obrazów widzowi. Obiekty XRView są zwracane w tablicy obiektu XRViewerPose. Liczba wyświetleń w tablicy jest różna. Na urządzeniach mobilnych scena AR ma jeden widok, który może, ale nie musi wypełniać ekran urządzenia. Słuchawki mają zwykle 2 widoki, po jednym dla każdego oka.

XRWebGLLayer

Warstwy stanowią źródło obrazów bitmapowych i opisów sposobu ich renderowania na urządzeniu. Opis nie oddaje w pełni funkcji tego odtwarzacza. Uważam, że jest to pośrednik między urządzeniem a WebGLRenderingContext. MDN ma podobne zdanie, twierdząc, że „zapewnia połączenie” między tymi dwoma elementami. W ten sposób zapewnia dostęp do innych graczy.

Ogólnie obiekty WebGL przechowują informacje o stanie do renderowania grafiki 2D i 3D.

WebGLFramebuffer

Bufor ramki przekazuje dane obrazu do WebGLRenderingContext. Po pobraniu go z XRWebGLLayer przekazujesz je do bieżącego WebGLRenderingContext. Poza wywołaniem funkcji bindFramebuffer() (więcej informacji znajdziesz poniżej) nie będziesz mieć bezpośredniego dostępu do tego obiektu. Wystarczy, że przekażesz go z XRWebGLLayer do WebGLRenderingContext.

XRViewport

Widok zawiera współrzędne i wymiary prostokątnego obszaru w WebGLFramebuffer.

WebGLRenderingContext

Kontekst renderowania to punkt dostępu programowego do rysunku (przestrzeni, w której rysujemy). Aby to zrobić, potrzebuje zarówno WebGLFramebuffer, jak i XRViewport.

Zwróć uwagę na zależność między polami XRWebGLLayerWebGLRenderingContext. Jeden odpowiada urządzeniu widza, a drugi stronie internetowej. WebGLFramebufferXRViewport są przekazywane z pierwszego do drugiego.

Relacja między XRWebGLLayer a WebGLRenderingContext
Relacja między XRWebGLLayerWebGLRenderingContext

Gra

Teraz, gdy już wiemy, kim są gracze, przyjrzyjmy się grze, w którą grają. To gra, która zaczyna się od nowa z każdą klatką. Pamiętaj, że klatki są częścią pętli klatek, która odbywa się z częstotliwością zależną od sprzętu. W przypadku aplikacji VR liczba klatek na sekundę może wynosić od 60 do 144. Rzeczywistość rozszerzona na Androida działa z częstotliwością 30 klatek na sekundę. Twój kod nie powinien zakładać żadnej konkretnej szybkości klatek.

Podstawowy proces pętli klatek wygląda tak:

  1. Zadzwoń do firmy XRSession.requestAnimationFrame(). W odpowiedzi na to klient użytkownika wywołuje XRFrameRequestCallback, który został przez Ciebie zdefiniowany.
  2. W funkcji wywołania zwrotnego:
    1. Zadzwoń ponownie do XRSession.requestAnimationFrame().
    2. Uzyskaj pozę widza.
    3. Przekazywanie (bindowanie) WebGLFramebufferXRWebGLLayer do WebGLRenderingContext.
    4. Przetwarzaj każdy obiekt XRView, pobierając z niego element XRViewport z obiektu XRWebGLLayer i przekazując go do obiektu WebGLRenderingContext.
    5. Rysowanie czegoś w ramce bufora.

Kroki 1 i 2a zostały omówione w poprzednim artykule, więc zacznę od kroku 2b.

Uzyskaj pozę widza

To chyba oczywiste. Aby narysować coś w AR lub VR, muszę wiedzieć, gdzie znajduje się widz i na co patrzy. Pozycja i orientacja widza są podawane przez obiekt XRViewerPose. Pobieram pozę widza, wywołując funkcję XRFrame.getViewerPose() w ramce animacji. Przekazuję mu przestrzeń referencyjną uzyskaną podczas konfigurowania sesji. Wartości zwracane przez ten obiekt zawsze odnoszą się do przestrzeni referencyjnej, o którą poproszono podczas wejścia do bieżącej sesji. Jak zapewne pamiętasz, muszę przekazać bieżącą przestrzeń odniesienia, gdy proszę o pozę.

function onXRFrame(hrTime, xrFrame) {
  let xrSession
= xrFrame.session;
  xrSession
.requestAnimationFrame(onXRFrame);
  let xrViewerPose
= xrFrame.getViewerPose(xrRefSpace);
 
if (xrViewerPose) {
   
// Render based on the pose.
 
}
}

Jest jedna pozycja widza, która przedstawia ogólną pozycję użytkownika, czyli głowę widza lub w przypadku smartfona kamerę telefonu. Poza informuje aplikację, gdzie znajduje się widz. Do renderowania obrazu służą obiekty XRView, o których opowiem za chwilę.

Zanim przejdę dalej, sprawdzam, czy położenie widza zostało zwrócone, na wypadek gdyby system stracił śledzenie lub zablokował położenie ze względów prywatności. Śledzenie to zdolność urządzenia XR do określania swojego położenia względem otoczenia. Śledzenie może zostać utracone na kilka sposobów, w zależności od metody śledzenia. Jeśli na przykład do śledzenia urządzenia używane są kamery w słuchawkach lub telefonie, urządzenie może nie być w stanie określić swojej lokalizacji w sytuacjach, gdy jest słabe światło lub brak światła albo gdy kamery są zasłonięte.

Przykładem blokowania pozy ze względów związanych z prywatnością jest sytuacja, gdy zestaw słuchawkowy wyświetla okno dialogowe dotyczące zabezpieczeń, np. prośbę o uprawnienia. Podczas wyświetlania tego okna przeglądarka może przestać dostarczać pozy do aplikacji. Ale już zadzwoniłeś/zadzwoniłaś do XRSession.requestAnimationFrame(), więc jeśli system wróci do normalnego działania, pętla będzie kontynuowana. W przeciwnym razie agent użytkownika zakończy sesję i wywoła interfejs obsługi zdarzeń end.

Krótki objazd

Następny krok wymaga obiektów utworzonych podczas konfiguracji sesji. Pamiętaj, że utworzyłem canvas i instrukcję tworzenia kontekstu renderowania WebGL zgodnego z XR, który otrzymałem, wywołując funkcję canvas.getContext(). Wszystkie rysunki są tworzone za pomocą interfejsu WebGL API, WebGL2 API lub frameworku opartego na WebGL, takiego jak Three.js. Ten kontekst został przekazany do obiektu sesji za pomocą funkcji updateRenderState() wraz z nowym wystąpieniem obiektu XRWebGLLayer.

let canvas = document.createElement('canvas');
// The rendering context must be based on WebGL or WebGL2
let webGLRenContext
= canvas.getContext('webgl', { xrCompatible: true });
xrSession
.updateRenderState({
    baseLayer
: new XRWebGLLayer(xrSession, webGLRenContext)
 
});

Przekazywanie (bind) WebGLFramebuffer

XRWebGLLayer udostępnia framebuffer dla WebGLRenderingContext, który jest przeznaczony do użytku w ramach WebXR i zastępuje domyślny framebuffer kontekstu renderowania. W języku WebGL nazywa się to „wiązaniem”.

function onXRFrame(hrTime, xrFrame) {
  let xrSession
= xrFrame.session;
  xrSession
.requestAnimationFrame(onXRFrame);
  let xrViewerPose
= xrFrame.getViewerPose(xrRefSpace);
 
if (xrViewerPose) {
    let glLayer
= xrSession.renderState.baseLayer;
    webGLRenContext
.bindFramebuffer(webGLRenContext.FRAMEBUFFER, glLayer.framebuffer);
   
// Iterate over the views
 
}
}

Przetwarzanie każdego obiektu XRView

Po pobraniu pozy i powiązaniu z ramką obrazu nadszedł czas na pobranie widocznych obszarów. XRViewerPose zawiera tablicę interfejsów XRView, z których każdy reprezentuje wyświetlacz lub jego część. Zawierają one informacje potrzebne do renderowania treści, które są prawidłowo umieszczone na urządzeniu i w widoku, takie jak pole widzenia, przesunięcie oka i inne właściwości optyczne. Rysuję dla 2 oczu, więc mam 2 widoki, które przewijam i rysuję oddzielne obrazy.

W przypadku implementacji rzeczywistości rozszerzonej na telefonie miałbym tylko 1 widok, ale i tak użyłbym pętli. Chociaż może się wydawać, że przechodzenie przez jeden widok jest bezcelowe, pozwala to na użycie jednej ścieżki renderowania dla całego spektrum wciągających wrażeń. To ważna różnica między WebXR a innymi systemami immersyjnymi.

function onXRFrame(hrTime, xrFrame) {
  let xrSession
= xrFrame.session;
  xrSession
.requestAnimationFrame(onXRFrame);
  let xrViewerPose
= xrFrame.getViewerPose(xrRefSpace);
 
if (xrViewerPose) {
    let glLayer
= xrSession.renderState.baseLayer;
    webGLRenContext
.bindFramebuffer(webGLRenContext.FRAMEBUFFER, glLayer.framebuffer);
   
for (let xrView of xrViewerPose.views) {
     
// Pass viewports to the context
   
}
 
}
}

Przekaż obiekt XRViewport do WebGLRenderingContext

Obiekt XRView odnosi się do tego, co jest widoczne na ekranie. Aby jednak narysować ten widok, potrzebuję współrzędnych i wymiarów specyficznych dla mojego urządzenia. Podobnie jak w przypadku ramki framebuffera, proszę o te dane z poziomu XRWebGLLayer i przekazuję je do WebGLRenderingContext.

function onXRFrame(hrTime, xrFrame) {
  let xrSession
= xrFrame.session;
  xrSession
.requestAnimationFrame(onXRFrame);
  let xrViewerPose
= xrFrame.getViewerPose(xrRefSpace);
 
if (xrViewerPose) {
    let glLayer
= xrSession.renderState.baseLayer;
    webGLRenContext
.bindFramebuffer(webGLRenContext.FRAMEBUFFER, glLayer.framebuffer);
   
for (let xrView of xrViewerPose.views) {
      let viewport
= glLayer.getViewport(xrView);
      webGLRenContext
.viewport(viewport.x, viewport.y, viewport.width, viewport.height);
     
// Draw something to the framebuffer
   
}
 
}
}

webGLRenContext

Podczas pisania tego artykułu prowadziłem z kilkoma kolegami debatę na temat nazwy obiektu webGLRenContext. Przykładowe skrypty i większości kodu WebXR po prostu wywołują tę zmienną gl. Podczas analizowania próbek ciągle zapominałam, do czego odnosi się gl. Nazywam go webGLRenContext, aby przypominać Ci podczas nauki, że jest to instancja WebGLRenderingContext.

Dzieje się tak, ponieważ użycie gl pozwala nazwom metod wyglądać tak samo jak ich odpowiedniki w interfejsie OpenGL ES 2.0, używanym do tworzenia VR w kompilowanych językach. Jest to oczywiste, jeśli tworzysz aplikacje VR za pomocą OpenGL, ale może być mylące, jeśli dopiero zaczynasz korzystać z tej technologii.

Rysowanie czegoś na framebufferze

Jeśli masz ambitne plany, możesz użyć WebGL bezpośrednio, ale nie zalecam tego. Znacznie łatwiej jest użyć jednej z ram wymienionych na górze.

Podsumowanie

To nie koniec aktualizacji ani artykułów na temat WebXR. Informacje o interfejsach i elementach WebXR znajdziesz w MDN. Informacje o nadchodzących ulepszeniach interfejsów znajdziesz na stronie Chrome Status.

Zdjęcie: JESHOOTS.COMUnsplash