從去年開始,Heresy 就有在試著用 Qt 搭配 OpenVR,開發支援 SteamVR 的虛擬實境的程式。當時在寫的時候,一部分是參考 GitHub 上的「QVRViewer」這個專案(連結),也算是成功地把 Qt OpenGL(官網)的框架,成功地和 OpenVR 整合到一定程度了。
而接下來,則是希望可以在虛擬實境的環境裡面,顯示圖形介面的部分。這部分,則是參考了 OpenVR 的「helloworldoverlay」這個範例(連結);在它裡面的「openvroverlaycontroller.cpp」這個檔案(連結)就有把 Qt 的 Widget 繪製成 OpenGL Texture、並手動傳送滑鼠事件進去的操作方法。
首先,他這邊的概念,就是透過 Qt Graphics View Framework 的框架,來繪製 QWidget;而和一般使用 QGraphicsScene 時不同的地方,是這邊不會去使用 QGraphicsView 來做呈現,而是特別去設定要使用的 QPainter,來決定要畫到哪裡。
它的使用邏輯,基本上如下(這邊把 OpenVR overlay 的部分省略):
- 初始化(Init())
在這邊由於他的 OpenGL 使用目的是拿來繪製貼圖,並沒有要直接畫出來,所以他不是直接使用 QOpenGLWindow 或 QOpenGLWidget;也因此,這邊必須要自己去管理 OpenGL context 以及 surface。
在 Init() 這個函式裡面,可以看到他自行建立了 QOpenGLContext 的物件、以及一個 QOffscreenSurface 物件,來做為繪製的目標。
而為了讓圖形介面在有更動時會自動更新對應的 OpenGL Texture,所以這邊他透過 Qt signal – slot 的機制,讓 QGraphicsScene(這邊是m_pScene 這個物件)在有場景變動的時候(changed 這個 signal),就去呼叫自行定義的 OnScreenChanged 這個函式,來重新繪製。
- 設定要繪製的介面(SetWidget())
接下來,在 SetWidget() 這個函式中,則是將要繪製的 QWidget 在移到原點後,放進 m_pScene 中。
之後,則是根據 QWidget 的大小,產生對應大小的 QOpenGLFramebufferObject,來做為對應的繪製目標。
- 繪製(OnSenceChanged())
在上面都設定好了之後,之後只要 m_pScene 裡面有變動、需要重新繪製的時候,就會觸發到 OnScreenChanged() 這個函式,進行繪製。
而在這個函式裡面可以看到,它的操作方法基本上就是先呼叫 m_pOpenGLContext 的 makeCurrent() 函式,然後再 bind 之前產生的 QOpenGLFramebufferObject(m_pFbo)。
之後,則是建立一個 QOpenGLPaintDevice 的物件 device、並指定他的大小等於 m_pFbo 的大小(也就是要繪製的 QWidget 的大小);然後,再建立一個 QPainter 的物件 painter、並告訴他要畫在 device 上。
而在上面設定都完成後,接下來就是呼叫 m_pScene 的 render() 這個函式,並告訴他要使用 painter 來做繪製。如此一來,m_pScene 就會把場景的內容,都繪製在 m_pFbo 這個 QOpenGLFramebufferObject 的物件上了。
繪製好之後,則是要先 release m_pFbo,然後就可以透過 m_pFbo 的 texture() 這個函式,取得一個 OpenGL 的 texture id;而這個 texture 的內容,就是 QWidget 的內容了~
上面基本上就是整個繪製流程的基本概念了。
而實際在使用的時候,如果本身就是繼承 QOpenGLWindow 或 QOpenGLWidget 來開發的話,那其實可以省略 QOpenGLContext 和 QOffscreenSurface 的部分,使用既有的 OpenGL context 和 surface 來操作。
另外,在事件處理的部分,「helloworldoverlay」這個範例是透過固定時間去執行 OnTimeoutPumpEvents() 這個函式的方法,在裡面透過 OpenVR overlay 的提供的 API,來取得對應的滑鼠事件的;不過由於 Heresy 這邊並不打算使用 OpenVR 的 overlay 架構,所以很多東西可能就得自己做了。
但是,在這個函式中,還是有提供了如何設定 QGraphicsSceneMouseEvent、手動建立滑鼠事件給 QGraphicsScene 的示範;這點對要自己處理的人來說,也是有相當的參考價值的~
理論上,這樣就可以用 OpenGL 當 backend、來把 Qt Widget 畫到 OpenGL 的 texture 了。
不過,現實其實沒這麼美好…在 Heresy 這邊的狀況來說,這樣的寫法,對於ㄧ般的線條、圖形都沒有問題,但是碰到文字,就會整個爛掉了…
上面的截圖,就是透過 QOpenGLPaintDevice 繪製出來的一個 QPushButton,它裡面的文字是「Hello World」;但是這邊可以看到,雖然部分字母還可以看的出來,但是很多字母是直接爛掉、完全看不出是什麼鬼了…(上下顛倒是座標系統的問題)
研究了一陣子後,感覺上這似乎是 Qt5 很久以來都有的問題,也有人向官方回報過了(QTBUG-35000)…而這個問題似乎只會出現在 Android 和 Windows 平台上,但是從 2013 到現在,似乎都還沒有解掉…
最後,為了讓他能正常繪製文字,Heresy 後來只好捨棄 QOpenGLPaintDevice、而是先畫到 QImage 上,再自己去把它轉換成 OpenGL texture 了。
要修改的部分,主要就是先把建立 QOpenGLFramebufferObject 物件的地方,改成建立一個 QImage 的物件(要指定大小和格式);同時自己手動透過 OpenGL 的 texture 相關函式,建立對應的 OpenGL Texture(或者也可以使用對應的 QOpenGLTexture)。
這邊的程式可能會寫成下面的樣子:
m_qbaseImage = QImage(m_pWidget->width(), m_pWidget->height(), QImage::Format_RGBA8888); initialTexture(m_qba seImage);
其中,initialTexture() 這個函式,則是自己撰寫、針對 QImage 建立 OpenGL Texture 的函式。
而在要繪製的部分,則相對簡單,可以寫成:
QPainter qPainter(&m_qbaseImage); m_Scene.render(&qPainter); updateTexture(m_qba seImage);
這邊就是用一般的 QPainter,指定他的繪製目標是 m_qba
在繪製完成後,則是再呼叫 updateTexture() 這個函式,把 m_qba
而這樣繪製的結果,文字的顯示效果就是正確的了!
另外,上面的介面看起來,除了按鈕的立體效果似乎消失了以外,其他似乎都還算可以接受?
不過實際上,如果真的把 Widget 的大小拿來當作 frame buffer object/QPixmap 的大小的話,那其實可能會發生文字太小,或是放大後介面的像素點太大、或是模糊的問題(如下圖)。
要解決這樣的問題,比較直接的方法,就是在建立 QOpenGLFramebufferObject 的物件的時候,把大小放大;像上面的圖,就是 Heresy 就是把長寬都乘 4,這樣畫出來才會比較好看。
這篇先寫到這邊。由於 Heresy 也還在邊寫邊試,也還沒寫完,這邊主要是怕自己忘記,先做個筆記了。