開発
開発
プラットフォームを選択

Riftへのレンダリング

Oculus RiftおよびRift Sでは、左右の目に対する歪み修正を加えた分割画面ステレオを使用し、レンズ関連の歪みを解消する必要があります。
歪みの修正は、歪みパラメーターがレンズタイプや各個人のアイレリーフによって異なるため、難しい問題となる場合があります。Oculus SDKでは、開発を容易にするため、コンポジターのプロセス内で歪み修正を自動的に処理します。また、遅延を削減するためのタイムワープの処理や、ヘッドセットへのフレームの表示も行います。
Oculus SDKがさまざまな処理を行うため、アプリとしては、シミュレーションを実行し、トラッキングするポーズに基づいてステレオ空間をレンダリングすることが主なジョブとなります。ステレオビューは1つか2つの個別のテクスチャーにレンダリングでき、ovr_WaitToBeginFrameovr_BeginFrame、およびovr_EndFrameを呼び出してコンポジターに送信できます。このセクションでは、このプロセスの詳細について詳しく説明します。

Riftへのレンダリング

Oculus RiftおよびRift Sでは、シーンを、画面の半分ずつを左右それぞれの目に使用する分割画面ステレオでレンダリングする必要があります。
RiftまたはRift Sを使用する場合、左目は画面の左半分、右目は右半分を見ています。個人によって違いはありますが、人間の瞳孔は左右で約65ミリ離れています。これは瞳孔間距離(IPD)と呼ばれます。アプリ内のカメラも、これと同じ距離で構成する必要があります。
これはカメラの回転ではなく平行移動であり、これ(およびそれに伴う視差効果)によって立体効果が生まれます。つまりアプリは、1回は左の仮想カメラ、もう1回は右の仮想カメラを使って、シーン全体を2回レンダリングする必要があります。
単一の完全なレンダリングビューから生成される左のビューと右のビューによる再投影ステレオレンダリング技術は通常、オブジェクトエッジに大量のアーティファクトが存在するため、HMDでは実行できません。
Rift内のレンズが画像を拡大するため、イマージョンを拡大する視野(FOV)が得られます。ただし、このプロセスでは画像が大きく歪みます。Riftの元の画像を表示するエンジンの場合、ユーザーが視認する画像に糸巻形収差が発生します。
この歪みに対処するため、SDKはレンダリングされたビューに対して、バレル歪みを正方向と逆方向でレンダリングする後処理を課します。それにより、この2つが相互に打ち消し合い、左右の目に歪みのないビューが生じます。またSDKは、色収差補正(レンズにより生じるエッジでの色の分散効果)も修正します。正確な歪みパラメーターは、レンズの特性とレンズに対する目の位置によって異なりますが、Oculus SDKでは歪みメッシュを生成する際に必要な計算がすべて処理されます。
RiftおよびRift Sでは、投影軸は次の図に示すように、左右の目に平行です。ただし、アプリではシーンのレンダリング時にこれを想定しません。ovrEyeRenderDesc構造体のHmdToEyePoseメンバーはovrPosef 6-DOF変換です。つまり、傾角ディスプレイ(ディスプレイが互いに平行ではない)のHMDでは、レンダリング中のカメラは回転し、HMDのポーズから変換されます。このようにして、ユーザーの前方向はHMDのポーズのZ軸で規定され、アイポーズは別の向きを示すZ軸になります。
実際には、Riftでの投影は、鼻が視界に入るために中心からわずかにずれることが多くあります。しかしここで重要なのは、Riftの左右の視界が完全に分かれており、テレビや映画の画面に生成されるステレオビューとは異なるという点です。つまり、テレビや映画向けに開発された方法は通常VRには適用できないため、そのような方法を使用する場合は慎重に検討する必要があります。
シーン内には2つの仮想カメラが配置され、ovrEyeRenderDesc::HmdToEyePoseで指定されるアイポーズと同じ方法で向きが設定されます。この2つのカメラの距離は、目の間の距離、つまり瞳孔間距離(IPD)と同じになります。
Riftのレンズはほとんどのユーザーにとってほぼ適切な距離となるよう配置されていますが、ユーザーのIPDと完全にマッチするとは限りません。ただし、それでも光学設計上、左右の目にはそれぞれ正しいビューが視認されます。重要な点は、仮想カメラ間の距離は、ソフトウェアによってRiftのレンズ間の距離ではなくユーザーのIPDとマッチングされるという点です。ユーザーのIPDは、設定ユーティリティで設定される、ユーザーのプロフィール内で指定されます。

レンダリングの設定概要

Oculus SDKでは、コンポジターのプロセスを使用してフレームを表示し、歪みを処理します。
Riftをターゲットに設定するには、シーンを1つまたは2つのテクスチャーにレンダリングし、そのテクスチャーをAPIに渡します。ランタイムでは歪みレンダリング、GPU同期化、フレームタイミング、HMDへのフレーム表示が処理されます。
SDKレンダリングでは以下のステップを実行します。
  1. 初期化。
  2. Oculus SDKを初期化し、前述のように、ovrSessionオブジェクトをヘッドセット用に作成します。
  3. ovrHmdDescデータに基づき、求められるFOVとテクスチャーサイズを計算します。
  4. アイバッファの表現に使用されるovrTextureSwapChainオブジェクトをAPI固有の方法で割り当てます。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_GetTextureSwapChainCurrentIndexovr_GetTextureSwapChainBufferDXovr_GetTextureSwapChainBufferGL、またはovr_GetTextureSwapChainBufferVkを使用して取得できます。テクスチャーへのレンダリングが完了すると、アプリでovr_CommitTextureSwapChainを呼び出す必要があります。
  8. ovr_WaitToBeginFrameを呼び出し、アプリでのフレームのレンダリングが可能になったら、ovr_BeginFrameを呼び出します。フレーム送信の準備が整ったら、ovr_EndFrameを呼び出し、ovrLayerEyeFov構造体で前の手順のスワップテクスチャーセットを渡します。フレームの送信に単一レイヤーが必要ですが、複数のレイヤーをとレイヤータイプを使用して高度なレンダリングを行えます。ovr_EndFrameはレイヤーテクスチャーをコンポジターに渡し、そこで歪み、タイムワープ、GPU同期化などを処理してから、ヘッドセットに表示します。ovr_WaitToBeginFrameovr_BeginFrameovr_EndFrameを組み合わせると、複数スレッド環境でパフォーマンス最適化手法を導入することができます。例えば、複数処理中のフレームを同時に分割し、重複させるなどです。前のリリースでは、これらの3つの関数が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とピクセル密度の値はどちらもパフォーマンス向上のために変更できますが、この例では、(session->DefaultEyeFovから取得した)推奨のFOVを使用します。関数ovr_GetFovTextureSizeは、これらのパラメーターに基づき、左右の目で求められるテクスチャーサイズを計算します。
Oculus APIでは、アイレンダリングに1つの共有テクスチャーを使用してもよく、2つの個別のテクスチャーを使用することもできます。この例では、分かりやすく単一の共有テクスチャーを使用して、両目のレンダリングに十分な大きさを確保します。
Vulkanを使用している場合、テクスチャースワップチェーンを作成する前に、3つのステップを追加する必要があります。
  • 初期化中、ovr_GetSessionPhysicalDeviceVkを呼び出し、LUIDとマッチする現在の物理デバイスを取得します。次に、返された物理デバイスと関連するVkDeviceを作成します。
  • Vulkanでは、AMDハードウェアに別の拡張を使用します。次の例に類似するコードを使用して、アプリの初期化中にAMD GPU拡張を処理します。このコードの例はWin32_VulkanAppUtil.hのものであり、Oculus SDKに同梱されるOculusRoomTiny_Advancedサンプルアプリに付属するものです。
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_CreateTextureSwapChainDX、またはovr_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();
     }
 }
Direct3D 12で実行するOculusRoomTinyサンプルのサンプルコードを次に示します。
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を呼び出すと、呼び出し元からID3D12DeviceではなくID3D12CommandQueueがSDKに提供されます。呼び出し元は、すべての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補正レンダリングを提供します。これにより、VRアプリにとって重要なポイントである表示のフォトリアリスティック、MSAAが向上し、テクスチャーサンプリングのエネルギー節約を実現します。上記のとおり、アプリはsRGBテクスチャースワップチェーンを作成することになっています。sRGBレンダリングを正しく処理することは複雑な課題ですが、このセクションでは概要のみを示し、詳細な情報についてはこのドキュメントでは取り扱いません。
リアルタイムのレンダリングアプリでsRGB補正シェーディングを実現するには複数のステップを実行する必要があり、その方法もいくつかあります。例えば、ほとんどのGPUではハードウェアを加速し、sRGB固有のインプットおよびアウトプットサーフェイスのためのガンマ補正シェーディングを向上させますが、一部のアプリではGPUシェーダー計算でさらに制御をカスタマイズします。Oculus SDKでは、アプリがsRGB空間テクスチャースワップチェーンを渡すと、コンポジターではGPUのサンプラーを使ってsRGBからリニアへのコンバージョンを実行します。
GPUシェーダーに取り込まれるカラーテクスチャーはすべて、sRGB補正形式で、OVR_FORMAT_R8G8B8A8_UNORM_SRGBのように正しくマーキングします。これは、スタティックテクスチャーをクワッドレイヤーテクスチャーとしてコンポジターに提供するアプリでも推奨されます。そうしない場合、テクスチャーが想定よりもかなり明るく表示されます。
D3D 11および12では、ovr_CreateTextureSwapChainDXの記述に示すテクスチャー形式は、テクスチャーのコンテンツを読み取るときにShaderResourceViewのディストーションコンポジターに使用されます。そのため、アプリではsRGB空間(OVR_FORMAT_R8G8B8A8_UNORM_SRGBなど)にあるテクスチャースワップチェーンの形式をリクエストする必要があります。
リニア形式のテクスチャー(OVR_FORMAT_R8G8B8A8_UNORMなど)にレンダリングし、HLSLコードを使用してリニアからガンマへのコンバージョンを実行するようアプリが構成されている場合や、ガンマ補正を考慮しない場合は次のように処理します。
  • sRGB形式(OVR_FORMAT_R8G8B8A8_UNORM_SRGBなど)のテクスチャースワップチェーンをリクエストする。
  • 記述の中で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補正レンダリングが行われるため、アプリではsRGB空間(OVR_FORMAT_R8G8B8A8_UNORM_SRGBなど)でテクスチャースワップチェーン形式をリクエストする必要があります。コンポジターはsRGBからリニアへのコンバージョンにGPUのハードウェアサンプラーを利用します。
アプリがリニア形式(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_BeginFrame、およびovr_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以降、フレーム送信ではレイヤーを使用して、複数のビューイメージまたはテクスチャークワッドを重ね合わせて作成できます。この例では、1つのレイヤーを使用してVRシーンを表します。そのために、視野全体に及ぶデュアルアイレイヤーを記述する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によって報告されます。
正確に予測するには、現在のフレームが実際にはいつ表示されるかを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);
このコードは、シーンのレンダリングのための複数のステップを実行します。
  • テクスチャーをレンダリングターゲットとして適用し、それをレンダリングのためにクリアします。この場合、同じテクスチャーが左右の目に使用されます。
  • 次に、ビューおよび投影のマトリクスが計算され、左右の目のビューポートシーンのレンダリングが設定されます。この例では、ビュー計算は始点のポーズ(originPosおよびoriginRotの値)を、トラッキング状態に基づいて計算され、レイヤーに保存されている新しいポーズと組み合わせます。これらの始点の値は、3Dでプレイヤーを動かすためのインプット値によって変更されることがあります。
  • テクスチャーのレンダリングが完了したら、ovr_EndFrameを呼び出してフレームデータをコンポジターに渡します。ここからはコンポジターが処理を担当し、共有メモリからテクスチャーデータにアクセスし、変形を加え、Riftに表示します。
送信されたフレームがキューに入れられると、ovr_EndFrameが返され、ランタイムで新しいフレームが受け入れられるようになります。完了すると、その戻り値はovrSuccessまたはovrSuccess_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_BeginFrame、およびovr_EndFrameに渡す必要があります。マルチスレッディングについて詳しくは、次の複数スレッドでのレンダリングのセクションで説明します。
特殊なframeIndex値0を両方の関数に使用して、SDKによるフレームインデックスの自動トラッキングをリクエストすることができます。ただし、これはすべてのフレームタイミングリクエストとレンダリング送信が同じスレッドで行われる場合に限ります。

複数スレッドでのレンダリング

一部のエンジンでは、レンダリング処理は複数のスレッドに分散されています。
例えば、1つのスレッドでは、あるシーンの各オブジェクトのカリングとレンダリング設定を行い(これを「メイン」スレッドとします)、別のスレッドで実際のD3DまたはOpenGL API呼び出しを行います(これを「レンダリング」スレッドとします)。そしてどちらのスレッドも、頭の姿勢の最適な予測を計算できるよう、フレームのディスプレイ時間の正確な推定値を必要とするとします。
これは、レンダリングスレッドがフレームをレンダリングする間、メインスレッドが次のフレームを処理している可能性があるという非同期手法のため、処理が困難になりがちです。この平行フレーム処理では、ゲームエンジンの設計によっては正確に1フレーム分、またはフレームの一部が同期を外れることがあります。デフォルトのグローバルステータスを使用してフレームタイミングにアクセスした場合、ovr_GetPredictedDisplayTimeの結果は、関数の呼び出し元であるスレッドによっては1フレームずれたり、あるいは、スレッドのスケジュールによってはランダムになり、不正確になる場合さえあります。この問題への対策として、前のセクションでは、アプリによってトラッキングされ、フレームデータとともにスレッドをまたいで渡されるframeIndexのコンセプトについて説明しました。
マルチスレッドレンダリングで正しい結果を得るには、(a)フレームタイミングに基づいて計算したポーズ予測が、アクセス元のスレッドに関係なく、同じフレームで一貫していること、および(b)実際のレンダリングに使用されたアイポーズがフレームインデックスとともにovr_EndFrameに渡されている必要があります(これはovr_WaitToBeginFrameおよびovr_BeginFrameを呼び出した後に行われる必要があります)。
実際に確認するための手順をまとめると、次のようになります。
  1. メインスレッドでは、フレームインデックスを、レンダリング処理中の最新のフレームに割り当てる必要があります。このインデックスをフレームごとに増分し、それをovr_GetPredictedDisplayTimeに渡してポーズ予測のための正しいタイミングを取得します。
  2. メインスレッドは、予測した時間の値とともにスレッドセーフ関数ovr_GetTrackingStateを呼び出します。また、レンダリング設定に必要であればovr_CalcEyePosesも呼び出すことができます。
  3. メインスレッドは最新のフレームインデックスとアイポーズを、必要なレンダリングコマンドまたはフレームデータとともにレンダリングスレッドに渡します。
  4. レンダリングコマンドがレンダリングスレッドで実行されたら、開発者は次のことを確認します。
  5. フレームレンダリングに使用された実際のポーズがレイヤーのRenderPoseに保存されている。
  6. メインスレッドで使用されたものと同じframeIndexの値がovr_BeginFrameおよびovr_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が別のコマンド再生リストで実行されたアイテクスチャーレンダリング作業のシングル結合ノードであることも確認する必要があります。

レイヤー

モニタービューを複数のウィンドウで作成する方法と同様に、ヘッドセットのディスプレイも複数のレイヤーで作成することができます。通常、これらのレイヤーのうち、少なくとも1つはユーザーの仮想の眼球からレンダリングされるビューですが、他のレイヤーとしてHUDレイヤー、キューブマップレイヤー、情報パネル、その世界のアイテムに添付されているテキストラベル、照準レチクルなどが考えられます。
それぞれのレイヤーの解像度、使用されるテクスチャー形式、使用される視野やサイズは異なる場合があり、モノかステレオかも異なります。レイヤーのテクスチャー内の情報が変更されない場合、そのテクスチャーをアップデートしないようアプリを設定することもできます。例えば、情報パネルのテキストが前のフレームから変更されていない場合、またはそのレイヤーが低フレームレートの動画ストリームのピクチャーインピクチャービューの場合などはアップデートする必要はありません。アプリでは、ミップマップしたテクスチャーを高品質のディストーションモードでレイヤーに加えることができます。これは、テキストパネルの読みやすさを向上させるには非常に効果的です。
すべてのフレームで、アクティブなレイヤーは乗算済みアルファブレンディングによって後ろから前に構成されています。レイヤー0は最も遠いレイヤーで、レイヤー1はその上、というように重なります。
レイヤーの優れた特徴は、それぞれ解像度を変えられることです。これにより、バーチャル世界を表示するメインのアイバッファレンダリングで解像度を落とし、テキストやマップなどの重要情報を解像度の高い別のレイヤーに保持することによって、低いパフォーマンスのシステムに縮小することができます。
レイヤーの型は複数あります。
レイヤーの型説明
EyeFov
前のSDKでも使用した標準の「アイバッファ」。通常、ユーザーの目の位置からレンダリングした仮想シーンのステレオビューです。アイバッファをモノにすることもできますが、不快感が生じることがあります。前のSDKには暗黙的な視野(FOV)やビューポートがありましたが、現在では明示的に提供されており、必要であればアプリでフレームごとに変更することができます。
Quad
バーチャル世界で特定のポーズとサイズの矩形で表示されるモノスコピック画像。ヘッドアップディスプレイ、テキスト情報、オブジェクトラベルなどに役立ちます。デフォルトでは、ポーズはユーザーの実際の空間に相対して指定され、クワッドはユーザーの頭や体の動きに合わせて動くのではなく、空間で固定されています。ヘッドロッククワッドでは、以下で説明するように、ovrLayerFlag_HeadLockedフラグを使用します。
キューブマップ
キューブマップは6つの矩形で構成されます。これらの矩形は、キューブが形成するルーム内部にユーザーが座っているかのように、ユーザーを囲んで配置されます。それぞれの壁はアプリが送信するテクスチャーです。キューブマップは無限遠で、原則としてアプリがレンダリングするすべてのオブジェクトの背景として表示されます。キューブマップはユーザーにはキューブとして見えません。無限遠の単なる背景として表示されます。例えば、キューブマップを使用して、VRエクスペリエンスの中ですべての建物の背後に現れる空を作成できます。前面でオブジェクトのオクルージョンを処理する必要はありません。キューブマップを設定するだけで、シーンのあらゆる場所で背景に空が表示されるようにすることができます。
EyeMatrix
EyeMatrixレイヤー型はEyeFovレイヤー型と類似しており、従来のSamsung Gear VRアプリとの互換性を保ちます。
Cylindrical
シリンドリカルレイヤーを使用すると、平面のクワッドではなく曲線クワッドを作成できます。ovrLayerCylinder は、中央に再配置された始点(下図のC)に相対して配置された単一の円柱であるovrLayerType_Cylinder型のレイヤーを表しています。以下のシリンドリカルレイヤーパラメーターを参照してください。
Depth Buffers
このレイヤーは、カラーテクスチャーに加えて深度テクスチャーを合わせて、モノスコピックビューまたはステレオスコピックビューを指定します。このレイヤーはovrLayerEyeFovDepth構造体で実装されます。これはovrLayerEyeFovに相当するものですが、DepthTextureとProjectionDescが加えられています。深度バッファは通常、ポジションのタイムワープをサポートするものです。
Disabled
コンポジターでは無視され、無効化されたレイヤーがパフォーマンスに影響することはありません。アプリで基本のフラスタムカリングを実行し、ビューの範囲外のレイヤーは無効にすることをおすすめします。ただし、1つのレイヤーをオフにしたときに、アプリでアクティブレイヤーのリストをまとめて再パックする必要はありません。無効にしたままでリストに残しておくだけで十分です。同様に、リストのレイヤーへのポインターをnullに設定できます。
シリンドリカルレイヤーパラメーター
この型のレイヤーは、バーチャル世界に配置された単一のオブジェクトを表し、ワールド自体のステレオビューではありません。円柱内側の表面のみが見えます。シンドリカルレイヤーはユーザーが円柱の範囲から出ることができないときにのみ使用します。アーティファクトは円柱の外側表面を表示するときに現れます。また、インターフェイスでは0から2 Piの角度(A)がサポートされていますが、円柱の縁が収束するアーティファクトを避けるため、この角度は常に1.9 Piを下回っている必要があります。この機能の使用方法について詳しくは、参照ドキュメントのovrLayerCylinderをご覧ください。

レイヤーパラメーター

それぞれのレイヤースタイルには、対応するovrLayerType enumのメンバーと、そのレイヤーの表示に必要なデータを保持する構造があります。例えば、EyeFovレイヤーは型番号ovrLayerType_EyeFovであり、ovrLayerEyeFov構造のデータで記述されます。それぞれの構造は類似するパラメーターセットを共有しますが、すべてのレイヤー型がすべてのパラメーターを必要とするわけではありません。
パラメーター説明
Header.Type
enum ovrLayerType
すべてのレイヤーに設定し、どの型であるかを指定します。
Header.Flags
ovrLayerFlagsのビットフィールド
詳細については、以下を参照してください。
ColorTexture
TextureSwapChain
レイヤーにカラーと半透明のデータを提供します。レイヤーは乗算済みアルファを使用して互いにブレンドされます。これにより、線形補間スタイルブレンディングや加算ブレンディング、またはこれらを組み合わせて表現することができます。レイヤーテクスチャーはRGBAまたはBGRA形式であり、ミップマップを持つ場合がありますが、配列やキューブにしたり、MSAAを持つことはできません。アプリでMSAAレンダリングを行いたい場合は、中間のMSAAカラーテクスチャーをレイヤーの非MSAAカラーテクスチャーに解像する必要があります。
Viewport
ovrRecti
実際に使用されるテクスチャーの矩形であり、それぞれ幅/高さとx/yの整数を含む2つの2Dベクターとして矩形のサイズとポジションを示すovrRectiとして指定されます。ポジションとして与えられたx/yの値は矩形の左下を表しています。理論的に、この領域外のテクスチャーデータはレイヤーには表示されません。ただし、特にミップマップされたテクスチャーなどには、テクスチャーサンプリングの一般的な注意事項が適用されます。表示されている領域周辺のRGBA(0,0,0,0)ピクセル、特に、隣り合わせで同じテクスチャーにパックされている2つのアイバッファの間には境界を残し、「ブリーディング」を避けることが推奨されます。境界のサイズは具体的な使用ケースによって異なりますが、ほとんどは8ピクセル前後で十分です。
Fov
ovrFovPort
Eyeレイヤー型でシーンのレンダリングに使用される視野。これはHMDのディスプレイは制御せず、どのFOVがレイヤーのテクスチャーデータのレンダリングに使用されているかをコンポジターに示します。コンポジターはこれを基に、実際のユーザーのFOVに合わせて調整を行います。アプリでは、特殊効果のためにFOVを動的に変更することができます。FOVを削減すると、低速マシンのパフォーマンスを向上させることができますが、通常は、FOV削減の前に解像度を下げる方が効果があります。
RenderPose
ovrPosef
Eyeレイヤー型でシーンのレンダリングに使用されるカメラのポーズ。これは通常、SDKとアプリでovr_GetTrackingStateおよびovr_CalcEyePoses関数を使用して予測されます。コンポジターは、このポーズと、表示時間における実際のアイポーズとの差を使用して、レイヤーにタイムワープを適用します。
SensorSampleTime
double
アプリがトラッキングステータスをサンプリングしたときの絶対時間。この値は通常、ovr_GetTrackingState呼び出しの次にovr_GetTimeInSecondsを呼び出して取得します。SDKではこの値を使用して、パフォーマンスHUDでのMotion-to-Photon遅延を報告します。アプリで一定のフレームにおいて複数のovrLayerType_EyeFovレイヤーを送信した場合、SDKがそれらのレイヤーをスクラブし、遅延の最も少ないタイミングを選択します。特定のフレームでovrLayerType_EyeFovレイヤーが送信されない場合、SDKでは、アプリのMotion-to-Photon遅延時間の代わりに、latencyMarkerovrTrueに設定したovr_GetTrackingStateが呼び出された時間が使用されます。
QuadPoseCenter
ovrPosef
Quadレイヤー型の中心点の向きとポジションを指定します。指定される向きはクワッドと垂直のベクトルです。ポジションは、実際の世界(アプリのバーチャル世界ではなく、ユーザーが実際にいる空間)のメートル法で表され、ovrLayerFlag_HeadLockedフラグが設定されていなければ、ovr_RecenterTrackingOriginまたはovr_SpecifyTrackingOriginで設定された「ゼロ」ポジションに相対します。
QuadSize
ovrVector2f
Quadレイヤー型の幅と高さを指定します。ポジションと同様に、これも実際のメートル法で表します。
ステレオ情報を使用するレイヤー(Quadレイヤー型以外のものすべて)は、大部分のパラメーターセットの2つをとり、それを次の3つの方法でそれぞれ使用します。
  • ステレオデータ、個別のテクスチャー - アプリは左右の目にそれぞれ別のovrTextureSwapChain、およびそれぞれ別のビューポートを提供します。
  • ステレオデータ、共有のテクスチャー - アプリは左右の目の両方に同じovrTextureSwapChainを提供しますが、それぞれに別のビューポートを提供します。こうすることで、アプリは左右のビューを同じテクスチャーバッファにレンダリングできます。前述のとおり、2つのビューの間に少しバッファを追加し、「ブリーディング」を防止します。
  • モノデータ - アプリは左右の両方の目に同じovrTextureSwapChainを提供し、それぞれに同じビューポートを提供します。
テクスチャーとビューポートのサイズは左右の目で別にすることができ、それぞれに異なる視野を持たせることもできます。ただし、ステレオの不均衡やユーザーの不快感を生じさせないように注意してください。
すべてのレイヤーに使用できるHeader.Flagsフィールドは、以下の論理和です。
  • ovrLayerFlag_HighQuality - このレイヤーについて、コンポジターで4倍のアニソトロピックサンプリングが可能です。これにより、特にテクスチャーにミップマップが含まれる場合などの読みやすさが大幅に向上するため、テキストやダイアグラムなどの高フリークエンシーの画像やQuadレイヤー型を使用する場合におすすめします。Eyeレイヤー型の場合も、周辺部に対する忠実度、または推奨される1:1のピクセル密度を超えるテクスチャーに取り込むときの忠実度を向上させます。最適な結果を得るには、特定のレイヤーに関連するテクスチャーにミップマップを作成する場合、テクスチャーサイズが累乗されていることを確認します。ただし、アプリではテクスチャー全体をレンダリングする必要はなく、そのテクスチャーで推奨されるサイズにレンダリングするビューポートによってパフォーマンスと品質の最適な比率が得られます。
  • ovrLayerFlag_TextureOriginAtBottomLeft - レイヤーのテクスチャー始点は左上隅にあるものと想定されます。ただし、一部のエンジン(特にOpenGLを使用するもの)では、このフラグを使用するため始点に左下が使われている場合があります。
  • ovrLayerFlag_HeadLocked - ほとんどのレイヤー型では、そのポーズの向きとポジションはovr_RecenterTrackingOriginを呼び出して定義された「ゼロポジション」に相対して指定されます。ただし、アプリによってはユーザーの顔に相対してレイヤーのポーズを指定する場合もあります。ユーザーが頭を動かすと、レイヤーもそれに従います。これは視線ベースの向き変更や選択に使用するレチクルに有用です。このフラグはすべてのレイヤー型に使用できますが、Direct型では効果はありません。
各フレームの終了時点では、アプリで更新するovrTextureSwapChainをレンダリングし、ovr_CommitTextureSwapChainを呼び出した後、各レイヤーのデータを関連するovrLayerEyeFov / ovrLayerQuad / ovrLayerDirect構造に配置します。次に、各構造の最初のメンバーとして確定しているヘッダーフィールドへのポインターなど、これらのレイヤー構造へのポインターのリストがアプリで作成されます。次に、必要なデータでovrViewScaleDesc構造体が構築され、ovr_WaitToBeginFrame関数、ovr_BeginFrame関数、ovr_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);
コンポジターはレイヤーをブレンドする前に、それぞれのレイヤーでタイムワープ、ディストーション、色収差補正を行います。クワッドをアイバッファにレンダリングする従来の方法には、2つのフィルタリングステップ(アイバッファ時に1回、ディストーション中に1回)があります。レイヤーを使用する場合、レイヤーイメージと最終フレームバッファの間のフィルタリングステップは1つだけです。これによってミップマップや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);
直線的キャプチャー
この機能によって、ユーザーがVR体験で見ているものに対応する、単一の歪みのない画像のスクリーンショット(90 fps)を取得できます。この機能を使用すると、VR体験を外部モニターにミラーリングできます。

HMDアイポーズでの操作

Oculus PC SDKでは、バージョン1.17の前までは、アイポーズは3自由度(DOF)まで、つまり移動だけでした。アイポーズはovr_GetRenderDesc関数が示すHmdToEyeOffsetベクターによって指定されていました。バージョン1.17以降、HmdToEyeOffsetHmdToEyePoseと名称変更され、PositionOrientationを含むovrPosef型を使用してアイポーズに6自由度が得られます。つまり、左右の目のレンダリングフラスタムは現在、SDKによって移動するだけでなく、HMDの元の方向から回転して離れることもできます。このため、アイフラスタムの軸は互いに平行、あるいはHMDの方向軸とは平行ではなくなりました。この概念によって、HMDジオメトリを定義する際のSDKの自由度が大幅に向上しました。ただし、VRアプリ開発者にとっては、これまでの前提、特にレンダリングに関しては慎重になる必要があります。
VRアプリで確実に正しくHmdToEyePoseを使用するポイントについて次に示します。
  • VRアプリで(バージョン1.17より前の)ベクター移動値HmdToEyeOffsetを使用する場合、代わりにHmdToEyePose.Positionを使用できます。ただし、自分の作業を確実に理解している場合を除き、ポジションと向きを別にするのではなく、HmdToEyePoseを変換全体として扱うほうが好ましいでしょう。
  • PC-SDKバージョン1.17の前は、画面全体の2Dクワッド平面レンダリング(矩形など)は常に可能でした。しかし、それぞれのアイフラスタムが独立して回転する可能性を考えると、VRアプリで左右の目の向きをクワッド変換に統合し、クワッドが左右の正しい視点でレンダリングされるようにする必要があります。この例について、(一部強調して)次に示します。
これは、「目の中心」の向き、またはHMDの向きを使用するようにクワッドの向きを決めるものです。3Dスプラッシュ画面などの画面に平行なその他のクワッド、または大きいフリップブックのスモーククワッドなどのパーティクルエフェクトにも適用されます。パーティクルを除き、そのようなクワッドをネイティブにレンダリングすることは避け、代わりにovrLayerQuadを使用することをおすすめします。
  • 一部のVRアプリでは、さまざまなレンダリング最適化を活用するために、両目のovrFovPort構造から単一のモノスコピックカメラフラスタムを生成します。これは通常、ovrFovPortを使用して実行されます。そうすると、フラスタムの4面すべてで、両目のovrFovPortを最大値にすることができます。ただし、この方法でモノスコピックフラスタムを生成する前に、FovPort::Uncantを呼び出し、ovr_math.hヘッダーにあるovrFovPort値から可能性のある回転を必ず削除してください。FovPort::Uncantの使用方法については、OculusWorldDemoサンプルコードをご覧ください。

非同期タイムワープ

非同期タイムワープ(ATW)とは、VRアプリやVR体験で遅延や揺れを削減するための技術です。
基本のVRゲームループでは、次のことが発生します。
  1. ソフトウェアがヘッドポジショニングをリクエストする。
  2. CPUが左右の目のシーンを処理する。
  3. GPUがシーンをレンダリングする。
  4. コンポジターがディストーションを加え、ヘッドセットにシーンを表示する。
ゲームループの基本的な例を以下に示します。
フレームレートが維持されると、リアルで快適な体験を楽しめるようになります。タイミングが遅れる場合は、前のフレームが表示されますが、方向感覚が失われることがあります。次の図は、基本のゲームループで発生した揺れの例を示しています。
頭を動かし、視界がそれに追いつけない場合、揺れが生じて没入感が損なわれます。
非同期タイムワープとは、レンダリングした画像をわずかにシフトし、頭の動きの変化に合わせて調整する技術です。画像には変化が加えられますが、頭の動きは少ないため、その変化はわずかです。
また、ユーザーのコンピューター、ゲーム設計やオペレーティングシステムの問題に対処するため、非同期タイムワープで「ポットホール」、つまりフレームレートが予期せず低下した瞬間を修復することができます。
次の図は、非同期タイムワープが適用されたときのフレームレートの低下の例を示しています。
更新間隔でコンポジターがタイムワープを最後にレンダリングしたフレームに適用します。その結果、タイムワープされたフレームは、フレームレートに関係なく常にユーザーに表示されるようになります。フレームレートが極端に低下していると、ディスプレイの周辺部分でフリッカーが見られるようになります。ただし、画像は引き続き安定しています。
非同期タイムワープはコンポジターによって自動的に適用されるため、有効にしたり、調整したりする必要はありません。非同期タイムワープが遅延を減らすことができるとはいえ、アプリやエクスペリエンスのフレームレートが維持されていることを確認してください。

Adaptive Queue Ahead

CPUおよびGPUの並列処理を向上させ、GPUがフレーム処理に費やせる時間を増やすため、SDKは自動的に最大で1フレーム分のQueue Aheadを適用します。
Queue Aheadを適用しない場合、前のフレーム表示の後にCPUによる次のフレームの処理がすぐに始まります。CPUの処理が完了すると、GPUがそのフレームを処理し、コンポジターがディストーションを適用してフレームがユーザーに表示されます。次の図は、Queue Aheadを使用しない場合のCPUとGPUの使用状況を示しています。
GPUがフレームを時間通り表示処理できない場合、前のフレームが表示されます。これにより揺れが生じます。
Queue Aheadを使用すると、CPUが先に開始するため、GPUはより多くの時間をフレーム処理に費やせます。次の図は、Queue Aheadを使用した場合のCPUとGPUの使用状況を示しています。
ナビゲーションロゴ
日本語
© 2026 Meta