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

| | 0 Comments| 17:07
Categories:

這邊延續前面的 part 1,繼續來講 glutCube 這個範例的顯示的部分。

首先,glut 的 display() 函式內容如下:

void display(void)
{
  gXRGL.processEvent();
  gXRGL.draw([](const COpenXRGL::TMatrix& matProj, const COpenXRGL::TMatrix& matModelView) {
    //draw
  });
  glutSwapBuffers();
}

這邊會先去呼叫 COpenXRGLprocessEvent() 這個函式、進行 OpenXR 的事件處理;然後再透過 draw() 這個函式,來繪製左右兩眼的畫面。

而這邊真正的繪製內容,則是需要接收 projection、model view matrix,來針對每次的攝影機位置、投影矩陣(兩眼不同)來進行繪製。


OpenXR 事件處理

在事件處理的部分,processEvent() 的部分目前簡化如下:

void processEvent()
{
  while (true) {
    XrEventDataBuffer eventBuffer{ XR_TYPE_EVENT_DATA_BUFFER };
    auto pollResult = xrPollEvent(m_xrInstance, &eventBuffer);
    //check(pollResult, "xrPollEvent");
    if (pollResult == XR_EVENT_UNAVAILABLE) {
      break;
    }
    switch (eventBuffer.type) {
    case XR_TYPE_EVENT_DATA_SESSION_STATE_CHANGED:
    {
      m_xrState = reinterpret_cast<XrEventDataSessionStateChanged&>(eventBuffer).state;
      switch (m_xrState) {
      case XR_SESSION_STATE_READY:
        beginSession();
        break;
      case XR_SESSION_STATE_STOPPING:
        endSession();
        break;
      }
    }
    break;
    default:
      break;
    }
  }
}

要取得 OpenXR 的事件,是要透過 xrPollEvent() 這個函式,來取得 XrEventDataBuffer官方文件)的物件、eventBuffer

當確定 xrPollEvent() 有正確取得事件資料後,則是要透過 eventBuffer.type 來確認事件的類型,並進行轉型。

在這個例子裡,就只有先針對 XR_TYPE_EVENT_DATA_SESSION_STATE_CHANGED 這個類型、也就是針對 OpenXR session 的狀態變化來做處理;所以這邊也需要把 eventBuffer 轉型成 XrEventDataSessionStateChanged 來做資料的讀取。

如果之後需要處理其他類型的事件的話,則也需要轉換成其他的型別。

在這邊,則是會在 XR_SESSION_STATE_READY 的時候,去呼叫 beginSession()、來開始 OpenXR 的工作階段;在 XR_SESSION_STATE_STOPPING 的時候,去呼叫 endSession() 來停止 OpenXR 的工作階段。

而這兩個函式的內容則如下:

bool beginSession()
{
  XrSessionBeginInfo sbi{ XR_TYPE_SESSION_BEGIN_INFO, nullptr, XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO };
  return check(xrBeginSession(m_xrSession, &sbi), "xrBeginSession");
}
bool endSession()
{
  return check(xrEndSession(m_xrSession), "xrEndSession");
}

理論上 endSession() 裡面應該也會需要告知主程式要結束主迴圈、停止 OpenGL 的繪製才對,不過這邊為了簡化就沒處理了。


繪製的流程

在負責繪製的 draw() 這個函式的部分,他主要的流程大致上如下:

  • xrWaitFrame()
    • xrBeginFrame()
      • xrAcquireSwapchainImage()
      • xrWaitSwapchainImage()
      • xrReleaseSwapchainImage()
    • xrEndFrame()

這邊,首先會先針對前面 processEvent() 取得的 m_xrState 做判斷,只有在狀態是需要繪製的時候,才會進入繪製的流程。

template<typename FUNC_DRAW>
void draw(FUNC_DRAW func_draw)
{

switch (m_xrState) { case XR_SESSION_STATE_READY: case XR_SESSION_STATE_FOCUSED: case XR_SESSION_STATE_SYNCHRONIZED: case XR_SESSION_STATE_VISIBLE: // Render1 break; default: break; } }

而在上面的「// Render1」的部分,內容則如下:

XrFrameState frameState{ XR_TYPE_FRAME_STATE }; 
XrFrameWaitInfo frameWaitInfo{ XR_TYPE_FRAME_WAIT_INFO, nullptr };
if (XR_UNQUALIFIED_SUCCESS(xrWaitFrame(m_xrSession, &frameWaitInfo, &frameState))) { uint32_t uWidth = m_vViews[0].maxImageRectWidth,
           uHeight = m_vViews[0].maxImageRectHeight; XrFrameBeginInfo frameBeginInfo{ XR_TYPE_FRAME_BEGIN_INFO }; check(xrBeginFrame(m_xrSession, &frameBeginInfo),"xrBeginFrame"); // Update views if (frameState.shouldRender) { // render2 } // End frame XrFrameEndInfo frameEndInfo{ XR_TYPE_FRAME_END_INFO, nullptr, frameState.predictedDisplayTime, XR_ENVIRONMENT_BLEND_MODE_OPAQUE }; if (frameState.shouldRender) { frameEndInfo.layerCount = (uint32_t)m_vLayersPointers.size(); frameEndInfo.layers = m_vLayersPointers.data(); } check(xrEndFrame(m_xrSession, &frameEndInfo),"xrEndFrame"); }

在這邊會先呼叫 xrWaitFrame()官方文件)來告訴 OpenXR 要針對畫面做同步、準備繪製下一個畫面。

之後,則是呼叫 xrBeginFrame() 來開始繪製,在繪製結束後,則是要呼叫 xrEndFrame() 來結束繪製。

而在 frameState 裡面,則也會有一些資訊,包括這個畫面預測會顯示的時間、以及是否需要繪製等等,有需要的話可以拿來使用。

而在上面的「// render2」的部分,內容則如下:

XrViewState vs{ XR_TYPE_VIEW_STATE };
XrViewLocateInfo vi{ XR_TYPE_VIEW_LOCATE_INFO, nullptr, XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO, frameState.predictedDisplayTime, m_xrSpace };
uint32_t eyeViewStateCount = 0;
std::vector<XrView> eyeViewStates; check(xrLocateViews(m_xrSession, &vi, &vs, eyeViewStateCount, &eyeViewStateCount, nullptr),"xrLocateViews-1"); eyeViewStates.resize(eyeViewStateCount, { XR_TYPE_VIEW }); check(xrLocateViews(m_xrSession, &vi, &vs, eyeViewStateCount, &eyeViewStateCount, eyeViewStates.data()),"xrLocateViews-2"); for (int i = 0; i < eyeViewStates.size(); ++i) { // render each eye } glBlitNamedFramebuffer(m_vViewDatas[0].m_glFrameBuffer, // backbuffer (GLuint)0, // drawFramebuffer (GLint)0, // srcX0 (GLint)0, // srcY0 (GLint)uWidth, // srcX1 (GLint)uHeight, // srcY1 (GLint)0, // dstX0 (GLint)0, // dstY0 (GLint)uWidth, // dstX1 (GLint)uHeight, // dstY1 (GLbitfield)GL_COLOR_BUFFER_BIT, // mask (GLenum)GL_LINEAR); // filter

這部份第一段主要是透過 xrLocateViews() 這個函式(官方文件),來取的左右兩眼的 view 的相關資訊。

之後,則是透過 for 迴圈,來針對兩個 view 各自進行繪製。

當繪製完後,則是透過 glBlitNamedFramebuffer() 來將 view 0 的畫面複製到預設的 frame buffer 來,繪製在視窗上。(老實說,適不適合這樣寫不是很確定)

至於「// render each eye」的部分呢,內容則如下:

const XrView& viewStates = eyeViewStates[i];
m_vProjectionLayerViews[i].fov = viewStates.fov;
m_vProjectionLayerViews[i].pose = viewStates.pose;
uint32_t swapchainIndex;
XrSwapchainImageAcquireInfo ai{ XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO, nullptr };
check(xrAcquireSwapchainImage(m_vViewDatas[i].m_xrSwapChain, &ai, &swapchainIndex),"xrAcquireSwapchainImage");
XrSwapchainImageWaitInfo wi{ XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO, nullptr, XR_INFINITE_DURATION };
check(xrWaitSwapchainImage(m_vViewDatas[i].m_xrSwapChain, &wi),"xrWaitSwapchainImage");
{
  glViewport(0, 0, uWidth, uHeight);
  glScissor(0, 0, uWidth, uHeight);
  glBindFramebuffer(GL_FRAMEBUFFER, m_vViewDatas[i].m_glFrameBuffer);
  glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_vViewDatas[i].m_vSwapchainImages[swapchainIndex].image, 0);
  auto proj = createProjectionMatrix(viewStates.fov, 0.01, 1000);
  auto view = createModelViewMatrix(viewStates.pose);
  func_draw(proj, view);
  glBindFramebuffer(GL_FRAMEBUFFER, 0);
  glFinish();
}
XrSwapchainImageReleaseInfo ri{ XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO, nullptr };
check(xrReleaseSwapchainImage(m_vViewDatas[i].m_xrSwapChain, &ri),"xrReleaseSwapchainImage");

這邊會先更新 m_vProjectionLayerViews 的相關資料,在最後呼叫 xrEndFrame() 的時候會送給 OpenXR Runtime。

之後,則是要先透過 xrAcquireSwapchainImage() 來取得當下要使用的 swapchain image 的編號;再來,則是要透過 xrWaitSwapchainImage() 來確認 OpenXR 目前沒有在存取這張影像。

等到要繪製的時候,則是要先 bind frame buffer,然後再呼叫 glFramebufferTexture2D(),讓 OpenGL 把東西畫到 OpenXR 的 swapchain image 上。

而不同 view(兩眼)畫面的差別,就在於攝影機位置、以及投影矩陣的差異;這邊會把 viewStates.fov 換算成投影矩陣、把 viewStates.pose 換算成 model view 矩陣後,傳給 func_draw() 這個實際繪製用的函式、讓他根據這兩個矩陣來繪製。

其中,pose 的型別是 XrPosef,裡面是以 quaternion 的形式來儲存方向/角度的資訊、以 vector 的形式來儲存位置的資訊;一般的矩陣計算函示庫(例如 glm)都算可以很簡單地處理。

代表投影矩陣的 fov 部分在 Heresy 來看則比較麻煩。他的型別是 XrFovf,提供的是上下左右四個方向的角度、數值範圍是 -π/2 到 π/2。

由於 glm 這類的函式庫似乎都沒有提供這種類型的投影矩陣的計算,所以後來 Heresy 這邊是參考 monado 這邊的公式(參考)了。

不過,現階段雖然在 SteamVR 上可以正確顯示,但是在 Oculus Rift S 上卻不正確…這部分不排除是這邊矩陣的處理沒有做好的關係。

當繪製完成後,則需要再呼叫 xrReleaseSwapchainImage()、告訴 OpenXR runtime 這邊不會再存取這張 swapchain image 了。


而目前這個程式執行起來後,則是會看到一個方塊,一邊打紅色光、另一邊則是綠色的光,使用者可以繞著他走動。

雖然現階段只是一個僅能針對 SteamVR 運作的簡單程式,但是也算是第一個可以動的範例了。

老實說,寫到現在…個人是覺得 OpenXR 相較於 Valve 的 OpenVR 麻煩、難寫很多啊…

感覺他在介面上太過低階,很多東西都需要自己處理,官方也沒有找到夠好的教學可以參考…
再加上他是提供一個標準規範,各家廠商都會有各自的實作,相容性也不完全相同;像是 Windows 的方案就完全不支援 OpenGL…

所以變成雖然他號稱是跨平台、跨裝置的業界標準,但是其實依據這標準下去寫,要在所有支援 OpenXR 的環境下使用,還是一件相當困難的事…

也因此…會不會有下一篇呢?不知道,可能要再看看狀況了。

Leave a Reply

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *