在事件處理的部分,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 了。