OpenXR 程式開發:簡單的顯示架構(part 1)


恩,在之前寫完《OpenXR 程式開發:初始環境設定》這篇後,拖得有點久,終於生出這篇來了。

會拖這麼久的原因,一方面是因為中間跑去做其他事,另一方面則是在學怎麼寫的時候,又踩到地雷了…而搞到現在,也只能算是寫出一個勉強可以動的範例了。

由於 Heresy 的目標是寫一個 Windows 下可以使用 OpenGL 來繪製的 OpenXR 程式,所以…很多網路上的範例(例如微軟的範例就是 Direct 3D 的)都不能用。

而本來是想參考 Bradley Austin Davis 的「sdl2_gl_single_file_example_c.cpp」的這個範例(連結),但是真的弄下去才發現他沒辦法動…而且感覺應該是有缺東西…

後來是改成試著把 monado 給 Linux 用的「openxr-simple-example」(連結)這個範例改成 Windows 版(參考),才能繼續下去… orz

總之,接下來就是來大概紀錄一下,目前寫出來最簡單的 Windows 環境下的 OpenGL + OpenXR 範例、glutCube 吧~


專案配置

這個範例專案放在:

https://github.com/KHeresy/OpenXR-Samples/tree/master/glutCube

除了 OpenXR Loader 外,還需要 GLEW 和 freeglut,可以直接透過跟目錄的「3rdPartyLibs.ps1」來自動下載、解壓縮到指定的地方。

裡面的程式碼只有兩個檔案,一個是包含主程式在內的「glutCube.cpp」(連結)、另一個則是包含所有 OpenXR 程式的「OpenXRGL.h」(連結)。

其中,「glutCube.cpp」這個檔案,是以 OpenGL 官網上的範例(連結)、「cube.c」(連結)這個簡單的 glut 範例修改而來的。

會選這種範例的原因,主要就是可以省去解釋 OpenGL 的內容;實際上現在要寫 OpenGL 的程式的話,應該還是用 shader-base 的 OpenGL 會比較好。

而在「OpenXRGL.h」內,則是定義了一個 COpenXRGL 的類別,把所有 OpenXR 相關的操作、都包在裡面了;理論上,比較簡單的 OpenGL 程式,應該都可以透過這個 header 檔案,透過簡單的修改來讓既有的程式支援 OpenXR。


主程式的部分

在主程式(glutCube.cpp)的部分,這邊主要是宣告了一個 COpenXRGL 的全域變數 gXRGL,負責所有 OpenXR 相關的操作。

而要呼叫 COpenXRGL 的函式的部分,主要包含了:

  • 在進入主迴圈開始繪製前,要呼叫 COpenXRGL::init()、進行相關的初始化。
    由於這邊也會建立 OpenGL 的 frame buffer,所以必須要在 OpenGL 初始化後才能呼叫。

  • 為了讓他會固定重繪,加入 glutIdleFunc()、讓他有空就去呼叫 glutPostRedisplay()

  • 本來的 display() 要做對應的修改:
    • 每次重繪前要先呼叫 COpenXRGL::processEvent()、處理 OpenXR 的事件
    • 要把本來的繪製的程式碼、透過 COpenXRGL::draw() 來做處理。
    • 要在每次畫面都使用新的 projection、model view matrix,所以要把相關程式碼移到這裡。

目前的 display() 內容則如下:

void display(void)
{
  gXRGL.processEvent();
  gXRGL.draw(
    [](const COpenXRGL::TMatrix& matProj, const COpenXRGL::TMatrix& matModelView) {
      glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

      /* Setup the view of the cube. */
      glMatrixMode(GL_PROJECTION);
      glLoadIdentity();
      glMultMatrixf(matProj.data());
 
      glMatrixMode(GL_MODELVIEW);
      glLoadIdentity();
      glMultMatrixf(matModelView.data());
 
      //glTranslatef(0.0, 0.0, -1.0);
      //glRotatef(60, 1.0, 0.0, 0.0);
    //glRotatef(-20, 0.0, 0.0, 1.0);

     drawBox();
    }
  ); glutSwapBuffers(); }

這邊是把用來繪製的程式碼,以 lambda expression 的形式傳遞給 COpenXRGL::draw()、讓他在裡面呼叫。

而這段程式每次更新畫面時都會被呼叫兩次,一次是左眼的畫面、一次是右眼的畫面;兩次做的事完全相同,只有傳進來的矩陣是不同的。


COpenXRGL 的 Header file

而在 COpenXRGL 中,如果是要在 Windows + OpenGL 環境下使用 OpenXR 的話,光是 include openxr.h 是不夠的。

這邊會建議寫成:

// Windows Header
#include <Windows.h>
 
// OpenXR
#define XR_USE_GRAPHICS_API_OPENGL
#define XR_USE_PLATFORM_WIN32
#include <openxr/openxr_platform.h>
#include <openxr/openxr.h>

定義 XR_USE_GRAPHICS_API_OPENGL 是告訴 OpenXR 這個程式是要使用 OpenGL,如果是要用 D3D 或 Vulkan 就要做對應的修改。

而定義 XR_USE_PLATFORM_WIN32 則是因為 OpenXR 在某些地方、針對不同的系統平台會有不同的物件型別(主要是 graphics binding 的部分),所以也必須要明確地告知 OpenXR 要使用哪一個系統平台;老實說,這點也是 Heresy 覺得比較討厭的事。

為了之後方便起見,這邊也需要另外 include openxr_platform.h 這個檔案,這樣才可以直接使用這些和系統相關的型別(基本上是 XrGraphicsBindingOpenGLWin32KHR)。

需要 Windows.h 是因為這邊系統相關的型別會需要使用 Windows SDK 環境定義的型別(HDCHGLRC),所以也需要加進來。


OpenXR 初始化

接下來,要初始化 OpenXR 相關環境的時候,這邊是設計成呼叫 COpenXRGL::init() 就好了,而它裡面實際的內容,則是拆分成好幾個函式:

bool init()
{
  useExtension(XR_KHR_OPENGL_ENABLE_EXTENSION_NAME);
  if (createInstance() &&
    getSystem() &&
    createSession() &&
    createReferenceSpace() &&
    checkViewConfiguration() &&
    createSwapChain() &&
    prepareCompositionLayer() ) { return createFrameBubber(); } return false; }

這邊很多內容在《OpenXR 程式開發:初始環境設定》這篇都已經介紹過了,所以在這邊應該會跳過不少,如果還沒看過的話,請先回去看這篇。

首先,在建立 OpenXR 的 instance 之前,要先設定要使用的 API Layer 和 extension;這邊是不指定 API Layer、extension 也只加入 XR_KHR_OPENGL_ENABLE_EXTENSION_NAME、告訴 OpenXR 這邊要使用 OpenGL。如果有需要使用其他 extension,也可以在呼叫 init() 前透過 useExtension() 這個函式來加入。

之後,則就是依序呼叫:

  • createInstance(): 建立符合需求的 OpenXR instance
  • getSystem():取得 OpenXR Instance 的系統 
  • createSession():建立 OpenXR 的工作階段
  • createReferenceSpace():建立 OpenXR 的參考空間系統
  • checkViewConfiguration():取得 OpenXR 的顯示相關設定。
  • createSwapChain():建立 swapchain 相關的物件 
  • prepareCompositionLayer():建立 composition layer 的相關資訊

其中,紅色的部分是之前沒有提到的東西。


XrSession

createSession() 這個函式裡面,基本上是要建立出 XrSession 這個管理 OpenXR 工作階段的物件、m_xrSession;函式的內容如下:

bool createSession()
{
  // Graphics requirements
  XrGraphicsRequirementsOpenGLKHR reqOpenGL{ XR_TYPE_GRAPHICS_REQUIREMENTS_OPENGL_KHR };

  // Magic code block....
  // link error if call xrGetOpenGLGraphicsRequirementsKHR() directlly
  PFN_xrGetOpenGLGraphicsRequirementsKHR func;
  if (check(xrGetInstanceProcAddr(m_xrInstance, "xrGetOpenGLGraphicsRequirementsKHR", (PFN_xrVoidFunction*)&func),"xrGetInstanceProcAddr"))
  {
    if (check(func(m_xrInstance, m_xrSystem, &reqOpenGL),"PFN_xrGetOpenGLGraphicsRequirementsKHR"))
    {
      XrGraphicsBindingOpenGLWin32KHR gbOpenGL{ XR_TYPE_GRAPHICS_BINDING_OPENGL_WIN32_KHR , nullptr, wglGetCurrentDC(), wglGetCurrentContext() };
      XrSessionCreateInfo infoSession{ XR_TYPE_SESSION_CREATE_INFO, &gbOpenGL, 0, m_xrSystem };
      return check(xrCreateSession(m_xrInstance, &infoSession, &m_xrSession),"xrCreateSession");
    }
  }
  return false;
}

這邊比較麻煩的,是在建立 XrSession 的時候,除了要知道既有的 XrInstance 外,也需要準備好 XrSessionCreateInfo 的資料(官方文件);而其中,他的 next 會需要是 OpenXR 所定義的 graphics binging 的物件指標。

根據所使用的環境、圖形 API 的不同,他會視不同的型別。在這邊因為是要使用 Windows + OpenGL,所以需要的是 XrGraphicsBindingOpenGLWin32KHR官方文件),而如果是使用 Linux 的 X11 的話,則可以使用 XrGraphicsBindingOpenGLXcbKHRXrGraphicsBindingOpenGLXlibKHR;其他像是要使用 Direct3D 或 Vulkan 的話,也各有各自的型別…

而在定義 XrGraphicsBindingOpenGLWin32KHR 時,也還需要 Windows 的硬體裝置 handle(HDC)以及 Windows 的 OpenGL context(HGLRC);這邊都可以透過 wgl 的函式來取得。

不過個人覺得比較麻煩的,是這邊還需要先設定 XrGraphicsRequirementsOpenGLKHR官方文件);如果沒有進行這一部分的話,session 的建立是會失敗的。

在處理的時候,這邊會需要呼叫 xrGetOpenGLGraphicsRequirementsKHR() 這個函式;但是這個函式雖然定義在 openxr_platform.h 這個檔案裡,但是卻沒有實體、沒辦法直接連結到。

所以要使用的時候,變成需要手動透過 xrGetInstanceProcAddr()官方文件)來取得這個函式的 function pointer、才能拿來呼叫。


參考空間

接下來,則是 createReferenceSpace() 的部分。

這部分算是相對單純,就是先準備 XrReferenceSpaceCreateInfo官方文件)、來讓 xrCreateReferenceSpace() 使用、然後建立出參考空間 m_xrSpace

bool createReferenceSpace()
{
  XrReferenceSpaceCreateInfo infoRefSpace{ XR_TYPE_REFERENCE_SPACE_CREATE_INFO, nullptr, XR_REFERENCE_SPACE_TYPE_LOCAL };
  infoRefSpace.poseInReferenceSpace.orientation = { 0,0,0,-1 };
  infoRefSpace.poseInReferenceSpace.position = { 0,0,0 };
  return check(xrCreateReferenceSpace(m_xrSession, &infoRefSpace, &m_xrSpace), "xrCreateReferenceSpace");
}

而這邊要注意的是,他的型別會是 XR_REFERENCE_SPACE_TYPE_LOCAL,然後要指定 poseInReferenceSpace 作為建立出來的空間原點的參考。


View Configuration Views

checkViewConfiguration() 的部分,算是之前提過的內容。

而由於這邊僅針對 XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO 做處理,所以就可以略過之前 xrEnumerateViewConfigurations() 的部分。

理論上由於是兩眼的立體顯示,所以會有兩個 view。

而為了方便後續的資料管理,這裡定義了一個 SViewData 的結構,用來保存兩眼各自的資料:

struct SViewData
{
  XrSwapchain  m_xrSwapChain;
  GLuint    m_glFrameBuffer = 0;
  std::vector<XrSwapchainImageOpenGLKHR>  m_vSwapchainImages;
};

Swapchain

createSwapChain() 這個函式中,主要是針對左右眼的兩個 view、各自去產生 XrSwapchain、並取得對應的 swapchain image。

下面就是這個函式的內容:

bool createSwapChain()
{
  XrSwapchainCreateInfo infoSwapchain;
  infoSwapchain.type = XR_TYPE_SWAPCHAIN_CREATE_INFO;
  infoSwapchain.next = nullptr;
  infoSwapchain.createFlags = 0;
  infoSwapchain.usageFlags = XR_SWAPCHAIN_USAGE_TRANSFER_DST_BIT;
  infoSwapchain.format = (int64_t)GL_SRGB8_ALPHA8;
  infoSwapchain.sampleCount = 1;
  infoSwapchain.width = m_vViews[0].recommendedImageRectWidth;
  infoSwapchain.height = m_vViews[0].recommendedImageRectHeight;
  infoSwapchain.faceCount = 1;
  infoSwapchain.arraySize = 1;
  infoSwapchain.mipCount = 1;

  bool bOK = true;
  for (auto& rVData : m_vViewDatas)
  {
    if (check(xrCreateSwapchain(m_xrSession, &infoSwapchain, &rVData.m_xrSwapChain),"xrCreateSwapchain"))
    {
      uint32_t uSwapchainNum = 0;
      if (check(xrEnumerateSwapchainImages(rVData.m_xrSwapChain, uSwapchainNum, &uSwapchainNum, nullptr),"xrEnumerateSwapchainImages-1") && uSwapchainNum > 0)
      {
        rVData.m_vSwapchainImages.resize(uSwapchainNum, { XR_TYPE_SWAPCHAIN_IMAGE_OPENGL_KHR });
        if (!check(xrEnumerateSwapchainImages(rVData.m_xrSwapChain, uSwapchainNum, &uSwapchainNum, (XrSwapchainImageBaseHeader*)rVData.m_vSwapchainImages.data()),"xrEnumerateSwapchainImages-2"))
          {
            bOK = false;
            break;
          }
} else { bOK = false; break; } } else { bOK = false; break; } } return bOK; }

這邊是會先建立一個 XrSwapchainCreateInfo 的物件 infoSwapchain、代表要產生的 swapchain 的資料;由於左右兩眼基本上會一致,所以這邊就共用了。

之後,則是針對兩眼的 SViewData、各自使用 xrCreateSwapchain() 來建立 XrSwapchain,然後再呼叫 xrEnumerateSwapchainImages()、列舉出對應的 swapchain image。

要注意的是,由於這邊是使用 OpenGL,所以實際上的 swapchain image 的型別是 XrSwapchainImageOpenGLKHR;而如果是使用 Direct3D 或 Vulkan 的話,也都必須把型別改掉。

但是為了統一介面,xrEnumerateSwapchainImages() 需要的是 XrSwapchainImageBaseHeader,所以在乎叫的時候會需要做型別的轉換。


Composition Layer

OpenXR 初始化的最後,則是要準備 composition layer 的資訊,告訴 OpenXR 要怎麼處理畫面。

bool prepareCompositionLayer()
{
  m_vProjectionLayerViews.resize(m_vViewDatas.size());
  for (int i = 0; i < m_vViewDatas.size(); ++i)
  {
    m_vProjectionLayerViews[i].type = XR_TYPE_COMPOSITION_LAYER_PROJECTION;
    m_vProjectionLayerViews[i].next = nullptr;
    m_vProjectionLayerViews[i].subImage.imageArrayIndex = 0;
    m_vProjectionLayerViews[i].subImage.swapchain = m_vViewDatas[i].m_xrSwapChain;
    m_vProjectionLayerViews[i].subImage.imageRect.extent = { (int32_t)m_vViews[0].recommendedImageRectWidth, (int32_t)m_vViews[0].recommendedImageRectHeight };
  }

  XrCompositionLayerProjection* pProjectionLayer = new XrCompositionLayerProjection{ XR_TYPE_COMPOSITION_LAYER_PROJECTION, nullptr, 0, m_xrSpace,(uint32_t) m_vProjectionLayerViews.size(), m_vProjectionLayerViews.data() };
  m_vLayersPointers.push_back((XrCompositionLayerBaseHeader*)pProjectionLayer);

  return true;
}

這邊要先準備一個 XrCompositionLayerProjectionView 的陣列 m_vProjectionLayerViews、然後填入對應的資料(要指定對應的 swapchain)。

之後,則是要建立 XrCompositionLayerProjection 的物件,然後放到 XrCompositionLayerBaseHeader 的陣列(m_vLayersPointers),在之後繪製的時候會用到。

這邊由於是雙眼的投影畫面,所以型別都是 XR_TYPE_COMPOSITION_LAYER_PROJECTION;而如果 OpenXR runtime 有支援的話,OpenXR 其實也支援 XR_TYPE_COMPOSITION_LAYER_QUAD 這種,在空間中插入一個平面的 layer,如果拿來做圖形介面、可能就可以考慮了。


到上面為止,就算是把 OpenXR 的初始化都完成了。

不過由於之後還有要把 OpenGL 繪製的部分讀取出來,所以這邊還要準備 OpenGL 的 frame buffer;這部分的內容寫在 createFrameBubber() 裡,其內容如下:

bool createFrameBubber()
{
  for (auto& rVData : m_vViewDatas)
    glGenFramebuffers(1, &rVData.m_glFrameBuffer);

  return true;
}

這邊就先寫到這裡了。之後事件處理和繪製的部分,就請看下一篇吧。


OpenXR 相關文章目錄

對「OpenXR 程式開發:簡單的顯示架構(part 1)」的想法

發表迴響

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

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.