OpenXR 程式開發:初始環境設定


之前在《OpenXR 架構簡單介紹》這篇文章大概介紹了 OpenXR 的架構了,但是都還沒講到程式碼的部分;而這一篇,則是要來真的寫 OpenXR 的程式了~

Heresy 這邊是在 Windows 環境下,使用 Visual Studio 2019 來進行原生 C++ 程式的開發;而在不牽扯到繪圖的部份的狀況下,只需要另外去下載官方預先建置好的 OpenXR Loader 就可以了。

其下載頁面是:https://github.com/KhronosGroup/OpenXR-SDK/releases,目前最新版本是 1.0.9。

下載解壓縮後,裡面基本上就是很一般的 Windows 環境的 C 函式庫的形式,包含了 header 檔、個平台的 lib 以及 dll。

當要使用 OpenXR 的 C API 的時候,基本上只要 include openxr.h 這個 header 檔就可以了。

裡面的型別基本上都會是「Xr」開頭、相關的巨集則會是「XR_」開頭;函式則是用「xr」開頭,一般來說會回傳 XrResult 這個列舉型別、代表執行的結果(結果意義)。整體來說算是相當好識別。

在流程上,最主要應該還是參考 OpenXR API Overview 這張圖:

在開發整個 OpenXR 的程式的時候,在外部主要的流程大致上會是:

  1. 建立 XrInstance 的物件
  2. 取得 XrSystemId、確認可存取 XR 系統
  3. 建立 XrSession、準備開始 XR 系統的操作
  4. 進入主迴圈
    • 處理事件
    • 繪圖、顯示
  5. 程式結束、釋放 XrInstance 的資源

當然,這邊省略了不少東西,不過就先這樣講吧~

而這邊的這個範例程式,基本上不會碰到 XrSession 以及其後的東西,只是單純先透過 XrInstanceXrSystemId 的相關函式,來初步了解 OpenXR 的 API 風格、以及使用方法。

完整的範例程式,放在:https://github.com/KHeresy/OpenXR-Samples/tree/master/basic_info

要注意的是,這隻程式僅會透過 console 輸出一些 OpenXR 的資訊,並不會真正地啟動 XR 的裝置、或是畫出東西來

下面則先用文字簡單介紹一下 XrInstanceXrSystemId,之後再來講程式的細節。


XrInstance(官方文件

OpenXR 的「instatnce」是對應到系統當下的 OpenXR runtime、讓應用程式可以和 OpenXR runtime 溝通的主要物件;而之後要呼叫幾乎所有 OpenXR 的函式的時候,也都需要給一個有效的 instance 才行。

Instance 負責管理 OpenXR 的各種狀態。,在撰寫的 OpenXR 的程式的時候,一定要先建立一個 XrInstance 的物件;在規格上,OpenXR 允許一個程式內有多個 instance、但是實際上可允許的數量是由 OpenXR Runtime 決定的。

而要建立 XrInstance 的時候,是要使用 xrCreateInstance() 這個函式,要先決定要使用的「API Layer」和「Extension」。


XrSystemId(官方文件

OpenXR 的「system」在概念上,是對應到系統中的「整套硬體裝置」,如果以一般的 VR 環境來說,就是包含了頭戴式顯示器、以及控制器了。

而由於 OpenXR 提供的是跨 AR/VR 的架構,所以他把系統分成了兩種「Form Factor」:

  • XR_FORM_FACTOR_HEAD_MOUNTED_DISPLAY:頭戴式顯示器
  • XR_FORM_FACTOR_HANDHELD_DISPLAY:手持式裝置(例如手機)

在要取透過既有的 OpenXR instance 取得可用的 system 的時候,必須要指定要使用哪一個類型。

之後則是要透過 XrSystemId 來做進一步的資料取得、並建立 XrSession 來進行繪製、主迴圈的控制;不過這篇還不會繼續提到 XrSession 相關的內容就是了。


接下來,就是實際程式的部分了。

完整的程式碼請參考 https://github.com/KHeresy/OpenXR-Samples/tree/master/basic_info 這個檔案,這邊則是抽一些東西出來講。

首先,如同前面所說,要撰寫 OpenXR 的程式、一定要先建立一個 XrInstance 的物件出來,而需要使用的函式,則是 xrCreateInstance()官方文件)。

這個函式需要兩個參數,第一個是型別為 XrInstanceCreateInfo 的資料(參考)、用來描述要建立的 instance 的參數;第二個參數則是 XrInstance 的指標,用來儲存建立好的 instance。

所以實際上,大概會是下面的形式:

XrInstance gInstance;
XrInstanceCreateInfo infoCreate;
XrResult eRS = xrCreateInstance(&infoCreate, &gInstance);

他回傳的結果會是 XrResult 這個型別(文件),其值是 0 以上(包含 0)都算成功(不過大於 0 的也都有特殊意義),小於 0 才算失敗。


XrInstanceCreateInfo 以及OpenXR 結構的概念

而在 XrInstanceCreateInfo 的部分,它的定義是:

typedef struct XrInstanceCreateInfo {
     XrStructureType             type;
     const void* XR_MAY_ALIAS    next;
     XrInstanceCreateFlags       createFlags;
     XrApplicationInfo           applicationInfo;
     uint32_t                    enabledApiLayerCount;
     const char* const*          enabledApiLayerNames;
     uint32_t                    enabledExtensionCount;
     const char* const*          enabledExtensionNames; } XrInstanceCreateInfo;

這邊剛好可以大概說明一下 OpenXR 這類的結構的設計。

首先,OpenXR 的許多結構,都會有「type」和「next」這兩個成員,他們的意義基本上是共通的。

type」的型別是 XrStructureType 成員,代表這個結構的型別;以 XrInstanceCreateInfo 來說,他的值一定要是 XR_TYPE_INSTANCE_CREATE_INFO

老實說,個人覺得這邊的設計相當繁瑣…因為在許多場合,使用者必須要手動指定他的值
明明已經很明確地宣告了一個型別是 XrInstanceCreateInfo 的變數了,卻還需要手動去設定它的成員變數、來定他的型別…這部分個人真的不太能理解 OpenXR 在幹嘛… orz

而「next」則是一個沒有指定型別的指標(const void*),在 OpenXR 的架構下,被稱為「Structure Pointer Chains」。

它的用處基本上是必要的時候,可以指到其他結構、作為超出原始設計的資料擴充方式,通常是給 extension 用的;而沒有特殊狀況,應該就是給他 nullptr 了。

上面兩個是 OpenXR 結構通用的部分,剩下的則是 XrInstanceCreateInfo 自己的資料。

  • createFlags 是建立 instance 時的 bitmask,現階段應該是一定是 0。
  • applicationInfo 則是這個應用程式的一些資訊(名稱、版本),其型別是 XrApplicationInfo文件);比較重要的是裡面還要指定要使用的 OpenXR API 版本。
  • enabledApiLayerCountenabledApiLayerNames 是一組的,代表了要啟用的 API Layer 的數量、以及對應的名稱字串的陣列。
  • enabledExtensionCountenabledExtensionNames 是一組的,代表了要啟用的 extension 的數量、以及對應的名稱字串的陣列。

至於有哪些 API Layer 和 entension 可以用呢?OpenXR 提供了 xrEnumerateApiLayerProperties()xrEnumerateInstanceExtensionProperties() 這些函式,會把可以用的資料列舉。

OpenXR 的列舉(enumerate)函式的介面,基本上至少都會有 capacity(配置好的記憶體容量)、count(實際數量)、配置好用來寫入資料的記憶體空間三種參數;在使用的邏輯上、應該都算是要執行兩次的:

  • 第一次是把 capacity 給 0、記憶體空間指標給 nullptr,讓函式告訴我們實際有多少筆資料
  • 根據回傳的數量、配置好記憶體空間後、第二次呼叫、真正地去取得資料

列舉 API Layer

以 API Layer 來說,程式碼大概會長得像下面這樣子:

std::vector<XrApiLayerProperties> vAPIs;

// Get Numbers of API layers
uint32_t uAPILayerNum = 0;
if (xrEnumerateApiLayerProperties(0, &uAPILayerNum, nullptr)==XR_SUCCESS)
{
  std::cout << " > Found " << uAPILayerNum << " API layers\n";
  if (uAPILayerNum > 0)
  {
    // enumrate and output API layer information
    vAPIs.resize(uAPILayerNum, { XR_TYPE_API_LAYER_PROPERTIES });
    if (xrEnumerateApiLayerProperties(uAPILayerNum, &uAPILayerNum, vAPIs.data()) == XR_SUCCESS)
    {
      // OK
    }
  }
}

在上面的程式碼中,第一次執行 xrEnumerateApiLayerProperties() 的目的,就是要求 OpenXR 告訴我們它有提供幾個 API Layer、把數值寫到 uAPILayerNum

之後,則是透過 std::vector<XrApiLayerProperties>、配置一塊大小符合的記憶體空間(vAPIs),然後再次執行 xrEnumerateApiLayerProperties() 讓 OpenXR 把資料寫入配置好記憶體空間中。

不過這邊要注意的是,XrApiLayerProperties 這個結構裡面也有 type 這個成員,而且預設是沒有指定值的;所以在配置記憶體空間的時候,一定要將 type 設定為 XR_TYPE_API_LAYER_PROPERTIES,否則之後 OpenXR 可能因為 type 不對、而無法正確執行函式。這也是前面 Heresy 說過,覺得 OpenXR 很討厭的點之一。

而目前的 OpenXR runtime 似乎都沒有真的支援 API layer,所以回傳的數量(uAPILayerNum)都會是 0,所以其實這部分的程式碼在現階段其實似乎算是沒有用的。


列舉 Extension

在 extension 的部分,基本上使用的形式是和 API Layer 是類似的,不過 xrEnumerateInstanceExtensionProperties() 這個函式的第一個參數是 layerName,代表是要從哪個 API Layer 中去找 extension。

而由於目前沒有可以用的 API Layer,所以這邊就直接給 nullptr 就可以了;之後如果有要使用 API layer 的話,則是可以從前一段的部分來取得可用的 API layer 名稱。

下面就是取得 extension 的程式碼:

std::vector<XrExtensionProperties> vSupportedExt;

// get numbers of supported extensions
uint32_t uExtensionNum = 0;
if (xrEnumerateInstanceExtensionProperties(nullptr, 0, &uExtensionNum, nullptr) == XR_SUCCESS)
{
  std::cout << " > Found " << uExtensionNum << " extensions\n";
  if (uExtensionNum > 0)
  {
    // enumrate and output extension information
    vSupportedExt.resize(uExtensionNum, { XR_TYPE_EXTENSION_PROPERTIES });
    if (xrEnumerateInstanceExtensionProperties(nullptr, uExtensionNum, &uExtensionNum, vSupportedExt.data())==XR_SUCCESS)
    {
      // OK
    }
  }
}

在 Heresy 這邊使用 SteamVR Beta 版的環境下,是得到下列的 extension:

XR_KHR_vulkan_enable (ver 6)
XR_KHR_D3D11_enable (ver 4)
XR_KHR_D3D12_enable (ver 6)
XR_KHR_opengl_enable (ver 8)
XR_KHR_visibility_mask (ver 2)
XR_KHR_win32_convert_performance_counter_time (ver 1)
XR_EXT_debug_utils (ver 3)

在不同的 OpenXR Runtime,可能會有不同的結果。


建立 Instance

接下來,則是要準備 XrInstanceCreateInfo 的資料、來真的建立 instance 了。

不過,由於 XrInstanceCreateInfo 中需要 extension 資訊是字串陣列,所以這邊還需要把自己需要的 extension 的名稱、合併為一個字串陣列。

這邊的做法是參考微軟的範例,透過 lambda function 來檢查、新增;這邊的程式碼寫成:

// setup required extensions
std::vector<const char*> vExtList;
auto addExtIfExist = [&vSupportedExt,&vExtList](const char* sExtName) {
  for (const auto& rExt : vSupportedExt)
  {
    if (strcmp( rExt.extensionName, sExtName) == 0)
    {
      vExtList.push_back(sExtName);
      return;
    }
  }
};
addExtIfExist("XR_KHR_opengl_enable");
addExtIfExist("XR_KHR_visibility_mask");

之後就可以拿 vExtList 來用了。

之後如果有 API Layer 可以用的話,應該也可以用同樣的寫法。

XrInstanceCreateInfo 的準備,這邊則是寫成:

XrInstanceCreateInfo infoCreate;
infoCreate.type = XR_TYPE_INSTANCE_CREATE_INFO;
infoCreate.next = nullptr;
infoCreate.createFlags = 0;
infoCreate.applicationInfo = { "TestApp", 1, "TestEngine", 1, XR_CURRENT_API_VERSION };
infoCreate.enabledApiLayerCount = 0;
infoCreate.enabledApiLayerNames = {};
infoCreate.enabledExtensionCount = (uint32_t)vExtList.size();
infoCreate.enabledExtensionNames = vExtList.data();

其中,applicationInfo 的相關資訊,可以視自己的需求修改。

之後,則就可以呼叫 xrCreateInstance() 來建立 instance 了。

XrInstance gInstance;
if (xrCreateInstance(&infoCreate, &gInstance) == XR_SUCCESS)
{
  //OK
}

gInstance 這個 XrInstance 的物件,在幾乎所有 OpenXR 的函式都會需要,所以如果不考慮架構、或是不想設計成物件導向的話,把它宣告成 global 物件應該會比較方便。

之後如果有需要,也可以透過 xrGetInstanceProperties() 這個函式來取得 instance 的一些簡單的資訊。

XrInstanceProperties mProp{ XR_TYPE_INSTANCE_PROPERTIES };
if (xrGetInstanceProperties(gInstance, &mProp) == XR_SUCCESS)
{
  //OK
}

在 Heresy 這邊,可以看到 instance 對應到的 runtime 的名稱是「SteamVR/OpenXR」,版本則是「0.1.0」。

而當最後、不需要使用 OpenXR 的環境的時候,則就需要呼叫 xrDestroyInstance() 這個函式,來釋放相關的資源了。


OpenXR System

建立好 OpenXR 的 instance 後,接下來就是要取得 OpenXR Runtime 提供的 system 了。

這邊基本上就是透過呼叫 xrGetSystem() 來取得一個可以用的 system 的 ID、型別是 XrSystemId。而這邊也需要準備一個 XrSystemGetInfo、來告訴 OpenXR 要取得怎樣的 system。

在目前來說,這邊主要就是前面提到的兩種「Form Factor」:

  • XR_FORM_FACTOR_HEAD_MOUNTED_DISPLAY:頭戴式顯示器
  • XR_FORM_FACTOR_HANDHELD_DISPLAY:手持式裝置(例如手機)

下面就是這邊的程式:

XrSystemId mSysId = XR_NULL_SYSTEM_ID;
XrSystemGetInfo vSysGetInfo = {  XR_TYPE_SYSTEM_GET_INFO, nullptr, XR_FORM_FACTOR_HEAD_MOUNTED_DISPLAY };
if (xrGetSystem(gInstance, &vSysGetInfo, &mSysId) == XR_SUCCESS)
{
  //OK
}

而 system 的部分,也可以透過 xrGetSystemProperties() 這個函式,來取得進一步的資訊;這邊可以取得的資料型別是 XrSystemProperties文件),資訊算是比 instance 多了不少。

XrSystemProperties mSysProp{ XR_TYPE_SYSTEM_PROPERTIES };
if (xrGetSystemProperties(gInstance, mSysId, &mSysProp) == XR_SUCCESS)
{
  //OK
}

以 Heresy 這邊用 SteamVR Beta 板搭配 Valve Index 來說,讀取到的資訊大致上如下:

- SteamVR/OpenXR : lighthouse (10462)
     - Graphics: 2468 * 2740 with 16 layer
     - Tracking:

個人覺得比較奇怪的,是 trackingPropertiesorientationTrackingpositionTracking 都是 false,讓人有點疑惑…


列舉 View Configuration

在取得 System 的 ID 後,則還可以透過 xrEnumerateViewConfigurations() 這個函式,來確認目前的 OpenXR 系統有哪些顯示模式。

這部分的程式,可以寫成:

uint32_t uViewConfNum = 0;
if (xrEnumerateViewConfigurations(gInstance, mSysId, 0, &uViewConfNum, nullptr) == XR_SUCCESS)
{
  if (uViewConfNum > 0)
  {
    std::vector<XrViewConfigurationType> vViewConf(uViewConfNum);
    if (xrEnumerateViewConfigurations(gInstance, mSysId, uViewConfNum, &uViewConfNum, vViewConf.data()) == XR_SUCCESS)
    {
      //OK
    }
  }
}

目前 OpenXR 定義的 View Configuration 有四種類型:

  • XR_VIEW_CONFIGURATION_TYPE_PRIMARY_MONO
  • XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO
  • XR_VIEW_CONFIGURATION_TYPE_PRIMARY_QUAD_VARJO
  • XR_VIEW_CONFIGURATION_TYPE_SECONDARY_MONO_FIRST_PERSON_OBSERVER_MSFT

其中,XR_VIEW_CONFIGURATION_TYPE_PRIMARY_MONO 主要是對應手機 AR 這類單一螢幕的類型,而一般的 VR 頭戴式顯示器則會是 XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO,要繪製的視角會有左右兩眼。

另外兩個,則是針對較為特殊的硬體設計的特別類型。

XR_VIEW_CONFIGURATION_TYPE_PRIMARY_QUAD_VARJO  應該是用來對應 VARJO VR-2 Pro(官網)這類型的複合顯示面板用的,所以需要四個視角(參考)。

XR_VIEW_CONFIGURATION_TYPE_SECONDARY_MONO_FIRST_PERSON_OBSERVER_MSFT 的話,應該是微軟的系統給外部顯示用的 2D 畫面?(參考
但是個人以 Windows MR 測試,好像也沒有這項功能?

總之,在 SteamVR 的系統上,會得到 XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO 這個模式,他會有兩個要繪製的視角。

當要使用的 View Configuration 類型確定後,接下來則可以透過下列函式,來取得進一步的資訊:

  • xrGetViewConfigurationProperties()
  • xrEnumerateViewConfigurationViews()
  • xrEnumerateEnvironmentBlendModes()

首先,xrGetViewConfigurationProperties() 能拿到的資料是 XrViewConfigurationProperties文件),裡面唯一的資訊,應該就是 FoV 是否可以由應用程式控制。

xrEnumerateViewConfigurationViews() 則是用來列舉出目前的 View Configuration 要繪製的視角資訊,其型別是 XrViewConfigurationView文件),裡面的資訊包括了建議的畫面大小、以及最大可接受的畫面大小,以及 Swapchain sample count。

最後的 xrEnumerateEnvironmentBlendModes() 則是用來確認 OpenXR 系統的 blend 模式用的,其型別是 XrEnvironmentBlendMode文件);VR 系統正常會是 XR_ENVIRONMENT_BLEND_MODE_OPAQUE,AR 系統則可能會是 XR_ENVIRONMENT_BLEND_MODE_ADDITIVEXR_ENVIRONMENT_BLEND_MODE_ALPHA_BLEND

而這部分的程式碼,基本上算是大同小異,就請接看 GitHub 上的檔案吧。


整個完整的範例程式放在:https://github.com/KHeresy/OpenXR-Samples/tree/master/basic_info

這邊比較不一樣的是,在取得 XrSystemId 的時候,這邊是兩種 form factor 都會去試試看;再來,就是檔案最前面宣告了一些輔助用的函式了。

而在 Heresy 這邊 SteamVR 1.13.10 Beta + Valve Index 的系統上,執行的結果是:

Try to get API Layers:
> Found 0 API layers

Try to get supported entensions:
> Found 7 extensions
  - XR_KHR_vulkan_enable (ver 6)
  - XR_KHR_D3D11_enable (ver 4)
  - XR_KHR_D3D12_enable (ver 6)
  - XR_KHR_opengl_enable (ver 8)
  - XR_KHR_visibility_mask (ver 2)
  - XR_KHR_win32_convert_performance_counter_time (ver 1)
  - XR_EXT_debug_utils (ver 3)

Create OpenXR instance
> prepare instance create information
> create instance
> get instance information
   - SteamVR/OpenXR (0.1.0)

Get systems
> Try to get system 1
   > get system information
    - SteamVR/OpenXR : lighthouse (10462)
     - Graphics: 2468 * 2740 with 16 layer
     - Tracking:
> Try to get system 2
   => Error: XR_ERROR_FORM_FACTOR_UNSUPPORTED

Enumerate View Configurations for system 1153152780005802223
  - XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO
   > 2 views:
     - 2468 * 2740 (MAX: 2468 * 2740) [sample: 1(1)]
     - 2468 * 2740 (MAX: 2468 * 2740) [sample: 1(1)]
   > 1 blend modes:
     - XR_ENVIRONMENT_BLEND_MODE_OPAQUE

這邊應該就把 OpenXR 可以透過 Instance 和 system 能到的資料,都輸出出來了~


OpenXR 相關文章目錄

對「OpenXR 程式開發:初始環境設定」的想法

  1. 请问Heresy有将OpenXRAPI添加到现有开源软件,从而实现软件可在AR设备上运行的经验或者范例吗?期待你的回复~

    • Heresy 自己也剛開始摸而已。
      這邊也要看你要用的軟體本來是怎麼寫的了,整體來說,要改也有一定的門檻。

發表迴響

在下方填入你的資料或按右方圖示以社群網站登入:

WordPress.com 標誌

您的留言將使用 WordPress.com 帳號。 登出 /  變更 )

Google photo

您的留言將使用 Google 帳號。 登出 /  變更 )

Twitter picture

您的留言將使用 Twitter 帳號。 登出 /  變更 )

Facebook照片

您的留言將使用 Facebook 帳號。 登出 /  變更 )

連結到 %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.