這邊延續前面的 part 1,繼續來講 glutCube 這個範例的顯示的部分。
首先,glut 的 display() 函式內容如下:
void display(void) { gXRGL.processEvent(); gXRGL.draw([](const COpenXRGL::TMatrix& matProj, const COpenXRGL::TMatrix& matModelView) { //draw }); glutSwapBuffers(); }
這邊會先去呼叫 COpenXRGL 的 processEvent() 這個函式、進行 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()
- xrBeginFrame()
這邊,首先會先針對前面 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 的環境下使用,還是一件相當困難的事…
也因此…會不會有下一篇呢?不知道,可能要再看看狀況了。