开发
开发
选择平台

针对 Rift 进行渲染

Oculus Rift 和 Rift S 需要为每只眼睛提供分屏立体显示并进行畸变校正,以消除透镜相关的畸变。
畸变校正可能颇具挑战性,因为不同类型的透镜以及不同的出瞳距离会导致畸变参数有所不同。为了简化开发流程,Oculus SDK 会在合成器进程中自动处理畸变校正;同时,它还会负责减少延迟的时间扭曲,并将帧呈现给头戴设备。
由于 Oculus SDK 已经完成了大部分工作,因此应用程序的主要任务是执行模拟,并根据追踪姿态渲染立体世界。立体视图可以渲染到一个或两个单独的纹理中,并通过调用 ovr_WaitToBeginFrameovr_BeginFrameovr_EndFrame 提交给合成器。我们将在本部分详细介绍这一过程。

针对 Rift 进行渲染

Oculus Rift 和 Rift S 需要将场景以分屏立体方式渲染,每个眼睛各占用屏幕的一半。
在使用 Rift 或 Rift S 时,左眼看到屏幕的左半部分,右眼看到屏幕的右半部分。尽管人与人之间存在差异,但通常情况下,人类双眼瞳孔之间的距离大约为 65 毫米。这被称为“瞳孔间距 (IPD)”。应用程序中的相机应该配置为相同的间距。
这是对相机的平移操作,而非旋转操作,正是这一平移操作(以及伴随的视差效应)产生了立体效果。这意味着您的应用程序需要将整个场景渲染两次,一次使用左侧虚拟相机,一次使用右侧虚拟相机。
依赖于从单个完全渲染的视图生成左右视图的再投影立体渲染技术,由于会在物体边缘产生明显的伪影,因此通常不适用于 HMD。
Rift 中的透镜会放大图像,以提供非常宽广的视场 (FOV),从而增强沉浸感。然而,这个过程会显著扭曲图像。如果引擎直接在 Rift 上显示原始图像,那么用户就会观察到枕形畸变。
为了抵消这种畸变,SDK 会对渲染视图应用后处理,施加一个大小相等但方向相反的桶形畸变,使两者相互抵消,从而为每只眼睛呈现一个无畸变的视图。此外,SDK 还会校正色差,这是透镜在边缘处引起的颜色分离效应。虽然具体的畸变参数取决于透镜特性和眼睛相对于透镜的位置,但 Oculus SDK 在生成畸变网格时会处理所有必要的计算。
在 Rift 和 Rift S 上,每只眼睛的投影轴是平行的,如下图所示。然而,在渲染场景时,应用程序不应假设这一点。ovrEyeRenderDesc 结构中的 HmdToEyePose 成员是一个 ovrPosef 6-DOF 变换。这意味着在具有倾斜显示屏的 HMD 中(即显示屏彼此不平行),渲染相机可能会相对于 HMD 姿势进行旋转和平移。因此,用户面向的方向由 HMD 姿势的 Z 轴决定,而眼睛的姿势可能有指向不同方向的 Z 轴。
实际上,Rift 中的投影通常会略微偏离中心,因为我们的鼻子会挡住一部分视线!但关键在于,Rift 中的左眼和右眼视图是完全分开的,这与电视或电影屏幕产生的立体视图不同。这意味着,如果您尝试使用为这些媒体开发的方法,应非常谨慎,因为这些方法通常不适用于 VR。
场景中的两个虚拟摄像机应该被放置在与由 ovrEyeRenderDesc::HmdToEyePose 指定的眼睛姿态相同的方向上,并且它们之间的距离应该与眼睛之间的距离(即瞳距,IPD)相同。
尽管 Rift 的透镜对于大多数用户来说距离大致正确,但它们可能并不完全匹配用户的 IPD。但由于光学设计的方式,每只眼睛仍然能看到正确的视图。重要的是,软件应确保虚拟相机之间的距离与用户在配置实用程序中设置的个人资料中的 IPD 相匹配,而不是与 Rift 透镜之间的距离相匹配。

渲染设置概述

Oculus SDK 利用合成器进程来呈现帧并处理畸变。
为了针对 Rift 进行渲染,您需要将场景渲染到一个或两个渲染纹理中,并将这些纹理传递给 API。运行时负责畸变渲染、GPU 同步、帧定时以及将帧呈现给 HMD。
以下是 SDK 渲染的步骤:
  1. 初始化:
  2. 初始化 Oculus SDK,并为头戴设备创建一个 ovrSession 对象,如前面所述。
  3. 根据 ovrHmdDesc 数据计算所需的 FOV 和纹理大小。
  4. 以特定于 API 的方式分配 ovrTextureSwapChain 对象,这些对象用于表示眼睛缓冲区:对于 Direct3D 11 或 12,调用 ovr_CreateTextureSwapChainDX;对于 OpenGL,调用 ovr_CreateTextureSwapChainGL;对于 Vulkan,调用 ovr_CreateTextureSwapChainVk
    注意:如果使用 Vulkan,对于 AMD 显卡使用呈现模式 VK_PRESENT_MODE_IMMEDIATE_KHR,对于 nVidia 显卡使用 VK_PRESENT_MODE_MAILBOX_KHR。这可以防止一个问题,即循环在渲染下一帧之前等待主显示器的 vsync,这会导致延迟并降低性能。
  5. 设置帧处理:
  6. 使用 ovr_GetTrackingStateovr_CalcEyePoses 根据帧定时信息计算视图渲染所需的眼姿。
  7. 以引擎特定的方式对每个眼睛进行渲染,将渲染结果输出到纹理集中的当前纹理。使用 ovr_GetTextureSwapChainCurrentIndex 以及 ovr_GetTextureSwapChainBufferDXovr_GetTextureSwapChainBufferGLovr_GetTextureSwapChainBufferVk 来获取当前纹理。向纹理渲染完成后,应用程序必须调用 ovr_CommitTextureSwapChain
  8. 调用 ovr_WaitToBeginFrame,然后当您的应用程序准备好开始渲染帧时,调用 ovr_BeginFrame。当您的应用程序准备好提交帧时,调用 ovr_EndFrame,并将上一步中的交换纹理集传递给 ovrLayerEyeFov 结构。虽然提交一帧需要一个图层,但您可以使用多个图层和图层类型进行高级渲染。ovr_EndFrame 将图层纹理传递给合成器,合成器在处理畸变、时间扭曲和 GPU 同步后,再将其呈现给头戴设备。请注意,ovr_WaitToBeginFrameovr_BeginFrameovr_EndFrame 的组合使您能够在多线程环境中实现性能优化技术,例如通过拆分和重叠多个帧的处理来同时处理它们。在之前的版本中,这三个函数组合成 ovr_SubmitFrame,但现在该调用已弃用;请使用 ovr_WaitToBeginFrameovr_BeginFrameovr_EndFrame 代替。
  9. 关闭:
  10. 调用 ovr_DestroyTextureSwapChain 来销毁交换纹理缓冲区。调用 ovr_DestroyMirrorTexture 来销毁镜像纹理。要销毁 ovrSession 对象,请调用 ovr_Destroy

纹理交换链初始化

本节描述渲染初始化,包括创建纹理交换链。
首先,您需要确定渲染的 FOV,并分配所需的 ovrTextureSwapChain。以下代码展示了如何计算所需的纹理大小:
// 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 );
渲染纹理的大小是根据 FOV 和眼睛中心所需的像素密度来确定的。尽管可以通过修改 FOV 和像素密度值来提高性能,但本示例使用的是推荐的 FOV(从 session->DefaultEyeFov 获得)。函数 ovr_GetFovTextureSize 根据这些参数计算每只眼睛所需的纹理大小。
Oculus API 允许应用程序在眼睛渲染时使用一个共享纹理或两个单独的纹理。为了简化操作,本示例使用了一个共享的纹理,并且该纹理足够大,可以容纳两只眼睛的渲染图像。
如果您使用的是 Vulkan,则在创建纹理交换链之前,需要添加三个步骤。
  • 在初始化期间,调用 ovr_GetSessionPhysicalDeviceVk 以获取与 luid 匹配的当前物理设备。然后,创建一个与返回的物理设备相关联的 VkDevice
  • AMD 硬件在 Vulkan 上使用了不同的扩展。在应用程序初始化期间,添加类似于以下示例的代码来处理 AMD GPU 扩展。此代码示例来自随 Oculus SDK 提供的 OculusRoomTiny_Advanced 示例应用程序中的 Win32_VulkanAppUtil.h 文件。
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
};
  • 然后,在游戏循环建立之后,确定在渲染时要同步哪个队列。调用 ovr_SetSynchonizationQueueVk 来指定该队列。
创建纹理交换链
一旦纹理大小确定,应用程序就可以调用 ovr_CreateTextureSwapChainGLovr_CreateTextureSwapChainDXovr_CreateTextureSwapChainVk,以特定于 API 的方式分配纹理交换链。
以下是在 OpenGL 中创建和访问纹理交换链的方法:
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);
    ...
}
以下是一个使用 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();
     }
 }
以下是来自提供的 OculusRoomTiny 示例(在 Direct3D 12 下运行)的示例代码:
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]);
}
注意:对于 Direct3D 12,在调用 ovr_CreateTextureSwapChainDX 时,调用者向 SDK 提供一个 ID3D12CommandQueue 而不是 ID3D12Device。调用者有责任确保所有 VR 眼睛纹理渲染都在这个 ID3D12CommandQueue 实例上执行。或者,它可以作为一个“连接节点”栅栏,等待由其他渲染 VR 眼睛纹理的命令队列执行的命令列表。
以下是如何使用 Vulkan 创建和访问纹理交换链的方法:
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;
}
在这些纹理和渲染目标成功创建后,便可使用它们来进行眼睛纹理的渲染。“帧渲染”部分更详细地描述了视口设置。
合成器提供 sRGB 校正渲染,从而得到更具照片真实感的视觉效果、更好的 MSAA 以及节能的纹理采样,这些对于 VR 应用来说非常重要。如上所示,应用程序需要创建 sRGB 纹理交换链。正确处理 sRGB 渲染是一个复杂的主题,尽管本节提供了概述,但详尽的信息超出了本文档的范围。
确保实时渲染的应用程序实现 sRGB 正确着色需要几个步骤,并且有多种方法可以实现。例如,大多数 GPU 提供硬件加速,以改善针对 sRGB 特定输入和输出表面的伽马校正着色,而一些应用程序则使用 GPU 着色器数学来进行更定制化的控制。对于Oculus SDK,当应用程序传递 sRGB 空间纹理交换链时,合成器依赖于 GPU 的采样器来执行 sRGB 到线性的转换。
所有输入到 GPU 着色器的颜色纹理都应该用 sRGB 正确的格式进行适当标记,如 OVR_FORMAT_R8G8B8A8_UNORM_SRGB。对于向合成器提供静态纹理作为四层纹理的应用程序,也建议使用此格式。否则,纹理看起来会比预期亮得多。
对于 D3D 11 和 12,在 ovr_CreateTextureSwapChainDX 的 desc 中提供的纹理格式会被扭曲合成器在读取纹理内容时用作 ShaderResourceView。因此,应用程序应该请求处于sRGB空间的纹理交换链格式(例如 OVR_FORMAT_R8G8B8A8_UNORM_SRGB)。
如果您的应用程序配置为渲染到线性格式的纹理(例如 OVR_FORMAT_R8G8B8A8_UNORM),并且使用 HLSL 代码处理线性到伽马的转换,或者不关心任何伽马校正,那么:
  • 请求 sRGB 格式(例如 OVR_FORMAT_R8G8B8A8_UNORM_SRGB)的纹理交换链。
  • 在 desc 中指定 ovrTextureMisc_DX_Typeless 标志。
  • 创建线性格式 RenderTargetView(例如 DXGI_FORMAT_R8G8B8A8_UNORM
注意:对于深度缓冲区格式(例如 OVR_FORMAT_D32),ovrTextureMisc_DX_Typeless 标志会被忽略,因为它们总是被转换为无类型的。
提供的代码示例展示了如何在 D3D11 中使用提供的 ovrTextureMisc_DX_Typeless 标志:
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();
}
对于 OpenGL,ofovr_CreateTextureSwapChainGL 的格式参数在扭曲合成器读取纹理内容时会被使用。因此,应用程序应该优先请求处于 sRGB 空间的纹理交换链格式(例如 OVR_FORMAT_R8G8B8A8_UNORM_SRGB)。此外,您的应用程序应该在向这些纹理渲染之前调用 glEnable(GL_FRAMEBUFFER_SRGB)。
尽管不推荐这样做,但如果您的应用程序配置为将纹理视为线性格式(例如 GL_RGBA),并在 GLSL 中执行线性到伽马的转换,或者不关心伽马校正,那么:
  • 请求 sRGB 格式(例如 OVR_FORMAT_R8G8B8A8_UNORM_SRGB)的纹理交换链。
  • 在向纹理渲染时不要调用 glEnable(GL_FRAMEBUFFER_SRGB)。
对于Vulkan,ovr_CreateTextureSwapChainVk 的格式参数在扭曲合成器读取纹理内容时使用。您的应用程序应该请求处于 sRGB 空间的纹理交换链格式(例如 OVR_FORMAT_R8G8B8A8_UNORM_SRGB),因为合成器会进行 sRGB 正确的渲染。合成器将依赖于 GPU 的硬件采样器来执行 sRGB 到线性的转换。
如果您的应用程序更喜欢渲染到线性格式(例如 OVR_FORMAT_R8G8B8A8_UNORM),同时通过 SPIRV 代码处理线性到伽马的转换,那么应用程序仍然必须请求相应的 sRGB 格式,并且在 ovrTextureSwapChainDesc 的标志字段中使用 ovrTextureMisc_DX_Typeless。这允许应用程序以线性格式创建一个 RenderTargetView,同时允许合成器将其视为 sRGB。否则会导致意外的伽马曲线伪影。对于深度缓冲区格式(例如 OVR_FORMAT_D32_FLOAT),ovrTextureMisc_DX_Typeless 标志会被忽略,因为它们总是被转换为无类型的。
除了 sRGB 之外,这些概念也适用于镜像纹理的创建。有关更多信息,请参阅分别为 D3D、OpenGL 和 Vulkan提供的 ovr_CreateMirrorTextureDXovr_CreateMirrorTextureGLovr_CreateMirrorTextureWithOptionsVk 函数的文档。

帧渲染

帧渲染通常涉及几个步骤:基于头戴设备追踪姿态获得预测的眼球姿态,为每个眼球渲染视图,最后通过 ovr_WaitToBeginFrameovr_BeginFrameovr_EndFrame 将眼球纹理提交给合成器。在帧提交之后,合成器会在将其呈现给头戴设备之前处理畸变矫正、时间扭曲和 GPU 同步。
在渲染帧之前,初始化一些可以在帧之间共享的数据结构是很有帮助的。例如,我们在渲染循环之外查询眼球描述符并初始化图层结构:
 // 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.
此代码示例首先根据所选的 FOV 获取每个眼球的渲染描述符。返回的 ovrEyeRenderDesc结构包含对渲染有用的值,包括每个眼球的 HmdToEyePose。眼视图偏移量稍后会用于调整双眼间距。
该代码还为全屏图层初始化了 ovrLayerEyeFov 结构。从 Oculus SDK 0.6 版本开始,帧提交使用图层将多个视图图像或纹理四边形相互叠加组合在一起。这个示例使用一个图层来呈现一个虚拟现实场景。为此,我们使用 ovrLayerEyeFov,它描述了一个覆盖整个眼睛视场的双眼图层。由于我们为双眼使用相同的纹理集,因此我们将双眼的颜色纹理都初始化为 pTextureSet,并配置视口,以便分别在此共享纹理的左侧和右侧进行绘制。
设置完成后,应用程序可以运行渲染循环。首先,我们需要获取眼睛的姿态来渲染左眼和右眼的视图。
// 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);
在 VR 中,渲染的眼睛视图取决于头戴设备在物理空间中的位置和朝向,这些位置和朝向是通过内部 IMU 和外部传感器的帮助进行追踪的。预测用于补偿系统中的延迟,从而为头戴设备显示帧时头戴设备可能所在的位置提供最佳估计。在 Oculus SDK 中,这个经过追踪和预测的姿势是由 ovr_GetTrackingState 函数报告的。
在 Oculus SDK 中,这个经过追踪和预测的姿势是由 ovr_GetTrackingState 函数报告的。上面的代码调用 ovr_GetPredictedDisplayTime 来获取当前帧的 displayMidpointSeconds,并使用它来计算最佳的预测追踪状态。然后,将追踪状态中的头部姿态传递给 ovr_CalcEyePoses,以计算每只眼睛的正确视图姿态。这些姿势直接存储到 layer.RenderPose[2] 阵列中。准备好眼睛的姿态后,我们就可以开始进行实际的帧渲染了。
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);
此代码通过多个步骤来渲染场景:
  • 它将纹理设置为渲染目标,并清除它以进行渲染。在这种情况下,双眼使用相同的纹理。
  • 然后,代码计算视图和投影矩阵,并为每只眼睛设置视口场景渲染。在此示例中,视图计算将原始姿态(originPosoriginRot 值)与基于追踪状态计算并存储在层中的新姿态相结合。这些原始值可以通过输入进行修改,以便在 3D 世界中移动玩家。
  • 在纹理渲染完成后,我们调用 ovr_EndFrame 来将帧数据传递给合成器。从此时开始,合成器通过访问共享内存中的纹理数据来接管,对其进行扭曲处理,并将其呈现在 Rift 上。
ovr_EndFrame 在提交的帧排队并且运行时能够接受新帧时返回。当成功时,它的返回值是 ovrSuccessovrSuccess_NotVisible
如果帧实际上未显示,则会返回 ovrSuccess_NotVisible,这可能会在 VR 应用程序失去焦点时发生。我们的示例代码通过更新由渲染逻辑检查的 isVisible 标志来处理这种情况。当帧不可见时,应暂停渲染以消除不必要的 GPU 负载。
如果您收到 ovrError_DisplayLost 错误,说明设备已移除,会话无效。释放共享资源 (ovr_DestroyTextureSwapChain),销毁会话 (ovr_Destroy),重新创建会话 (ovr_Create),并创建新的资源 (ovr_CreateTextureSwapChainXXX)。除非新的 ovr_Create 调用返回不同的 GraphicsLuid,否则应用程序现有的私有图形资源不需要重新创建。

帧时序

Oculus SDK 通过 ovr_GetPredictedDisplayTime 函数报告帧时序信息,它依赖于应用程序提供的帧索引来确保在不同线程之间报告正确的时序。
为了准确预测头部运动,需要精确的帧和传感器时序,这对于获得良好的 VR 体验至关重要。预测需要确切知道当前帧将在未来何时出现在屏幕上。如果我们知道传感器和显示屏的扫描输出时间,就可以预测未来的头部位置,从而提高图像的稳定性。如果计算这些值不正确,可能会导致预测不足或过度预测,从而降低感知到的延迟,并可能引发“晃动”的过冲现象。
为了确保精确的时序,Oculus SDK 使用绝对系统时间(以双精度浮点数存储)来表示传感器和帧的时序值。当前的绝对时间由 ovr_GetTimeInSeconds 返回。然而,很少应该使用当前时间,因为当依赖于 ovr_GetPredictedDisplayTime 返回的时序值时,模拟和运动预测将产生更好的结果。此函数的签名如下:
ovr_GetPredictedDisplayTime(ovrSession session, long long frameIndex);
frameIndex 参数指定了我们正在渲染的是哪一个应用程序帧。使用多线程渲染的应用程序必须保持一个内部帧索引,并手动递增它,同时将其与帧数据一起在线程之间传递,以确保正确的时序和预测。用于获取帧值时序的同一个 frameIndex 必须传递给 ovr_WaitToBeginFrameovr_BeginFrameovr_EndFrame。多线程时序的详细信息将在下一节在不同线程上渲染中介绍。
在这两个函数中,可以使用一个特殊的 frameIndex 值 0,来请求 SDK 自动跟踪帧索引。但是,这仅在所有帧时序请求和渲染提交都在同一个线程上完成时才有效。

在不同线程上渲染

在一些引擎中,渲染处理是分布在多个线程上进行的。
例如,一个线程可能对场景中的每个对象执行剔除和渲染设置(我们称之为“主”线程),而第二个线程则发出实际的 D3D 或 OpenGL API 调用(我们称之为“渲染”线程)。这两个线程都可能需要帧显示时间的准确估计,以便计算出头部位姿的最佳可能预测。
这种方法的异步性质使其具有挑战性:当渲染线程正在渲染一帧时,主线程可能正在处理下一帧。这种并行帧处理可能会根据游戏引擎的设计而有一帧或一帧以内的不同步。如果我们使用默认的全局状态来访问帧时序,则 ovr_GetPredictedDisplayTime 的结果可能会根据调用该函数的线程而相差一帧,或者更糟糕的是,由于线程调度的不同,结果可能会随机出现错误。为了解决这个问题,前面的部分介绍了由应用程序跟踪并随帧数据一起在线程间传递的 frameIndex 的概念。
为了确保多线程渲染结果的正确性,必须满足以下条件:(a) 基于帧时序计算得出的姿态预测对于同一帧而言必须保持一致,无论它是从哪个线程访问的;(b) 实际用于渲染的眼部姿态必须连同帧索引一起传递给 ovr_EndFrame。(这必须在调用 ovr_WaitToBeginFrameovr_BeginFrame 之后进行。)
以下是您可以采取以确保这种情况发生的步骤的总结:
  1. 主线程需要为当前正在处理以进行渲染的帧分配一个帧索引。它会在每帧递增这个索引,并将其传递给 ovr_GetPredictedDisplayTime 以获得正确的姿态预测时间。
  2. 主线程应该使用预测的时间值调用线程安全函数 ovr_GetTrackingState。如果渲染设置需要,它还可以调用 ovr_CalcEyePoses
  3. 主线程需要将当前帧索引和眼部姿态传递给渲染线程,以及它需要的任何渲染命令或帧数据。
  4. 当在渲染线程上执行渲染命令时,开发者需要确保以下几点:
  5. 用于帧渲染的实际姿态存储到该层的 RenderPose 中。
  6. 与主线程上所用相同的 frameIndex 值传递给 ovr_BeginFrameovr_EndFrame
以下代码对此进行了更详细的说明:
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);
}
Oculus SDK 还支持 Direct3D 12,它允许从多个 CPU 线程向 GPU 提交渲染工作。当应用程序调用 ovr_CreateTextureSwapChainDX 时,Oculus SDK 会缓存调用者提供的 ID3D12CommandQueue 以供将来使用。当应用程序调用 ovr_EndFrame 时,SDK 会在缓存的 ID3D12CommandQueue 上放置一个栅栏,以便确切地知道给定的一组眼部纹理何时准备好供 SDK 合成器使用。
对于给定的应用程序,在单个线程上使用单个 ID3D12CommandQueue 是最简单的。但是,它也可能将每个眼部纹理对的 CPU 渲染工作负载分开,或者将非眼部纹理的渲染工作(如阴影、反射贴图等)推送到不同的命令队列上。如果应用程序从多个线程填充和执行命令列表,那么它还需要确保提供给 SDK 的 ID3D12CommandQueue 是通过不同命令队列执行的眼部纹理渲染工作的单个连接节点。

图层

类似于监视器视图可以由多个窗口组成的方式,头戴设备上的显示也可以由多个图层组成。通常,这些图层中至少有一个是从用户的虚拟眼球渲染的视图,但其他图层可能是 HUD 图层、立方体映射图层、信息面板、附加到世界中物品上的文本标签、瞄准准星等。
每个图层可以有不同的分辨率,可以使用不同的纹理格式,可以使用不同的视场或大小,并且可以是单视场或立体视场。如果图层中的信息没有变化,应用程序也可以配置为不更新该图层的纹理。例如,如果信息面板中的文本自上一帧以来没有变化,或者如果图层是低帧率视频流的画中画视图,那么它可能就不会更新。应用程序可以向图层提供多级渐变纹理,并且配合高质量失真模式,这可以非常有效地提高文本面板的可读性。
每一帧,所有活动图层都会使用预乘 alpha 混合从后向前进行合成。图层 0 是最底层,图层 1 在其上方,以此类推。
图层的一个强大功能是每个图层都可以有不同的分辨率。这允许应用程序通过降低显示虚拟世界的主视觉缓冲区渲染的分辨率来适应性能较低的系统,同时在不同图层中以更高分辨率保留关键信息,如文本或地图。
有以下几种图层类型可供选择:
图层类型描述
EyeFov
之前 SDK 中常见的标准“视觉缓冲区”,这通常是从用户眼睛的位置渲染的虚拟场景的立体视图。尽管视觉缓冲区可以是单视场,但这可能会引起不适。之前 SDK 有隐式的视场 (FOV) 和视口;而现在这些参数是明确提供的,并且如果应用程序需要,可以在每一帧中更改它们。
四边形
一个单眼图像,它在虚拟世界中以给定的位置和大小作为矩形显示。这对于平视显示器、文本信息、对象标签等非常有用。默认情况下,位置是相对于用户的真实世界空间来指定的,并且四边形将在空间中保持固定,而不是随着用户的头部或身体运动而移动。对于头部锁定四边形,请使用下面描述的 ovrLayerFlag_HeadLocked 标志。
Cubemap
一个立方体贴图由六个矩形组成。这些矩形放置在用户的周围,就好像用户坐在一个立方体形状的房间里面一样。每一面墙都是应用程序提交的一个纹理。立方体贴图看起来像是在无限远的距离,它实质上是应用程序渲染的所有其他对象背后的背景。用户看到的立方体贴图并不像一个立方体。而仅仅是看起来像是处于无限远处的背景。例如,可以使用立方体贴图来创建在 VR 体验中将会出现在所有建筑后面的天空。不需要处理前景物体造成的遮挡。只需设置好立方体贴图,它就会在您场景中的任何地方作为背景出现。
EyeMatrix
EyeMatrix 图层类型与 EyeFov 图层类型相似,用于帮助与旧版 Samsung Gear VR 应用程序的兼容性。
圆柱形
可以使用圆柱形图层来创建弯曲的四边形,而不是平坦的四边形。ovrLayerCylinder 描述了一个类型为 ovrLayerType_Cylinder 的图层,它是一个相对于重新定位的原点(在下面的图示中用 C 表示)进行定位的单个圆柱体。请参阅下面的圆柱形图层参数
深度缓冲区
此图层指定一个单色视图或立体视图,除了颜色纹理外,还包括深度纹理。此图层由 ovrLayerEyeFovDepth 结构体实现。它相当于 ovrLayerEyeFov,但额外添加了 DepthTexture 和 ProjectionDesc。深度缓冲区通常用于支持位置时间扭曲。
禁用
被合成器忽略的禁用图层不会影响性能。建议应用程序执行基本的视锥体裁剪,并禁用不在视图范围内的图层。但在关闭一个图层时,应用程序无需将活动图层的列表紧密地重新打包在一起;只需禁用该图层并将其留在列表中即可。换句话说,也可以将列表中指向该图层的指针设为空值。
圆柱形图层参数
这种图层类型代表放置在场景中的一个单独对象,而不是场景本身的立体视图。只有圆柱体的内表面是可见的。仅当用户不能离开圆柱体的范围时,才应该使用圆柱图层。在查看圆柱体外表面时可能会出现伪像。此外,虽然接口支持的角度 (A) 范围是 0 到 2Pi,但为了避免圆柱体边缘交汇处出现伪像,角度应始终小于 1.9PI。有关使用此功能的更多信息,请参阅参考文档中的 ovrLayerCylinder

图层参数

每种图层样式都有一个对应的 ovrLayerType 枚举成员,以及一个包含显示该图层所需数据的相关结构。例如,EyeFov 图层是类型为 ovrLayerType_EyeFov 的图层,并由 ovrLayerEyeFov 结构中的数据描述。这些结构共享一组类似的参数,尽管并非所有图层类型都需要所有参数:
参数类型描述
Header.Type
枚举 ovrLayerType
所有图层都必须设置此参数,以指定它们的类型。
Header.Flags
ovrLayerFlags 的位字段
参阅下文了解详细信息。
ColorTexture
TextureSwapChain
为图层提供颜色和透明度数据。图层使用预乘 alpha 值相互融合。这允许它们实现线性插值 (lerp) 风格的混合、加法混合,或者这两种混合的组合。图层纹理必须采用 RGBA 或 BGRA 格式,可能包含多级渐远纹理,但不能是阵列、立方体,也不能具有 MSAA。如果应用程序希望进行 MSAA 渲染,那么它必须将中间 MSAA 颜色纹理解析到图层的非 MSAA 颜色纹理中。
Viewport
ovrRecti
实际使用的纹理矩形,由 ovrRecti 结构指定,该结构以两个二维向量提供矩形的大小和位置,分别包含宽度/高度和 x/y 的整数。提供的 x/y 位置值代表矩形左下角的顶点位置。理论上,此区域之外的纹理数据在图层中是不可见的。然而,关于纹理采样的通常注意事项仍然适用,特别是对于使用多级渐远纹理的情况。在显示区域的周围留下一圈 RGBA(0,0,0,0) 像素的边框是一个好习惯,这可以避免“渗色”,特别是在将两个视觉缓冲区并排打包到同一个纹理中时。边框的大小取决于具体的使用情况,但在大多数情况下,大约 8 个像素的边框似乎效果良好。
Fov
ovrFovPort
用于在眼睛图层类型中渲染场景的视场。请注意,这并不会控制 HMD 的显示,它只是告诉合成器在该图层中渲染纹理数据时使用了什么 FOV,然后合成器会根据用户的实际 FOV 进行相应的调整。应用程序可以动态更改 FOV,以实现特殊效果。减小 FOV 也有助于在较慢的机器上提高性能,但通常而言,在减小 FOV 之前降低分辨率会更为有效。
RenderPose
ovrPosef
应用程序在眼睛图层类型中渲染场景时所使用的相机位置和方向。这通常由 SDK 和应用程序使用 ovr_GetTrackingStateovr_CalcEyePoses 函数来预测。合成器会利用这个预测位置与显示时眼睛实际位置之间的差异,来对图层应用时间扭曲。
SensorSampleTime
双精度
应用程序采样跟踪状态的绝对时间。获取此值的典型方法是在 ovr_GetTrackingState 调用旁边紧接着调用 ovr_GetTimeInSeconds。SDK 使用此值在性能 HUD 中报告应用程序的运动到光子延迟。如果应用程序在任何给定帧中提交了多个 ovrLayerType_EyeFov 图层,SDK 会遍历这些图层,并选择延迟最小的时间。在给定帧中,如果没有提交 ovrLayerType_EyeFov 图层,SDK 将使用调用 ovr_GetTrackingState 时设置 latencyMarkerovrTrue 的时间点,作为应用程序运动到光子延迟时间的替代值。
QuadPoseCenter
ovrPosef
指定四边形图层类型中心点的方向和位置。提供的方向是与四边形垂直的向量。位置是以现实世界中的米为单位(不是应用程序的虚拟世界,而是用户所处的实际世界),并且相对于通过 ovr_RecenterTrackingOriginovr_SpecifyTrackingOrigin 设置的“零”位置,除非使用了 ovrLayerFlag_HeadLocked 标志。
QuadSize
ovrVector2f
指定四边形图层类型的宽度和高度。与位置一样,这也是以现实世界中的米为单位。
接受立体信息的图层(除四边形图层类型以外的所有图层)需要两组大多数参数,这些参数可以以三种不同的方式使用:
  • 立体数据,单独的纹理——应用程序为左眼和右眼分别提供一个不同的 ovrTextureSwapChain,并为每个眼睛提供一个视口。
  • 立体数据,共享纹理——应用程序为左眼和右眼提供相同的 ovrTextureSwapChain,但为每个眼睛提供一个不同的视口。这允许应用程序将左眼和右眼的视图都渲染到同一个纹理缓冲区中。如上所述,请记得在两个视图之间添加一个小的缓冲区,以防止“串色”。
  • 单色数据——应用程序为左眼和右眼提供相同的 ovrTextureSwapChain,并且为每个眼睛提供相同的视口。
左眼和右眼的纹理和视口大小可能不同,甚至每个眼睛都可以有不同的视场。但是要注意避免给用户带来立体视差和不适感。
所有图层可用的 Header.Flags 字段是以下内容的“逻辑或”(logical-or):
  • ovrLayerFlag_HighQuality — 为此图层的合成器启用 4 倍异向性采样。这可以显著提升清晰度,特别是当与包含多级渐近纹理的纹理一起使用时;对于文本或图表等高频图像以及在使用四边形图层类型时,建议采用此方式。对于眼球图层类型,它还可以提高边缘的视觉保真度,或者在输入像素密度超过推荐的 1:1 比例的纹理时提高视觉保真度。为了获得最佳效果,在为与特定图层相关联的纹理创建多级渐近纹理时,请确保纹理尺寸是 2 的幂。但应用程序无需对整个纹理进行渲染;一个对纹理中推荐尺寸进行渲染的视口将提供最佳的性能与质量比。
  • ovrLayerFlag_TextureOriginAtBottomLeft — 图层的纹理原点假定为左上角。然而,一些引擎(特别是使用 OpenGL 的引擎)更喜欢使用左下角作为原点,并且它们应该使用此标志。
  • ovrLayerFlag_HeadLocked — 大多数图层类型都会通过调用 ovr_RecenterTrackingOrigin 定义的“零位”来指定其姿态方向和位置。但应用程序可能希望指定一个图层相对于用户面部的姿态。当用户移动头部时,图层会随之移动。这对于基于注视的瞄准或选择中使用的准星很有用。此标志可用于所有图层类型,但在“直接”类型上使用时无效果。
在每帧结束时,应用程序在希望更新的 ovrTextureSwapChain 上进行渲染并调用 ovr_CommitTextureSwapChain 后,每个图层的数据会放入相关的 ovrLayerEyeFovovrLayerQuadovrLayerDirect 结构中。然后,应用程序会创建一个指向这些图层结构(特别是指向每个结构中保证为第一个成员的标头字段)的指针列表。然后,应用程序使用所需的数据构建一个 ovrViewScaleDesc 结构,并调用 ovr_WaitToBeginFrameovr_BeginFrameovr_EndFrame 函数。
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);
合成器在将这些图层混合在一起之前,会分别对每个图层进行时间扭曲、畸变校正和色差校正。将四边形渲染到眼缓冲区的传统方法涉及两个过滤步骤(一次到视觉缓冲区,一次在畸变期间)。使用图层时,图层图像和最终帧缓冲区之间只有一个筛选步骤。这可以显著提高文本质量,特别是当与 Mip 贴图和 ovrLayerFlag_HighQuality 标志结合使用时。
图层当前的一个缺点是,无法对最终合成的图像执行后处理,例如柔焦效果、光晕效果或图层数据的 Z 轴交叉。其中一些效果可以在图层的内容上实现,以达到类似的视觉效果。
调用 ovr_EndFrame 会将图层排队以待显示,并将 ovrTextureSwapChains 中已提交的纹理的控制权转移给合成器。重要的是要理解,这些纹理是在应用程序线程和合成器线程之间共享的(而不是复制的),并且合成不一定在调用 ovr_EndFrame 时发生,因此必须小心处理。要继续向纹理交换链中渲染,应用程序在渲染之前应始终使用 ovr_GetTextureSwapChainCurrentIndex 获取下一个可用索引。例如:
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);
矩形线性捕捉
此功能可让您的应用程序以每秒 90 帧的速度获取单个未失真图像的截图,该图像对应于用户在 VR 体验中所看到的内容。您可能会使用此功能将 VR 体验镜像到外部显示器上。

使用 HMD 眼睛姿态

在 Oculus PC SDK 的 1.17 版本之前,眼睛姿态只有三个自由度 (DOF),即仅有平移。眼睛姿态是通过 ovr_GetRenderDesc 函数提供的 HmdToEyeOffset 向量来指定的。从 1.17 版本开始,HmdToEyeOffset 已重命名为 HmdToEyePose,并使用 ovrPosef 类型,其中包含位置方向,从而有效地为眼睛姿态提供六个自由度。这意味着除了 SDK 进行平移之外,每只眼睛的渲染视锥现在可以相对于 HMD 的方向进行旋转。因此,眼睛视锥体的轴不再保证彼此平行或与 HMD 的方向轴平行。这种泛化使得 SDK 在定义 HMD 几何形状方面具有更大的自由度。但这也意味着,作为 VR 应用开发者,需要更加小心您之前的假设,尤其是在渲染方面。
以下是一些确保您的 VR 应用正确使用 HmdToEyePose 的要点:
  • 如果您的VR应用需要(1.17 版本之前的)HmdToEyeOffset 的向量平移值,可以改用 HmdToEyePose.Position。不过,除非您完全确定自己在做什么,否则很有可能您实际上想要将 HmdToEyePose 作为一个整体的变换来处理,而不是将方向与位置分开。
  • 在 PC-SDK 1.17 版本之前,在屏幕上渲染一个二维平面四边形(例如矩形)一直是可行的。但是,由于每个眼睛的视锥体都可以独立旋转,您的 VR 应用需要将每个眼睛的方向纳入四边形的变换中,以便在每个眼睛中以正确的透视效果渲染四边形。以下是一个(有些夸张)的示例:
这个想法是让四边形定向,使其看起来要么使用“中心眼”方向或 HMD 方向。这也适用于其他与屏幕对齐的四边形,如 3D 启动画面,或粒子效果,如大型翻页书式烟雾四边形。除了粒子之外,避免直接渲染这样的四边形;最好使用 ovrLayerQuad 来代替。
  • 一些 VR 应用会从两只眼睛的 ovrFovPort 结构中生成一个单一的单眼相机视锥体,以便利用各种渲染优化。这通常是通过使用一个 ovrFovPort 来完成的,它取两只眼睛在视锥体四个边上的 ovrFovPort 值的最大值。但在通过这种方式生成单眼视锥体之前,请确保通过调用位于 ovr_math.h 标头中的 FovPort::Uncant 函数,从 ovrFovPort 值中移除任何潜在的旋转。请参阅 OculusWorldDemo 示例代码,了解如何使用 FovPort::Uncant

Asynchronous TimeWarp

异步时间扭曲 (ATW) 是一种减少 VR 应用和体验中延迟和卡顿的技术。
在一个基本的 VR 游戏循环中,会发生以下情况:
  1. 软件会请求您的头部位置。
  2. CPU 为每只眼睛处理场景。
  3. GPU 渲染这些场景。
  4. 合成器会应用畸变并将场景显示在头戴设备上。
以下是一个游戏循环的基本示例:
当帧率保持稳定时,体验会感觉真实且令人愉悦。如果这种情况没有及时发生,就会显示上一帧,这可能会让人感到迷惑。下图展示了基本游戏循环中卡顿的一个示例:
当您移动头部而画面没有跟上时,这可能会让人感到突兀,从而打破沉浸感。
ATW 是一种通过轻微移动渲染图像以调整头部运动变化的技术。尽管图像修改了,但您的头部并没有移动太多,所以这种变化是细微的。
此外,为了缓解用户电脑、游戏设计或操作系统方面的问题,ATW 可以帮助解决“坑洼”或帧率意外下降的情况。
下图展示了应用 ATW 时帧率下降的一个示例:
在刷新间隔期间,合成器会对最后一帧渲染的图像应用时间扭曲。因此,无论帧率如何,用户始终会看到应用了时间扭曲的帧。如果帧率极低,在屏幕边缘就会观察到明显的闪烁。但图像仍然稳定。
合成器会自动应用异步时间扭曲 (ATW),您无需启用或调整它。然而,尽管 ATW 能够减少延迟,但请确保您的应用或体验能够达到一定的帧率。

自适应前置队列

为了提高 CPU 和 GPU 的并行度,并增加 GPU 处理一帧所需的时间,SDK 会自动应用最多提前一帧的队列技术。
如果不使用前置队列技术,则在前一帧显示完之后,CPU 会立即开始处理下一帧。CPU 处理完毕后,GPU 会对帧进行处理,然后合成器会应用畸变校正,最终将帧显示给用户。下图展示了不使用前置队列时的 CPU 和 GPU 利用率:
如果 GPU 无法及时处理帧以供显示,就会显示上一帧。这会导致画面卡顿。
使用前置队列技术后,CPU 可以提前开始工作,从而为 GPU 提供更多处理帧的时间。下图展示了使用前置队列技术时的 CPU 和 GPU 利用率: