之前在《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 的程式的時候,在外部主要的流程大致上會是:
- 建立 XrInstance 的物件
- 取得 XrSystemId、確認可存取 XR 系統
- 建立 XrSession、準備開始 XR 系統的操作
- 進入主迴圈
- 處理事件
- 繪圖、顯示
- 程式結束、釋放 XrInstance 的資源
當然,這邊省略了不少東西,不過就先這樣講吧~
而這邊的這個範例程式,基本上不會碰到 XrSession 以及其後的東西,只是單純先透過 XrInstance、XrSystemId 的相關函式,來初步了解 OpenXR 的 API 風格、以及使用方法。
完整的範例程式,放在:https://github.com/KHeresy/OpenXR-Samples/tree/master/basic_info。
要注意的是,這隻程式僅會透過 console 輸出一些 OpenXR 的資訊,並不會真正地啟動 XR 的裝置、或是畫出東西來。
下面則先用文字簡單介紹一下 XrInstance 和 XrSystemId,之後再來講程式的細節。
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 版本。
- enabledApiLayerCount 和 enabledApiLayerNames 是一組的,代表了要啟用的 API Layer 的數量、以及對應的名稱字串的陣列。
- enabledExtensionCount 和 enabledExtensionNames 是一組的,代表了要啟用的 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 layersn"; 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 << " extensionsn"; 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:
個人覺得比較奇怪的,是 trackingProperties 的 orientationTracking 和 positionTracking 都是 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_ADDITIVE 或 XR_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 layersTry 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_UNSUPPORTEDEnumerate 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 能到的資料,都輸出出來了~