Entwickeln
Entwickeln
Deine Plattform auswählen

Rendering auf dem Oculus Rift

Oculus Rift und Rift S erfordern Splitscreen-Stereobilder mit Verzeichnungskorrektur für jedes Auge, um linsenbedingte Verzeichnungen auszugleichen.
Die Verzeichnungskorrektur kann eine Herausforderung darstellen, da die Verzeichnungsparameter je nach Linsentyp und individuellem Augenabstand variieren. Um die Entwicklung zu erleichtern, sorgt das Oculus-SDK innerhalb des Compositor-Prozesses automatisch für die Verzeichnungskorrektur; außerdem kümmert es sich um den latenzreduzierenden Timewarp und präsentiert dem Headset Einzelbilder.
Da das Oculus-SDK viele Aufgaben übernimmt, besteht die Hauptaufgabe der App darin, die Simulation durchzuführen und die Stereo-Welt auf Grundlage der Trackinghaltung zu rendern. Stereo-Ansichten können in eine oder zwei individuelle Texturen gerendert werden und werden durch Aufrufen von ovr_WaitToBeginFrame, ovr_BeginFrame und ovr_EndFrame an den Compositor gesendet. Dieser Prozess wird in diesem Abschnitt detailliert beschrieben.

Rendering auf dem Oculus Rift

Oculus Rift und Rift S erfordern, dass die Szene als Splitscreen-Stereobild gerendert wird, wobei jeweils eine Hälfte des Bildschirms für jedes Auge verwendet wird.
Bei Verwendung von Rift oder Rift S sieht das linke Auge die linke Hälfte des Bildschirms und das rechte Auge sieht die rechte Hälfte. Der Abstand zwischen den Pupillen beträgt bei Menschen etwa 65 mm, auch wenn dies individuell variiert. Dies ist auch als Augenabstand (Interpupillary Distance, IPD) bekannt. Die In-App-Kameras sollten mit demselben Abstand konfiguriert werden.
Hierbei handelt es sich um eine Translation der Kamera, nicht um eine Rotation. Diese Translation (und der damit einhergehende Parallaxeneffekt) verursacht den stereoskopischen Effekt. Das bedeutet, dass deine App die ganze Szene zweimal rendern muss: einmal mit der linken virtuellen Kamera und einmal mit der rechten.
Die Stereo-Renderingtechnik der Reprojektion, die darauf beruht, dass linke und rechte Ansichten aus einer einzigen, vollständig gerenderten Ansicht generiert werden, ist mit HMD in der Regel nicht brauchbar, weil erhebliche Artefakte an Objekträndern auftreten.
Die Linsen im Rift vergrößern das Bild, um ein sehr breites Sichtfeld (Field of View, FOV) zu bieten und so die Immersion zu verstärken. Dieser Prozess verzeichnet das Bild allerdings erheblich. Wenn die Engine auf dem Rift die Originalbilder anzeigen würde, würden die Nutzer*innen diese mit einer Kissenverzeichnung sehen.
Um diese Verzeichnung auszugleichen, wendet das SDK im Rahmen des Post-Processing eine gleichwertige und umgekehrte tonnenförmige Verzeichnung an, sodass beide sich gegenseitig ausgleichen. Das Ergebnis ist eine unverzeichnete Ansicht für jedes Auge. Darüber hinaus korrigiert das SDK auch die chromatische Abweichung, wobei es sich um einen durch die Linse verursachten Farbseparationseffekt an den Rändern handelt. Auch wenn die exakten Verzeichnungsparameter von den Linseneigenschaften und der Augenposition relativ zur Linse abhängen, übernimmt das Oculus-SDK alle nötigen Berechnungen zum Generieren des Verzeichnungsgitters.
Bei Rift und Rift S sind die Projektionsachsen für jedes Auge parallel, wie in der folgenden Abbildung dargestellt. Eine App sollte dies jedoch beim Rendern der Szene nicht als gegeben voraussetzen. Das HmdToEyePose-Mitglied des ovrEyeRenderDesc-Struct ist eine ovrPosef-6-DOF-Transformation. Das bedeutet, dass bei HMDs mit geneigten Displays (Displays nicht parallel zueinander) die Rendering-Kameras von der HMD-Haltung weg rotiert und translatiert werden können. Die vorwärtsgewandte Richtung des*der Nutzer*in wird von der Z-Achse der HMD-Haltung vorgegeben, während die Augenhaltungen gegebenenfalls Z-Achsen aufweisen, die in unterschiedliche Richtungen zeigen.
In der Praxis sind die Projektionen auf dem Rift oft leicht außermittig, weil unsere Nasen im Weg sind. Aber der Punkt ist, dass die Bilder des linken und rechten Auges auf dem Rift komplett voneinander getrennt sind, anders als Stereobilder auf einem Fernseher oder Kinobildschirm. Das bedeutet, dass du mit der Verwendung von Methoden, die für diese Medien entwickelt wurden, sehr vorsichtig sein solltest, weil sie in der Regel nicht auf VR anwendbar sind.
Die beiden virtuellen Kameras in der Szene sollten so positioniert werden, dass sie genauso wie die Augenhaltungen ausgerichtet sind, die durch ovrEyeRenderDesc::HmdToEyePose spezifiziert werden. Dabei sollte der Abstand zwischen beiden Kameras dem Augenabstand (IPD) entsprechen.
Auch wenn die Rift-Linsen für die meisten Nutzer*innen ungefähr den passenden Abstand haben, entsprechen sie möglicherweise nicht genau dem jeweiligen Augenabstand. Aufgrund des Optik-Designs sieht jedes Auge aber dennoch das richtige Bild. Es ist wichtig, dass die Software den Abstand zwischen den virtuellen Kameras übereinstimmend mit dem Augenabstand laut Nutzer*innenprofil einstellt (im Konfigurationshilfsprogramm festgelegt), nicht übereinstimmend mit dem Abstand zwischen den Rift-Linsen.

Beschreibung der Rendering-Einrichtung

Das Oculus-SDK verwendet einen Compositor-Prozess, um Einzelbilder zu präsentieren und Verzeichnungen zu handhaben.
Wenn der Rift dein Ziel ist, renderst du die Szene in eine oder zwei Render-Texturen und gibst diese Texturen an die API weiter. Die Runtime kümmert sich um Verzeichnungsrendering, GPU-Synchronisierung, Einzelbild-Timing und Einzelbild-Präsentation für das HMD.
Im Folgenden werden die Schritte für das SDK-Rendering beschrieben:
  1. Initialisieren:
  2. Initialisiere das Oculus-SDK und erstelle ein ovrSession-Objekt für das Headset, wie bereits beschrieben.
  3. Berechne das gewünschte FOV und die Texturgrößen anhand von ovrHmdDesc-Daten.
  4. Weise ovrTextureSwapChain-Objekte, die für Eye-Render-Buffer stehen, auf API-spezifische Weise zu: Rufe ovr_CreateTextureSwapChainDX für Direct3D 11 oder 12 ab, ovr_CreateTextureSwapChainGL für OpenGL oder ovr_CreateTextureSwapChainVk für Vulkan.
    Hinweis: Wenn du Vulkan verwendest, verwende den Present Mode VK_PRESENT_MODE_IMMEDIATE_KHR für AMD und VK_PRESENT_MODE_MAILBOX_KHR für nVidia. Dadurch wird ein Problem vermieden, bei dem der Loop vor dem Rendern des nächsten Einzelbilds auf Vsync am Hauptmonitor wartet, wodurch Latenz entsteht und die Leistung verschlechtert wird.
  5. Einrichten des Umgangs mit Einzelbildern:
  6. Verwende ovr_GetTrackingState und ovr_CalcEyePoses, um die Augenhaltungen zu berechnen, die für das Ansichtsrendering auf Grundlage von Einzelbild-Timinginformationen erforderlich sind.
  7. Führe das Rendering für jedes Auge auf enginespezifische Weise durch, wobei in die aktuelle Textur innerhalb der Texturauswahl gerendert wird. Die aktuelle Textur wird abgerufen mithilfe von ovr_GetTextureSwapChainCurrentIndex und ovr_GetTextureSwapChainBufferDX, ovr_GetTextureSwapChainBufferGL oder ovr_GetTextureSwapChainBufferVk. Nachdem das Rendering in die Textur abgeschlossen ist, muss die App ovr_CommitTextureSwapChain aufrufen.
  8. Rufe ovr_WaitToBeginFrame auf, und wenn deine App bereit ist, mit dem Rendering des Einzelbilds zu beginnen, rufe ovr_BeginFrame auf. Wenn deine App bereit ist, das Einzelbild zu senden, rufe ovr_EndFrame auf und gib die Swap-Texturauswahl(en) aus dem vorherigen Schritt innerhalb einer ovrLayerEyeFov-Struktur weiter. Zwar ist nur ein einzelnes Layer erforderlich, um ein Einzelbild zu senden, aber du kannst für fortgeschrittenes Rendering auch mehrere Layers und Layer-Typen verwenden. ovr_EndFrame gibt Layer-Texturen an den Compositor weiter, der sich um Verzeichnung, Timewarp und GPU-Synchronisierung kümmert, bevor er sie dem Headset präsentiert. Beachte, dass die Kombination von ovr_WaitToBeginFrame, ovr_BeginFrame und ovr_EndFrame es dir ermöglicht, Techniken zur Leistungsoptimierung in Multi-Thread-Umgebungen zu implementieren, zum Beispiel durch eine aufgeteilte und überlappende Verarbeitung mehrerer Einzelbilder zur gleichen Zeit. In früheren Versionen waren diese Funktionen in ovr_SubmitFrame kombiniert, aber dieser Aufruf wurde jetzt eingestellt. Verwende bitte stattdessen ovr_WaitToBeginFrame, ovr_BeginFrame und ovr_EndFrame.
  9. Beenden:
  10. Rufe ovr_DestroyTextureSwapChain auf, um Swap-Texturpuffer zu löschen. Rufe ovr_DestroyMirrorTexture, um eine Spiegeltextur zu löschen. Rufe ovr_Destroy ab, um das ovrSession-Objekt zu löschen.

Initialisieren von Textur-Swapchains

In diesem Abschnitt wird die Rendering-Initialisierung beschrieben, einschließlich des Erstellens von Textur-Swapchains.
Zu Beginn bestimmst du das Rendering-FOV und weist die erforderliche ovrTextureSwapChain zu. Der folgende Code zeigt, wie sich die erforderliche Texturgröße berechnen lässt:
// Configure Stereo settings.
Sizei recommenedTex0Size = ovr_GetFovTextureSize(session, ovrEye_Left,
   session->DefaultEyeFov[0], 1.0f);
Sizei recommenedTex1Size = ovr_GetFovTextureSize(session, ovrEye_Right,
   session->DefaultEyeFov[1], 1.0f);
Sizei bufferSize;
bufferSize.w  = recommenedTex0Size.w + recommenedTex1Size.w;
bufferSize.h = max ( recommenedTex0Size.h, recommenedTex1Size.h );
Die gerenderte Texturgröße wird auf Grundlage des FOV und der gewünschten Pixeldichte in der Augenmitte bestimmt. Auch wenn sowohl FOV als auch Pixeldichtewerte zur Leistungsverbesserung angepasst werden können, wird in diesem Beispiel das empfohlene FOV verwendet (erhalten von session->DefaultEyeFov). Die Funktion ovr_GetFovTextureSize berechnet die gewünschte Texturgröße für jedes Auge auf Grundlage dieser Parameter.
Die Oculus-API gestattet der App, entweder eine gemeinsame Textur oder zwei separate Texturen für das Augen-Rendering zu verwenden. In diesem Beispiel wird der Einfachheit halber eine einzige gemeinsame Textur verwendet, die groß genug für beide Augen-Renderings gemacht wird.
Wenn du Vulkan verwendest, musst du drei zusätzliche Schritte durchführen, bevor du die Textur-Swapchain erstellen kannst.
  • Rufe während der Initialisierung ovr_GetSessionPhysicalDeviceVk ab, um das aktuelle physische Gerät mit übereinstimmender LUID zu erhalten. Erstelle dann ein mit dem zurückgegebenen physischen Gerät verknüpftes VkDevice.
  • AMD-Hardware verwendet auf Vulkan unterschiedliche Erweiterungen. Füge Code wie im folgenden Beispiel hinzu, um während der App-Initialisierung die AMD-GPU-Erweiterungen zu handhaben. Dieses Codebeispiel stammt aus dem Win32_VulkanAppUtil.h, das der Beispiel-App OculusRoomTiny_Advanced beiliegt, die mit dem Oculus-SDK geliefert wird.
static const uint32_t AMDVendorId = 0x1002;
isAMD = (gpuProps.vendorID == AMDVendorId);

static const char* deviceExtensions[] =
{
   VK_KHR_SWAPCHAIN_EXTENSION_NAME,
   VK_KHX_EXTERNAL_MEMORY_EXTENSION_NAME,
   #if defined(VK_USE_PLATFORM_WIN32_KHR)
      VK_KHX_EXTERNAL_MEMORY_WIN32_EXTENSION_NAME,
   #endif
};

static const char* deviceExtensionsAMD[] =
{
  VK_KHR_SWAPCHAIN_EXTENSION_NAME
};
  • Nachdem der Gameplay-Loop etabliert wurde, identifiziere die Warteschlange, die beim Rendering synchronisiert werden soll. Rufe ovr_SetSynchonizationQueueVk auf, um die Warteschlange zu identifizieren.
Erstellen der Textur-Swapchain
Sobald die Texturgröße bekannt ist, kann die App ovr_CreateTextureSwapChainGL, ovr_CreateTextureSwapChainDX oder ovr_CreateTextureSwapChainVk aufrufen, um die Textur-Swapchains auf API-spezifische Weise zuzuweisen.
So kann eine Textur-Swapchain unter OpenGL erstellt und aufgerufen werden:
ovrTextureSwapChain textureSwapChain = 0;

ovrTextureSwapChainDesc desc = {};
desc.Type = ovrTexture_2D;
desc.ArraySize = 1;
desc.Format = OVR_FORMAT_R8G8B8A8_UNORM_SRGB;
desc.Width = bufferSize.w;
desc.Height = bufferSize.h;
desc.MipLevels = 1;
desc.SampleCount = 1;
desc.StaticImage = ovrFalse;

if (ovr_CreateTextureSwapChainGL(session, &desc, &textureSwapChain) == ovrSuccess)
{
    // Sample texture access:
    int texId;
    ovr_GetTextureSwapChainBufferGL(session, textureSwapChain, 0, &texId);
    glBindTexture(GL_TEXTURE_2D, texId);
    ...
}
Hier ist ein ähnliches Beispiel für das Erstellen und Aufrufen von Textur-Swapchains mit Direct3D 11:
ovrTextureSwapChain textureSwapChain = 0;
std::vector<ID3D11RenderTargetView*> texRtv;

ovrTextureSwapChainDesc desc = {};
desc.Type = ovrTexture_2D;
desc.Format = OVR_FORMAT_R8G8B8A8_UNORM_SRGB;
desc.ArraySize = 1;
desc.Width = bufferSize.w;
desc.Height = bufferSize.h;
desc.MipLevels = 1;
desc.SampleCount = 1;
desc.StaticImage = ovrFalse;
desc.MiscFlags = ovrTextureMisc_None;
desc.BindFlags = ovrTextureBind_DX_RenderTarget;

 if (ovr_CreateTextureSwapChainDX(session, DIRECTX.Device, &desc, &textureSwapChain) == ovrSuccess)
 {
     int count = 0;
     ovr_GetTextureSwapChainLength(session, textureSwapChain, &count);
     texRtv.resize(textureCount);
     for (int i = 0; i < count; ++i)
     {
         ID3D11Texture2D* texture = nullptr;
         ovr_GetTextureSwapChainBufferDX(session, textureSwapChain, i, IID_PPV_ARGS(&texture));
         DIRECTX.Device->CreateRenderTargetView(texture, nullptr, &texRtv[i]);
         texture->Release();
     }
 }
Hier ist Beispielcode aus dem bereitgestellten OculusRoomTiny-Beispiel, der in Direct3D 12 läuft:
ovrTextureSwapChain TexChain;
std::vector<D3D12_CPU_DESCRIPTOR_HANDLE> texRtv;
std::vector<ID3D12Resource*> TexResource;

ovrTextureSwapChainDesc desc = {};
desc.Type = ovrTexture_2D;
desc.ArraySize = 1;
desc.Format = OVR_FORMAT_R8G8B8A8_UNORM_SRGB;
desc.Width = sizeW;
desc.Height = sizeH;
desc.MipLevels = 1;
desc.SampleCount = 1;
desc.MiscFlags = ovrTextureMisc_DX_Typeless;
desc.StaticImage = ovrFalse;
desc.BindFlags = ovrTextureBind_DX_RenderTarget;

// DIRECTX.CommandQueue is the ID3D12CommandQueue used to render the eye textures by the app
ovrResult result = ovr_CreateTextureSwapChainDX(session, DIRECTX.CommandQueue, &desc, &TexChain);
if (!OVR_SUCCESS(result))
    return false;

int textureCount = 0;
ovr_GetTextureSwapChainLength(Session, TexChain, &textureCount);
texRtv.resize(textureCount);
TexResource.resize(textureCount);
for (int i = 0; i < textureCount; ++i)
{
    result = ovr_GetTextureSwapChainBufferDX(Session, TexChain, i, IID_PPV_ARGS(&TexResource[i]));
    if (!OVR_SUCCESS(result))
        return false;

    D3D12_RENDER_TARGET_VIEW_DESC rtvd = {};
    rtvd.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
    rtvd.ViewDimension = D3D12_RTV_DIMENSION_TEXTURE2D;
    texRtv[i] = DIRECTX.RtvHandleProvider.AllocCpuHandle(); // Gives new D3D12_CPU_DESCRIPTOR_HANDLE
    DIRECTX.Device->CreateRenderTargetView(TexResource[i], &rtvd, texRtv[i]);
}
Hinweis: Für Direct3D 12 stellt der Aufrufer beim Aufruf von ovr_CreateTextureSwapChainDX eine ID3D12CommandQueue statt eines ID3D12Device für das SDK bereit. Es liegt in der Verantwortung des Aufrufers, sicherzustellen, dass sämtliches VR-Augentextur-Rendering in dieser ID3D12CommandQueue-Instanz ausgeführt wird. Alternativ kann sie als „Join-Node“-Fence verwendet werden, um auf die Befehlslisten zu warten, die durch andere Befehlswarteschlangen ausgeführt werden, welche die VR-Augentexturen rendern.
So kann eine Textur-Swapchain mittels Vulkan erstellt und aufgerufen werden:
bool Create(ovrSession aSession, VkExtent2D aSize, RenderPass& renderPass, DepthBuffer& depthBuffer)
{
    session = aSession;
    size = aSize;

    ovrTextureSwapChainDesc desc = {};
    desc.Type = ovrTexture_2D;
    desc.ArraySize = 1;
    desc.Format = OVR_FORMAT_R8G8B8A8_UNORM_SRGB;
    desc.Width = (int)size.width;
    desc.Height = (int)size.height;
    desc.MipLevels = 1;
    desc.SampleCount = 1;
    desc.MiscFlags = ovrTextureMisc_DX_Typeless;
    desc.BindFlags = ovrTextureBind_DX_RenderTarget;
    desc.StaticImage = ovrFalse;

    ovrResult result = ovr_CreateTextureSwapChainVk(session, Platform.device, &desc, &textureChain);
    if (!OVR_SUCCESS(result))
        return false;

    int textureCount = 0;
    ovr_GetTextureSwapChainLength(session, textureChain, &textureCount);
    texElements.reserve(textureCount);
    for (int i = 0; i < textureCount; ++i)
    {
        VkImage image;
        result = ovr_GetTextureSwapChainBufferVk(session, textureChain, i, &image);
        texElements.emplace_back(RenderTexture());
        CHECK(texElements.back().Create(image, VK_FORMAT_R8G8B8A8_SRGB, size, renderPass, depthBuffer.view));
   }

    return true;
}
Sobald diese Texturen und Renderingziele erfolgreich erstellt wurden, kannst du sie für das Augentextur-Rendering verwenden. Im Abschnitt „Einzelbild-Rendering“ wird die Einrichtung des Viewport genauer beschrieben.
Der Compositor bietet sRGB-korrektes Rendering, das photorealistischere Bilder, besseres MSAA und energiesparende Texturabtastung zur Folge hat – sehr wichtig für VR-Apps. Wie oben gezeigt, wird von Apps erwartet, dass sie sRGB-Textur-Swapchains generieren. Der richtige Umgang mit sRGB-Rendering ist ein komplexes Thema, und auch wenn dieser Abschnitt einen Überblick bietet, würden ausführliche Informationen den Rahmen dieses Dokuments sprengen.
Sicherzustellen, dass eine in Echtzeit gerenderte App sRGB-korrektes Shading erzielt, erfordert mehrere Schritte und ist auf verschiedenen Wegen möglich. Zum Beispiel bieten die meisten GPUs Hardwarebeschleunigung zur Verbesserung des gamma-korrekten Shadings für sRGB-spezifische Eingabe- und Ausgabeoberflächen, wohingegen manche Apps GPU-Shader-Mathematik für eine stärker personalisierte Steuerung verwenden. Für das Oculus-SDK greift der Compositor auf den GPU-Sampler zurück, um die sRGB-Linear-Konvertierung durchzuführen, wenn eine App Textur-Swapchains im sRGB-Raum weitergibt.
Alle in einen GPU-Shader eingespeisten Farbtexturen sollten auf geeignete Weise mit dem sRGB-korrekten Format markiert werden, etwa OVR_FORMAT_R8G8B8A8_UNORM_SRGB. Dies wird auch für Apps empfohlen, die statische Texturen als Quad-Layer-Texturen für den Compositor bereitstellen. Wird dies unterlassen, sieht die Textur viel heller aus als erwartet.
Für D3D 11 und 12 wird das in Desc für ovr_CreateTextureSwapChainDX bereitgestellte Texturformat vom Verzeichnungs-Compositor für ShaderResourceView verwendet, wenn der Inhalt der Textur gelesen wird. Demzufolge sollte die App Textur-Swapchain-Formate im sRGB-Raum anfordern (z. B. OVR_FORMAT_R8G8B8A8_UNORM_SRGB).
Wenn deine App so konfiguriert ist, dass sie in eine Textur mit linearem Format rendert (z. B. OVR_FORMAT_R8G8B8A8_UNORM) und die Linear-Gamma-Konvertierung mittels HLSL-Code handhabt oder jegliche Gammakorrektur ignoriert, dann:
  • Fordere eine Textur-Swapchain im sRGB-Format an (z. B. OVR_FORMAT_R8G8B8A8_UNORM_SRGB).
  • Spezifiziere das ovrTextureMisc_DX_Typeless-Flag in Desc.
  • Erstelle ein RenderTargetView mit linearem Format (z. B. DXGI_FORMAT_R8G8B8A8_UNORM)
Hinweis: Das ovrTextureMisc_DX_Typeless-Flag für Tiefepufferformate (z. B. OVR_FORMAT_D32) wird ignoriert, da diese immer als Typeless konvertiert werden.
Das Codebeispiel demonstriert, wie das bereitgestellte ovrTextureMisc_DX_Typeless-Flag in D3D 11 verwendet wird:
ovrTextureSwapChainDesc desc = {};
desc.Type = ovrTexture_2D;
desc.ArraySize = 1;
desc.Format = OVR_FORMAT_R8G8B8A8_UNORM_SRGB;
desc.Width = sizeW;
desc.Height = sizeH;
desc.MipLevels = 1;
desc.SampleCount = 1;
desc.MiscFlags = ovrTextureMisc_DX_Typeless;
desc.BindFlags = ovrTextureBind_DX_RenderTarget;
desc.StaticImage = ovrFalse;

ovrResult result = ovr_CreateTextureSwapChainDX(session, DIRECTX.Device, &desc, &textureSwapChain);

if(!OVR_SUCCESS(result))
    return;

int count = 0;
ovr_GetTextureSwapChainLength(session, textureSwapChain, &count);
for (int i = 0; i < count; ++i)
{
    ID3D11Texture2D* texture = nullptr;
    ovr_GetTextureSwapChainBufferDX(session, textureSwapChain, i, IID_PPV_ARGS(&texture));
    D3D11_RENDER_TARGET_VIEW_DESC rtvd = {};
    rtvd.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
    rtvd.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2D;
    DIRECTX.Device->CreateRenderTargetView(texture, &rtvd, &texRtv[i]);
    texture->Release();
}
Bei OpenGL verwendet der Verzeichnungs-Compositor den Formatparameter ofovr_CreateTextureSwapChainGL beim Lesen des Texturinhalts. Demzufolge sollte die App Textur-Swapchain-Formate vorzugsweise im sRGB-Raum anfordern (z. B. OVR_FORMAT_R8G8B8A8_UNORM_SRGB). Außerdem sollte deine App glEnable (GL_FRAMEBUFFER_SRGB) aufrufen, bevor sie in diese Texturen rendert.
Auch wenn dies nicht empfohlen wird – wenn deine App so konfiguriert ist, dass sie die Textur als lineares Format (z. B. GL_RGBA) behandelt und eine Linear-Gamma-Konvertierung in GLSL durchführt oder die Gammakorrektur ignoriert, dann:
  • Fordere eine Textur-Swapchain im sRGB-Format an (z. B. OVR_FORMAT_R8G8B8A8_UNORM_SRGB).
  • Rufe nicht glEnable (GL_FRAMEBUFFER_SRGB) auf, wenn in die Textur gerendert wird.
Bei Vulkan verwendet der Verzeichnungs-Compositor den Formatparameter ovr_CreateTextureSwapChainVk beim Lesen des Texturinhalts. Deine App sollte Textur-Swapchainformate im sRGB-Raum (z. B. OVR_FORMAT_R8G8B8A8_UNORM_SRGB) anfordern, da der Compositor sRGB-korrekt rendert. Der Compositor greift auf den GPU-Hardwaresampler zurück, um die sRGB-Linear-Konvertierung durchzuführen.
Wenn deine App bevorzugt in ein lineares Format rendert (z. B. OVR_FORMAT_R8G8B8A8_UNORM) und die Linear-Gamma-Konvertierung per SPIRV-Code handhabt, muss die App dennoch das entsprechende sRGB-Format anfordern und auch ovrTextureMisc_DX_Typeless im Flag-Feld von ovrTextureSwapChainDesc verwenden. Dies gestattet der App, ein RenderTargetView im linearen Format zu generieren, während der Compositor es als sRGB behandeln kann. Wird dies unterlassen, treten unerwartete Gammakurven-Artefakte auf. Das ovrTextureMisc_DX_Typeless-Flag für Tiefepufferformate (z. B. OVR_FORMAT_D32_FLOAT) wird ignoriert, da diese immer als Typeless konvertiert werden.
Neben sRGB gelten diese Konzepte auch für die Erstellung von Spiegeltexturen. Weitere Informationen findest du in der bereitgestellten Funktionsdokumentation für ovr_CreateMirrorTextureDX, ovr_CreateMirrorTextureGL und ovr_CreateMirrorTextureWithOptionsVk für D3D, OpenGL beziehungsweise Vulkan.

Einzelbild-Rendering

Das Einzelbild-Rendering umfasst in der Regel mehrere Schritte: das Erhalten prognostizierter Augenhaltungen auf Grundlage der Headset-Trackinghaltung, das Rendering der Ansicht für jedes Auge und schließlich das Senden von Augentexturen an den Compositor über ovr_WaitToBeginFrame, ovr_BeginFrame und ovr_EndFrame. Nachdem das Einzelbild übermittelt wurde, kümmert sich der Compositor um Verzeichnung, Timewarp und GPU-Synchronisierung, bevor er es dem Headset präsentiert.
Bevor Einzelbilder gerendert werden, ist es hilfreich, einige Datenstrukturen zu initialisieren, die mehrere Einzelbilder gemeinsam haben können. Als Beispiel rufen wir Augen-Deskriptoren ab und initialisieren die Layerstruktur außerhalb des Rendering-Loops:
 // Initialize VR structures, filling out description.
ovrEyeRenderDesc eyeRenderDesc[2];
ovrPosef      hmdToEyeViewPose[2];
ovrHmdDesc hmdDesc = ovr_GetHmdDesc(session);
eyeRenderDesc[0] = ovr_GetRenderDesc(session, ovrEye_Left, hmdDesc.DefaultEyeFov[0]);
eyeRenderDesc[1] = ovr_GetRenderDesc(session, ovrEye_Right, hmdDesc.DefaultEyeFov[1]);
hmdToEyeViewPose[0] = eyeRenderDesc[0].HmdToEyePose;
hmdToEyeViewPose[1] = eyeRenderDesc[1].HmdToEyePose;

// Initialize our single full screen Fov layer.
ovrLayerEyeFov layer;
layer.Header.Type      = ovrLayerType_EyeFov;
layer.Header.Flags     = 0;
layer.ColorTexture[0]  = textureSwapChain;
layer.ColorTexture[1]  = textureSwapChain;
layer.Fov[0]           = eyeRenderDesc[0].Fov;
layer.Fov[1]           = eyeRenderDesc[1].Fov;
layer.Viewport[0]      = Recti(0, 0,                bufferSize.w / 2, bufferSize.h);
layer.Viewport[1]      = Recti(bufferSize.w / 2, 0, bufferSize.w / 2, bufferSize.h);
// ld.RenderPose and ld.SensorSampleTime are updated later per frame.
Dieses Codebeispiel ruft erst die Rendering-Deskriptoren für jedes Auge ab, abhängig vom gewählten FOV. Die zurückgegebene ovrEyeRenderDesc-Struktur enthält nützliche Werte für das Rendering, einschließlich der HmdToEyePose für jedes Auge. Die Augenansicht-Offsets werden später in Verbindung mit der Augentrennung berücksichtigt.
Der Code initialisiert auch die ovrLayerEyeFovStruktur für ein Vollbild-Layer. Ab Oculus-SDK 0.6 werden bei der Einzelbildeinreichung Layers verwendet, um mehrere Ansichtsbilder oder Texture-Quads übereinander zu kombinieren. In diesem Beispiel wird ein einzelnes Layer verwendet, um eine VR-Szene zu präsentieren. Zu diesem Zweck verwenden wir ovrLayerEyeFov, das ein Dual-Eye-Layer beschreibt, welches das gesamte Sichtfeld ausfüllt. Da wir dasselbe Texturset für beide Augen verwenden, initialisieren wir beide Augenfarbtexturen zu pTextureSet und konfigurieren Viewports, um jeweils links und rechts von dieser gemeinsamen Textur zu zeichnen.
Wenn die Einrichtung abgeschlossen ist, kann die App den Rendering-Loop aktivieren. Zuerst brauchen wir die Augenhaltungen zum Rendern der linken und rechten Ansichten.
// Get both eye poses simultaneously, with IPD offset already included.
double displayMidpointSeconds = ovr_GetPredictedDisplayTime(session, 0);
ovrTrackingState hmdState = ovr_GetTrackingState(session, displayMidpointSeconds, ovrTrue);
ovr_CalcEyePoses(hmdState.HeadPose.ThePose, hmdToEyeViewPose, layer.RenderPose);
In VR sind die gerenderten Augenansichten von der Position des Headsets und der Ausrichtung im physischen Raum abhängig, welche mithilfe interner IMU und externer Sensoren getrackt werden. Prognosen werden zur Kompensierung der Systemlatenz verwendet und liefern bestmögliche Schätzwerte für die Position des Headsets zum Zeitpunkt der Anzeige des Einzelbilds auf dem Headsets. Im Oculus-SDK wird diese getrackte, prognostizierte Haltung durch ovr_GetTrackingState gemeldet.
Für eine genaue Prognose muss ovr_GetTrackingState wissen, wann das aktuelle Einzelbild wirklich angezeigt wird. Der obige Code ruft ovr_GetPredictedDisplayTime auf, um displayMidpointSeconds für das aktuelle Einzelbild zu erhalten und daraus den bestmöglich prognostizierten Trackingstatus zu berechnen. Die Kopfhaltung aus dem Trackingstatus wird dann an ovr_CalcEyePoses weitergegeben, um die richtigen Ansichtshaltungen für jedes Auge zu berechnen. Diese Haltungen werden direkt im Array layer.RenderPose[2] gespeichert. Wenn die Augenhaltungen bereit sind, können wir mit dem eigentlichen Einzelbild-Rendering fortfahren.
if (isVisible)
{
  // Get next available index of the texture swap chain
    int currentIndex = 0;
    ovr_GetTextureSwapChainCurrentIndex(session, textureSwapChain, &currentIndex);
    ++frameIndex;
    ovrResult result = ovr_WaitToBeginFrame(session, frameIndex);

    // Clear and set up render-target.
    DIRECTX.SetAndClearRenderTarget(pTexRtv[currentIndex], pEyeDepthBuffer);

    // Render Scene to Eye Buffers
    result = ovr_BeginFrame(session, frameIndex);
    for (int eye = 0; eye < 2; eye++)
    {
        // Get view and projection matrices for the Rift camera
        Vector3f pos = originPos + originRot.Transform(layer.RenderPose[eye].Position);
        Matrix4f rot = originRot * Matrix4f(layer.RenderPose[eye].Orientation);

        Vector3f finalUp      = rot.Transform(Vector3f(0, 1, 0));
        Vector3f finalForward = rot.Transform(Vector3f(0, 0, -1));
        Matrix4f view         = Matrix4f::LookAtRH(pos, pos + finalForward, finalUp);

        Matrix4f proj = ovrMatrix4f_Projection(layer.Fov[eye], 0.2f, 1000.0f, 0);
        // Render the scene for this eye.
        DIRECTX.SetViewport(layer.Viewport[eye]);
        roomScene.Render(proj * view, 1, 1, 1, 1, true);
    }

  // Commit the changes to the texture swap chain
  ovr_CommitTextureSwapChain(session, textureSwapChain);
}

// Submit frame with one layer we have.
ovrLayerHeader* layers = &layer.Header;
result = ovr_EndFrame(session, frameIndex, nullptr, &layers, 1);
isVisible = (result == ovrSuccess);
Dieser Code umfasst einige Schritte zum Rendern der Szene:
  • Er wendet die Textur als Renderingziel an und gibt sie für das Rendering frei. In diesem Fall wird dieselbe Textur für beide Augen verwendet.
  • Der Code berechnet dann die Ansichts- und Projektionsmatrizen und stellt das Viewport-Szenenrendering für jedes Auge ein. In diesem Beispiel kombiniert die Ansichtsberechnung die Original-Haltung (Werte originPos und originRot) mit der neuen Haltung, die auf Grundlage des Trackingstatus berechnet und im Layer gespeichert wurde. Diese Originalwerte können durch Eingaben modifiziert werden, um den*die Spieler*in in der 3D-Welt zu bewegen.
  • Wenn das Textur-Rendering abgeschlossen ist, rufen wir ovr_EndFrame auf, um Einzelbilddaten an den Compositor weiterzugeben. Ab diesem Moment übernimmt der Compositor, indem er die Texturdaten über den gemeinsamen Speicher abruft, sie verzeichnet und auf dem Rift präsentiert.
ovr_EndFrame gibt einen Wert zurück, sobald das gesendete Einzelbild in die Warteschlange eingereiht ist und die Runtime bereit ist, ein neues Einzelbild anzunehmen. Wenn erfolgreich, ist der zurückgegebene Wert entweder ovrSuccess oder ovrSuccess_NotVisible.
ovrSuccess_NotVisible wird zurückgegeben, wenn das Einzelbild nicht angezeigt wurde; das kann passieren, wenn die VR-App den Fokus verliert. Unser Beispielcode handhabt diesen Fall durch Aktualisierung des isVisible-Flags, das durch die Renderinglogik geprüft wird. Während die Einzelbilder nicht zu sehen sind, sollte das Rendering pausiert werden, um unnötige GPU-Last zu vermeiden.
Wenn du ovrError_DisplayLost erhältst, wurde das Gerät entfernt und die Session ist ungültig. Gib die gemeinsamen Ressourcen frei (ovr_DestroyTextureSwapChain), lösche die Session (ovr_Destroy), erstelle sie neu (ovr_Create) und erstelle neue Ressourcen (ovr_CreateTextureSwapChainXXX). Die vorhandenen privaten Grafikressourcen der App müssen nicht neu erstellt werden, es sei denn, der neue ovr_Create-Aufruf gibt eine abweichende GraphicsLuid zurück.

Einzelbild-Timing

Das Oculus-SDK meldet Einzelbild-Timinginformationen über die Funktion ovr_GetPredictedDisplayTime und verlässt sich dabei auf den von der App bereitgestellten Einzelbildindex, um korrekte Timing-Meldungen für unterschiedliche Threads zu gewährleisten.
Genaues Einzelbild- und Sensor-Timing ist für eine genaue Prognose der Kopfbewegung benötigt, die wiederum entscheidend für ein gutes VR-Erlebnis ist. Die Prognose erfordert genaues Wissen darüber, zu welchem künftigen Zeitpunkt das aktuelle Einzelbild auf dem Bildschirm erscheint. Wenn wir sowohl die Sensor- als auch die Display-Scanoutzeiten kennen, können wir die künftige Kopfhaltung vorhersagen und die Bildstabilität verbessern. Werden diese Werte falsch berechnet, können die Prognosen nach unten oder oben abweichen, die wahrgenommene Latenz kann sich verschlechtern und es kann potenziell zu Overshoot-Verwacklungen kommen.
Um ein genaues Timing sicherzustellen, verwendet das Oculus-SDK die absolute Systemzeit, die als Double gespeichert wird, um die Sensor- und Einzelbild-Timingwerte darzustellen. Die aktuelle absolute Zeit wird durch ovr_GetTimeInSeconds zurückgegeben. Die aktuelle Zeit sollte jedoch selten verwendet werden, da die Simulation und die Bewegungsprognose bessere Ergebnisse zeigen, wenn sie auf die durch ovr_GetPredictedDisplayTime zurückgegebenen Timingwerten zurückgreifen. Diese Funktion hat die folgende Signatur:
ovr_GetPredictedDisplayTime(ovrSession session, long long frameIndex);
Das frameIndex-Argument spezifiziert, welches App-Einzelbild wir rendern. Apps, die Multi-Thread-Rendering verwenden, müssen einen internen Einzelbildindex führen und diesen manuell inkrementieren, indem sie ihn zusammen mit den Einzelbilddaten über Threads weitergeben, um ein korrektes Timing und eine korrekte Prognose zu gewährleisten. Derselbe frameIndex, der verwendet wurde, um das Timing für den Einzelbildwert zu erhalten, muss an ovr_WaitToBeginFrame, ovr_BeginFrame und ovr_EndFrame weitergegeben werden. Details zum Multi-Thread-Timing findest du im nächsten Abschnitt Rendering auf verschiedenen Threads.
Ein spezieller frameIndex-Wert von 0 kann in beiden Funktionen verwendet werden, um anzufordern, dass das SDK die Einzelbildindizes automatisch trackt. Das funktioniert allerdings nur, wenn alle Einzelbild-Timinganfragen und Rendering-Einreichungen auf demselben Thread stattfinden.

Rendering auf verschiedenen Threads

Bei manchen Engines ist die Rendering-Verarbeitung auf mehrere Threads aufgeteilt.
So kann zum Beispiel ein Thread das Culling und die Rendering-Konfiguration für jedes Objekt in der Szene durchführen (der sog. Hauptthread), während ein zweiter Thread für die eigentlichen D3D- oder OpenGL-API-Aufrufe zuständig ist (der sog. Render-Thread). Beide Threads benötigen gegebenenfalls genaue Schätzwerte zur Einzelbild-Anzeigezeit, um bestmögliche Prognosen der Kopfhaltung zu berechnen.
Die Herausforderung liegt in der asynchronen Herangehensweise: Während der Render-Thread ein Einzelbild rendert, verarbeitet der Hauptthread möglicherweise schon das nächste Einzelbild. Diese parallele Einzelbildverarbeitung kann um genau ein Einzelbild oder einen Teil eines Einzelbilds asynchron sein, abhängig vom Design der Spiel-Engine. Wenn wir den globalen Standardstatus für den Zugriff auf das Einzelbild-Timing verwenden, könnte das Ergebnis von ovr_GetPredictedDisplayTime entweder um ein Einzelbild abweichen, je nachdem, von welchem Thread die Funktion aufgerufen wird, oder sogar zufällige Fehler aufweisen, abhängig davon, wie die Threads geplant werden. Um dieses Problem anzugehen, wurde im vorherigen Abschnitt das Konzept des frameIndex vorgestellt, der von der App getrackt und zusammen mit Einzelbilddaten über Threads weitergegeben wird.
Für ein korrektes Multi-Thread-Rendering-Ergebnis müssen die folgenden Voraussetzungen erfüllt sein: (a) die auf Grundlage des Einzelbild-Timings berechnete Haltungsprognose muss für ein Einzelbild unabhängig davon konstant bleiben, von welchem Thread aus der Zugriff erfolgt; und (b) Augenhaltungen, die tatsächlich für das Rendering verwendet wurden, müssen zusammen mit dem Einzelbildindex an ovr_EndFrame weitergegeben werden. (Dies muss geschehen, nachdem ovr_WaitToBeginFrame und ovr_BeginFrame aufgerufen wurden.)
Hier ist eine Zusammenfassung der Schritte, mit denen du dies sicherstellen kannst:
  1. Der Hauptthread muss dem aktuell für das Rendering verarbeiteten Einzelbild einen Einzelbildindex zuweisen. Er inkrementiert diesen Index mit jedem Einzelbild und gibt ihn an ovr_GetPredictedDisplayTime weiter, um das richtige Timing für die Haltungsprognose zu erhalten.
  2. Der Hauptthread sollte die Thread-sichere Funktion ovr_GetTrackingState mit dem prognostizierten Zeitwert aufrufen. Er kann auch ovr_CalcEyePoses aufrufen, falls dies für die Rendering-Konfiguration nötig ist.
  3. Der Hauptthread muss den aktuellen Einzelbildindex und die Augenhaltungen an den Render-Thread weitergeben, gemeinsam mit allen von diesem benötigten Renderingbefehlen oder Einzelbilddaten.
  4. Wenn die Renderingbefehle auf dem Render-Thread ausgeführt werden, müssen Entwickler*innen Folgendes sicherstellen:
  5. Die tatsächlich für das Einzelbild-Rendering verwendeten Haltungen werden in der RenderPose für das Layer gespeichert.
  6. Derselbe frameIndex-Wert, der auf dem Hauptthread verwendet wurde, wird an ovr_BeginFrame und ovr_EndFrame weitergegeben.
Der folgende Code veranschaulicht dies detaillierter:
void MainThreadProcessing()
{
    frameIndex++;
    ovrResult result = ovr_WaitToBeginFrame(session, frameIndex);

    // Ask the API for the times when this frame is expected to be displayed.
    double frameTiming = ovr_GetPredictedDisplayTime(session, frameIndex);

    // Get the corresponding predicted pose state.
    ovrTrackingState state = ovr_GetTrackingState(session, frameTiming, ovrTrue);
    ovrPosef         eyePoses[2];
    ovr_CalcEyePoses(state.HeadPose.ThePose, hmdToEyeViewOffset, eyePoses);

    SetFrameHMDData(frameIndex, eyePoses);

    // Do render pre-processing for this frame.
    ...
}

void RenderThreadProcessing()
{
    int       frameIndex;
    ovrPosef  eyePoses[2];
    GetFrameHMDData(&frameIndex, eyePoses);
    ovrResult result = ovr_BeginFrame(session, frameIndex);

    layer.RenderPose[0] = eyePoses[0];
    layer.RenderPose[1] = eyePoses[1];

    // Execute actual rendering to eye textures.
    ...

   // Submit frame with the one layer we have.
   ovrLayerHeader* layers = &layer.Header;
   result = ovr_EndFrame(session, frameIndex, nullptr, &layers, 1);
}
Das Oculus-SDK unterstützt auch Direct3D 12, womit von mehreren CPU-Threads Rendering-Aufgaben an die GPU gesendet werden können. Wenn die App ovr_CreateTextureSwapChainDX aufruft, speichert das Oculus-SDK die vom Aufrufer bereitgestellte ID3D12CommandQueue zur zukünftigen Verwendung im Cache. Wenn die App ovr_EndFrame aufruft, legt das SDK ein Fence um die zwischengespeicherte ID3D12CommandQueue an, um genau zu wissen, wann eine bestimmte Auswahl von Augentexturen bereit für den SDK-Compositor ist.
Für eine App ist es am einfachsten, eine einzige ID3D12CommandQueue auf einem einzigen Thread zu verwenden. Sie könnte aber auch die CPU-Rendering-Last für jedes Augentexturpaar aufteilen oder die Rendering-Aufgaben ohne Bezug zu Augentexturen, wie Schatten, Reflexionskarten und so weiter, auf unterschiedliche Befehlswarteschlangen verteilen. Wenn die App Befehlslisten aus mehreren Threads befüllt und ausführt, muss sie auch dafür sorgen, dass die dem SDK bereitgestellte ID3D12CommandQueue als einziger Join-Node für die Augentextur-Renderingaufgaben dient, die durch unterschiedliche Befehlswarteschlangen ausgeführt werden.

Layers

Ähnlich, wie eine Monitoransicht aus mehreren Fenstern bestehen kann, kann die Anzeige auf dem Headset aus mehreren Layers zusammengesetzt sein. In der Regel ist mindestens eines dieser Layers eine Ansicht, die von den virtuellen Augen des*der Nutzer*in aus gerendert wird. Bei den anderen Layers kann es sich aber um HUD- oder Cubemap-Layers, Informationspanels, Textlabel an Gegenständen in der Welt, Zielfadenkreuze und so weiter handeln.
Jedes Layer kann eine andere Auflösung haben, ein anderes Texturformat oder Sichtfeld oder eine andere Größe verwenden und in Mono oder Stereo vorliegen. Die App kann auch so konfiguriert werden, dass sie die Textur eines Layers nicht aktualisiert, wenn sich die darin enthaltenen Informationen nicht verändert haben. Sie könnte zum Beispiel auf die Aktualisierung verzichten, wenn der Text in einem Informationspanel sich seit dem letzten Einzelbild nicht verändert hat oder wenn es sich beim Layer um eine Bild-in-Bild-Ansicht eines Videostreams mit geringer Bildrate handelt. Apps können Texturen mit Mip-Mapping für ein Layer bereitstellen, was in Verbindung mit einem hochwertigen Verzeichnungsmodus sehr wirksam ist, um die Lesbarkeit von Textpanels zu verbessern.
Für jedes Einzelbild werden alle aktiven Layers von hinten nach vorn kombiniert, wobei prämultipliziertes Alpha Blending verwendet wird. Layer 0 ist das entfernteste Layer, Layer 1 liegt über diesem und so weiter.
Ein wirkungsvolles Feature von Layers ist, dass jedes eine unterschiedliche Auflösung haben kann. Das gestattet einer App, für Systeme mit geringerer Leistung zu skalieren, indem die Auflösung des zentralen Eye-Render-Buffers gesenkt wird, der die virtuelle Welt zeigt, essenzielle Informationen wie ein Text oder eine Karte aber in einem anderen Layer weiter mit höherer Auflösung dargestellt werden.
Es sind mehrere Layer-Typen verfügbar:
Layer-TypBeschreibung
EyeFov
Der standardmäßige Eye-Render-Buffer, der aus früheren SDKs bekannt ist. Typischerweise handelt es sich um ein Stereobild einer virtuellen Szene, die aus der Augenposition des*der Nutzer*in gerendert wird. Eye-Render-Buffer sind auch als Monobilder möglich, dies kann aber Unwohlsein verursachen. Frühere SDKs hatten ein implizites Sichtfeld (FOV) und Viewport. Diese werden nun explizit bereitgestellt und die App kann sie für jedes Einzelbild ändern, wenn gewünscht.
Quad
Ein monoskopisches Bild, das als Rechteck mit einer bestimmten Haltung und Größe in der virtuellen Welt angezeigt wird. Dies ist nützlich für Heads-Up-Displays, Textinformationen, Objektlabels und so weiter. Standardmäßig wird die Haltung relativ zum physischen Raum des*der Nutzer*in spezifiziert. Das Quad bleibt im Raum fixiert, statt sich mit der Kopf- oder Körperbewegung des*der Nutzer*in mitzubewegen. Für kopffixierte Quads verwendest du das ovrLayerFlag_HeadLocked-Flag wie unten beschrieben.
Cubemap
Eine Cubemap besteht aus sechs Rechtecken. Diese Rechtecke werden um den*die Nutzer*in herum angeordnet, als ob er*sie in einem würfelförmigen Raum sitzen würde. Jede Wand ist eine Textur, die von deiner App gesendet wird. Die Cubemap erscheint wie in unendlicher Entfernung und stellt im Wesentlichen den Hintergrund hinter allen anderen Objekten dar, die von deiner App gerendert werden. Die Cubemap sieht für den*die Nutzer*in nicht wie ein Würfel aus. Vielmehr wirkt sie einfach wie der Hintergrund in unendlicher Entfernung. Du kannst Cubemaps zum Beispiel verwenden, um den Himmel zu erstellen, der in deinem VR-Erlebnis hinter allen Gebäuden zu sehen sein wird. Du brauchst dich nicht um Verdeckungen durch Objekte im Vordergrund zu kümmern. Richte einfach die Cubemap ein, dann erscheint sie überall in deiner Szene im Hintergrund.
EyeMatrix
Der Layer-Typ EyeMatrix ähnelt dem Layer-Typ EyeFov und wird bereitgestellt, um die Kompatibilität mit veralteten Samsung Gear VR-Apps zu unterstützen.
Zylindrisch
Du kannst zylindrische Layers verwenden, um statt flachen Quads gebogene Quads zu erstellen. ovrLayerCylinder beschreibt ein Layer des Typs ovrLayerType_Cylinder: ein einzelner Zylinder, der relativ zu einem neu zentrierten Ursprungspunkt positioniert wird (in der Abbildung unten durch C dargestellt). Siehe Parameter für zylindrische Layers unten.
Tiefepuffer
Dieses Layer spezifiziert eine monoskopische oder stereoskopische Ansicht, mit Tiefetexturen zusätzlich zu Farbtexturen. Dieses Layer wird durch das ovrLayerEyeFovDepth-Struct implementiert. Es ist äquivalent zu ovrLayerEyeFov, jedoch mit zusätzlichem DepthTexture und ProjectionDesc. Tiefepuffer werden in der Regel verwendet, um positionellen Timewarp zu unterstützen.
Deaktiviert
Wird vom Compositor ignoriert, deaktivierte Layers nehmen keine Leistung in Anspruch. Wir empfehlen, dass Apps grundlegendes Frustum Culling durchführen und Layers außerhalb des Sichtfelds deaktivieren. Es ist jedoch nicht erforderlich, dass die App die Liste der aktiven Layers neu packt, wenn ein Layer deaktiviert wird; es reicht aus, es zu deaktivieren und in der Liste zu belassen. Gleichermaßen kann der Zeiger auf das Layer in der Liste auf Null gesetzt werden.
Parameter für zylindrische Layers
Dieser Layer-Typ stellt ein einzelnes, in der Welt platziertes Objekt dar, kein Stereobild der Welt an sich. Nur die Innenfläche des Zylinders ist sichtbar. Du solltest zylindrische Layers nur dann verwenden, wenn der*die Nutzer*in die Begrenzung des Zylinders nicht verlassen kann. Beim Ansehen der Zylinderaußenfläche können Artefakte auftreten. Außerdem unterstützt das Interface zwar einen Winkel (A) zwischen 0 und 2Pi, der Winkel sollte aber immer kleiner als 1,9Pi liegen, um Artefakte an den Stellen zu vermeiden, wo die Zylinderränder konvergieren. Weitere Informationen zur Verwendung dieses Features findest du unter ovrLayerCylinder in der Referenzdokumentation.

Layer-Parameter

Für jeden Layer-Stil gibt es ein entsprechendes Mitglied der ovrLayerType-Enum sowie eine verknüpfte Struktur, welche die für die Anzeige dieses Layers benötigten Daten beinhaltet. So ist das Layer EyeFov vom Typ Nummer ovrLayerType_EyeFov und wird durch die Daten in der Struktur ovrLayerEyeFov beschrieben. Diese Strukturen verwenden eine gemeinsame Parameterauswahl, auch wenn nicht alle Layer-Typen alle Parameter erfordern:
ParameterTypBeschreibung
Header.Type
Enum ovrLayerType
Muss durch alle Layers festgelegt werden, um ihren Typ zu spezifizieren.
Header.Flags
Ein Bitfield von ovrLayerFlags
Weitere Informationen dazu findest du unten.
ColorTexture
TextureSwapChain
Stellt Farb- und Lichtdurchlässigkeitsdaten für das Layer bereit. Layers werden mittels prämultipliziertem Alpha überblendet. Dadurch können sie entweder lerp-Blending, additives Blending oder eine Kombination aus beidem ausdrücken. Die Layertexturen müssen als RGBA oder BGRA vorliegen und können Mip-Mapping aufweisen, dürfen aber keine Arrays oder Cubes sein oder MSAA aufweisen. Wenn die App MSAA-Rendering ausführen will, muss sie die MSAA-Zwischen-Farbtextur in die nicht-MSAA-ColorTexture umwandeln.
Viewport
ovrRecti
Das Rechteck der tatsächlich verwendeten Textur, angegeben als ovrRecti-Struktur, die Größe und Position des Rechtecks in Form von zwei 2D-Vektoren bereitstellt, die jeweils Ganzzahlen für Breite/Höhe und x/y enthalten. Die für die Position bereitgestellten x/y-Werte geben die linke untere Ecke des Rechtecks an. Theoretisch sind Texturdaten außerhalb dieses Bereichs nicht im Layer sichtbar. Allerdings gelten die üblichen Einschränkungen in Verbindung mit der Texturabtastung, besonders bei Texturen mit Mip-Mapping. Es hat sich bewährt, einen Rand aus RGBA(0,0,0,0)-Pixeln um den angezeigten Bereich herum zu belassen, um ein „Ineinanderfließen“ zu verhindern, vor allem zwischen zwei nebeneinander in dieselbe Textur gepackten Eye-Render-Buffers. Die Größe des Rands ist vom genauen Anwendungsfall abhängig, aber in den meisten Fällen scheint es mit etwa 8 Pixeln gut zu funktionieren.
Fov
ovrFovPort
Das Sichtfeld, das verwendet wird, um die Szene in einem Eye-Layer-Typ zu rendern. Beachte, dass dies nicht die HMD-Anzeige steuert, sondern einfach nur dem Compositor mitteilt, welches FOV zum Rendern der Texturdaten im Layer verwendet wurde. Der Compositor nimmt dann entsprechend dem tatsächlichen FOV des*der Nutzer*in geeignete Anpassungen vor. Apps können das FOV für Spezialeffekte dynamisch verändern. Die Verkleinerung des FOV kann außerdem bei langsameren Rechnern die Leistung verbessern, auch wenn es in der Regel wirksamer ist, die Auflösung zu senken, bevor das FOV verkleinert wird.
RenderPose
ovrPosef
Die Kamerahaltung, die von der App verwendet wurde, um die Szene in einem Eye-Layer-Typ zu rendern. Wird in der Regel von SDK und App mithilfe der Funktionen ovr_GetTrackingState und ovr_CalcEyePoses prognostiziert. Die Differenz zwischen dieser Haltung und der tatsächlichen Haltung des Auges zum Anzeigezeitpunkt wird vom Compositor verwendet, um Timewarp auf das Layer anzuwenden.
SensorSampleTime
Double
Die absolute Zeit zum Zeitpunkt der Abtastung des Tracking-Status durch die App. Dieser Wert wird in der Regel durch einen ovr_GetTimeInSeconds-Aufruf gleich neben einem ovr_GetTrackingState-Aufruf erhalten. Das SDK verwendet diesen Wert, um die Bewegung-zu-Photon-Latenz der App im Performance HUD zu melden. Wenn die App an einem beliebigen Einzelbild mehr als ein ovrLayerType_EyeFov-Layer eingereicht hat, bereinigt das SDK diese Layers und wählt das Timing mit der geringsten Latenz. Wenn in einem Einzelbild keine ovrLayerType_EyeFov-Layers eingereicht werden, verwendet das SDK den Zeitpunkt, zu dem ovr_GetTrackingState aufgerufen wurde, wobei der latencyMarker als Ersatz für die Bewegung-zu-Photon-Latenzzeit der App auf ovrTrue eingestellt ist.
QuadPoseCenter
ovrPosef
Spezifiziert Ausrichtung und Position des Mittelpunkts eines Quad-Layer-Typs. Die angegebene Richtung ist der senkrecht zum Quad stehende Vektor. Die Position wird in echten Metern angegeben (keine Meter in der virtuellen App-Welt, sondern in der physischen Welt des*der Nutzer*in) und ist relativ zur Null-Position, die durch ovr_RecenterTrackingOrigin oder ovr_SpecifyTrackingOrigin festgelegt wird, es sei denn, das ovrLayerFlag_HeadLocked-Flag wird verwendet.
QuadSize
ovrVector2f
Spezifiziert Breite und Höhe eines Quad-Layer-Typs. Wie bei der Position handelt es sich um Meter in der echten Welt.
Layers, die Stereoinformationen aufnehmen (alle genannten außer Quad-Layer-Typen), nehmen zwei Sets der meisten Parameter auf, die auf drei verschiedene Arten verwendet werden können:
  • Stereodaten, separate Texturen – die App stellt eine unterschiedliche ovrTextureSwapChain für das linke und das rechte Auge sowie einen Viewport für jedes Auge bereit.
  • Stereodaten, gemeinsame Textur – die App stellt dieselbe ovrTextureSwapChain für das linke und das rechte Auge sowie einen unterschiedlichen Viewport für jedes Auge bereit. Dadurch kann die App sowohl linke als auch rechte Ansichten in denselben Textur-Buffer rendern. Denke daran, wie oben besprochen zwischen den beiden Ansichten einen kleinen Buffer einzufügen, um „Ineinanderfließen“ zu verhindern.
  • Monodaten – die App stellt dieselbe ovrTextureSwapChain für das linke und das rechte Auge sowie denselben Viewport für jedes Auge bereit.
Die Größen von Textur und Viewport können für das linke und das rechte Auge unterschiedlich sein. Jedes Auge kann sogar ein unterschiedliches Sichtfeld haben. Achte jedoch darauf, keine Stereo-Unstimmigkeiten und Unwohlsein bei deinen Nutzer*innen zu verursachen.
Das für alle Layers verfügbare Header.Flags-Feld ist ein logisches Oder des Folgenden:
  • ovrLayerFlag_HighQuality – ermöglicht vierfache anisotrope Filterung im Compositor für dieses Layer. Das kann eine erheblich verbesserte Lesbarkeit bewirken, besonders bei Verwendung mit einer Textur mit Mip-Mapping; dies wird für hochfrequente Bilder wie Texte oder Diagramme sowie bei Verwendung mit den Quad-Layer-Typen empfohlen. Für Eye-Layer-Typen steigert dies auch die visuelle Wiedergabetreue in Richtung Peripherie beziehungsweise beim Einspeisen von Texturen, die die empfohlene Pixeldichte von 1:1 überschreiten. Stelle für beste Ergebnisse sicher, dass es sich bei den Texturgrößen um Zweierpotenzen handelt, wenn du Mipmaps für die mit dem jeweiligen Layer verknüpften Texturen erstellst. Die App muss allerdings nicht die gesamte Textur rendern. Ein Viewport, der in der Textur auf die empfohlene Größe rendert, liefert das beste Verhältnis von Leistung zu Qualität.
  • ovrLayerFlag_TextureOriginAtBottomLeft – es wird davon ausgegangen, dass der Ursprungspunkt einer Layer-Textur in der oberen linken Ecke liegt. Manche Engines (vor allem jene, die OpenGL verwenden) bevorzugen allerdings die untere linke Ecke als Ursprungspunkt und sollten dieses Flag verwenden.
  • ovrLayerFlag_HeadLocked – für die meisten Layer-Typen wird die Haltung, Ausrichtung und Position relativ zur Null-Position spezifiziert, die durch Aufruf von ovr_RecenterTrackingOrigin definiert wird. Unter Umständen soll die App aber die Haltung eines Layers relativ zum Gesicht des*der Nutzer*in spezifizieren. Wenn der*die Nutzer*in den Kopf bewegt, folgt das Layer. Das ist nützlich für Fadenkreuze, die zum blickbasierten Zielen oder zur Auswahl verwendet werden. Dieses Flag kann für alle Layer-Typen verwendet werden, auch wenn es beim Direkt-Typ keinen Effekt hat.
Am Ende jedes Einzelbilds, nachdem in die jeweilige ovrTextureSwapChain gerendert wurde, die die App aktualisieren will, und ovr_CommitTextureSwapChain aufgerufen wurde, werden die Daten für jedes Layer in die relevante ovrLayerEyeFov-/ovrLayerQuad-/ovrLayerDirect-Struktur eingefügt. Die App erstellt dann eine Liste mit Zeigern auf diese Layer-Strukturen, insbesondere auf das Header-Feld, das garantiert das erste Mitglied jeder Struktur ist. Dann erstellt die App ein ovrViewScaleDesc-Struct mit den erforderlichen Daten und ruft die Funktionen ovr_WaitToBeginFrame, ovr_BeginFrame und ovr_EndFrame auf.
ovrResult result = ovr_WaitToBeginFrame(Session, frameIndex);
result = ovr_BeginFrame(Session, frameIndex);
// Create eye layer.
ovrLayerEyeFov eyeLayer;
eyeLayer.Header.Type    = ovrLayerType_EyeFov;
eyeLayer.Header.Flags   = 0;
for ( int eye = 0; eye < 2; eye++ )
{
  eyeLayer.ColorTexture[eye] = EyeBufferSet[eye];
  eyeLayer.Viewport[eye]     = EyeViewport[eye];
  eyeLayer.Fov[eye]          = EyeFov[eye];
  eyeLayer.RenderPose[eye]   = EyePose[eye];
}

// Create HUD layer, fixed to the player's torso
ovrLayerQuad hudLayer;
hudLayer.Header.Type    = ovrLayerType_Quad;
hudLayer.Header.Flags   = ovrLayerFlag_HighQuality;
hudLayer.ColorTexture   = TheHudTextureSwapChain;
// 50cm in front and 20cm down from the player's nose,
// fixed relative to their torso.
hudLayer.QuadPoseCenter.Position.x =  0.00f;
hudLayer.QuadPoseCenter.Position.y = -0.20f;
hudLayer.QuadPoseCenter.Position.z = -0.50f;
hudLayer.QuadPoseCenter.Orientation.x = 0;
hudLayer.QuadPoseCenter.Orientation.y = 0;
hudLayer.QuadPoseCenter.Orientation.z = 0;
hudLayer.QuadPoseCenter.Orientation.w = 1;
// HUD is 50cm wide, 30cm tall.
hudLayer.QuadSize.x = 0.50f;
hudLayer.QuadSize.y = 0.30f;

ID3D11Texture2D* tex = nullptr;
ovr_GetTextureSwapChainBufferDX(Session, TheHudTextureSwapChain, 0, IID_PPV_ARGS(&tex));
D3D11_TEXTURE2D_DESC desc;
tex->GetDesc(&desc);
// Display all of the HUD texture.
hudLayer.Viewport.Pos.x = 0.0f;
hudLayer.Viewport.Pos.y = 0.0f;
hudLayer.Viewport.Size.w = desc.Width;
hudLayer.Viewport.Size.h = desc.Height;

// The list of layers.
ovrLayerHeader *layerList[2];
layerList[0] = &eyeLayer.Header;
layerList[1] = &hudLayer.Header;

// Set up positional data.
ovrViewScaleDesc viewScaleDesc;
viewScaleDesc.HmdSpaceToWorldScaleInMeters = 1.0f;
viewScaleDesc.HmdToEyeViewOffset[0] = HmdToEyePose[0];
viewScaleDesc.HmdToEyeViewOffset[1] = HmdToEyePose[1];

result = ovr_EndFrame(Session, frameIndex, &viewScaleDesc, layerList, 2);
Der Compositor führt für jedes Layer separat Timewarp, Verzeichnung und Korrektur der chromatischen Abweichung durch, bevor die Layers miteinander kombiniert werden. Die herkömmliche Methode zum Rendering eines Quads in den Eye-Render-Buffer erfordert zwei Filterschritte (einmal in den Eye-Render-Buffer, dann einmal während der Verzeichnung). Bei Verwendung von Layers gibt es nur einen einzigen Filterschritt zwischen Layer-Bild und finalem Einzelbild-Buffer. Dies kann eine erhebliche Verbesserung der Textqualität bewirken, besonders in Verbindung mit Mipmaps und dem ovrLayerFlag_HighQuality-Flag.
Ein gegenwärtiger Nachteil von Layers besteht darin, dass kein Post-Processing auf das finale kombinierte Bild angewendet werden kann, etwa Weichzeichnereffekte, Bloom-Effekte oder die Z-Schnittmenge der Layerdaten. Manche dieser Effekte können mit ähnlichen visuellen Ergebnissen auf die Inhalte des Layers angewendet werden.
Durch Aufrufen von ovr_EndFrame werden die Layers zur Anzeige eingereiht, zudem wird die Steuerung der übergebenen Texturen in der ovrTextureSwapChains an den Compositor übertragen. Es ist wichtig zu verstehen, dass diese Texturen von App und Compositor-Threads geteilt (und nicht kopiert) werden und dass die Komposition nicht unbedingt zum selben Zeitpunkt erfolgt, zu dem ovr_EndFrame aufgerufen wird. Gehe also sorgfältig vor. Um mit dem Rendering in eine Textur-Swapchain fortzufahren, sollte die App immer den nächsten verfügbaren Index mit ovr_GetTextureSwapChainCurrentIndex abrufen, bevor sie mit dem Rendering beginnt. Beispiel:
ovrResult result = ovr_WaitToBeginFrame(Hmd, frameIndex);
result = ovr_BeginFrame(Hmd, frameIndex);
// Create two TextureSwapChains to illustrate.
ovrTextureSwapChain eyeTextureSwapChain;
ovr_CreateTextureSwapChainDX ( ... &eyeTextureSwapChain );
ovrTextureSwapChain hudTextureSwapChain;
ovr_CreateTextureSwapChainDX ( ... &hudTextureSwapChain );

// Set up two layers.
ovrLayerEyeFov eyeLayer;
ovrLayerEyeFov hudLayer;
eyeLayer.Header.Type = ovrLayerType_EyeFov;
eyeLayer...etc... // set up the rest of the data.
hudLayer.Header.Type = ovrLayerType_Quad;
hudLayer...etc... // set up the rest of the data.

// the list of layers
ovrLayerHeader *layerList[2];
layerList[0] = &eyeLayer.Header;
layerList[1] = &hudLayer.Header;

// Each frame...
int currentIndex = 0;
ovr_GetTextureSwapChainCurrentIndex(... eyeTextureSwapChain, &currentIndex);
// Render into it. It is recommended the app use ovr_GetTextureSwapChainBufferDX for each index on texture chain creation to cache
// textures or create matching render target views. Each frame, the currentIndex value returned can be used to index directly into that.
ovr_CommitTextureSwapChain(... eyeTextureSwapChain);

ovr_GetTextureSwapChainCurrentIndex(... hudTextureSwapChain, &currentIndex);
// Render into it. It is recommended the app use ovr_GetTextureSwapChainBufferDX for each index on texture chain creation to cache
// textures or create matching render target views. Each frame, the currentIndex value returned can be used to index directly into that.
ovr_CommitTextureSwapChain(... hudTextureSwapChain);

eyeLayer.ColorTexture[0] = eyeTextureSwapChain;
eyeLayer.ColorTexture[1] = eyeTextureSwapChain;
hudLayer.ColorTexture = hudTextureSwapChain;

result = ovr_EndFrame(Hmd, frameIndex, nullptr, layerList, 2);
Geradlinige Aufnahme
Dieses Feature gestattet deinen Apps, Screenshots (mit 90 Einzelbildern pro Sekunde) von einem einzelnen, unverzeichneten Bild zu erhalten, die dem entsprechen, was der*die Nutzer*in im VR-Erlebnis sieht. Du kannst diese Funktion etwa dafür verwenden, das VR-Erlebnis auf einem externen Monitor zu spiegeln.

Umgang mit HMD-Augenhaltungen

Vor Version 1.17 des Oculus-PC-SDK hatten Augenhaltungen nur drei Freiheitsgrade (Degrees of Freedom, DOF), d. h. nur Translation. Augenhaltungen wurden im HmdToEyeOffset-Vektor spezifiziert, der von der ovr_GetRenderDesc-Funktion bereitgestellt wurde. Ab Version 1.17 wurde HmdToEyeOffset in HmdToEyePose umbenannt, wobei der Typ ovrPosef verwendet wird, der Position und Orientation enthält und Augenhaltungen so im Endeffekt sechs Freiheitsgrade verleiht. Das bedeutet, dass das Render Frustum jedes Auges jetzt zusätzlich zu seiner Translation durch das SDK auch von der HMD-Ausrichtung weg rotiert werden kann. Aus diesem Grund sind die Eye-Frustum-Achsen nicht mehr garantiert parallel zueinander oder zu den Achsen der HMD-Ausrichtung. Diese Generalisierung gibt dem SDK größeren Spielraum bei der Definition der HMD-Geometrie. Das heißt aber auch, dass du als VR-App-Entwickler*in in Bezug auf deine vorherigen Annahmen vorsichtiger sein musst, besonders wenn es ums Rendering geht.
Hier sind einige Tipps, um sicherzustellen, dass deine VR-App HmdToEyePose richtig verwendet:
  • Wenn deine VR-App den Vektortranslationswert von HmdToEyeOffset (vor Version 1.17) benötigt, kannst du stattdessen HmdToEyePose.Position verwenden. Wenn du dir mit deiner Vorgehensweise allerdings nicht absolut sicher bist, solltest du HmdToEyePose wahrscheinlich eher als vollständiges Transform behandeln, statt die Ausrichtung von der Position zu trennen.
  • Vor PC-SDK-Version 1.17 wäre es immer akzeptabel gewesen, ein 2D-Quad flach und quer auf dem Bildschirm zu rendern (z. B. ein Rechteck). Doch in Anbetracht der Möglichkeit, jedes Eye Frustum unabhängig zu rotieren, muss deine VR-App die Ausrichtung jedes Auges in die Transformation des Quads einbeziehen, damit das Quad mit der richtigen Perspektive für jedes Auge gerendert wird. Hier ist ein (leicht übertriebenes) Beispiel:
Die Idee ist, das Quad so auszurichten, dass es entweder die Center-Eye-Ausrichtung oder die HMD-Ausrichtung zu verwenden scheint. Dies gilt auch für andere am Bildschirm ausgerichtete Quads wie 3D-Splash-Screens oder Partikeleffekte wie etwa große Flipbook-Smoke-Quads. Mit Ausnahme von Partikeln solltest du es vermeiden, solche Quads nativ zu rendern. Verwende stattdessen lieber ovrLayerQuad.
  • Manche VR-Apps generieren ein einzelnes monoskopisches Kamera-Frustum anhand der ovrFovPort-Strukturen beider Augen, um die Vorteile verschiedener Renderingoptimierungen zu nutzen. Das geschieht normalerweise mithilfe einer ovrFovPort, die das Maximum der ovrFovPort-Werte für beide Augen auf allen vier Seiten des Frustums übernimmt. Bevor du das monoskopische Frustum jedoch auf diese Weise generierst, stelle sicher, dass du die ovrFovPort-Werte um jede potenzielle Rotation bereinigt hast, indem du FovPort::Uncant aufrufst, das sich im ovr_math.h-Header befindet. Siehe den OculusWorldDemo-Beispielcode zur Verwendung von FovPort::Uncant.

Asynchroner Timewarp

Asynchroner Timewarp (ATW) ist eine Technik zur Reduzierung von Latenz und Stottern in VR-Apps und -Erlebnissen.
In einem einfachen VR-Gameplay-Loop geschieht Folgendes:
  1. Die Software fordert deine Kopfposition an.
  2. Der Prozessor verarbeitet die Szenen für beide Augen.
  3. Die Grafikkarte rendert die Szenen.
  4. Der Compositor wendet die Verzeichnung an und stellt die Szenen auf dem Headset dar.
Der folgende Ablauf zeigt ein einfaches Beispiel für einen Gameplay-Loop:
Eine stabile Bildwiederholrate garantiert ein realistisches und angenehmes Erlebnis. Wenn die Bilder nicht rechtzeitig eintreffen, wird das vorherige Einzelbild angezeigt, was die Orientierung der Nutzer*innen stören kann. Die folgende Grafik zeigt ein Beispiel für ein solches Stottern beim Gameplay-Loop:
Wenn du deinen Kopf bewegst und die Welt nicht Schritt hält, kann dies sehr unangenehm wirken und die Immersion stören.
ATW ist eine Technik, die das gerenderte Bild minimal verschiebt, um aktuelle Kopfbewegungen zu berücksichtigen. Das Bild wird zwar verändert, aber dein Kopf hat sich nur minimal bewegt, daher ist die Veränderung auch sehr klein.
Außerdem kann ATW Unregelmäßigkeiten oder Momente, in denen die Bildwiederholrate unerwartet abnimmt, glätten, um Probleme im Zusammenhang mit dem Computer des*der Nutzer*in, dem Spieledesign oder dem Betriebssystem auszugleichen.
Die folgende Grafik zeigt ein Beispiel für ausgelassene Einzelbilder, wenn ATW angewendet wird:
Im Aktualisierungsintervall wendet der Compositor TimeWarp auf das zuletzt gerenderte Einzelbild an. Daher wird dem*der Nutzer*in unabhängig von der Bildwiederholrate immer ein Einzelbild mit TimeWarp angezeigt. Bei sehr niedrigen Bildwiederholraten kann ein spürbares Flackern im äußeren Anzeigebereich auftreten. Das Bild bleibt jedoch weiterhin stabil.
Der Compositor wendet ATW automatisch an. Du musst diese Funktion nicht aktivieren oder konfigurieren. ATW kann die Latenz zwar reduzieren, aber du solltest trotzdem sicherstellen, dass deine App bzw. dein Erlebnis die nötige Bildwiederholrate erreicht.

Adaptive Queue Ahead

Zur Verbesserung der CPU- und GPU-Parallelität und um der GPU mehr Zeit für die Verarbeitung eines Einzelbilds zu geben, wendet das SDK Queue Ahead automatisch für bis zu 1 Einzelbild an.
Ohne Queue Ahead beginnt die CPU sofort mit der Verarbeitung des nächsten Einzelbilds, wenn das vorherige Einzelbild dargestellt wird. Wenn die CPU fertig ist, verarbeitet die GPU das Einzelbild, der Compositor wendet die Verzeichnung an und das Einzelbild wird dem*der Nutzer*in angezeigt. Die folgende Grafik zeigt die CPU- und GPU-Nutzung ohne Queue Ahead:
Wenn die GPU das Einzelbild nicht rechtzeitig für die Anzeige verarbeiten kann, wird das vorherige Einzelbild angezeigt. Das Ergebnis ist Stottern (Judder).
Mit Queue Ahead kann die CPU früher beginnen, was der GPU mehr Zeit für die Verarbeitung des Einzelbilds gibt. Die folgende Grafik zeigt die CPU- und GPU-Nutzung mit Queue Ahead: