渲染到Oculus Rift

来源:互联网 发布:mysql备份 编辑:程序博客网 时间:2024/05/16 09:10

Rift属于分屏立体渲染,通过透镜若要观看到正常自然的立体图像,就需要为每只眼睛做畸变校正处理以消除镜头相关的畸变。

这里以OculusWorldDemo应用程序为例进行讲解。

立体渲染:

畸变校正的畸变参数因镜头类型以及个人眼睛的接受能力不同而不同,可知这是个颇有挑战性的事情。为了让开发者更容易处理畸变校正,Oculus SDK在Oculus图像混合阶段自动处理畸变校正;在此阶段还会处理降低延迟的时间扭曲以及将混合后的帧渲染到Rift中去。

由于SDK做了大量的工作,因此应用程序主要工作就是使用追踪姿态数据来处理仿真以及渲染3D世界。3D视野可渲染进一个或者两个独立的纹理中并调用ovr_SubmitFrame函数将它们提交到混合器进行混合。在本章会重点讨论该过程。


渲染到Rift:

Rift要求以分屏方式渲染场景,每只眼睛使用屏幕的一半。

使用Rift时,左眼看到屏幕的左半部分,右眼看到右半部分。尽管因人而异,但是人类瞳距大约都为65mm,也即是所谓的IPD。游戏内部的摄像头距离应该与瞳距配置成一样的才行。

备注:

这是属于摄像头的位移,而不是旋转,就是这种位移(还伴随着视觉差)让应用程序内场景产生立体感。这也意味着应用程序需要渲染整个场景两次,一次对左半部虚拟摄像头的渲染,一次对右半部的渲染。

Rift中的镜头放大图像,提供一个非常广的视场角(FOV)以增强沉浸感。然而,这种处理也非常明显地扭曲了图像。若该引擎在Rift上只是显示原始的图像,那么用户将会看到一个枕形的畸变图像。

枕形和桶形畸变:


为了抵消这种畸变,SDK对渲染好的视野使用一个大小相等,方向相反的桶形畸变进行了后处理操作,让两个畸变相互抵消,最后呈现在每只眼睛面前就是正常的图像了。第二点,SDK还会校正色差。色差是在镜头边沿由镜头引起的一种颜色分离的效果。虽然准确的畸变参数取决于镜头属性以及眼睛相对于镜头的位置,但是SDK在生成畸变网格时会进行所有的计算。

在给Rift渲染时,投射轴线应该相互平行,正如接下来的流程所表明的,左右视野是完全相互独立的。这也意味着除了摄像头需要位移到一边用来调整每只眼睛的位置之外,它的配置使用与正常的非立体渲染是类似的。


HMD眼部视锥:



实际上,在Rift中的投影常常是有轻微的偏离中心点的,这是鼻子挡着了。但重点是,Rift中左右眼视野是完全相互独立的,它不像TV或电影屏幕所生成的立体视野。因此,这就意味着TV或电影院那种方式生成的立体视频是不能在VR中使用的。

场景中两个虚拟摄像头应该定位好并指向相同的方向(该方向取决于现实世界中HMD的方向),并且它们之间的距离也是要和双眼之间的距离(IPD)一样。这可直接通过添加ovrEyeRenderDesc::HmdToEyeOffset位移向量到视野矩阵的位移组件中来完成。

尽管Rift镜头可能与用户的瞳距匹配,但实际上真正要匹配用户瞳距的是程序中的两个虚拟摄像头之间的距离,可通过配置工具设置这个距离。


渲染流程:

SDK利用混合进程来渲染帧和处理畸变。

连接Rift,渲染场景到一个或两个渲染纹理中,将这些纹理传到API中。Oculus Runtime处理畸变渲染,GPU同步,帧时序以及渲染帧到HMD中去。

以下步骤为SDK渲染流程:

1.Initialize(初始化):

a.初始化SDK和给头盔创建一个ovrSession对象

b.根据ovrHmdDesc计算期望的FOV以及纹理大小

c.申请ovrTextureSwapChain对象,用来代表眼部缓冲。对应的API为:ovr_CreateTextureSwapChainDX(Direct3D 11或12) 或 ovr_CreateTextureSwapChainGL(OpenGL)

2.帧处理配置:

a.根据帧时序信息,调用函数ovr_GetTrackingState和ovr_CalcEyePoses计算眼睛姿态(为视野渲染做准备)

b.渲染纹理集中当前的纹理时,用引擎自己的方式为每只眼睛做渲染。可使用ovr_GetTextureSwapChainCurrentIndex()和ovrGetTextureSwapChainBufferDX()或ovr_GetTextureSwapChainBufferGL()获取纹理。渲染纹理结束后,应用程序调用ovr_CommitTextureSwapChain()提交纹理交换链。

c.调用ovr_SubmitFrame,将前面步骤重点交换纹理集传入到ovrLayerEyeFov结构体中。虽然单层就要求提交一帧,但是可以使用多层以及多种层类型做高级渲染。ovr_SubmitFrame将层纹理传入到混合器,混合器在呈现到头盔前,还会处理畸变,时间扭曲,以及GPU同步。

3.shutdown(结束)

a.调用ovr_DestroyTextureSwapChain销毁交换纹理缓冲区。调用ovr_DestroyMirrorTexture销毁镜像纹理。调用ovr_Destroy销毁ovrSession。


纹理交换链初始化:

本节说的是渲染初始化,包括纹理交换链的创建。

初始化,自己决定渲染FOV以及申请ovrTextureSwapChain。以下代码片段显示怎样计算所申请的纹理大小:

        // Configure Stereo settings. Default pixel density is 1.0f.        Sizei recommenedTex0Size = ovr_GetFovTextureSize(Hmd, ovrEye_Left, g_EyeFov[0], DesiredPixelDensity);        Sizei recommenedTex1Size = ovr_GetFovTextureSize(Hmd, ovrEye_Right, g_EyeFov[1], DesiredPixelDensity);Sizei  rtSize(recommenedTex0Size.w + recommenedTex1Size.w,                Alg::Max(recommenedTex0Size.h, recommenedTex1Size.h));


渲染纹理大小取决于FOV以及眼睛中心期望的像素密度。虽然可以通过修改FOV和像素密度来改善性能,但是该实例使用推荐的FOV值(来自于HmdDesc.DefaultEyeFov)。函数ovr_GetFovTextureSize根据这些参数为每只眼睛计算期望的纹理大小

Oculus API允许应用程序使用一个共享的纹理或两个独立的纹理为眼睛做渲染。该实例只是简单地使用了一个共享纹理,并让它足够大以满足两只眼的渲染。一旦纹理大小确定了,应用程序就会调用ovr_CreateTextureSwapChainGL或ovr_CreateTextureSwapChainDX去申请纹理交换链。以下显示在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);    ...}


以下为D3D 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的D3D 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 appovrResult 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]);}


备注:

针对D3D 12来说,当调用ovr_CreateTextureSwapChainDX时,调用者提供一个ID3D12CommandQueue参数而不是ID3D12Device参数给到SDK。调用者要确保该ID3D12CommandQueue实例是所有VR眼睛纹理执行渲染的地方。或者,它可被作为一个连接节点的栅栏来等待命令列表,该命令列表由其他用来渲染VR眼睛纹理的命令队列执行。

一旦纹理和渲染目标创建成功,可使用它们来做眼睛纹理渲染。帧渲染章节详细地描述了视口配置。

Oculus混合器提供了sRGB色彩空间的渲染,可产生逼真的视觉效果,更好的MSAA(多重采样抗锯齿)以及节能的纹理采样。

由几个步骤确保实时渲染应用程序实现sRGB颜色空间着色以及不同的方式实现它。比如,大多数GPU为sRGB输入和输出界面提供硬件加速以改善gamma着色,同时一些应用程序为更多的客制化控制使用GPU数学着色器。对SDK而言,当一个应用程序传入到sRGB纹理交换链时,混合器依赖GPU的采样来做sRGB到线性的转换。

所有喂给GPU着色器的颜色纹理应该的使用合适的sRGB格式标记好,比如OVR_FORMAT_R8G8B8A8_UNORM_SRGB。这也推荐应用程序以四层纹理方式给Oculus混合器提供静态纹理。若没有这么做这些动作,将会使纹理看起来比预期更加明亮。

对D3D 11和12而言,当读取纹理内容时,畸变混合器的ShaderResourceView使用ovr_CreateTextureSwapChainDX由参数desc所提供的纹理格式。因此,应用程序应该请求sRGB颜色空间(比如:OVR_FORMAT_R8G8B8A8_UNORM_SRGB)的纹理交换链的格式


若自己的应用程序配置为渲染进一个线性格式的纹理中(比如:OVR_FORMAT_R8G8B8A8_UNORM_SRGB)以及使用HSLS代码来处理线性到gamma的转换,或者不关心任何gamma校正,那么:

1.请求一个sRGB格式(比如:OVR_FORMAT_R8G8B8A8_UNORM_SRGB)的纹理交换链。

2.在desc中指定ovrTextureMisc_DX_Typeless标志

3.创建一个线性格式的渲染目标视野(比如:OVR_FORMAT_R8G8B8A8_UNORM_SRGB)


备注:对深度缓冲格式而言,ovrTextureMisc_DX_Typeless标志是可以忽略的。


对OpenGL而言,当读取纹理内容时,畸变混合器使用的ovr_CreateTextureSwapChainGL的格式参数。因此,应用程序应该在sRGB空间更好地请求纹理交换链格式(比如:OVR_FORMAT_R8G8B8A8_UNORM_SRGB),在渲染进这些纹理之前,应该调用glEnable(GL_FRAMEBUFFER_SRGB)。


若应用程序打算将纹理作为一个线性格式(比如:GL_RGBA)并且在GLSL中处理线性到gamma的转换或者不关心gamma校正,那么:

1.请求一个sRGB格式(比如:OVR_FORMAT_R8G8B8A8_UNORM_SRGB)的纹理交换链。

2.当渲染进纹理时,不调用dlEnable(GL_FRAMEBUFFER_SRGB)。


以下代码片段显示在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();}


除sRGB之外,这些概念同样适用于镜像纹理的创建。


帧渲染:

帧渲染有以下几步:

1.根据头盔追踪姿态获取眼睛姿态

2.为每只眼睛渲染视野

3.通过ovr_SubmitFrame提交眼睛纹理到混合器

在帧提交后,混合器处理畸变以及将帧呈现到Rift上。

在渲染帧之前,初始化一些可在整个帧中共享使用的数据结构。比如,在渲染循环外查询眼睛描述符以及初始化层结构:

// Initialize VR structures, filling out description.ovrEyeRenderDesc eyeRenderDesc[2];ovrVector3f      hmdToEyeViewOffset[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]);hmdToEyeViewOffset[0] = eyeRenderDesc[0].HmdToEyeOffset;hmdToEyeViewOffset[1] = eyeRenderDesc[1].HmdToEyeOffset;// 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结构体包含在渲染时需要用到的参数值,包括每只眼的HmdToEyeOffset值,它在随后用来调整眼睛的分离。

代码还为全屏层初始化ovrLayerEyeFov结构体。从SDK 0.6开始,帧提交使用层来混合多个视野图像或纹理四边形(在彼此顶部)。该实例使用单个层来呈现一个VR场景。因此,在这就使用了ovrLayerEyeFov,它可以描述一个双眼层,该双眼层能够覆盖整个眼睛视场角(FOV)。由于双眼都使用了同样的纹理集,我们初始化双眼的颜色纹理给到pTextureSet并且配置视口来将它们各自画到该共享纹理的左边和右边。

备注:尽管在一开始通常一次就足够初始化好视口,由于把它们指定为被提交的每一帧层结构的一部分这件事,若需要的话,还是会允许应用程序动态地去改变渲染目标的大小。这对优化渲染性能很有帮助。


所有配置完成之后,应用程序允许渲染循环。首先,需要获取眼睛姿态以渲染左右视野。

// Get both eye poses simultaneously, with IPD offset already included. double displayMidpointSeconds = GetPredictedDisplayTime(session, 0);ovrTrackingState hmdState = ovr_GetTrackingState(session, displayMidpointSeconds, ovrTrue);ovr_CalcEyePoses(hmdState.HeadPose.ThePose, hmdToEyeViewOffset, layer.RenderPose);

在VR中,是根据物理空间中的头盔位置和方向渲染眼睛视野的,以内部IMU和外部传感器为辅助进行跟踪。预测用来补偿系统中的延迟,当帧显示在头盔中时对头盔将会在哪儿给出最佳的评估。在SDK中,由函数ovr_GetTrackingState报告这个跟踪的预测的姿态。

为了做出准确的预测,ovr_GetTrackingState需要知道当前帧实际上在什么时候会显示。以上代码调用GetPredictedDisplayTime 获取当前帧的displayMidpointSeconds,用它来计算最佳预测的追踪状态。来自追踪状态的头部姿态随后传到ovr_CalcEyePoses,用来计算每只眼的正确的视野姿态。这些姿态直接存储进layer.RenderPose[2]数组中。在每只眼的姿态准备好之后,我们就可以实际的帧渲染了。

if (isVisible){    // Get next available index of the texture swap chainint currentIndex = 0;    ovr_GetTextureSwapChainCurrentIndex(session, textureSwapChain, ¤tIndex);        // Clear and set up render-target.                DIRECTX.SetAndClearRenderTarget(pTexRtv[currentIndex], pEyeDepthBuffer);    // Render Scene to Eye Buffers    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 chainovr_CommitTextureSwapChain(session, textureSwapChain);}// Submit frame with one layer we have.ovrLayerHeader* layers = &layer.Header;ovrResult       result = ovr_SubmitFrame(session, 0, nullptr, &layers, 1);isVisible = (result == ovrSuccess);


该代码使用了以下步骤来渲染场景:

1.它把纹理作为一个渲染目标来以及清除它来渲染。在这种情况下,同样的纹理可以为两只眼都所使用。

2.代码随后为每只眼睛计算视野以及投影矩阵并设置视口场景渲染。在该例中,视野计算将原始姿态(originPos和originRot值,追踪状态值)和新姿态(存储在层中的状态值)相结合。这些原始值可通过移动3D世界中的玩家来修改。

3.纹理渲染完成后,调用ovr_SubmitFrame将帧数据传到混合器。从这开始,混合器通过共享内存访问纹理数据接管整个流程,对它进行畸变处理以及呈现到Rift上。


一旦被提交的帧排队等候了ovrSubmit就会返回,并且runtime可接收新的帧了。若返回成功,则它的返回值为ovrSuccess或ovrSuccess_NotVisible。

若帧没有显示则返回ovrSuccess_NotVisible,这种情况在VR应用程序失去焦点时会发生的。实例代码通过更新isVisible标志来处理这种情况,该标志由渲染逻辑来检查。当帧不可见时,渲染应该被停止以消除不必要的GPU负担。

若接收到ovrError_DisplayLost,那么设备已经被删除且会话是无效的。释放共享资源(ovr_DestroyTextureSwapChain),销毁会话(ovr_Destroy),重新创建(ovr_Create),并创建一个新的资源(ovr_CreateTextureSwapChainXXX)。除非新的ovr_Create调用返回一个不同的GraphicsLuid,否则应用程序的已存在的私有显示资源不必重新创建。


帧时序:

SDK通过函数ovr_GetPredictedDisplayTime来报告帧时序信息,在不同的线程间依赖应用程序提供的帧索引来确保报告正确的时序。

精准的头部动态预测要求具备精准的帧和传感器时序,这对良好的VR体验是很有意义的。预测要求准确地知道当前帧在未来什么时候将会在屏幕上显示。如果我们即知道传感器又知道显示扫描输出的时间,那么就能够预测未来的头部姿态并改良图像稳定性。若错误地计算了这些值将会导致低于或过渡预测,降低感知延迟以及潜在地引起严重的抖动。

为确保精准时序,SDK使用绝对系统时间,以双精度存储,来表示传感器和帧时序的值。当前的绝对时间由函数ovr_GetTimeInSeconds返回。然而,当由ovr_GetPredictedDisplayTime返回的时序值会让仿真以及动作预测产生更好的结果时起,当前时间应该很少被使用。该函数如下:

ovr_GetPredictedDisplayTime(ovrSession session, long long frameIndex);


frameIndex参数指定应用程序当前正在渲染的帧。应用程序利用多线程渲染的必须维护一个内部帧索引并且人工对它进行递增,跨线程传递它时需附带帧数据来确保时序以及预测的正确。传给ovr_SubmitFrame的帧索引值必须与获取该帧的时序值的索引值相同。在下一章节会讨论多线程的时序问题。

帧索引值”0“能够在两个上面函数中用来请求SDK自动跟踪帧索引值。然而,这只有当在同一个线程中所有的帧时序请求以及渲染提交已经完成的情况下才有效。


在不同的线程上渲染:

在一些引擎中,渲染处理分布在不止一个线程上。

比如,一个线程可能负责在场景中为每个对象处理裁剪以及渲染配置工作(所谓的main线程),同时,第二个线程调用D3D或OpenGL API(所谓的render线程)。这两个线程都可能需要精准的帧显示时间的预测,以便计算头部姿态的最佳可能值。


这种方法的天然异步本性产生了问题:当渲染线程正在渲染一帧时,主线程可能正在处理下一帧。这种并行帧处理方式可能会恰恰由于一帧或一帧的小片段而脱离同步,这依赖于游戏引擎的设计。若我们使用默认的全局状态去访问帧时序,GetPredictedDisplayTime返回的结果可能会由于帧(取决于来自哪一个线程所调用的函数)而脱离实际,或者更糟的情况,可能会由于线程的设计方式而随机地产生错误。为了解决这个问题,前面章节引进了帧索引的概念,由应用程序来跟踪这个索引值并附带帧数据进行跨线程的传递。

要使得多线程渲染结果正确,必须做以下事情:

a.姿态预测,它是根据帧时序计算出来的,不用顾虑是哪一个线程访问它,对同一个帧必须是一致的。

b.实际用来渲染的眼睛姿态数据必须传入到ovr_SubmitFrame,附带帧索引值。

以下为总结的步骤:

1.main线程必须给当前正在渲染的帧赋一个索引值。它将会在每帧增加这个索引值并将它传入GetPredictedDisplayTime为姿态预测获取正确的时序。

2.main线程应该调用带有预测时间值的线程安全函数ovr_GetTrackingState。如果有必要,它还会调用ovr_CalcEyePoses为渲染做准备。

3.main线程必须将当前帧索引以及眼睛姿态传到render线程,并附带render线程所需要的任何渲染命令或帧数据。

4.当在render线程执行渲染命令时,开发者需确保以下这些东西保留:

a.该层帧渲染实际使用到的姿态数据已经存储在RenderPose中

b.在main线程中使用到的同一个帧索引值也已经被传入到ovr_SubmitFrame中

以下代码更详细地展示了该步骤流程:

void MainThreadProcessing(){    frameIndex++;            // Ask the API for the times when this frame is expected to be displayed.     double frameTiming = 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);    layer.RenderPose[0] = eyePoses[0];    layer.RenderPose[1] = eyePoses[1];        // Execute actual rendering to eye textures.    ...           // Submit frame with one layer we have.   ovrLayerHeader* layers = &layer.Header;   ovrResult       result = ovr_SubmitFrame(session, frameIndex, nullptr, &layers, 1);}

SDK还支持D3D 12,允许从多个CPU线程提交渲染任务到GPU。当应用程序调用ovr_CreateTextureSwapChainDX时,SDK为后续阶段缓存了调用者的ID3D12CommandQueue指令。当应用程序调用ovr_SubmitFrame时,SDK投下一个栅栏(drop a fence ?)在缓存好的ID3D12CommandQueue上以便了解一个已经准备好输出到混合器的眼睛纹理集到来的准确时间。

对一个给定应用程序而言,在单个线程上使用单个ID3D12CommandQueue是最简单的。但是,它也可能会在不同的任务队列上为每只眼睛纹理分担CPU的渲染工作量或者推进非眼睛纹理的渲染工作,比如阴影,反射贴图等。若该应用程序填充并执行来自多个线程的命令列表,它也将会不得不确保,提供给SDK的ID3D12CommandQueue是单个连接节点,这些节点是为眼睛纹理渲染工作做准备的,这些渲染工作为不同命令队列所执行。


层:

类似于一个显示器视野是由多个窗口组成的方式,在头盔中的显示是由多个图层组成的。一般这些图层中至少有一个图层是一个视图,它会在用户的虚拟眼球上渲染,但是其他图层可能就是一些HUD图层(透明指示层),信息面板,依附在世界菜单里的文本标签,瞄准刻线等。


每个图层可能有不同的分辨率,可能使用不同的纹理格式,可能使用不同的视场角或大小,以及可能是在平面或立体画面中。若图层的纹理中的信息没有发生改变,应用程序可配置不更新图层的纹理。比如,如果一个信息面板上的文本自从最后一帧以来就没有发生改变,那么不要去更新它,亦或者如果图层是一个底帧率的视频流中的画中画的视图,也不要去更新它。应用程序能够以一个高质量的畸变模式应用纹理映射到一个图层,这在改善文本面板可读性方面非常有效。

每一个帧,所有激活的图层是使用预倍增α混合从后向前合成的。图层0是最远的图层,图层1在它之上,等等;即使应用了深度缓冲,也没有图层的深度缓冲交叉测试。

图层的一个强大属性就是每个图层有它自己的分辨率。这允许应用程序通过降低在主眼睛缓冲渲染的分辨率来匹配低性能的系统,但是在另外一个不同的图层以更高的分辨率保存必要的信息,比如文本或者贴图。

有以下几个可用的图层类型:

EyeFov:以前的SDK为人所熟知的标准的”眼睛缓冲“,它通常是从用户眼睛的位置渲染的一个虚拟场景中的立体视图。虽然眼睛缓冲可能是平面的,但是这会引起不舒服的感觉。之前的SDK有一个隐含的视场角(FOV)以及视口;这些现在已经显式地提供并且应用程序可以在每帧中随心所欲的去改变它们。

Quad:一个四边形的平面图像,它在虚拟世界中以一个给定的姿态和大小显示。对抬头显示,文本信息,对象标签等而言,这很有用途。默认姿态是相对于用户的真实世界空间的并且四边形在空间中保存固定而不是跟着用户的头或身体移动。针对头部锁定的四边形,使用下面的ovrLayerFlag_HeadLocked标志描述。

EyeMatrix:这种图层类型于EyeFov图层类型有点相似,它是用来辅助对Gear VR应用程序的兼容。更多的信息,可参考Mobile SDK文档。

Disabled:它是被混合器所忽略的,禁止的图层不消耗性能。推荐应用程序处理基本的视锥裁剪并禁止不在视野中的图层。然而,当关闭一个图层时,应用程序没有必要紧紧地去重新打包激活的图层列表;禁止它并让它呆在列表中是最有效的方法。相应地,列表中指向图层的指针可被设置为null。


每个图层类型有一个对应的ovrLayerType枚举的成员,并且一个关联的结构体保存要显示的那个图层的数据。比如,EyeFov图层有一个类型为ovrLayerType_EyeFov且被该数据在结构体中描述为ovrLayerEyeFov。这些结构体有相似的参数集,尽管并没有一个图层类型会用到所有的参数:

参数                                     类型                                        说明

Header.Type                      enum ovrLayerType                 所有的图层必须指定它们的类型

Header.Flags                     ovrLayerFlags的一个位域        更多信息参考后面

ColorTexture                     TextureSwapChain                  为图层提供颜色和半透明数据,图层是使用预倍增α混合的方式相互合成的。这允许它们使用lerp风格合成,additive合成或这两者结合合成。图层纹理必须是RGBA或BGRA格式以及可能有贴图映射,但是不可能是数组,立方体或者有多重抗锯齿。若应用程序期望做多重抗锯齿渲染,那么它必须分解中间的多重抗锯齿颜色纹理到图层的非多重抗锯齿颜色纹理中去。

Viewport                            ovrRecti                                    纹理的四边形在使用,指定在0~1纹理UV坐标系空间中(非像素)。理论上,在该图层内该区域之外的纹理数据是不可见的。然而,关于纹理采样的通常的附加说明应用,尤其是对贴图映射的纹理。让RGBA(0,0,0,0)像素的镶边在显示区域四周以避免”流血“是一个好的惯例,尤其是在两眼缓冲之间肩并肩地打包到同一个纹理中时。边框大小根据具体使用案例决定,但在大多数案例中大概8个像素看起来效果很好。

Fov                                    ovrFovPort                                视场角用来在一个眼睛图层类型中渲染场景。注意这不是控制HMD的显示,它仅仅只是告诉混合器哪种FOV用来渲染纹理数据到图层中 --- 混合器随后将会调整到实际的用户视场角。因为需要特殊效果应用程序可能会动态地改变FOV值。 尽管一般在低速机器上在降低FOV之前降低分辨率会更有效,但是降低FOV也可能帮助提升性能。

RenderPose                      ovrPosef                                   摄像头姿态被应用程序用来渲染Eye图层类型。摄像头姿态一般由SDK和应用程序使用ovr_GetTrackingState和ovr_CalcEyePose函数来预测。该姿态和在显示时刻实际的眼睛姿态之间的不同被混合器通过时间扭曲应用到图层上。    

SensorSampleTime          double                                      应用程序采样追踪状态的绝对时间。获取该值的一般方式是在调用ovr_GetTrackingState之后紧接着调用ovr_GetTimeInSeconds。在HUD中,SDK使用该值报告应用程序的motion-to-photon(运动到光子?)的延迟 。若该应用程序在任何一个给定帧中有超过一个ovrLayerType_EyeFov图层被提交,那么SDK取消通过这些图层并选择最低延迟的时序。在一个给定的帧中,若没有ovrLayerType_EyeFov图层被提交,那么该SDK将使用这个时间点(当调用ovr_GetTrackingState且附带的参数latencyMarkerset给到ovrTrue时)作为该替代应用程序运动光子延迟时间。 

QuadPoseCenter             ovrPosef                               指定一个四边形图层类型的中心点的方位。所提供的方向是向量垂直于四边形的方向。除非ovrLayerFlag_HeadLocked标志被使用了,否则位置是真实世界的米以及相对于由函数ovr_RecenterTrackingOrigin或ovr_SpecifyTrackingOrigin设置的”0“点位置。

QuadSize                         ovrVector2f                           指定一个四边形图层类型的宽和高。至于位置,则这是真实世界中的米。


携带立体信息的图层(除Quad图层类型之外的所有图层)带有两套大量的参数且能使用以下三种不同的方式使用它们:

1.立体数据,独立纹理---app为左右眼睛提供一个不同的ovrTextureSwapChain,且各自提供一个视口。

2.立体数据,共享纹理 --- app为两只眼睛提供同一个ovrTextureSwapChain,但是为它们各自提供不同的视口。这允许应用程序渲染左右视图到同一个纹理缓冲。记住在它们之间添加一个小的缓冲来避免”流血?“,正如上面所提到的。

3.平面数据 --- app为左右眼提供同样的ovrTextureSwapChain,以及同样的视口。

纹理和视口大小左右眼可能都不同,甚至各自可能还有不同的视场角。然后要意识到在用户那里可能会引起立体不一致以及不舒服。

Header.Flags域适用于所有的图层,它是下面集合的一个逻辑或运算:

1.ovrLayerFlag_HighQuality --- 在混合器内对该图层使能4倍的各向异性采样。在清晰度方面会有显著地提高,尤其是当使用的纹理包含的是贴图时更是。当使用高频图像比如文本或图表时推荐使用以及当使用四边形图层类型时。对于Eye图层类型而言,它还会增加外围的视觉保真度,或者在它喂进纹理时有超过1:1的像素密度。最好的结果是,当为特定图层的纹理创建贴图时,确保纹理大小为2的幂。然而,应用程序不必渲染到整个纹理;一个视口渲染到推荐大小的纹理中会提高最佳的性能到质量系数。

2.ovrLayerFlag_TextureOriginAtBottomLeft --- 一个图层纹理的原点设定在左上角。然而,某些引擎(尤其是那些使用OpenGL的引擎)更喜欢使用左下角作为原点,此时,它们会使用该标志。

3.ovrLayerFlag_HeadLocked --- 大多数图层类型有它们相对于零点位置(调用ovr_RecenterTrackingOrigin得到的)的姿态方位值。然而应用程序可能希望指定的图层的姿态是相对于用户的脸的。当用户移动它们的头部,图层也跟着移动。这对于使用标线凝视对准或选择菜单之类的是很有帮助的。该标志可能在所有图层类型使用,虽然它在Direct类型使用时一点效果都没有。


在每一帧的结尾处,在应用程序更新渲染到ovrTextureSwapChain以及调用ovr_CommitTextureSwapChain之后,每个图层的数据都被放入了相关的ovrLayerEyeFov/ovrLayerQuad/ovrLayerDirect结构体中。应用程序随后给那些图层结构体创建一个指针列表,尤其是Header域,它是每个结构体的第一个成员。随后应用程序使用请求的数据创建一个ovrViewScaleDesc结构体,并调用ovr_SubmitFrame函数。

// 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 torsoovrLayerQuad 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;// Display all of the HUD texture.hudLayer.Viewport.Pos.x = 0.0f;hudLayer.Viewport.Pos.y = 0.0f;hudLayer.Viewport.Size.w = 1.0f;hudLayer.Viewport.Size.h = 1.0f;// 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] = HmdToEyeOffset[0];viewScaleDesc.HmdToEyeViewOffset[1] = HmdToEyeOffset[1];ovrResult result = ovr_SubmitFrame(Session, 0, &viewScaleDesc, layerList, 2);

混合器在将图层混合之前,在每个图层上独立地处理时间扭曲,畸变以及色差矫正。传统的渲染一个四边形到眼睛缓冲的方法涉及到两个过滤步骤(一是到眼睛缓冲,随后是畸变时期)。如果使用图层,那么就只有图层图像和最终帧缓冲之间的这一个过滤步骤。这对文本质量而言能够提供一个重大的改善,尤其是当贴图映射和ovrLayerFlag_HighQuality相结合时更是。

当前图层的一个劣势是在最后的混合图像上没有后处理操作,比如软聚焦效果,光开花效果,或者图层数据的Z轴交叉。这些效果能够在带有类似的视觉结果的图层上进行处理。

调用ovr_SubmitFrame来对图层进行排队显示,并转移ovrTextureSwapChains内的提交的纹理的控制权给到混合器。必须知道的是这些纹理正在被应用程序和混合器线程共享,而不是被拷贝,并且混合操作不是必须在调用ovr_SubmitFrame时产生,因此必须小心处理。为了继续渲染进一个纹理交换链,在渲染纹理之前,应用程序应该使用函数ovr_GetTextureSwapChainCurrentIndex获取下一个可用的纹理索引。比如:

// 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 layersovrLayerHeader *layerList[2];layerList[0] = &eyeLayer.Header;layerList[1] = &hudLayer.Header;// Each frame...int currentIndex = 0;ovr_GetTextureSwapChainCurrentIndex(... eyeTextureSwapChain, ¤tIndex);// 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, ¤tIndex);// 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;ovr_SubmitFrame(Hmd, 0, nullptr, layerList, 2);


异步时间扭曲:

异步事件扭曲(ATW)是一种让VR应用程序以及用户体验上减少延迟和抖动的技术。

在一个基本VR游戏循环内,以下动作一般都会发生:

1.软件请求头部位置

2.CPU为每只眼睛处理场景

3.GPU渲染场景

4.Oculus混合器对场景进行畸变处理并将它显示在头盔上


1.基本游戏循环

当帧率稳定时,体验感会很真实并让人很爽。当当前帧没有及时显示时,若显示的是前一帧,这会让人感到迷失了方向。

2.带抖动的基本游戏循环

当移动头时,游戏世界没有跟上头部移动的节奏,这会导致很不和谐并且破坏沉浸感。

ATW可以在头部移动时将已经渲染好的图像转移过去以调整头部位置的改变。尽管图像被修改了,头部移动幅度不是很大,因此这种改变也是轻微的。

除此之外,针对用户电脑,游戏设计或操作系的流畅性问题,或者在非预期的掉帧出现时,ATW能够帮助修复这些”坑“。

3.带ATW的游戏循环

在刷新间隔时,混合器应用时间扭曲到最后一个渲染好的帧。结果,不管帧率是否正常,时间扭曲帧将总会显示给用户。当帧率非常糟糕时,在显示边缘可以看到闪烁很明显。但是图像仍然很稳定。

ATW是由混合器自动添加的;不需要自己使能或调整它。然而,尽管ATW会减少延迟,但还是要确保应用程序或体验产生正常帧率。


前面的自适应队列

为了改善CPU和GPU并行性并增加GPU处理一帧的时间量,SDK自动适用队列高达1帧。

没有前头队列的话,CPU在前一帧显示后,马上开始处理下一帧。CPU处理完后,GPU处理该帧,混合器处理畸变,随后该帧显示到用户面前。


没有前头队列使用CPU和GPU

如果GPU不能够即使的处理当前帧的显示,那么就会显示前面的一帧。这会导致抖动。

若有队列在前头,CPU能够更早的开始工作;这让GPU有更多的时间来处理当前帧。



参考网址












阅读全文
0 0
原创粉丝点击